2 Commits

Author SHA1 Message Date
jdl
b972784d90 WIP 2026-06-05 19:43:27 +02:00
jdl
8b2c9709fc WIP 2026-06-04 16:00:21 +02:00
65 changed files with 1162 additions and 3383 deletions

10
go.mod
View File

@@ -9,7 +9,17 @@ require (
)
require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect
)

22
go.sum
View File

@@ -1,12 +1,34 @@
git.crumpington.com/lib/go v0.9.1 h1:xLBzcgiZRB6Ky3Ce9hKE+Ko0YbkA4USF4eJk5i5RJF4=
git.crumpington.com/lib/go v0.9.1/go.mod h1:5nnfjdnUnj/FHhakaliKQKsKeSkUb0GEUKF3PqRgUXg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=

View File

@@ -21,6 +21,8 @@ var migrations embed.FS
type API struct {
db *sql.DB
lock sync.Mutex
sessionsMu sync.Mutex
sessions map[string]*Session
}
func New(dbPath string) (*API, error) {
@@ -35,6 +37,7 @@ func New(dbPath string) (*API, error) {
a := &API{
db: sqlDB,
sessions: make(map[string]*Session),
}
return a, a.ensurePassword()
@@ -75,47 +78,41 @@ func (a *API) Config_Update(conf *Config) error {
}
func (a *API) Session_Delete(sessionID string) error {
return db.Session_Delete(a.db, sessionID)
a.sessionsMu.Lock()
defer a.sessionsMu.Unlock()
delete(a.sessions, sessionID)
return nil
}
func (a *API) Session_Get(sessionID string) (*Session, error) {
if sessionID == "" {
return a.session_CreatePub()
}
a.sessionsMu.Lock()
defer a.sessionsMu.Unlock()
session, err := db.Session_Get(a.db, sessionID)
if err != nil {
return a.session_CreatePub()
if sessionID != "" {
s, ok := a.sessions[sessionID]
if ok {
if timeSince(s.LastSeenAt) <= 86400*21 {
if timeSince(s.LastSeenAt) > 86400*7 {
s.LastSeenAt = time.Now().Unix()
}
if timeSince(session.LastSeenAt) > 86400*21 {
return a.session_CreatePub()
return s, nil
}
if timeSince(session.LastSeenAt) > 86400*7 {
session.LastSeenAt = time.Now().Unix()
if err := db.Session_UpdateLastSeenAt(a.db, session.SessionID); err != nil {
log.Printf("Failed to update session: %v", err)
delete(a.sessions, sessionID)
}
}
return session, nil
return a.session_Create(), nil
}
func (a *API) session_CreatePub() (*Session, error) {
// caller must hold sessionsMu
func (a *API) session_Create() *Session {
s := &Session{
SessionID: idgen.NewToken(),
CSRF: idgen.NewToken(),
SignedIn: false,
CreatedAt: time.Now().Unix(),
LastSeenAt: time.Now().Unix(),
}
err := db.Session_Insert(a.db, s)
return s, err
}
func (a *API) Session_DeleteBefore(timestamp int64) error {
return db.Session_DeleteBefore(a.db, timestamp)
a.sessions[s.SessionID] = s
return s
}
func (a *API) Session_SignIn(s *Session, pwd string) error {
@@ -123,8 +120,10 @@ func (a *API) Session_SignIn(s *Session, pwd string) error {
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return ErrNotAuthorized
}
return db.Session_SetSignedIn(a.db, s.SessionID)
a.sessionsMu.Lock()
s.SignedIn = true
a.sessionsMu.Unlock()
return nil
}
func (a *API) Network_Create(n *Network) error {
@@ -147,8 +146,7 @@ func (a *API) Network_List() ([]*Network, error) {
func (a *API) Peer_CreateNew(p *Peer) error {
p.Version = idgen.NextID(0)
p.PubKey = []byte{}
p.PubSignKey = []byte{}
p.WGPubKey = []byte{}
p.APIKey = idgen.NewToken()
return db.Peer_Insert(a.db, p)
@@ -159,8 +157,7 @@ func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) error {
defer a.lock.Unlock()
peer.Version = idgen.NextID(0)
peer.PubKey = args.EncPubKey
peer.PubSignKey = args.PubSignKey
peer.WGPubKey = args.WGPubKey
return db.Peer_UpdateFull(a.db, peer)
}

View File

@@ -123,7 +123,9 @@ func Config_Get(
) {
row = &Config{}
r := tx.QueryRow("SELECT ConfigID,Password FROM config WHERE ConfigID=?", ConfigID)
err = r.Scan(&row.ConfigID, &row.Password)
if err = r.Scan(&row.ConfigID, &row.Password); err != nil {
row = nil
}
return
}
@@ -137,7 +139,9 @@ func Config_GetWhere(
) {
row = &Config{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.ConfigID, &row.Password)
if err = r.Scan(&row.ConfigID, &row.Password); err != nil {
row = nil
}
return
}
@@ -182,124 +186,6 @@ func Config_List(
return l, nil
}
// ----------------------------------------------------------------------------
// Table: sessions
// ----------------------------------------------------------------------------
type Session struct {
SessionID string
CSRF string
SignedIn bool
CreatedAt int64
LastSeenAt int64
}
const Session_SelectQuery = "SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions"
func Session_Insert(
tx TX,
row *Session,
) (err error) {
Session_Sanitize(row)
if err = Session_Validate(row); err != nil {
return err
}
_, err = tx.Exec("INSERT INTO sessions(SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt) VALUES(?,?,?,?,?)", row.SessionID, row.CSRF, row.SignedIn, row.CreatedAt, row.LastSeenAt)
return err
}
func Session_Delete(
tx TX,
SessionID string,
) (err error) {
result, err := tx.Exec("DELETE FROM sessions WHERE SessionID=?", SessionID)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows deleted")
}
}
func Session_Get(
tx TX,
SessionID string,
) (
row *Session,
err error,
) {
row = &Session{}
r := tx.QueryRow("SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions WHERE SessionID=?", SessionID)
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
return
}
func Session_GetWhere(
tx TX,
query string,
args ...any,
) (
row *Session,
err error,
) {
row = &Session{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
return
}
func Session_Iterate(
tx TX,
query string,
args ...any,
) iter.Seq2[*Session, error] {
rows, err := tx.Query(query, args...)
if err != nil {
return func(yield func(*Session, error) bool) {
yield(nil, err)
}
}
return func(yield func(*Session, error) bool) {
defer rows.Close()
for rows.Next() {
row := &Session{}
err := rows.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
if !yield(row, err) {
return
}
}
}
}
func Session_List(
tx TX,
query string,
args ...any,
) (
l []*Session,
err error,
) {
for row, err := range Session_Iterate(tx, query, args...) {
if err != nil {
return nil, err
}
l = append(l, row)
}
return l, nil
}
// ----------------------------------------------------------------------------
// Table: networks
// ----------------------------------------------------------------------------
@@ -385,7 +271,9 @@ func Network_Get(
) {
row = &Network{}
r := tx.QueryRow("SELECT NetworkID,Name,Network FROM networks WHERE NetworkID=?", NetworkID)
err = r.Scan(&row.NetworkID, &row.Name, &row.Network)
if err = r.Scan(&row.NetworkID, &row.Name, &row.Network); err != nil {
row = nil
}
return
}
@@ -399,7 +287,9 @@ func Network_GetWhere(
) {
row = &Network{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.NetworkID, &row.Name, &row.Network)
if err = r.Scan(&row.NetworkID, &row.Name, &row.Network); err != nil {
row = nil
}
return
}
@@ -454,14 +344,15 @@ type Peer struct {
Version int64
APIKey string
Name string
PublicIP []byte
Port uint16
PublicIP1 []byte
Port1 uint16
PublicIP2 []byte
Port2 uint16
Relay bool
PubKey []byte
PubSignKey []byte
WGPubKey []byte
}
const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey FROM peers"
const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey FROM peers"
func Peer_Insert(
tx TX,
@@ -472,7 +363,7 @@ func Peer_Insert(
return err
}
_, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey) VALUES(?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.PublicIP, row.Port, row.Relay, row.PubKey, row.PubSignKey)
_, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey) VALUES(?,?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.PublicIP1, row.Port1, row.PublicIP2, row.Port2, row.Relay, row.WGPubKey)
return err
}
@@ -485,7 +376,7 @@ func Peer_Update(
return err
}
result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,PublicIP=?,Port=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.PublicIP, row.Port, row.Relay, row.NetworkID, row.PeerIP)
result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,PublicIP1=?,Port1=?,PublicIP2=?,Port2=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.PublicIP1, row.Port1, row.PublicIP2, row.Port2, row.Relay, row.NetworkID, row.PeerIP)
if err != nil {
return err
}
@@ -513,7 +404,7 @@ func Peer_UpdateFull(
return err
}
result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,PublicIP=?,Port=?,Relay=?,PubKey=?,PubSignKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.PublicIP, row.Port, row.Relay, row.PubKey, row.PubSignKey, row.NetworkID, row.PeerIP)
result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,PublicIP1=?,Port1=?,PublicIP2=?,Port2=?,Relay=?,WGPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.PublicIP1, row.Port1, row.PublicIP2, row.Port2, row.Relay, row.WGPubKey, row.NetworkID, row.PeerIP)
if err != nil {
return err
}
@@ -565,8 +456,10 @@ func Peer_Get(
err error,
) {
row = &Peer{}
r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP)
err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey)
r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP)
if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP1, &row.Port1, &row.PublicIP2, &row.Port2, &row.Relay, &row.WGPubKey); err != nil {
row = nil
}
return
}
@@ -580,7 +473,9 @@ func Peer_GetWhere(
) {
row = &Peer{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey)
if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP1, &row.Port1, &row.PublicIP2, &row.Port2, &row.Relay, &row.WGPubKey); err != nil {
row = nil
}
return
}
@@ -600,7 +495,7 @@ func Peer_Iterate(
defer rows.Close()
for rows.Next() {
row := &Peer{}
err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey)
err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP1, &row.Port1, &row.PublicIP2, &row.Port2, &row.Relay, &row.WGPubKey)
if !yield(row, err) {
return
}

View File

@@ -21,13 +21,6 @@ func Config_Validate(c *Config) error {
return nil
}
func Session_Sanitize(s *Session) {
}
func Session_Validate(s *Session) error {
return nil
}
func Network_Sanitize(n *Network) {
n.Name = strings.TrimSpace(n.Name)
@@ -66,25 +59,39 @@ func Network_Validate(c *Network) error {
func Peer_Sanitize(p *Peer) {
p.Name = strings.TrimSpace(p.Name)
if len(p.PublicIP) != 0 {
addr, ok := netip.AddrFromSlice(p.PublicIP)
if ok && addr.Is4() {
p.PublicIP = addr.AsSlice()
if len(p.PublicIP1) != 0 {
if addr, ok := netip.AddrFromSlice(p.PublicIP1); ok {
p.PublicIP1 = addr.AsSlice()
}
}
if p.Port == 0 {
p.Port = 456
if len(p.PublicIP2) != 0 {
if addr, ok := netip.AddrFromSlice(p.PublicIP2); ok {
p.PublicIP2 = addr.AsSlice()
}
}
if p.Port1 == 0 {
p.Port1 = 456
}
if len(p.PublicIP2) != 0 && p.Port2 == 0 {
p.Port2 = 456
}
}
func Peer_Validate(p *Peer) error {
if len(p.PublicIP) > 0 {
_, ok := netip.AddrFromSlice(p.PublicIP)
if !ok {
if len(p.PublicIP1) > 0 {
if _, ok := netip.AddrFromSlice(p.PublicIP1); !ok {
return ErrInvalidIP
}
}
if p.Port == 0 {
if len(p.PublicIP2) > 0 {
if _, ok := netip.AddrFromSlice(p.PublicIP2); !ok {
return ErrInvalidIP
}
if p.Port2 == 0 {
return ErrInvalidPort
}
}
if p.Port1 == 0 {
return ErrInvalidPort
}
@@ -98,7 +105,6 @@ func Peer_Validate(p *Peer) error {
if c == '.' || c == '-' || c == '_' {
continue
}
return ErrInvalidPeerName
}

View File

@@ -3,14 +3,6 @@ TABLE config OF Config (
Password []byte
);
TABLE sessions OF Session NoUpdate (
SessionID string PK,
CSRF string,
SignedIn bool,
CreatedAt int64,
LastSeenAt int64
);
TABLE networks OF Network (
NetworkID int64 PK,
Name string NoUpdate,
@@ -23,9 +15,10 @@ TABLE peers OF Peer (
Version int64,
APIKey string NoUpdate,
Name string,
PublicIP []byte,
Port uint16,
PublicIP1 []byte,
Port1 uint16,
PublicIP2 []byte,
Port2 uint16,
Relay bool,
PubKey []byte NoUpdate,
PubSignKey []byte NoUpdate
WGPubKey []byte NoUpdate
);

View File

@@ -1,31 +1,5 @@
package db
import "time"
func Session_UpdateLastSeenAt(
tx TX,
id string,
) (err error) {
_, err = tx.Exec("UPDATE sessions SET LastSeenAt=? WHERE SessionID=?", time.Now().Unix(), id)
return err
}
func Session_SetSignedIn(
tx TX,
id string,
) (err error) {
_, err = tx.Exec("UPDATE sessions SET SignedIn=1 WHERE SessionID=?", id)
return err
}
func Session_DeleteBefore(
tx TX,
timestamp int64,
) (err error) {
_, err = tx.Exec("DELETE FROM sessions WHERE LastSeenAt<?", timestamp)
return err
}
func Peer_ListAll(tx TX, networkID int64) ([]*Peer, error) {
const query = Peer_SelectQuery + ` WHERE NetworkID=? ORDER BY PeerIP ASC`
return Peer_List(tx, query, networkID)

View File

@@ -3,16 +3,6 @@ CREATE TABLE config (
Password BLOB NOT NULL -- bcrypt password for web interface
) WITHOUT ROWID;
CREATE TABLE sessions (
SessionID TEXT NOT NULL PRIMARY KEY,
CSRF TEXT NOT NULL,
SignedIn INTEGER NOT NULL,
CreatedAt INTEGER NOT NULL,
LastSeenAt INTEGER NOT NULL
) WITHOUT ROWID;
CREATE INDEX sessions_last_seen_index ON sessions(LastSeenAt);
CREATE TABLE networks (
NetworkID INTEGER NOT NULL PRIMARY KEY,
Name TEXT NOT NULL UNIQUE, -- Network/interface name.
@@ -25,10 +15,11 @@ CREATE TABLE peers (
Version INTEGER NOT NULL, -- Changes when updated.
APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key.
Name TEXT NOT NULL UNIQUE, -- For humans.
PublicIP BLOB NOT NULL,
Port INTEGER NOT NULL,
Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. Must also have public address.
PubKey BLOB NOT NULL,
PubSignKey BLOB NOT NULL,
PublicIP1 BLOB NOT NULL,
Port1 INTEGER NOT NULL,
PublicIP2 BLOB NOT NULL,
Port2 INTEGER NOT NULL,
Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets.
WGPubKey BLOB NOT NULL,
PRIMARY KEY(NetworkID, PeerIP)
) WITHOUT ROWID;

View File

@@ -3,6 +3,12 @@ package api
import "vppn/hub/api/db"
type Config = db.Config
type Session = db.Session
type Network = db.Network
type Peer = db.Peer
type Session struct {
SessionID string
SignedIn bool
CreatedAt int64
LastSeenAt int64
}

View File

@@ -2,6 +2,7 @@ package hub
import (
"embed"
"encoding/base64"
"html/template"
"net/http"
"path/filepath"
@@ -47,6 +48,19 @@ func NewApp(conf Config) (*App, error) {
return app, nil
}
func (app *App) Handler() http.Handler {
cop := http.NewCrossOriginProtection()
return cop.Handler(app.mux)
}
var templateFuncs = template.FuncMap{
"ipToString": ipBytesTostring,
"wgKeyString": wgKeyString,
}
func wgKeyString(key []byte) string {
if len(key) == 0 {
return "not set"
}
return base64.StdEncoding.EncodeToString(key)
}

View File

@@ -26,11 +26,6 @@ func (app *App) handlePub(pattern string, fn handlerFunc) {
if r.Method == http.MethodPost {
r.ParseMultipartForm(64 * 1024)
if r.FormValue("CSRF") != s.CSRF {
log.Printf("%s != %s", r.FormValue("CSRF"), s.CSRF)
http.Error(w, "CSRF mismatch", http.StatusBadRequest)
return
}
} else {
r.ParseForm()
}

View File

@@ -142,22 +142,27 @@ func (a *App) _adminPeerCreate(s *api.Session, w http.ResponseWriter, r *http.Re
}
func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
var ipStr string
var ip1Str, ip2Str string
p := &api.Peer{}
err := webutil.NewFormScanner(r.Form).
Scan("NetworkID", &p.NetworkID).
Scan("IP", &p.PeerIP).
Scan("Name", &p.Name).
Scan("PublicIP", &ipStr).
Scan("Port", &p.Port).
Scan("PublicIP1", &ip1Str).
Scan("Port1", &p.Port1).
Scan("PublicIP2", &ip2Str).
Scan("Port2", &p.Port2).
Scan("Relay", &p.Relay).
Error()
if err != nil {
return err
}
if p.PublicIP, err = stringToIP(ipStr); err != nil {
if p.PublicIP1, err = stringToIP(ip1Str); err != nil {
return err
}
if p.PublicIP2, err = stringToIP(ip2Str); err != nil {
return err
}
@@ -199,19 +204,24 @@ func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *htt
return err
}
var ipStr string
var ip1Str, ip2Str string
err = webutil.NewFormScanner(r.Form).
Scan("Name", &peer.Name).
Scan("PublicIP", &ipStr).
Scan("Port", &peer.Port).
Scan("PublicIP1", &ip1Str).
Scan("Port1", &peer.Port1).
Scan("PublicIP2", &ip2Str).
Scan("Port2", &peer.Port2).
Scan("Relay", &peer.Relay).
Error()
if err != nil {
return err
}
if peer.PublicIP, err = stringToIP(ipStr); err != nil {
if peer.PublicIP1, err = stringToIP(ip1Str); err != nil {
return err
}
if peer.PublicIP2, err = stringToIP(ip2Str); err != nil {
return err
}
@@ -295,6 +305,11 @@ func (a *App) _adminPasswordSubmit(s *api.Session, w http.ResponseWriter, r *htt
}
func (a *App) _peerInit(peer *api.Peer, w http.ResponseWriter, r *http.Request) error {
if len(peer.WGPubKey) != 0 {
http.Error(w, "Already initialized", http.StatusConflict)
return nil
}
args := m.PeerInitArgs{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
return err
@@ -338,16 +353,17 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) {
}
for _, p := range l {
if len(p.PubKey) != 0 {
if len(p.WGPubKey) != 0 {
peers[p.PeerIP] = &m.Peer{
PeerIP: p.PeerIP,
Version: p.Version,
Name: p.Name,
PublicIP: p.PublicIP,
Port: p.Port,
PublicIP1: p.PublicIP1,
Port1: p.Port1,
PublicIP2: p.PublicIP2,
Port2: p.Port2,
Relay: p.Relay,
PubKey: p.PubKey,
PubSignKey: p.PubSignKey,
WGPubKey: p.WGPubKey,
}
}
}

View File

@@ -31,7 +31,7 @@ func Main() {
srv := &http.Server{
Addr: conf.ListenAddr,
Handler: app.mux,
Handler: app.Handler(),
}
log.Fatal(webutil.ListenAndServe(srv))

View File

@@ -2,7 +2,6 @@
<h2>Create Network</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<label>Name</label><br>
<input type="text" name="Name">

View File

@@ -2,7 +2,6 @@
<h2>Change Password</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<label>Current Password</label><br>
<input type="password" name="CurrentPassword">

View File

@@ -2,7 +2,6 @@
<h2>Sign Out</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<button type="submit">Sign Out</button>
<a href="/">Cancel</a>

View File

@@ -5,7 +5,6 @@
<p>You must first delete all peers.</p>
{{- else -}}
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<input type="hidden" name="NetworkID" value="{{.Network.NetworkID}}">
<p>
<button type="submit">Delete</button>

View File

@@ -22,8 +22,10 @@
<tr>
<th>PeerIP</th>
<th>Name</th>
<th>Public IP</th>
<th>Port</th>
<th>Public IP 1</th>
<th>Port 1</th>
<th>Public IP 2</th>
<th>Port 2</th>
<th>Relay</th>
</tr>
</thead>
@@ -36,8 +38,10 @@
</a>
</td>
<td>{{.Name}}</td>
<td>{{ipToString .PublicIP}}</td>
<td>{{.Port}}</td>
<td>{{ipToString .PublicIP1}}</td>
<td>{{.Port1}}</td>
<td>{{ipToString .PublicIP2}}</td>
<td>{{.Port2}}</td>
<td>{{if .Relay}}T{{else}}F{{end}}</td>
</tr>
</tbody>

View File

@@ -2,7 +2,6 @@
<h3>New Peer</h3>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<input type="hidden" name="NetworkID" value="{{.Network.NetworkID}}">
<p>
<label>IP</label><br>
@@ -13,12 +12,20 @@
<input type="text" name="Name">
</p>
<p>
<label>Public IP</label><br>
<input type="text" name="PublicIP">
<label>Public IP 1</label><br>
<input type="text" name="PublicIP1">
</p>
<p>
<label>Port</label><br>
<input type="number" name="Port" value="456">
<label>Port 1</label><br>
<input type="number" name="Port1" value="456">
</p>
<p>
<label>Public IP 2 (optional)</label><br>
<input type="text" name="PublicIP2">
</p>
<p>
<label>Port 2</label><br>
<input type="number" name="Port2" value="0">
</p>
<p>
<label>

View File

@@ -3,7 +3,6 @@
{{with .Peer -}}
<form method="POST">
<input type="hidden" name="CSRF" value="{{$.Session.CSRF}}">
<input type="hidden" name="NetworkID" value="{{.NetworkID}}">
<input type="hidden" name="NetworkID" value="{{.PeerIP}}">
<p>

View File

@@ -3,7 +3,6 @@
{{with .Peer -}}
<form method="POST">
<input type="hidden" name="CSRF" value="{{$.Session.CSRF}}">
<p>
<label>Peer IP</label><br>
<input type="text" value="{{.PeerIP}}" disabled>
@@ -13,12 +12,20 @@
<input type="text" name="Name" value="{{.Name}}">
</p>
<p>
<label>Public IP</label><br>
<input type="text" name="PublicIP" value="{{ipToString .PublicIP}}">
<label>Public IP 1</label><br>
<input type="text" name="PublicIP1" value="{{ipToString .PublicIP1}}">
</p>
<p>
<label>Port</label><br>
<input type="number" name="Port" value="{{.Port}}">
<label>Port 1</label><br>
<input type="number" name="Port1" value="{{.Port1}}">
</p>
<p>
<label>Public IP 2 (optional)</label><br>
<input type="text" name="PublicIP2" value="{{ipToString .PublicIP2}}">
</p>
<p>
<label>Port 2</label><br>
<input type="number" name="Port2" value="{{.Port2}}">
</p>
<p>
<label>

View File

@@ -8,10 +8,12 @@
{{with .Peer -}}
<table class="def-list">
<tr><td>Peer IP</td><td>{{.PeerIP}}</td></tr>
<tr><td>Public IP</td><td>{{ipToString .PublicIP}}</td></tr>
<tr><td>Port</td><td>{{.Port}}</td></tr>
<tr><td>Public IP 1</td><td>{{ipToString .PublicIP1}}</td></tr>
<tr><td>Port 1</td><td>{{.Port1}}</td></tr>
<tr><td>Public IP 2</td><td>{{ipToString .PublicIP2}}</td></tr>
<tr><td>Port 2</td><td>{{.Port2}}</td></tr>
<tr><td>Relay</td><td>{{if .Relay}}T{{else}}F{{end}}</td></tr>
</td></tr>
<tr><td>WG Public Key</td><td>{{wgKeyString .WGPubKey}}</td></tr>
</table>
<details>
@@ -19,7 +21,6 @@
<p>{{.APIKey}}</p>
</details>
{{- end}}
{{- end}}

View File

@@ -2,7 +2,6 @@
<h2>Sign In</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<label>Password</label><br>
<input type="password" name="Password">

View File

@@ -2,8 +2,7 @@
package m
type PeerInitArgs struct {
EncPubKey []byte
PubSignKey []byte
WGPubKey []byte
}
type PeerInitResp struct {
@@ -16,11 +15,12 @@ type Peer struct {
PeerIP byte
Version int64
Name string
PublicIP []byte
Port uint16
PublicIP1 []byte
Port1 uint16
PublicIP2 []byte
Port2 uint16
Relay bool
PubKey []byte
PubSignKey []byte
WGPubKey []byte
}
type NetworkState struct {

View File

@@ -1,21 +0,0 @@
package peer
const bitSetSize = 512 // Multiple of 64.
type bitSet [bitSetSize / 64]uint64
func (bs *bitSet) Set(i int) {
bs[i/64] |= 1 << (i % 64)
}
func (bs *bitSet) Clear(i int) {
bs[i/64] &= ^(1 << (i % 64))
}
func (bs *bitSet) ClearAll() {
clear(bs[:])
}
func (bs *bitSet) Get(i int) bool {
return bs[i/64]&(1<<(i%64)) != 0
}

View File

@@ -1,48 +0,0 @@
package peer
import (
"math/rand"
"testing"
)
func TestBitSet(t *testing.T) {
state := make([]bool, bitSetSize)
for i := range state {
state[i] = rand.Float32() > 0.5
}
bs := bitSet{}
for i := range state {
if state[i] {
bs.Set(i)
}
}
for i := range state {
if bs.Get(i) != state[i] {
t.Fatal(i, state[i], bs.Get(i))
}
}
for i := range state {
if rand.Float32() > 0.5 {
state[i] = false
bs.Clear(i)
}
}
for i := range state {
if bs.Get(i) != state[i] {
t.Fatal(i, state[i], bs.Get(i))
}
}
bs.ClearAll()
for i := range state {
if bs.Get(i) {
t.Fatal(i, bs.Get(i))
}
}
}

View File

@@ -1,26 +0,0 @@
package peer
import "golang.org/x/crypto/nacl/box"
type controlCipher struct {
sharedKey [32]byte
}
func newControlCipher(privKey, pubKey []byte) *controlCipher {
shared := [32]byte{}
box.Precompute(&shared, (*[32]byte)(pubKey), (*[32]byte)(privKey))
return &controlCipher{shared}
}
func (cc *controlCipher) Encrypt(h Header, data, out []byte) []byte {
const s = controlHeaderSize
out = out[:s+controlCipherOverhead+len(data)]
h.Marshal(out[:s])
box.SealAfterPrecomputation(out[s:s], data, (*[24]byte)(out[:s]), &cc.sharedKey)
return out
}
func (cc *controlCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) {
const s = controlHeaderSize
return box.OpenAfterPrecomputation(out[:0], encrypted[s:], (*[24]byte)(encrypted[:s]), &cc.sharedKey)
}

View File

@@ -1,122 +0,0 @@
package peer
import (
"bytes"
"crypto/rand"
"reflect"
"testing"
"golang.org/x/crypto/nacl/box"
)
func newControlCipherForTesting() (c1, c2 *controlCipher) {
pubKey1, privKey1, err := box.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
pubKey2, privKey2, err := box.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
return newControlCipher(privKey1[:], pubKey2[:]),
newControlCipher(privKey2[:], pubKey1[:])
}
func TestControlCipher(t *testing.T) {
c1, c2 := newControlCipherForTesting()
maxSizePlaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead)
rand.Read(maxSizePlaintext)
testCases := [][]byte{
make([]byte, 0),
{1},
{255},
{1, 2, 3, 4, 5},
[]byte("Hello world"),
maxSizePlaintext,
}
for _, plaintext := range testCases {
h1 := Header{
StreamID: controlStreamID,
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
encrypted := make([]byte, bufferSize)
encrypted = c1.Encrypt(h1, plaintext, encrypted)
h2 := Header{}
h2.Parse(encrypted)
if !reflect.DeepEqual(h1, h2) {
t.Fatal(h1, h2)
}
decrypted, ok := c2.Decrypt(encrypted, make([]byte, bufferSize))
if !ok {
t.Fatal(ok)
}
if !bytes.Equal(decrypted, plaintext) {
t.Fatal("not equal")
}
}
}
func TestControlCipher_ShortCiphertext(t *testing.T) {
c1, _ := newControlCipherForTesting()
shortText := make([]byte, controlHeaderSize+controlCipherOverhead-1)
rand.Read(shortText)
_, ok := c1.Decrypt(shortText, make([]byte, bufferSize))
if ok {
t.Fatal(ok)
}
}
func BenchmarkControlCipher_Encrypt(b *testing.B) {
c1, _ := newControlCipherForTesting()
h1 := Header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
encrypted = c1.Encrypt(h1, plaintext, encrypted)
}
}
func BenchmarkControlCipher_Decrypt(b *testing.B) {
c1, c2 := newControlCipherForTesting()
h1 := Header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
encrypted = c1.Encrypt(h1, plaintext, encrypted)
decrypted := make([]byte, bufferSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
decrypted, _ = c2.Decrypt(encrypted, decrypted)
}
}

View File

@@ -1,61 +0,0 @@
package peer
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"log"
)
type dataCipher struct {
key [32]byte
aead cipher.AEAD
}
func newDataCipher() *dataCipher {
key := [32]byte{}
if _, err := rand.Read(key[:]); err != nil {
log.Fatalf("Failed to read random data: %v", err)
}
return newDataCipherFromKey(key)
}
func newDataCipherFromKey(key [32]byte) *dataCipher {
block, err := aes.NewCipher(key[:])
if err != nil {
log.Fatalf("Failed to create new cipher: %v", err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
log.Fatalf("Failed to create new GCM: %v", err)
}
return &dataCipher{key: key, aead: aead}
}
func (sc *dataCipher) Key() [32]byte {
return sc.key
}
func (sc *dataCipher) Encrypt(h Header, data, out []byte) []byte {
const s = dataHeaderSize
out = out[:s+dataCipherOverhead+len(data)]
h.Marshal(out[:s])
sc.aead.Seal(out[s:s], out[:s], data, nil)
return out
}
func (sc *dataCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) {
const s = dataHeaderSize
if len(encrypted) < s+dataCipherOverhead {
ok = false
return
}
var err error
data, err = sc.aead.Open(out[:0], encrypted[:s], encrypted[s:], nil)
ok = err == nil
return
}

View File

@@ -1,141 +0,0 @@
package peer
import (
"bytes"
"crypto/rand"
mrand "math/rand/v2"
"reflect"
"testing"
)
func TestDataCipher(t *testing.T) {
maxSizePlaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(maxSizePlaintext)
testCases := [][]byte{
make([]byte, 0),
{1},
{255},
{1, 2, 3, 4, 5},
[]byte("Hello world"),
maxSizePlaintext,
}
for _, plaintext := range testCases {
h1 := Header{
StreamID: dataStreamID,
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
h2 := Header{}
h2.Parse(encrypted)
dc2 := newDataCipherFromKey(dc1.Key())
decrypted, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize))
if !ok {
t.Fatal(ok)
}
if !bytes.Equal(plaintext, decrypted) {
t.Fatal("not equal")
}
if !reflect.DeepEqual(h1, h2) {
t.Fatalf("%v != %v", h1, h2)
}
}
}
func TestDataCipher_ModifyCiphertext(t *testing.T) {
maxSizePlaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(maxSizePlaintext)
testCases := [][]byte{
make([]byte, 0),
{1},
{255},
{1, 2, 3, 4, 5},
[]byte("Hello world"),
maxSizePlaintext,
}
for _, plaintext := range testCases {
h1 := Header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
encrypted[mrand.IntN(len(encrypted))]++
dc2 := newDataCipherFromKey(dc1.Key())
_, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize))
if ok {
t.Fatal(ok)
}
}
}
func TestDataCipher_ShortCiphertext(t *testing.T) {
dc1 := newDataCipher()
shortText := make([]byte, dataHeaderSize+dataCipherOverhead-1)
rand.Read(shortText)
_, ok := dc1.Decrypt(shortText, make([]byte, bufferSize))
if ok {
t.Fatal(ok)
}
}
func BenchmarkDataCipher_Encrypt(b *testing.B) {
h1 := Header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
b.ResetTimer()
for i := 0; i < b.N; i++ {
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
}
}
func BenchmarkDataCipher_Decrypt(b *testing.B) {
h1 := Header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
decrypted := make([]byte, bufferSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
decrypted, _ = dc1.Decrypt(encrypted, decrypted)
}
}

View File

@@ -1,46 +0,0 @@
package peer
import (
"log"
"net"
"net/netip"
)
type ConnReader struct {
Globals
conn *net.UDPConn
buf []byte
}
func NewConnReader(g Globals, conn *net.UDPConn) *ConnReader {
return &ConnReader{
Globals: g,
conn: conn,
buf: make([]byte, bufferSize),
}
}
func (r *ConnReader) Run() {
for {
r.handleNextPacket()
}
}
func (r *ConnReader) handleNextPacket() {
buf := r.buf[:bufferSize]
n, remoteAddr, err := r.conn.ReadFromUDPAddrPort(buf)
if err != nil {
log.Fatalf("Failed to read from UDP port: %v", err)
}
if n < headerSize {
return
}
remoteAddr = netip.AddrPortFrom(remoteAddr.Addr().Unmap(), remoteAddr.Port())
buf = buf[:n]
h := parseHeader(buf)
r.RemotePeers[h.SourceIP].Load().HandlePacket(h, remoteAddr, buf)
}

View File

@@ -1,64 +0,0 @@
package peer
import (
"net/netip"
"vppn/m"
)
// ----------------------------------------------------------------------------
type controlMsg[T any] struct {
SrcIP byte
SrcAddr netip.AddrPort
Packet T
}
func parseControlMsg(srcIP byte, srcAddr netip.AddrPort, buf []byte) (any, error) {
switch buf[0] {
case packetTypeInit:
packet, err := parsePacketInit(buf)
return controlMsg[packetInit]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
case packetTypeSyn:
packet, err := parsePacketSyn(buf)
return controlMsg[packetSyn]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
case packetTypeAck:
packet, err := parsePacketAck(buf)
return controlMsg[packetAck]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
case packetTypeProbe:
packet, err := parsePacketProbe(buf)
return controlMsg[packetProbe]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
default:
return nil, errUnknownPacketType
}
}
// ----------------------------------------------------------------------------
type peerUpdateMsg struct {
Peer *m.Peer
}
// ----------------------------------------------------------------------------
type pingTimerMsg struct{}

View File

@@ -1,30 +1,15 @@
package peer
import (
"crypto/rand"
"log"
"golang.org/x/crypto/nacl/box"
"golang.org/x/crypto/nacl/sign"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type cryptoKeys struct {
PubKey []byte
PrivKey []byte
PubSignKey []byte
PrivSignKey []byte
}
func generateKeys() cryptoKeys {
pubKey, privKey, err := box.GenerateKey(rand.Reader)
func generateWGKey() wgtypes.Key {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
log.Fatalf("Failed to generate encryption keys: %v", err)
log.Fatalf("Failed to generate WireGuard private key: %v", err)
}
pubSignKey, privSignKey, err := sign.GenerateKey(rand.Reader)
if err != nil {
log.Fatalf("Failed to generate signing keys: %v", err)
}
return cryptoKeys{pubKey[:], privKey[:], pubSignKey[:], privSignKey[:]}
return key
}

View File

@@ -1,76 +0,0 @@
package peer
type dupCheck struct {
bitSet
head int
tail int
headCounter uint64
tailCounter uint64 // Also next expected counter value.
}
func newDupCheck(headCounter uint64) *dupCheck {
return &dupCheck{
headCounter: headCounter,
tailCounter: headCounter + 1,
tail: 1,
}
}
func (dc *dupCheck) IsDup(counter uint64) bool {
// Before head => it's late, say it's a dup.
if counter < dc.headCounter {
return true
}
// It's within the counter bounds.
if counter < dc.tailCounter {
index := (int(counter-dc.headCounter) + dc.head) % bitSetSize
if dc.Get(index) {
return true
}
dc.Set(index)
return false
}
// It's more than 1 beyond the tail.
delta := counter - dc.tailCounter
// Full clear.
if delta >= bitSetSize-1 {
dc.ClearAll()
dc.Set(0)
dc.tail = 1
dc.head = 2
dc.tailCounter = counter + 1
dc.headCounter = dc.tailCounter - bitSetSize + 1
return false
}
// Clear if necessary.
for range delta {
dc.put(false)
}
dc.put(true)
return false
}
func (dc *dupCheck) put(set bool) {
if set {
dc.Set(dc.tail)
} else {
dc.Clear(dc.tail)
}
dc.tail = (dc.tail + 1) % bitSetSize
dc.tailCounter++
if dc.head == dc.tail {
dc.head = (dc.head + 1) % bitSetSize
dc.headCounter++
}
}

View File

@@ -1,57 +0,0 @@
package peer
import (
"testing"
)
func TestDupCheck(t *testing.T) {
dc := newDupCheck(0)
for i := range bitSetSize {
if dc.IsDup(uint64(i)) {
t.Fatal("!")
}
}
type TestCase struct {
Counter uint64
Dup bool
}
testCases := []TestCase{
{511, true},
{0, true},
{1, true},
{2, true},
{3, true},
{63, true},
{256, true},
{510, true},
{511, true},
{512, false},
{0, true},
{512, true},
{513, false},
{517, false},
{512, true},
{513, true},
{514, false},
{515, false},
{516, false},
{517, true},
{2512, false},
{2512, true},
{2001, true},
{2002, false},
{2002, true},
{4000, false},
{4000 - 511, true}, // Too old.
{4000 - 510, false}, // Just in the window.
}
for i, tc := range testCases {
if ok := dc.IsDup(tc.Counter); ok != tc.Dup {
t.Fatal(i, ok, tc)
}
}
}

View File

@@ -1,8 +0,0 @@
package peer
import "errors"
var (
errMalformedPacket = errors.New("malformed packet")
errUnknownPacketType = errors.New("unknown packet type")
)

View File

@@ -11,14 +11,7 @@ import (
type LocalConfig struct {
LocalPeerIP byte
Network []byte
PubKey []byte
PrivKey []byte
PubSignKey []byte
PrivSignKey []byte
}
type startupCount struct {
Count uint16
WGPrivKey string
}
func configDir(netName string) string {
@@ -41,10 +34,6 @@ func peerStatePath(netName string) string {
return filepath.Join(configDir(netName), "state.json")
}
func startupCountPath(netName string) string {
return filepath.Join(configDir(netName), "startup_count.json")
}
func statusSocketPath(netName string) string {
return filepath.Join(configDir(netName), "status.sock")
}
@@ -106,10 +95,3 @@ func loadNetworkState(netName string) (ps m.NetworkState, err error) {
return ps, loadJson(peerStatePath(netName), &ps)
}
func loadStartupCount(netName string) (c startupCount, err error) {
return c, loadJson(startupCountPath(netName), &c)
}
func storeStartupCount(netName string, c startupCount) error {
return storeJson(c, startupCountPath(netName))
}

View File

@@ -1,28 +1,15 @@
package peer
import (
"io"
"net"
"net/netip"
"sync"
"sync/atomic"
"time"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const (
version = 1
bufferSize = 8192 // Enough for data packets and encryption buffers.
if_mtu = 1200
if_queue_len = 2048
controlCipherOverhead = 16
dataCipherOverhead = 16
signingOverhead = 64
pingInterval = 8 * time.Second
timeoutInterval = 30 * time.Second
broadcastInterval = 16 * time.Second
broadcastErrorTimeoutInterval = 8 * time.Second
)
@@ -31,79 +18,22 @@ var multicastAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(
netip.AddrFrom4([4]byte{224, 0, 0, 157}),
4560))
// ----------------------------------------------------------------------------
type Globals struct {
LocalConfig // Embed, immutable.
// The number of startups
StartupCount uint16
// WireGuard private key, client, and device name. Immutable after init.
WGPrivKey wgtypes.Key
WGClient *wgctrl.Client
WGDevName string
// Local public address (if available). Immutable.
LocalAddr netip.AddrPort
// True if local public address is valid. Immutable.
LocalAddrValid bool
// All remote peers by VPN IP.
RemotePeers [256]*atomic.Pointer[Remote]
// Discovered public addresses.
PubAddrs *pubAddrStore
// Attempts to ensure that we have a relay available.
RelayHandler *relayHandler
// Send UDP - Global function to write UDP packets.
SendUDP func(b []byte, addr netip.AddrPort) (n int, err error)
// Global TUN interface.
IFace io.ReadWriteCloser
// For trace ID.
NewTraceID func() uint64
}
func NewGlobals(
localConfig LocalConfig,
startupCount startupCount,
localAddr netip.AddrPort,
conn *net.UDPConn,
iface io.ReadWriteCloser,
) (g Globals) {
func NewGlobals(localConfig LocalConfig, localAddr netip.AddrPort) (g Globals) {
g.LocalConfig = localConfig
g.StartupCount = startupCount.Count
g.LocalAddr = localAddr
g.LocalAddrValid = localAddr.IsValid()
g.PubAddrs = newPubAddrStore(localAddr)
g.RelayHandler = newRelayHandler()
// Use a lock here avoids starvation, at least on my Linux machine.
sendLock := sync.Mutex{}
g.SendUDP = func(b []byte, addr netip.AddrPort) (int, error) {
sendLock.Lock()
n, err := conn.WriteToUDPAddrPort(b, addr)
sendLock.Unlock()
return n, err
}
g.IFace = iface
traceID := (uint64(g.StartupCount) << 48) + 1
g.NewTraceID = func() uint64 {
return atomic.AddUint64(&traceID, 1)
}
for i := range g.RemotePeers {
g.RemotePeers[i] = &atomic.Pointer[Remote]{}
}
for i := range g.RemotePeers {
g.RemotePeers[i].Store(newRemote(g, byte(i)))
}
return g
}

View File

@@ -1,47 +0,0 @@
package peer
import "unsafe"
// ----------------------------------------------------------------------------
const (
headerSize = 12
controlHeaderSize = 24
dataHeaderSize = 12
dataStreamID = 1
controlStreamID = 2
)
type Header struct {
Version byte
StreamID byte
SourceIP byte
DestIP byte
Counter uint64 // Init with time.Now().Unix << 30 to ensure monotonic.
}
func parseHeader(b []byte) (h Header) {
h.Version = b[0]
h.StreamID = b[1]
h.SourceIP = b[2]
h.DestIP = b[3]
h.Counter = *(*uint64)(unsafe.Pointer(&b[4]))
return h
}
func (h *Header) Parse(b []byte) {
h.Version = b[0]
h.StreamID = b[1]
h.SourceIP = b[2]
h.DestIP = b[3]
h.Counter = *(*uint64)(unsafe.Pointer(&b[4]))
}
func (h *Header) Marshal(buf []byte) {
buf[0] = h.Version
buf[1] = h.StreamID
buf[2] = h.SourceIP
buf[3] = h.DestIP
*(*uint64)(unsafe.Pointer(&buf[4])) = h.Counter
}

View File

@@ -1,21 +0,0 @@
package peer
import "testing"
func TestHeaderMarshalParse(t *testing.T) {
nIn := Header{
StreamID: 23,
Counter: 3212,
SourceIP: 34,
DestIP: 200,
}
buf := make([]byte, headerSize)
nIn.Marshal(buf)
nOut := Header{}
nOut.Parse(buf)
if nIn != nOut {
t.Fatal(nIn, nOut)
}
}

View File

@@ -5,13 +5,17 @@ import (
"io"
"log"
"net/http"
"net/netip"
"net/url"
"time"
"vppn/m"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type HubPoller struct {
Globals
holePunch *HolePunch
client *http.Client
req *http.Request
versions [256]int64
@@ -20,6 +24,7 @@ type HubPoller struct {
func NewHubPoller(
g Globals,
hp *HolePunch,
netName,
hubURL,
apiKey string,
@@ -41,6 +46,7 @@ func NewHubPoller(
return &HubPoller{
Globals: g,
holePunch: hp,
client: client,
req: req,
netName: netName,
@@ -98,14 +104,45 @@ func (hp *HubPoller) applyNetworkState(state m.NetworkState) {
if i == int(hp.LocalPeerIP) {
continue
}
if peer != nil && peer.Version == hp.versions[i] {
continue
}
hp.RemotePeers[i].Load().HandlePeerUpdate(peerUpdateMsg{Peer: state.Peers[i]})
hp.applyPeerConfig(peer)
if peer != nil {
hp.versions[i] = peer.Version
}
}
}
func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
return
}
if len(peer.PublicIP1) == 0 || peer.Port1 == 0 {
return
}
pubKey, err := wgtypes.NewKey(peer.WGPubKey)
if err != nil {
hp.logf("Invalid WG key for peer %d: %v", peer.PeerIP, err)
return
}
ip, ok := netip.AddrFromSlice(peer.PublicIP1)
if !ok {
hp.logf("Invalid public IP for peer %d", peer.PeerIP)
return
}
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port1)
if peer.Relay {
if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil {
hp.logf("Failed to update relay config: %v", err)
}
return
}
if hp.holePunch != nil {
hp.holePunch.OnEndpointLearned(peer.PeerIP, pubKey, endpoint, true)
}
}

View File

@@ -1,73 +0,0 @@
package peer
import (
"log"
)
type IFReader struct {
Globals
}
func NewIFReader(g Globals) *IFReader {
return &IFReader{Globals: g}
}
func (r *IFReader) Run() {
packet := make([]byte, bufferSize)
for {
r.handleNextPacket(packet)
}
}
func (r *IFReader) handleNextPacket(packet []byte) {
packet = r.readNextPacket(packet)
remoteIP, ok := r.parsePacket(packet)
if !ok {
return
}
r.RemotePeers[remoteIP].Load().SendDataTo(packet)
}
func (r *IFReader) readNextPacket(buf []byte) []byte {
n, err := r.IFace.Read(buf[:cap(buf)])
if err != nil {
log.Fatalf("Failed to read from interface: %v", err)
}
return buf[:n]
}
// parsePacket returns the VPN ip for the packet, and a boolean indicating
// success.
func (r *IFReader) parsePacket(buf []byte) (byte, bool) {
n := len(buf)
if n == 0 {
return 0, false
}
version := buf[0] >> 4
switch version {
case 4:
if n < 20 {
r.logf("Short IPv4 packet: %d", len(buf))
return 0, false
}
return buf[19], true
case 6:
if len(buf) < 40 {
r.logf("Short IPv6 packet: %d", len(buf))
return 0, false
}
return buf[39], true
default:
r.logf("Invalid IP packet version: %v", version)
return 0, false
}
}
func (*IFReader) logf(s string, args ...any) {
log.Printf("[IFReader] "+s, args...)
}

View File

@@ -1,81 +0,0 @@
package peer
/*
func TestIFReader_IPv4(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
pkt := make([]byte, 1234)
pkt[0] = 4 << 4
pkt[19] = 2 // IP.
p1.IFace.UserWrite(pkt)
p1.IFReader.handleNextPacket(newBuf())
packets := p2.Conn.Packets()
if len(packets) != 1 {
t.Fatal(packets)
}
}
func TestIFReader_IPv6(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
pkt := make([]byte, 1234)
pkt[0] = 6 << 4
pkt[39] = 2 // IP.
p1.IFace.UserWrite(pkt)
p1.IFReader.handleNextPacket(newBuf())
packets := p2.Conn.Packets()
if len(packets) != 1 {
t.Fatal(packets)
}
}
func TestIFReader_parsePacket_emptyPacket(t *testing.T) {
r := NewIFReader(nil, nil)
pkt := make([]byte, 0)
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(ip, ok)
}
}
func TestIFReader_parsePacket_invalidIPVersion(t *testing.T) {
r := NewIFReader(nil, nil)
for i := byte(1); i < 16; i++ {
if i == 4 || i == 6 {
continue
}
pkt := make([]byte, 1234)
pkt[0] = i << 4
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(i, ip, ok)
}
}
}
func TestIFReader_parsePacket_shortIPv4(t *testing.T) {
r := NewIFReader(nil, nil)
pkt := make([]byte, 19)
pkt[0] = 4 << 4
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(ip, ok)
}
}
func TestIFReader_parsePacket_shortIPv6(t *testing.T) {
r := NewIFReader(nil, nil)
pkt := make([]byte, 39)
pkt[0] = 6 << 4
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(ip, ok)
}
}
*/

View File

@@ -1,137 +0,0 @@
package peer
import (
"fmt"
"io"
"net"
"os"
"syscall"
"golang.org/x/sys/unix"
)
func openInterface(network []byte, localIP byte, name string) (io.ReadWriteCloser, error) {
if len(network) != 4 {
return nil, fmt.Errorf("expected network to be 4 bytes, got %d", len(network))
}
ip := net.IPv4(network[0], network[1], network[2], localIP)
//////////////////////////
// Create TUN Interface //
//////////////////////////
tunFD, err := syscall.Open("/dev/net/tun", syscall.O_RDWR|unix.O_CLOEXEC, 0600)
if err != nil {
return nil, fmt.Errorf("failed to open TUN device: %w", err)
}
// New interface request.
req, err := unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create new TUN interface request: %w", err)
}
// Flags:
//
// IFF_NO_PI => don't add packet info data to packets sent to the interface.
// IFF_TUN => create a TUN device handling IP packets.
req.SetUint16(unix.IFF_NO_PI | unix.IFF_TUN)
err = unix.IoctlIfreq(tunFD, unix.TUNSETIFF, req)
if err != nil {
return nil, fmt.Errorf("failed to set TUN device settings: %w", err)
}
// Name may not be exactly the same?
name = req.Name()
/////////////
// Set MTU //
/////////////
// We need a socket file descriptor to set other options for some reason.
sockFD, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
if err != nil {
return nil, fmt.Errorf("failed to open socket: %w", err)
}
defer unix.Close(sockFD)
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create MTU interface request: %w", err)
}
req.SetUint32(if_mtu)
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFMTU, req); err != nil {
return nil, fmt.Errorf("failed to set interface MTU: %w", err)
}
//////////////////////
// Set Queue Length //
//////////////////////
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create IP interface request: %w", err)
}
req.SetUint16(if_queue_len)
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFTXQLEN, req); err != nil {
return nil, fmt.Errorf("failed to set interface queue length: %w", err)
}
/////////////////////
// Set IP and Mask //
/////////////////////
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create IP interface request: %w", err)
}
if err := req.SetInet4Addr(ip.To4()); err != nil {
return nil, fmt.Errorf("failed to set interface request IP: %w", err)
}
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFADDR, req); err != nil {
return nil, fmt.Errorf("failed to set interface IP: %w", err)
}
// SET MASK - must happen after setting address.
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create mask interface request: %w", err)
}
if err := req.SetInet4Addr(net.IPv4(255, 255, 255, 0).To4()); err != nil {
return nil, fmt.Errorf("failed to set interface request mask: %w", err)
}
if err := unix.IoctlIfreq(sockFD, unix.SIOCSIFNETMASK, req); err != nil {
return nil, fmt.Errorf("failed to set interface mask: %w", err)
}
////////////////////////
// Bring Interface Up //
////////////////////////
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create up interface request: %w", err)
}
// Get current flags.
if err = unix.IoctlIfreq(sockFD, unix.SIOCGIFFLAGS, req); err != nil {
return nil, fmt.Errorf("failed to get interface flags: %w", err)
}
flags := req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING
// Set UP flag / broadcast flags.
req.SetUint16(flags)
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFFLAGS, req); err != nil {
return nil, fmt.Errorf("failed to set interface up: %w", err)
}
return os.NewFile(uintptr(tunFD), "tun"), nil
}

View File

@@ -1,5 +0,0 @@
package peer
func newBuf() []byte {
return make([]byte, bufferSize)
}

View File

@@ -1,47 +1,66 @@
package peer
import (
"encoding/binary"
"fmt"
"log"
"net"
"net/netip"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func RunMCReader(g Globals) {
func RunMCReader(g Globals, hp *HolePunch, netName string) {
for {
runMCReaderInner(g)
if err := runMCReaderInner(g, hp, netName); err != nil {
log.Printf("[MCReader] %v", err)
}
time.Sleep(broadcastErrorTimeoutInterval)
}
}
func runMCReaderInner(g Globals) {
var (
buf = make([]byte, bufferSize)
logf = func(s string, args ...any) {
log.Printf("[MCReader] "+s, args...)
}
)
func runMCReaderInner(g Globals, hp *HolePunch, netName string) error {
conn, err := net.ListenMulticastUDP("udp", nil, multicastAddr)
if err != nil {
logf("Failed to bind to multicast address: %v", err)
return
return fmt.Errorf("bind: %w", err)
}
defer conn.Close()
buf := make([]byte, 64)
for {
conn.SetReadDeadline(time.Now().Add(32 * time.Second))
n, remoteAddr, err := conn.ReadFromUDPAddrPort(buf[:bufferSize])
n, src, err := conn.ReadFromUDPAddrPort(buf)
if err != nil {
logf("Failed to read from UDP port): %v", err)
return fmt.Errorf("read: %w", err)
}
if n != beaconLen {
continue
}
handleBeacon(g, hp, netName, buf[:n], src)
}
}
func handleBeacon(g Globals, hp *HolePunch, netName string, beacon []byte, src netip.AddrPort) {
peerIPByte := beacon[0]
if peerIPByte == g.LocalPeerIP {
return
}
buf = buf[:n]
h, ok := headerFromLocalDiscoveryPacket(buf)
if !ok {
logf("Failed to open discovery packet?")
continue
pubKey, err := wgtypes.NewKey(beacon[1:33])
if err != nil {
return
}
g.RemotePeers[h.SourceIP].Load().HandleLocalDiscoveryPacket(h, remoteAddr, buf)
// Skip relay peers: probing would replace their /24 AllowedIPs with empty.
if state, err := loadNetworkState(netName); err == nil {
if p := state.Peers[peerIPByte]; p != nil && p.Relay {
return
}
}
wgPort := binary.BigEndian.Uint16(beacon[33:35])
endpoint := netip.AddrPortFrom(src.Addr().Unmap(), wgPort)
hp.OnEndpointLearned(peerIPByte, pubKey, endpoint, false)
}

View File

@@ -1,132 +0,0 @@
package peer
/*
type mcMockConn struct {
packets chan []byte
}
func newMCMockConn() *mcMockConn {
return &mcMockConn{make(chan []byte, 32)}
}
func (c *mcMockConn) WriteToUDP(in []byte, addr *net.UDPAddr) (int, error) {
c.packets <- bytes.Clone(in)
return len(in), nil
}
func (c *mcMockConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
buf := <-c.packets
b = b[:len(buf)]
copy(b, buf)
return len(b), netip.AddrPort{}, nil
}
func TestMCReader(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
w := newMCWriter(conn, 1, keys.PrivSignKey)
r := newMCReader(conn, super, peers)
w.SendLocalDiscovery()
r.handleNextPacket()
if len(super.Messages) != 1 {
t.Fatal(super.Messages)
}
msg, ok := super.Messages[0].(controlMsg[PacketLocalDiscovery])
if !ok || msg.SrcIP != 1 {
t.Fatal(ok, msg)
}
}
func TestMCReader_noHeader(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
r := newMCReader(conn, super, peers)
conn.WriteToUDP([]byte("0123546789"), nil)
r.handleNextPacket()
if len(super.Messages) != 0 {
t.Fatal(super.Messages)
}
}
func TestMCReader_noPeer(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[2] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
w := newMCWriter(conn, 2, keys.PrivSignKey)
r := newMCReader(conn, super, peers)
w.SendLocalDiscovery()
r.handleNextPacket()
if len(super.Messages) != 0 {
t.Fatal(super.Messages)
}
}
func TestMCReader_badSignature(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
w := newMCWriter(conn, 1, keys.PrivSignKey)
w.SendLocalDiscovery()
// Break signing.
packet := <-conn.packets
packet[0]++
conn.packets <- packet
r := newMCReader(conn, super, peers)
r.handleNextPacket()
if len(super.Messages) != 0 {
t.Fatal(super.Messages)
}
}
*/

View File

@@ -1,54 +1,43 @@
package peer
import (
"encoding/binary"
"fmt"
"log"
"net"
"time"
"golang.org/x/crypto/nacl/sign"
)
func createLocalDiscoveryPacket(localIP byte, signingKey []byte) []byte {
h := Header{
SourceIP: localIP,
DestIP: 255,
}
buf := make([]byte, headerSize)
h.Marshal(buf)
out := make([]byte, headerSize+signingOverhead)
return sign.Sign(out[:0], buf, (*[64]byte)(signingKey))
}
func headerFromLocalDiscoveryPacket(pkt []byte) (h Header, ok bool) {
if len(pkt) != headerSize+signingOverhead {
return
}
h.Parse(pkt[signingOverhead:])
ok = true
return
}
func verifyLocalDiscoveryPacket(pkt, buf []byte, pubSignKey []byte) bool {
_, ok := sign.Open(buf[:0], pkt, (*[32]byte)(pubSignKey))
return ok
}
// ----------------------------------------------------------------------------
func RunMCWriter(localIP byte, signingKey []byte) {
discoveryPacket := createLocalDiscoveryPacket(localIP, signingKey)
const beaconLen = 35 // 1 VPN IP byte + 32 WG pubkey + 2 WG listen port
func RunMCWriter(g Globals) {
conn, err := net.ListenMulticastUDP("udp", nil, multicastAddr)
if err != nil {
log.Fatalf("[MCWriter] Failed to bind to multicast address: %v", err)
log.Fatalf("[MCWriter] bind: %v", err)
}
for range time.Tick(broadcastInterval) {
log.Printf("[MCWriter] Broadcasting on %v...", multicastAddr)
_, err := conn.WriteToUDP(discoveryPacket, multicastAddr)
beacon, err := buildBeacon(g)
if err != nil {
log.Printf("[MCWriter] Failed to write multicast: %v", err)
log.Printf("[MCWriter] build beacon: %v", err)
continue
}
log.Printf("[MCWriter] Broadcasting on %v...", multicastAddr)
if _, err := conn.WriteToUDP(beacon, multicastAddr); err != nil {
log.Printf("[MCWriter] write: %v", err)
}
}
}
func buildBeacon(g Globals) ([]byte, error) {
dev, err := g.WGClient.Device(g.WGDevName)
if err != nil {
return nil, fmt.Errorf("get WG device: %w", err)
}
beacon := make([]byte, beaconLen)
beacon[0] = g.LocalPeerIP
pubKey := g.WGPrivKey.PublicKey()
copy(beacon[1:33], pubKey[:])
binary.BigEndian.PutUint16(beacon[33:35], uint16(dev.ListenPort))
return beacon, nil
}

View File

@@ -1,98 +0,0 @@
package peer
/*
// ----------------------------------------------------------------------------
// Testing that we can create and verify a local discovery packet.
func TestVerifyLocalDiscoveryPacket_valid(t *testing.T) {
keys := generateKeys()
created := createLocalDiscoveryPacket(55, keys.PrivSignKey)
header, ok := headerFromLocalDiscoveryPacket(created)
if !ok {
t.Fatal(ok)
}
if header.SourceIP != 55 || header.DestIP != 255 {
t.Fatal(header)
}
if !verifyLocalDiscoveryPacket(created, make([]byte, 1024), keys.PubSignKey) {
t.Fatal("Not valid")
}
}
// Testing that we don't try to parse short packets.
func TestVerifyLocalDiscoveryPacket_tooShort(t *testing.T) {
keys := generateKeys()
created := createLocalDiscoveryPacket(55, keys.PrivSignKey)
_, ok := headerFromLocalDiscoveryPacket(created[:len(created)-1])
if ok {
t.Fatal(ok)
}
}
// Testing that modifying a packet makes it invalid.
func TestVerifyLocalDiscoveryPacket_invalid(t *testing.T) {
keys := generateKeys()
created := createLocalDiscoveryPacket(55, keys.PrivSignKey)
buf := make([]byte, 1024)
for i := range created {
modified := bytes.Clone(created)
modified[i]++
if verifyLocalDiscoveryPacket(modified, buf, keys.PubSignKey) {
t.Fatal("Verification should have failed.")
}
}
}
// ----------------------------------------------------------------------------
type testUDPWriter struct {
written [][]byte
}
func (w *testUDPWriter) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) {
w.written = append(w.written, bytes.Clone(b))
return len(b), nil
}
func (w *testUDPWriter) Written() [][]byte {
out := w.written
w.written = [][]byte{}
return out
}
// ----------------------------------------------------------------------------
// Testing that the mcWriter sends local discovery packets as expected.
func TestMCWriter_SendLocalDiscovery(t *testing.T) {
keys := generateKeys()
writer := &testUDPWriter{}
mcw := newMCWriter(writer, 42, keys.PrivSignKey)
mcw.SendLocalDiscovery()
out := writer.Written()
if len(out) != 1 {
t.Fatal(out)
}
pkt := out[0]
header, ok := headerFromLocalDiscoveryPacket(pkt)
if !ok {
t.Fatal(ok)
}
if header.SourceIP != 42 || header.DestIP != 255 {
t.Fatal(header)
}
if !verifyLocalDiscoveryPacket(pkt, make([]byte, 1024), keys.PubSignKey) {
t.Fatal("Verification should succeed.")
}
}
*/

View File

@@ -1,31 +0,0 @@
package peer
import "bytes"
type TestIFace struct {
out *bytes.Buffer // Toward the network.
in *bytes.Buffer // From the network
}
func NewTestIFace() *TestIFace {
return &TestIFace{
out: &bytes.Buffer{},
in: &bytes.Buffer{},
}
}
func (iface *TestIFace) Write(b []byte) (int, error) {
return iface.in.Write(b)
}
func (iface *TestIFace) Read(b []byte) (int, error) {
return iface.out.Read(b)
}
func (iface *TestIFace) UserWrite(b []byte) (int, error) {
return iface.out.Write(b)
}
func (iface *TestIFace) UserRead(b []byte) (int, error) {
return iface.in.Read(b)
}

View File

@@ -1,80 +0,0 @@
package peer
import (
"bytes"
"net"
"net/netip"
"sync"
)
type TestPacket struct {
Addr netip.AddrPort
Data []byte
}
type TestNetwork struct {
lock sync.Mutex
packets map[netip.AddrPort]chan TestPacket
}
func NewTestNetwork() *TestNetwork {
return &TestNetwork{packets: map[netip.AddrPort]chan TestPacket{}}
}
func (n *TestNetwork) NewUDPConn(localAddr netip.AddrPort) *TestUDPConn {
n.lock.Lock()
defer n.lock.Unlock()
if _, ok := n.packets[localAddr]; !ok {
n.packets[localAddr] = make(chan TestPacket, 1024)
}
return &TestUDPConn{
addr: localAddr,
n: n,
packets: n.packets[localAddr],
}
}
func (n *TestNetwork) write(b []byte, from, to netip.AddrPort) {
n.lock.Lock()
defer n.lock.Unlock()
if _, ok := n.packets[to]; !ok {
n.packets[to] = make(chan TestPacket, 1024)
}
n.packets[to] <- TestPacket{
Addr: from,
Data: bytes.Clone(b),
}
}
type TestUDPConn struct {
addr netip.AddrPort
n *TestNetwork
packets chan TestPacket
}
func (c *TestUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
c.n.write(b, c.addr, addr)
return len(b), nil
}
func (c *TestUDPConn) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) {
return c.WriteToUDPAddrPort(b, addr.AddrPort())
}
func (c *TestUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
pkt := <-c.packets
b = b[:len(pkt.Data)]
copy(b, pkt.Data)
return len(b), pkt.Addr, nil
}
func (c *TestUDPConn) Packets() (out []TestPacket) {
for {
select {
case pkt := <-c.packets:
out = append(out, pkt)
default:
return
}
}
}

View File

@@ -1,182 +0,0 @@
package peer
import (
"net/netip"
"unsafe"
)
// ----------------------------------------------------------------------------
type binWriter struct {
b []byte
i int
}
func newBinWriter(buf []byte) *binWriter {
buf = buf[:cap(buf)]
return &binWriter{buf, 0}
}
func (w *binWriter) Bool(b bool) *binWriter {
if b {
return w.Byte(1)
}
return w.Byte(0)
}
func (w *binWriter) Byte(b byte) *binWriter {
w.b[w.i] = b
w.i++
return w
}
func (w *binWriter) SharedKey(key [32]byte) *binWriter {
copy(w.b[w.i:w.i+32], key[:])
w.i += 32
return w
}
func (w *binWriter) Uint16(x uint16) *binWriter {
*(*uint16)(unsafe.Pointer(&w.b[w.i])) = x
w.i += 2
return w
}
func (w *binWriter) Uint64(x uint64) *binWriter {
*(*uint64)(unsafe.Pointer(&w.b[w.i])) = x
w.i += 8
return w
}
func (w *binWriter) Int64(x int64) *binWriter {
*(*int64)(unsafe.Pointer(&w.b[w.i])) = x
w.i += 8
return w
}
func (w *binWriter) AddrPort(addrPort netip.AddrPort) *binWriter {
w.Bool(addrPort.IsValid())
addr := addrPort.Addr().As16()
copy(w.b[w.i:w.i+16], addr[:])
w.i += 16
return w.Uint16(addrPort.Port())
}
func (w *binWriter) AddrPort8(l [8]netip.AddrPort) *binWriter {
for _, addrPort := range l {
w.AddrPort(addrPort)
}
return w
}
func (w *binWriter) Build() []byte {
return w.b[:w.i]
}
// ----------------------------------------------------------------------------
type binReader struct {
b []byte
i int
err error
}
func newBinReader(buf []byte) *binReader {
return &binReader{b: buf}
}
func (r *binReader) hasBytes(n int) bool {
if r.err != nil || (len(r.b)-r.i) < n {
r.err = errMalformedPacket
return false
}
return true
}
func (r *binReader) Bool(b *bool) *binReader {
var bb byte
r.Byte(&bb)
*b = bb != 0
return r
}
func (r *binReader) Byte(b *byte) *binReader {
if !r.hasBytes(1) {
return r
}
*b = r.b[r.i]
r.i++
return r
}
func (r *binReader) SharedKey(x *[32]byte) *binReader {
if !r.hasBytes(32) {
return r
}
*x = ([32]byte)(r.b[r.i : r.i+32])
r.i += 32
return r
}
func (r *binReader) Uint16(x *uint16) *binReader {
if !r.hasBytes(2) {
return r
}
*x = *(*uint16)(unsafe.Pointer(&r.b[r.i]))
r.i += 2
return r
}
func (r *binReader) Uint64(x *uint64) *binReader {
if !r.hasBytes(8) {
return r
}
*x = *(*uint64)(unsafe.Pointer(&r.b[r.i]))
r.i += 8
return r
}
func (r *binReader) Int64(x *int64) *binReader {
if !r.hasBytes(8) {
return r
}
*x = *(*int64)(unsafe.Pointer(&r.b[r.i]))
r.i += 8
return r
}
func (r *binReader) AddrPort(x *netip.AddrPort) *binReader {
if !r.hasBytes(19) {
return r
}
var (
valid bool
port uint16
)
r.Bool(&valid)
addr := netip.AddrFrom16(([16]byte)(r.b[r.i : r.i+16])).Unmap()
r.i += 16
r.Uint16(&port)
if valid {
*x = netip.AddrPortFrom(addr, port)
} else {
*x = netip.AddrPort{}
}
return r
}
func (r *binReader) AddrPort8(x *[8]netip.AddrPort) *binReader {
for i := range x {
r.AddrPort(&x[i])
}
return r
}
func (r *binReader) Error() error {
return r.err
}

View File

@@ -1,76 +0,0 @@
package peer
import (
"net/netip"
"reflect"
"testing"
)
func TestBinWriteRead_invalidAddrPort(t *testing.T) {
addr := netip.AddrPort{}
buf := make([]byte, 1024)
buf = newBinWriter(buf).
AddrPort(addr).
Build()
var addr2 netip.AddrPort
err := newBinReader(buf).
AddrPort(&addr2).
Error()
if err != nil {
t.Fatal(err)
}
if addr2.IsValid() {
t.Fatal(addr, addr2)
}
}
func TestBinWriteRead(t *testing.T) {
buf := make([]byte, 1024)
type Item struct {
Type byte
TraceID uint64
Addrs [8]netip.AddrPort
DestAddr netip.AddrPort
}
in := Item{
1,
2,
[8]netip.AddrPort{},
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 22),
}
in.Addrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{0, 1, 2, 3}), 20)
in.Addrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 5}), 22)
in.Addrs[3] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 3}), 23)
in.Addrs[4] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 4}), 24)
in.Addrs[5] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 5}), 25)
in.Addrs[6] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 6}), 26)
in.Addrs[7] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{7, 8, 9, 7}), 27)
buf = newBinWriter(buf).
Byte(in.Type).
Uint64(in.TraceID).
AddrPort(in.DestAddr).
AddrPort8(in.Addrs).
Build()
out := Item{}
err := newBinReader(buf).
Byte(&out.Type).
Uint64(&out.TraceID).
AddrPort(&out.DestAddr).
AddrPort8(&out.Addrs).
Error()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(in, out) {
t.Fatal(in, out)
}
}

View File

@@ -1,120 +0,0 @@
package peer
import (
"net/netip"
)
const (
packetTypeSyn = 1
packetTypeInit = 2
packetTypeAck = 3
packetTypeProbe = 4
packetTypeAddrDiscovery = 5
)
// ----------------------------------------------------------------------------
type packetInit struct {
TraceID uint64
Direct bool
Version uint64
}
func (p packetInit) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeInit).
Uint64(p.TraceID).
Bool(p.Direct).
Uint64(p.Version).
Build()
}
func parsePacketInit(buf []byte) (p packetInit, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
Bool(&p.Direct).
Uint64(&p.Version).
Error()
return
}
// ----------------------------------------------------------------------------
type packetSyn struct {
TraceID uint64 // TraceID to match response w/ request.
SharedKey [32]byte // Our shared key.
Direct bool
PossibleAddrs [8]netip.AddrPort // Possible public addresses of the sender.
}
func (p packetSyn) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeSyn).
Uint64(p.TraceID).
SharedKey(p.SharedKey).
Bool(p.Direct).
AddrPort8(p.PossibleAddrs).
Build()
}
func parsePacketSyn(buf []byte) (p packetSyn, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
SharedKey(&p.SharedKey).
Bool(&p.Direct).
AddrPort8(&p.PossibleAddrs).
Error()
return
}
// ----------------------------------------------------------------------------
type packetAck struct {
TraceID uint64
ToAddr netip.AddrPort
PossibleAddrs [8]netip.AddrPort // Possible public addresses of the sender.
}
func (p packetAck) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeAck).
Uint64(p.TraceID).
AddrPort(p.ToAddr).
AddrPort8(p.PossibleAddrs).
Build()
}
func parsePacketAck(buf []byte) (p packetAck, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
AddrPort(&p.ToAddr).
AddrPort8(&p.PossibleAddrs).
Error()
return
}
// ----------------------------------------------------------------------------
// A probeReqPacket is sent from a client to a server to determine if direct
// UDP communication can be used.
type packetProbe struct {
TraceID uint64
}
func (p packetProbe) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeProbe).
Uint64(p.TraceID).
Build()
}
func parsePacketProbe(buf []byte) (p packetProbe, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
Error()
return
}
// ----------------------------------------------------------------------------
type packetLocalDiscovery struct{}

View File

@@ -1,64 +0,0 @@
package peer
import (
"crypto/rand"
"net/netip"
"reflect"
"testing"
)
func TestSynPacket(t *testing.T) {
p := packetSyn{
TraceID: 2342342345,
Direct: true,
}
rand.Read(p.SharedKey[:])
p.PossibleAddrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 234)
p.PossibleAddrs[1] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 2, 3, 4}), 12399)
p.PossibleAddrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{3, 2, 3, 4}), 60000)
buf := p.Marshal(newBuf())
p2, err := parsePacketSyn(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, p2) {
t.Fatal(p2)
}
}
func TestAckPacket(t *testing.T) {
p := packetAck{
TraceID: 123213,
ToAddr: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 234),
}
p.PossibleAddrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{8, 2, 3, 4}), 100)
p.PossibleAddrs[1] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 2, 3, 4}), 12399)
p.PossibleAddrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{3, 2, 3, 4}), 60000)
buf := p.Marshal(newBuf())
p2, err := parsePacketAck(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, p2) {
t.Fatal(p2)
}
}
func TestProbePacket(t *testing.T) {
p := packetProbe{
TraceID: 12345,
}
buf := p.Marshal(newBuf())
p2, err := parsePacketProbe(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, p2) {
t.Fatal(p2)
}
}

View File

@@ -3,11 +3,8 @@ package peer
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net"
"net/http"
"net/netip"
"net/url"
@@ -15,12 +12,15 @@ import (
"vppn/m"
"git.crumpington.com/lib/go/flock"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type peerMain struct {
Globals
ifReader *IFReader
connReader *ConnReader
netName string
holePunch *HolePunch
controlServer *ControlServer
endpointReporter *EndpointReporter // non-nil on relay peers only
hubPoller *HubPoller
lockFile *os.File
}
@@ -59,89 +59,104 @@ func newPeerMain(args mainArgs) *peerMain {
log.Fatalf("Failed to load network state: %v", err)
}
startupCount, err := loadStartupCount(args.NetName)
wgPrivKey, err := wgtypes.ParseKey(config.WGPrivKey)
if err != nil {
if !os.IsNotExist(err) {
log.Fatalf("Failed to load startup count: %v", err)
}
}
if startupCount.Count == math.MaxUint16 {
log.Fatalf("Startup counter overflow.")
}
startupCount.Count += 1
if err := storeStartupCount(args.NetName, startupCount); err != nil {
log.Fatalf("Failed to write startup count: %v", err)
}
iface, err := openInterface(config.Network, config.LocalPeerIP, args.NetName)
if err != nil {
log.Fatalf("Failed to open interface: %v", err)
log.Fatalf("Failed to parse WireGuard private key: %v", err)
}
localPeer := state.Peers[config.LocalPeerIP]
var listenPort int
if localPeer != nil {
listenPort = int(localPeer.Port1)
}
myAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", localPeer.Port))
vpnIP := netip.AddrFrom4([4]byte{
config.Network[0],
config.Network[1],
config.Network[2],
config.LocalPeerIP,
})
wgClient, err := createWGDevice(args.NetName, wgPrivKey, listenPort, vpnIP, config.Network)
if err != nil {
log.Fatalf("Failed to resolve UDP address: %v", err)
log.Fatalf("Failed to create WireGuard device: %v", err)
}
logf("Listening on %v...", myAddr)
conn, err := net.ListenUDP("udp", myAddr)
for _, p := range state.Peers {
if p == nil || !p.Relay || p.PeerIP == config.LocalPeerIP {
continue
}
if len(p.WGPubKey) != wgtypes.KeyLen || len(p.PublicIP1) == 0 || p.Port1 == 0 {
continue
}
relayPubKey, err := wgtypes.NewKey(p.WGPubKey)
if err != nil {
log.Fatalf("Failed to open UDP port: %v", err)
logf("Invalid relay WG key: %v", err)
continue
}
relayIP, ok := netip.AddrFromSlice(p.PublicIP1)
if !ok {
continue
}
relayEndpoint := netip.AddrPortFrom(relayIP.Unmap(), p.Port1)
if err := applyBaseConfig(wgClient, args.NetName, relayPubKey, relayEndpoint, config.Network); err != nil {
logf("Failed to apply relay base config: %v", err)
}
break
}
conn.SetReadBuffer(1024 * 1024 * 8)
conn.SetWriteBuffer(1024 * 1024 * 8)
g := NewGlobals(config, netip.AddrPort{})
g.WGPrivKey = wgPrivKey
g.WGClient = wgClient
g.WGDevName = args.NetName
var localAddr netip.AddrPort
ip, localAddrValid := netip.AddrFromSlice(localPeer.PublicIP)
if localAddrValid {
localAddr = netip.AddrPortFrom(ip, localPeer.Port)
holePunch := NewHolePunch(g)
controlServer, err := NewControlServer(g, holePunch, args.NetName)
if err != nil {
log.Fatalf("Failed to create control server: %v", err)
}
g := NewGlobals(config, startupCount, localAddr, conn, iface)
var endpointReporter *EndpointReporter
if localPeer != nil && localPeer.Relay {
if err := enableForwarding(args.NetName); err != nil {
log.Fatalf("Failed to enable IP forwarding: %v", err)
}
endpointReporter = NewEndpointReporter(g, controlServer, args.NetName)
}
hubPoller, err := NewHubPoller(g, args.NetName, args.HubAddress, args.APIKey)
hubPoller, err := NewHubPoller(g, holePunch, args.NetName, args.HubAddress, args.APIKey)
if err != nil {
log.Fatalf("Failed to create hub poller: %v", err)
}
// Start status server.
go runStatusServer(g, statusSocketPath(args.NetName))
return &peerMain{
Globals: g,
ifReader: NewIFReader(g),
connReader: NewConnReader(g, conn),
netName: args.NetName,
holePunch: holePunch,
controlServer: controlServer,
endpointReporter: endpointReporter,
hubPoller: hubPoller,
lockFile: lockFile,
}
}
func (p *peerMain) Run() {
for i := range p.RemotePeers {
remote := p.RemotePeers[i].Load()
go newRemoteFSM(remote).Run()
go p.controlServer.Run()
if p.endpointReporter != nil {
go p.endpointReporter.Run()
}
go p.ifReader.Run()
go p.connReader.Run()
if !p.LocalAddrValid {
go RunMCWriter(p.LocalPeerIP, p.PrivSignKey)
go RunMCReader(p.Globals)
}
go RunMCWriter(p.Globals)
go RunMCReader(p.Globals, p.holePunch, p.netName)
go p.hubPoller.Run()
select {}
}
func initPeerWithHub(args mainArgs) {
keys := generateKeys()
privKey := generateWGKey()
pubKey := privKey.PublicKey()
initURL, err := url.Parse(args.HubAddress)
if err != nil {
@@ -150,8 +165,7 @@ func initPeerWithHub(args mainArgs) {
initURL.Path = "/peer/init/"
initArgs := m.PeerInitArgs{
EncPubKey: keys.PubKey,
PubSignKey: keys.PubSignKey,
WGPubKey: pubKey[:],
}
buf := &bytes.Buffer{}
@@ -176,18 +190,23 @@ func initPeerWithHub(args mainArgs) {
log.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode == http.StatusConflict {
log.Fatalf("WireGuard key already registered (HTTP 409). Delete and re-create the peer to re-register.")
}
if resp.StatusCode != http.StatusOK {
log.Fatalf("Hub returned unexpected status %d: %s", resp.StatusCode, data)
}
initResp := m.PeerInitResp{}
if err := json.Unmarshal(data, &initResp); err != nil {
log.Fatalf("Failed to parse configuration: %v\n%s", err, data)
}
config := LocalConfig{}
config.LocalPeerIP = initResp.PeerIP
config.Network = initResp.Network
config.PubKey = keys.PubKey
config.PrivKey = keys.PrivKey
config.PubSignKey = keys.PubSignKey
config.PrivSignKey = keys.PrivSignKey
config := LocalConfig{
LocalPeerIP: initResp.PeerIP,
Network: initResp.Network,
WGPrivKey: privKey.String(),
}
if err := storeNetworkState(args.NetName, initResp.NetworkState); err != nil {
log.Fatalf("Failed to store network state: %v", err)

View File

@@ -1,86 +0,0 @@
package peer
import (
"net/netip"
"sort"
"sync"
"time"
)
type pubAddrStore struct {
lock sync.Mutex
localPub bool
localAddr netip.AddrPort
lastSeen map[netip.AddrPort]time.Time
addrList []netip.AddrPort
}
func newPubAddrStore(localAddr netip.AddrPort) *pubAddrStore {
return &pubAddrStore{
localPub: localAddr.IsValid(),
localAddr: localAddr,
lastSeen: map[netip.AddrPort]time.Time{},
addrList: make([]netip.AddrPort, 0, 32),
}
}
func (store *pubAddrStore) Store(addr netip.AddrPort) {
if store.localPub {
return
}
if !addr.IsValid() {
return
}
if addr.Addr().IsPrivate() {
return
}
store.lock.Lock()
defer store.lock.Unlock()
if _, exists := store.lastSeen[addr]; !exists {
store.addrList = append(store.addrList, addr)
}
store.lastSeen[addr] = time.Now()
store.sort()
}
func (store *pubAddrStore) Get() (addrs [8]netip.AddrPort) {
store.lock.Lock()
defer store.lock.Unlock()
store.clean()
if store.localPub {
addrs[0] = store.localAddr
return
}
copy(addrs[:], store.addrList)
return
}
func (store *pubAddrStore) clean() {
if store.localPub {
return
}
for ip, lastSeen := range store.lastSeen {
if time.Since(lastSeen) > timeoutInterval {
delete(store.lastSeen, ip)
}
}
store.addrList = store.addrList[:0]
for ip := range store.lastSeen {
store.addrList = append(store.addrList, ip)
}
store.sort()
}
func (store *pubAddrStore) sort() {
sort.Slice(store.addrList, func(i, j int) bool {
return store.lastSeen[store.addrList[j]].Before(store.lastSeen[store.addrList[i]])
})
}

View File

@@ -1,29 +0,0 @@
package peer
import (
"net/netip"
"testing"
"time"
)
func TestPubAddrStore(t *testing.T) {
s := newPubAddrStore(netip.AddrPort{})
l := []netip.AddrPort{
netip.AddrPortFrom(netip.AddrFrom4([4]byte{0, 1, 2, 3}), 20),
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 2, 3}), 21),
netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 1, 2, 3}), 22),
}
for i := range l {
s.Store(l[i])
time.Sleep(time.Millisecond)
}
s.clean()
l2 := s.Get()
if l2[0] != l[2] || l2[1] != l[1] || l2[2] != l[0] {
t.Fatal(l, l2)
}
}

View File

@@ -1,54 +0,0 @@
package peer
import (
"log"
"sync"
"sync/atomic"
)
type relayHandler struct {
lock sync.Mutex
relays map[byte]*Remote
relay atomic.Pointer[Remote]
}
func newRelayHandler() *relayHandler {
return &relayHandler{
relays: make(map[byte]*Remote, 256),
}
}
func (h *relayHandler) Add(r *Remote) {
h.lock.Lock()
defer h.lock.Unlock()
h.relays[r.RemotePeerIP] = r
if h.relay.Load() == nil {
log.Printf("Setting Relay: %v", r.conf().Peer.Name)
h.relay.Store(r)
}
}
func (h *relayHandler) Remove(r *Remote) {
h.lock.Lock()
defer h.lock.Unlock()
log.Printf("Removing relay %d...", r.RemotePeerIP)
delete(h.relays, r.RemotePeerIP)
if h.relay.Load() == r {
// Remove current relay.
h.relay.Store(nil)
// Find new relay.
for _, r := range h.relays {
h.relay.Store(r)
break
}
}
}
func (h *relayHandler) Load() *Remote {
return h.relay.Load()
}

View File

@@ -1,351 +0,0 @@
package peer
import (
"fmt"
"log"
"net/netip"
"strings"
"sync/atomic"
"vppn/m"
)
// ----------------------------------------------------------------------------
// The remoteConfig is the shared, immutable configuration for a remote
// peer. It's read and written atomically. See remote.config.
// ----------------------------------------------------------------------------
type remoteConfig struct {
Up bool // True if peer is up and we can send data.
Server bool // True if role is server.
Direct bool // True if this is a direct connection.
DirectAddr netip.AddrPort // Remote address if directly connected.
ControlCipher *controlCipher
DataCipher *dataCipher
Peer *m.Peer
}
// CanRelay returns true if the remote configuration is able to relay packets.
// to other hosts.
func (rc remoteConfig) CanRelay() bool {
return rc.Up && rc.Direct && rc.Peer.Relay
}
// A Remote represents a remote peer and contains functions for handling
// incoming control, data, and multicast packets, peer udpates, as well as
// sending, forwarding, and relaying packets.
type Remote struct {
Globals
RemotePeerIP byte // Immutable.
dupCheck *dupCheck
sendCounter uint64 // init to startupCount << 48. Atomic access only.
// config should be accessed via conf() and updateConf(...) methods.
config atomic.Pointer[remoteConfig]
messages chan any
}
func newRemote(g Globals, remotePeerIP byte) *Remote {
r := &Remote{
Globals: g,
RemotePeerIP: remotePeerIP,
dupCheck: newDupCheck(0),
sendCounter: (uint64(g.StartupCount) << 48) + 1,
messages: make(chan any, 8),
}
r.config.Store(&remoteConfig{})
return r
}
// ----------------------------------------------------------------------------
func (r *Remote) conf() remoteConfig {
return *(r.config.Load())
}
func (r *Remote) updateConf(conf remoteConfig) {
old := r.config.Load()
r.config.Store(&conf)
if !old.CanRelay() && conf.CanRelay() {
r.RelayHandler.Add(r)
}
if old.CanRelay() && !conf.CanRelay() {
r.RelayHandler.Remove(r)
}
}
// ----------------------------------------------------------------------------
func (r *Remote) sendUDP(b []byte, addr netip.AddrPort) {
if _, err := r.SendUDP(b, addr); err != nil {
r.logf("Failed to send UDP packet: %v", err)
}
}
// ----------------------------------------------------------------------------
func (r *Remote) encryptData(conf remoteConfig, destIP byte, packet []byte) []byte {
h := Header{
StreamID: dataStreamID,
Counter: atomic.AddUint64(&r.sendCounter, 1),
SourceIP: r.Globals.LocalPeerIP,
DestIP: destIP,
}
return conf.DataCipher.Encrypt(h, packet, packet[len(packet):cap(packet)])
}
func (r *Remote) encryptControl(conf remoteConfig, packet []byte) []byte {
h := Header{
StreamID: controlStreamID,
Counter: atomic.AddUint64(&r.sendCounter, 1),
SourceIP: r.LocalPeerIP,
DestIP: r.RemotePeerIP,
}
return conf.ControlCipher.Encrypt(h, packet, packet[len(packet):cap(packet)])
}
func (r *Remote) Status() (RemoteStatus, bool) {
conf := r.conf()
if conf.Peer == nil {
return RemoteStatus{}, false
}
return RemoteStatus{
PeerIP: conf.Peer.PeerIP,
Up: conf.Up,
Name: conf.Peer.Name,
PublicIP: conf.Peer.PublicIP,
Port: conf.Peer.Port,
Relay: conf.Peer.Relay,
Server: conf.Server,
Direct: conf.Direct,
DirectAddr: conf.DirectAddr,
}, true
}
// ----------------------------------------------------------------------------
// SendDataTo sends a data packet to the remote, called by the IFReader.
func (r *Remote) SendDataTo(data []byte) {
conf := r.conf()
if !conf.Up {
r.logf("Cannot send: link down")
return
}
// Direct:
if conf.Direct {
r.sendUDP(r.encryptData(conf, conf.Peer.PeerIP, data), conf.DirectAddr)
return
}
// Relayed:
relay := r.RelayHandler.Load()
if relay == nil {
r.logf("Connot send: no relay")
return
}
relay.relayData(conf.Peer.PeerIP, r.encryptData(conf, conf.Peer.PeerIP, data))
}
func (r *Remote) relayData(toIP byte, enc []byte) {
conf := r.conf()
if !conf.Up || !conf.Direct {
r.logf("Cannot relay: not up or not a direct connection")
return
}
r.sendUDP(r.encryptData(conf, toIP, enc), conf.DirectAddr)
}
func (r *Remote) sendControl(conf remoteConfig, data []byte) {
// Direct:
if conf.Direct {
enc := r.encryptControl(conf, data)
r.sendUDP(enc, conf.DirectAddr)
return
}
// Relayed:
relay := r.RelayHandler.Load()
if relay == nil {
r.logf("Connot send: no relay")
return
}
relay.relayData(conf.Peer.PeerIP, r.encryptControl(conf, data))
}
func (r *Remote) sendControlToAddr(buf []byte, addr netip.AddrPort) {
enc := r.encryptControl(r.conf(), buf)
r.sendUDP(enc, addr)
}
func (r *Remote) forwardPacket(data []byte) {
conf := r.conf()
if !conf.Up || !conf.Direct {
r.logf("Cannot forward to %d: not a direct connection", conf.Peer.PeerIP)
return
}
r.sendUDP(data, conf.DirectAddr)
}
// ----------------------------------------------------------------------------
// HandlePacket is called by the ConnReader to handle an incoming packet.
func (r *Remote) HandlePacket(h Header, srcAddr netip.AddrPort, data []byte) {
switch h.StreamID {
case controlStreamID:
r.handleControlPacket(h, srcAddr, data)
case dataStreamID:
r.handleDataPacket(h, data)
default:
r.logf("Unknown stream ID: %d", h.StreamID)
}
}
// Handle a control packet. Decrypt, verify, etc.
func (r *Remote) handleControlPacket(h Header, srcAddr netip.AddrPort, data []byte) {
conf := r.conf()
if conf.ControlCipher == nil {
r.logf("No control cipher")
return
}
dec, ok := conf.ControlCipher.Decrypt(data, data[len(data):cap(data)])
if !ok {
r.logf("Failed to decrypt control packet")
return
}
if r.dupCheck.IsDup(h.Counter) {
r.logf("Dropping control packet as duplicate: %d", h.Counter)
return
}
msg, err := parseControlMsg(h.SourceIP, srcAddr, dec)
if err != nil {
r.logf("Failed to parse control packet: %v", err)
return
}
select {
case r.messages <- msg:
default:
r.logf("Dropping control message")
}
}
func (r *Remote) handleDataPacket(h Header, data []byte) {
conf := r.conf()
if conf.DataCipher == nil {
return
}
dec, ok := conf.DataCipher.Decrypt(data, data[len(data):cap(data)])
if !ok {
r.logf("Failed to decrypt data packet")
return
}
if r.dupCheck.IsDup(h.Counter) {
r.logf("Dropping data packet as duplicate: %d", h.Counter)
return
}
// For local.
if h.DestIP == r.LocalPeerIP {
if _, err := r.IFace.Write(dec); err != nil {
// This could be a malformed packet from a peer, so we don't crash if it
// happens.
r.logf("Failed to write to interface: %v", err)
}
return
}
// Forward.
dest := r.RemotePeers[h.DestIP].Load()
dest.forwardPacket(dec)
}
// ----------------------------------------------------------------------------
// HandleLocalDiscoveryPacket is called by the MCReader.
func (r *Remote) HandleLocalDiscoveryPacket(h Header, srcAddr netip.AddrPort, data []byte) {
conf := r.conf()
if conf.Peer == nil {
r.logf("No peer for discovery packet.")
return
}
if conf.Peer.PubSignKey == nil {
r.logf("No signing key for discovery packet.")
return
}
if !verifyLocalDiscoveryPacket(data, data[len(data):cap(data)], conf.Peer.PubSignKey) {
r.logf("Invalid signature on discovery packet.")
return
}
msg := controlMsg[packetLocalDiscovery]{
SrcIP: h.SourceIP,
SrcAddr: srcAddr,
}
select {
case r.messages <- msg:
default:
r.logf("Dropping discovery message.")
}
}
// ----------------------------------------------------------------------------
// HandlePeerUpdate is called by the HubPoller when it gets a new version of
// the associated peer configuration.
func (r *Remote) HandlePeerUpdate(msg peerUpdateMsg) {
r.messages <- msg
}
// ----------------------------------------------------------------------------
func (s *Remote) logf(format string, args ...any) {
conf := s.conf()
b := strings.Builder{}
name := ""
if conf.Peer != nil {
name = conf.Peer.Name
}
b.WriteString(fmt.Sprintf("%03d", s.RemotePeerIP))
b.WriteString(fmt.Sprintf("%30s: ", name))
if conf.Server {
b.WriteString("SERVER | ")
} else {
b.WriteString("CLIENT | ")
}
if conf.Direct {
b.WriteString("DIRECT | ")
} else {
b.WriteString("RELAYED | ")
}
if conf.Up {
b.WriteString("UP | ")
} else {
b.WriteString("DOWN | ")
}
log.Printf(b.String()+format, args...)
}

View File

@@ -1,448 +0,0 @@
package peer
import (
"net/netip"
"time"
"vppn/m"
)
type stateFunc func(msg any) stateFunc
type sentProbe struct {
SentAt time.Time
Addr netip.AddrPort
}
type remoteFSM struct {
*Remote
pingTimer *time.Ticker
lastSeen time.Time
traceID uint64
probes map[uint64]sentProbe
buf []byte
}
func newRemoteFSM(r *Remote) *remoteFSM {
fsm := &remoteFSM{
Remote: r,
pingTimer: time.NewTicker(timeoutInterval),
probes: map[uint64]sentProbe{},
buf: make([]byte, bufferSize),
}
fsm.pingTimer.Stop()
return fsm
}
func (r *remoteFSM) Run() {
go func() {
for range r.pingTimer.C {
r.messages <- pingTimerMsg{}
}
}()
state := r.enterDisconnected()
for msg := range r.messages {
state = state(msg)
}
}
// ----------------------------------------------------------------------------
func (r *remoteFSM) enterDisconnected() stateFunc {
r.updateConf(remoteConfig{})
return r.stateDisconnected
}
func (r *remoteFSM) stateDisconnected(iMsg any) stateFunc {
switch msg := iMsg.(type) {
case peerUpdateMsg:
return r.enterPeerUpdating(msg.Peer)
case controlMsg[packetInit]:
r.logf("Unexpected INIT")
case controlMsg[packetSyn]:
r.logf("Unexpected SYN")
case controlMsg[packetAck]:
r.logf("Unexpected ACK")
case controlMsg[packetProbe]:
r.logf("Unexpected probe")
case controlMsg[packetLocalDiscovery]:
// Ignore
case pingTimerMsg:
r.logf("Unexpected ping")
default:
r.logf("Ignoring message: %#v", iMsg)
}
return r.stateDisconnected
}
// ----------------------------------------------------------------------------
func (r *remoteFSM) enterPeerUpdating(peer *m.Peer) stateFunc {
if peer == nil {
return r.enterDisconnected()
}
conf := remoteConfig{
Peer: peer,
ControlCipher: newControlCipher(r.PrivKey, peer.PubKey),
}
r.updateConf(conf)
if _, isValid := netip.AddrFromSlice(peer.PublicIP); isValid {
if r.LocalAddrValid && r.LocalPeerIP < peer.PeerIP {
return r.enterServer()
}
return r.enterClientInit()
}
if r.LocalAddrValid || r.LocalPeerIP < peer.PeerIP {
return r.enterServer()
}
return r.enterClientInit()
}
// ----------------------------------------------------------------------------
func (r *remoteFSM) enterServer() stateFunc {
conf := r.conf()
conf.Server = true
r.updateConf(conf)
r.logf("==> Server")
r.pingTimer.Reset(pingInterval)
r.lastSeen = time.Now()
return r.stateServer
}
func (r *remoteFSM) stateServer(iMsg any) stateFunc {
switch msg := iMsg.(type) {
case peerUpdateMsg:
return r.enterPeerUpdating(msg.Peer)
case controlMsg[packetInit]:
r.stateServer_onInit(msg)
case controlMsg[packetSyn]:
r.stateServer_onSyn(msg)
case controlMsg[packetAck]:
r.logf("Unexpected ACK")
case controlMsg[packetProbe]:
r.stateServer_onProbe(msg)
case controlMsg[packetLocalDiscovery]:
// Ignore
case pingTimerMsg:
r.stateServer_onPingTimer()
default:
r.logf("Unexpected message: %#v", iMsg)
}
return r.stateServer
}
func (r *remoteFSM) stateServer_onInit(msg controlMsg[packetInit]) {
conf := r.conf()
conf.Up = false
conf.Direct = msg.Packet.Direct
conf.DirectAddr = msg.SrcAddr
r.updateConf(conf)
init := packetInit{
TraceID: msg.Packet.TraceID,
Direct: conf.Direct,
Version: version,
}
// Reset traceID to force state update on SYN.
r.traceID = 0
r.sendControl(conf, init.Marshal(r.buf))
}
func (r *remoteFSM) stateServer_onSyn(msg controlMsg[packetSyn]) {
r.lastSeen = time.Now()
p := msg.Packet
conf := r.conf()
// New trace ID => Update the route configuration.
if p.TraceID != r.traceID {
r.traceID = p.TraceID
conf.Up = true
conf.Direct = p.Direct
conf.DirectAddr = msg.SrcAddr
conf.DataCipher = newDataCipherFromKey(p.SharedKey)
r.updateConf(conf)
r.logf("Got SYN.")
}
r.sendControl(conf, packetAck{
TraceID: p.TraceID,
ToAddr: conf.DirectAddr,
PossibleAddrs: r.PubAddrs.Get(),
}.Marshal(r.buf))
if p.Direct {
return
}
// Send probes if not a direct connection. The server sends probes without
// trace IDs unless responding to a client probe.
for _, addr := range msg.Packet.PossibleAddrs {
if !addr.IsValid() {
break
}
r.logf("Probing %v...", addr)
r.sendControlToAddr(packetProbe{}.Marshal(r.buf), addr)
}
}
func (r *remoteFSM) stateServer_onProbe(msg controlMsg[packetProbe]) {
if !msg.SrcAddr.IsValid() {
return
}
data := packetProbe{TraceID: msg.Packet.TraceID}.Marshal(r.buf)
r.sendControlToAddr(data, msg.SrcAddr)
}
func (r *remoteFSM) stateServer_onPingTimer() {
conf := r.conf()
if time.Since(r.lastSeen) > timeoutInterval && conf.Up {
// Reset trace ID to ensure connection goes up on next SYN.
r.traceID = 0
conf.Up = false
r.updateConf(conf)
r.logf("Timeout.")
}
}
// ----------------------------------------------------------------------------
func (r *remoteFSM) enterClientInit() stateFunc {
conf := r.conf()
ip, ipValid := netip.AddrFromSlice(conf.Peer.PublicIP)
conf.Up = false
conf.Server = false
conf.Direct = ipValid
conf.DirectAddr = netip.AddrPortFrom(ip, conf.Peer.Port)
conf.DataCipher = newDataCipher()
r.updateConf(conf)
r.logf("==> ClientInit")
r.lastSeen = time.Now()
r.pingTimer.Reset(pingInterval)
r.stateClientInit_sendInit()
return r.stateClientInit
}
func (r *remoteFSM) stateClientInit(iMsg any) stateFunc {
switch msg := iMsg.(type) {
case peerUpdateMsg:
return r.enterPeerUpdating(msg.Peer)
case controlMsg[packetInit]:
return r.stateClientInit_onInit(msg)
case controlMsg[packetSyn]:
r.logf("Unexpected SYN")
case controlMsg[packetAck]:
r.logf("Unexpected ACK")
case controlMsg[packetProbe]:
// Ignore
case controlMsg[packetLocalDiscovery]:
// Ignore
case pingTimerMsg:
return r.stateClientInit_onPing()
default:
r.logf("Unexpected message: %#v", iMsg)
}
return r.stateClientInit
}
func (r *remoteFSM) stateClientInit_sendInit() {
conf := r.conf()
r.traceID = r.NewTraceID()
init := packetInit{
TraceID: r.traceID,
Direct: conf.Direct,
Version: version,
}
r.sendControl(conf, init.Marshal(r.buf))
}
func (r *remoteFSM) stateClientInit_onInit(msg controlMsg[packetInit]) stateFunc {
if msg.Packet.TraceID != r.traceID {
r.logf("Invalid trace ID on INIT.")
return r.stateClientInit
}
r.logf("Got INIT version %d.", msg.Packet.Version)
return r.enterClient()
}
func (r *remoteFSM) stateClientInit_onPing() stateFunc {
if time.Since(r.lastSeen) < timeoutInterval {
r.stateClientInit_sendInit()
return r.stateClientInit
}
// Direct connect failed. Try indirect.
conf := r.conf()
if conf.Direct {
conf.Direct = false
r.updateConf(conf)
r.lastSeen = time.Now()
r.stateClientInit_sendInit()
r.logf("Direct connection failed. Attempting indirect connection.")
return r.stateClientInit
}
// Indirect failed. Re-enter init state.
r.logf("Timeout.")
return r.enterClientInit()
}
// ----------------------------------------------------------------------------
func (r *remoteFSM) enterClient() stateFunc {
conf := r.conf()
clear(r.probes)
r.traceID = r.NewTraceID()
r.stateClient_sendSyn(conf)
r.pingTimer.Reset(pingInterval)
r.logf("==> Client")
return r.stateClient
}
func (r *remoteFSM) stateClient(iMsg any) stateFunc {
switch msg := iMsg.(type) {
case peerUpdateMsg:
return r.enterPeerUpdating(msg.Peer)
case controlMsg[packetAck]:
r.stateClient_onAck(msg)
case controlMsg[packetProbe]:
r.stateClient_onProbe(msg)
case controlMsg[packetLocalDiscovery]:
r.stateClient_onLocalDiscovery(msg)
case pingTimerMsg:
return r.stateClient_onPingTimer()
default:
r.logf("Ignoring message: %v", iMsg)
}
return r.stateClient
}
func (r *remoteFSM) stateClient_onAck(msg controlMsg[packetAck]) {
if msg.Packet.TraceID != r.traceID {
return
}
r.lastSeen = time.Now()
conf := r.conf()
if !conf.Up {
conf.Up = true
r.updateConf(conf)
r.logf("Got ACK.")
}
if conf.Direct {
r.PubAddrs.Store(msg.Packet.ToAddr)
return
}
// Relayed.
r.stateClient_cleanProbes()
for _, addr := range msg.Packet.PossibleAddrs {
if !addr.IsValid() {
break
}
r.stateClient_sendProbeTo(addr)
}
}
func (r *remoteFSM) stateClient_cleanProbes() {
for key, sent := range r.probes {
if time.Since(sent.SentAt) > pingInterval {
delete(r.probes, key)
}
}
}
func (r *remoteFSM) stateClient_sendProbeTo(addr netip.AddrPort) {
probe := packetProbe{TraceID: r.NewTraceID()}
r.probes[probe.TraceID] = sentProbe{
SentAt: time.Now(),
Addr: addr,
}
r.logf("Probing %v...", addr)
r.sendControlToAddr(probe.Marshal(r.buf), addr)
}
func (r *remoteFSM) stateClient_onProbe(msg controlMsg[packetProbe]) {
conf := r.conf()
if conf.Direct {
return
}
r.stateClient_cleanProbes()
sent, ok := r.probes[msg.Packet.TraceID]
if !ok {
return
}
conf.Direct = true
conf.DirectAddr = sent.Addr
r.updateConf(conf)
r.traceID = r.NewTraceID()
r.stateClient_sendSyn(conf)
r.logf("Successful probe to %v.", sent.Addr)
}
func (r *remoteFSM) stateClient_onLocalDiscovery(msg controlMsg[packetLocalDiscovery]) {
conf := r.conf()
if conf.Direct {
return
}
// The source port will be the multicast port, so we'll have to
// construct the correct address using the peer's listed port.
addr := netip.AddrPortFrom(msg.SrcAddr.Addr(), conf.Peer.Port)
r.stateClient_sendProbeTo(addr)
}
func (r *remoteFSM) stateClient_onPingTimer() stateFunc {
conf := r.conf()
if time.Since(r.lastSeen) > timeoutInterval {
if conf.Up {
r.logf("Timeout.")
}
return r.enterClientInit()
}
r.stateClient_sendSyn(conf)
return r.stateClient
}
func (r *remoteFSM) stateClient_sendSyn(conf remoteConfig) {
syn := packetSyn{
TraceID: r.traceID,
SharedKey: conf.DataCipher.Key(),
Direct: conf.Direct,
PossibleAddrs: r.PubAddrs.Get(),
}
r.sendControl(conf, syn.Marshal(r.buf))
}

View File

@@ -35,32 +35,11 @@ func runStatusServer(g Globals, socketPath string) {
report := StatusReport{
LocalPeerIP: g.LocalPeerIP,
Network: g.Network,
Remotes: make([]RemoteStatus, 0, 255),
}
relay := g.RelayHandler.Load()
if relay != nil {
if relayStatus, ok := relay.Status(); ok {
report.RelayPeerIP = relayStatus.PeerIP
}
}
for i := range g.RemotePeers {
remote := g.RemotePeers[i].Load()
status, ok := remote.Status()
if !ok {
continue
}
report.Remotes = append(report.Remotes, status)
}
json.NewEncoder(w).Encode(report)
}
server := http.Server{
Handler: http.HandlerFunc(handler),
}
server := http.Server{Handler: http.HandlerFunc(handler)}
unixListener, err := net.Listen("unix", socketPath)
if err != nil {
log.Fatalf("Failed to bind to unix socket: %v", err)

View File

@@ -0,0 +1,224 @@
// Package wginterface demonstrates creating and destroying a WireGuard network
// interface using only raw system calls — no netlink library.
//
// Creating a typed interface (kind = "wireguard") requires the NETLINK_ROUTE
// protocol; there is no ioctl path for it. Everything else — assigning an IP
// address and bringing the link up — can be done with the older AF_INET ioctl
// interface, exactly as one would for a TUN device.
//
// The package requires CAP_NET_ADMIN and the wireguard kernel module.
package wginterface
import (
"encoding/binary"
"fmt"
"net"
"slices"
"golang.org/x/sys/unix"
)
// Create creates a WireGuard interface named name, assigns vpnIP/prefixLen to
// it, and brings it up.
func Create(name string, vpnIP net.IP, prefixLen int) error {
if err := nlNewLink(name); err != nil {
return fmt.Errorf("failed to create wireguard link: %w", err)
}
if err := ioctlSetAddr(name, vpnIP, prefixLen); err != nil {
_ = Delete(name)
return fmt.Errorf("assign address: %w", err)
}
if err := ioctlLinkUp(name); err != nil {
_ = Delete(name)
return fmt.Errorf("link up: %w", err)
}
return nil
}
// Delete removes the named interface.
func Delete(name string) error {
return nlDelLink(name)
}
// ---------------------------------------------------------------------------
// Netlink link management
//
// Creating a WireGuard interface requires an RTM_NEWLINK message with a nested
// IFLA_LINKINFO attribute whose IFLA_INFO_KIND is "wireguard". The full
// message layout is:
//
// nlmsghdr (16 bytes)
// ifinfomsg (16 bytes, all zeros for a new link)
// rtattr IFLA_IFNAME → name + \0
// rtattr IFLA_LINKINFO
// rtattr IFLA_INFO_KIND → "wireguard" + \0
//
// All multi-byte integers are in native byte order (little-endian on
// x86/arm64). Every attribute is padded to a 4-byte boundary; the len field
// in the header records the unpadded length but the attribute occupies the
// padded size.
const (
nlmsgHdrLen = 16 // sizeof(struct nlmsghdr)
sizeofIfInfo = 16 // sizeof(struct ifinfomsg)
// Attribute types not exposed by the unix package at the level we need.
iflaLinkInfo = 18 // IFLA_LINKINFO — container for link-type attributes
iflaInfoKind = 1 // IFLA_INFO_KIND — link type string, nested inside IFLA_LINKINFO
)
// nlNewLink creates the wireguard interface using Netlink.
func nlNewLink(name string) error {
// Build innermost attribute first, then wrap outward.
infoKind := nlAttr(iflaInfoKind, cstring("wireguard"))
linkInfo := nlAttr(iflaLinkInfo, infoKind)
ifName := nlAttr(unix.IFLA_IFNAME, cstring(name))
// ifinfomsg: all-zero = AF_UNSPEC, no index, no flags (kernel assigns index).
ifInfo := make([]byte, sizeofIfInfo)
payload := slices.Concat(ifInfo, ifName, linkInfo)
flags := uint16(unix.NLM_F_REQUEST | unix.NLM_F_ACK | unix.NLM_F_CREATE | unix.NLM_F_EXCL)
return nlRoundtrip(unix.RTM_NEWLINK, flags, payload)
}
func nlDelLink(name string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
// For RTM_DELLINK the kernel identifies the link by ifi_index. ifi_index
// sits at byte offset 4 in the ifinfomsg struct.
ifInfo := make([]byte, sizeofIfInfo)
binary.NativeEndian.PutUint32(ifInfo[4:8], uint32(iface.Index))
return nlRoundtrip(unix.RTM_DELLINK, uint16(unix.NLM_F_REQUEST|unix.NLM_F_ACK), ifInfo)
}
// nlRoundtrip opens a NETLINK_ROUTE socket, sends one request, reads the
// NLMSG_ERROR acknowledgement, and closes the socket.
func nlRoundtrip(msgType uint16, flags uint16, payload []byte) error {
fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE)
if err != nil {
return fmt.Errorf("socket: %w", err)
}
defer unix.Close(fd)
if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
return fmt.Errorf("bind: %w", err)
}
msg := nlMsg(msgType, flags, payload)
if err := unix.Sendto(fd, msg, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
return fmt.Errorf("sendto: %w", err)
}
resp := make([]byte, 4096)
n, _, err := unix.Recvfrom(fd, resp, 0)
if err != nil {
return fmt.Errorf("recvfrom: %w", err)
}
return nlAckErr(resp[:n])
}
// nlMsg prepends an nlmsghdr to payload.
func nlMsg(msgType uint16, flags uint16, payload []byte) []byte {
buf := make([]byte, nlmsgHdrLen+len(payload))
binary.NativeEndian.PutUint32(buf[0:4], uint32(len(buf))) // nlmsg_len
binary.NativeEndian.PutUint16(buf[4:6], msgType) // nlmsg_type
binary.NativeEndian.PutUint16(buf[6:8], flags) // nlmsg_flags
binary.NativeEndian.PutUint32(buf[8:12], 1) // nlmsg_seq
binary.NativeEndian.PutUint32(buf[12:16], 0) // nlmsg_pid (0 = kernel)
copy(buf[nlmsgHdrLen:], payload)
return buf
}
// nlAckErr parses an NLMSG_ERROR response. The error field is a negated errno
// (0 = success, -EEXIST = interface exists, etc.).
func nlAckErr(resp []byte) error {
if len(resp) < nlmsgHdrLen+4 {
return fmt.Errorf("netlink response too short (%d bytes)", len(resp))
}
if binary.NativeEndian.Uint16(resp[4:6]) != unix.NLMSG_ERROR {
return fmt.Errorf("unexpected nlmsg_type %d", binary.NativeEndian.Uint16(resp[4:6]))
}
// Error code follows the nlmsghdr; it is a signed int32 holding -errno.
code := int32(binary.NativeEndian.Uint32(resp[nlmsgHdrLen:]))
if code != 0 {
return unix.Errno(-code)
}
return nil
}
// nlAttr encodes one netlink attribute: [len:u16][type:u16][data][pad to 4
// bytes]. The len field counts the header + data (before padding); the
// allocation is padded so that the next attribute starts on a 4-byte boundary.
func nlAttr(attrType uint16, data []byte) []byte {
const hdr = 4
attrLen := hdr + len(data)
padded := (attrLen + 3) &^ 3
buf := make([]byte, padded)
binary.NativeEndian.PutUint16(buf[0:2], uint16(attrLen))
binary.NativeEndian.PutUint16(buf[2:4], attrType)
copy(buf[hdr:], data)
return buf
}
// ---------------------------------------------------------------------------
// ioctl-based address assignment and link-up
//
// These operations could also be done via RTM_NEWADDR / RTM_NEWLINK netlink
// messages, but the AF_INET ioctl interface is simpler.
func ioctlSetAddr(name string, ip net.IP, prefixLen int) error {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
if err != nil {
return err
}
defer unix.Close(fd)
req, err := unix.NewIfreq(name)
if err != nil {
return err
}
if err := req.SetInet4Addr(ip.To4()); err != nil {
return err
}
if err := unix.IoctlIfreq(fd, unix.SIOCSIFADDR, req); err != nil {
return err
}
req, err = unix.NewIfreq(name)
if err != nil {
return err
}
mask := net.CIDRMask(prefixLen, 32)
if err := req.SetInet4Addr([]byte(mask)); err != nil {
return err
}
return unix.IoctlIfreq(fd, unix.SIOCSIFNETMASK, req)
}
func ioctlLinkUp(name string) error {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
if err != nil {
return err
}
defer unix.Close(fd)
req, err := unix.NewIfreq(name)
if err != nil {
return err
}
if err := unix.IoctlIfreq(fd, unix.SIOCGIFFLAGS, req); err != nil {
return err
}
req.SetUint16(req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING)
return unix.IoctlIfreq(fd, unix.SIOCSIFFLAGS, req)
}
// cstring returns b as a null-terminated byte slice.
func cstring(s string) []byte {
return append([]byte(s), 0)
}

165
peer/wginterface/manage.go Normal file
View File

@@ -0,0 +1,165 @@
package wginterface
import (
"fmt"
"net"
"net/netip"
"os"
"time"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const (
// RekeyTimeout is the WireGuard session lifetime before a new handshake
// is initiated. Sessions older than this but younger than SessionTimeout
// remain valid.
RekeyTimeout = 120 * time.Second
// SessionTimeout is the WireGuard session lifetime after which sessions
// are rejected. A peer with LastHandshakeTime older than this is
// effectively disconnected.
SessionTimeout = 180 * time.Second
)
var (
probeKeepalive = 5 * time.Second
zeroKeepalive = time.Duration(0)
)
// Device wraps a wgctrl client bound to a named WireGuard interface.
type Device struct {
client *wgctrl.Client
name string
}
// Open attaches to an existing WireGuard interface.
func Open(name string) (*Device, error) {
client, err := wgctrl.New()
if err != nil {
return nil, fmt.Errorf("wgctrl: %w", err)
}
return &Device{client: client, name: name}, nil
}
// Close releases the underlying wgctrl client.
func (d *Device) Close() error {
return d.client.Close()
}
// Configure sets the device's private key and UDP listen port.
func (d *Device) Configure(privKey wgtypes.Key, listenPort int) error {
return d.client.ConfigureDevice(d.name, wgtypes.Config{
PrivateKey: &privKey,
ListenPort: &listenPort,
})
}
// Peers returns the current state of all peers on the device.
func (d *Device) Peers() ([]wgtypes.Peer, error) {
dev, err := d.client.Device(d.name)
if err != nil {
return nil, fmt.Errorf("get device %q: %w", d.name, err)
}
return dev.Peers, nil
}
// Peer returns the current state of a single peer by public key.
func (d *Device) Peer(pubKey wgtypes.Key) (wgtypes.Peer, error) {
peers, err := d.Peers()
if err != nil {
return wgtypes.Peer{}, err
}
for _, p := range peers {
if p.PublicKey == pubKey {
return p, nil
}
}
return wgtypes.Peer{}, fmt.Errorf("peer %v not found in %q", pubKey, d.name)
}
// SetRelay configures the relay peer with AllowedIPs covering the entire VPN
// network prefix. This is the fallback route for all VPN traffic.
func (d *Device) SetRelay(pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) error {
masked := network.Masked()
a4 := masked.Addr().As4()
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(masked.Bits(), 32),
}},
ReplaceAllowedIPs: true,
}},
})
}
// AddProbe adds a peer with no AllowedIPs and a 5s keepalive. WireGuard will
// attempt handshakes without routing any traffic through this peer yet.
func (d *Device) AddProbe(pubKey wgtypes.Key, endpoint netip.AddrPort) error {
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &probeKeepalive,
}},
})
}
// Promote upgrades a probe entry to a /32 AllowedIPs and removes the probe
// keepalive, causing WireGuard to prefer this peer's direct path over the
// relay's wider route.
func (d *Device) Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error {
a4 := vpnIP.As4()
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(32, 32),
}},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &zeroKeepalive,
}},
})
}
// AddDirect adds a peer with a known endpoint and /32 AllowedIPs in one step,
// for peers with a stable public endpoint reported by the hub.
func (d *Device) AddDirect(pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error {
a4 := vpnIP.As4()
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(32, 32),
}},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &zeroKeepalive,
}},
})
}
// RemovePeer removes a peer from the device.
func (d *Device) RemovePeer(pubKey wgtypes.Key) error {
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Remove: true,
}},
})
}
// EnableForwarding enables IPv4 forwarding on the interface, required for
// relay peers that forward traffic between VPN peers.
func (d *Device) EnableForwarding() error {
path := fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/forwarding", d.name)
return os.WriteFile(path, []byte("1\n"), 0644)
}

View File

@@ -0,0 +1,303 @@
//go:build integration
package wginterface_test
import (
"fmt"
"log"
"net"
"net/netip"
"os"
"strings"
"testing"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"vppn/peer/wginterface"
)
const (
testBasePort = 59100
)
func TestMain(m *testing.M) {
if os.Getuid() != 0 {
fmt.Fprintln(os.Stderr, "wginterface integration tests require root; skipping")
os.Exit(0)
}
os.Exit(m.Run())
}
type testPeer struct {
Name string
VpnIP netip.Addr
Port int
PrivKey wgtypes.Key
PubKey wgtypes.Key
Dev *wginterface.Device
}
func (p *testPeer) Endpoint() netip.AddrPort {
return netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), uint16(p.Port))
}
func newTestPeer(t *testing.T, name string, vpnIP netip.Addr, port int) *testPeer {
t.Helper()
privKey, err := wgtypes.GenerateKey()
if err != nil {
t.Fatalf("generate key: %v", err)
}
a4 := vpnIP.As4()
if err := wginterface.Create(name, net.IP(a4[:]), 24); err != nil {
t.Fatalf("create %s: %v", name, err)
}
t.Cleanup(func() {
if err := wginterface.Delete(name); err != nil {
log.Printf("Failed to delete interface %s: %v", name, err)
}
})
dev, err := wginterface.Open(name)
if err != nil {
t.Fatalf("open %s: %v", name, err)
}
t.Cleanup(func() { dev.Close() })
if err := dev.Configure(privKey, port); err != nil {
t.Fatalf("configure %s: %v", name, err)
}
return &testPeer{
Name: name,
VpnIP: vpnIP,
Port: port,
PrivKey: privKey,
PubKey: privKey.PublicKey(),
Dev: dev,
}
}
// waitHandshake polls until the named peer has completed a handshake or the timeout elapses.
func waitHandshake(t *testing.T, dev *wginterface.Device, pubKey wgtypes.Key, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
p, err := dev.Peer(pubKey)
if err != nil {
t.Fatalf("peer lookup: %v", err)
}
if !p.LastHandshakeTime.IsZero() {
return
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("no handshake within %v", timeout)
}
func TestDirectHandshake(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
if err := p1.Dev.AddDirect(p2.PubKey, p2.Endpoint(), p2.VpnIP); err != nil {
t.Fatalf("p1 AddDirect: %v", err)
}
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("p2 AddDirect: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
waitHandshake(t, p2.Dev, p1.PubKey, 30*time.Second)
}
func TestProbeAndPromote(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
// p2 needs a peer entry for p1 so it can respond to the handshake initiation.
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("p2 AddDirect: %v", err)
}
if err := p1.Dev.AddProbe(p2.PubKey, p2.Endpoint()); err != nil {
t.Fatalf("AddProbe: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
if err := p1.Dev.Promote(p2.PubKey, p2.VpnIP); err != nil {
t.Fatalf("Promote: %v", err)
}
peer, err := p1.Dev.Peer(p2.PubKey)
if err != nil {
t.Fatalf("Peer: %v", err)
}
checkAllowedIP(t, peer, p2.VpnIP, 32)
}
func TestRelayHandshakes(t *testing.T) {
vpnNetwork := netip.MustParsePrefix("192.168.99.0/24")
relay := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
peer1 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
peer2 := newTestPeer(t, "wgtest2", netip.MustParseAddr("192.168.99.3"), testBasePort+2)
if err := relay.Dev.AddDirect(peer1.PubKey, peer1.Endpoint(), peer1.VpnIP); err != nil {
t.Fatalf("relay AddDirect peer1: %v", err)
}
if err := relay.Dev.AddDirect(peer2.PubKey, peer2.Endpoint(), peer2.VpnIP); err != nil {
t.Fatalf("relay AddDirect peer2: %v", err)
}
if err := peer1.Dev.SetRelay(relay.PubKey, relay.Endpoint(), vpnNetwork); err != nil {
t.Fatalf("peer1 SetRelay: %v", err)
}
if err := peer2.Dev.SetRelay(relay.PubKey, relay.Endpoint(), vpnNetwork); err != nil {
t.Fatalf("peer2 SetRelay: %v", err)
}
waitHandshake(t, relay.Dev, peer1.PubKey, 30*time.Second)
waitHandshake(t, relay.Dev, peer2.PubKey, 30*time.Second)
waitHandshake(t, peer1.Dev, relay.PubKey, 30*time.Second)
waitHandshake(t, peer2.Dev, relay.PubKey, 30*time.Second)
// relay has /32 entries for each peer
p, err := relay.Dev.Peer(peer1.PubKey)
if err != nil {
t.Fatalf("relay peer1: %v", err)
}
checkAllowedIP(t, p, peer1.VpnIP, 32)
p, err = relay.Dev.Peer(peer2.PubKey)
if err != nil {
t.Fatalf("relay peer2: %v", err)
}
checkAllowedIP(t, p, peer2.VpnIP, 32)
// peers have /24 fallback route via relay
p, err = peer1.Dev.Peer(relay.PubKey)
if err != nil {
t.Fatalf("peer1 relay: %v", err)
}
checkAllowedIP(t, p, vpnNetwork.Masked().Addr(), 24)
p, err = peer2.Dev.Peer(relay.PubKey)
if err != nil {
t.Fatalf("peer2 relay: %v", err)
}
checkAllowedIP(t, p, vpnNetwork.Masked().Addr(), 24)
}
func TestRemovePeer(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
if err := p1.Dev.AddDirect(p2.PubKey, p2.Endpoint(), p2.VpnIP); err != nil {
t.Fatalf("AddDirect: %v", err)
}
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("AddDirect: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
if err := p1.Dev.RemovePeer(p2.PubKey); err != nil {
t.Fatalf("RemovePeer: %v", err)
}
if _, err := p1.Dev.Peer(p2.PubKey); err == nil {
t.Fatal("expected error after RemovePeer, got nil")
}
}
func TestEnableForwarding(t *testing.T) {
p := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
if err := p.Dev.EnableForwarding(); err != nil {
t.Fatalf("EnableForwarding: %v", err)
}
data, err := os.ReadFile(fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/forwarding", p.Name))
if err != nil {
t.Fatalf("read forwarding: %v", err)
}
if strings.TrimSpace(string(data)) != "1" {
t.Fatalf("expected forwarding=1, got %q", string(data))
}
}
func TestPromoteKeepalive(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("p2 AddDirect: %v", err)
}
if err := p1.Dev.AddProbe(p2.PubKey, p2.Endpoint()); err != nil {
t.Fatalf("AddProbe: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
if err := p1.Dev.Promote(p2.PubKey, p2.VpnIP); err != nil {
t.Fatalf("Promote: %v", err)
}
peer, err := p1.Dev.Peer(p2.PubKey)
if err != nil {
t.Fatalf("Peer: %v", err)
}
if peer.PersistentKeepaliveInterval != 0 {
t.Fatalf("expected keepalive disabled after promote, got %v", peer.PersistentKeepaliveInterval)
}
}
func TestPeersCount(t *testing.T) {
relay := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
peer1 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
peer2 := newTestPeer(t, "wgtest2", netip.MustParseAddr("192.168.99.3"), testBasePort+2)
if err := relay.Dev.AddDirect(peer1.PubKey, peer1.Endpoint(), peer1.VpnIP); err != nil {
t.Fatalf("AddDirect peer1: %v", err)
}
if err := relay.Dev.AddDirect(peer2.PubKey, peer2.Endpoint(), peer2.VpnIP); err != nil {
t.Fatalf("AddDirect peer2: %v", err)
}
peers, err := relay.Dev.Peers()
if err != nil {
t.Fatalf("Peers: %v", err)
}
if len(peers) != 2 {
t.Fatalf("expected 2 peers, got %d", len(peers))
}
if err := relay.Dev.RemovePeer(peer1.PubKey); err != nil {
t.Fatalf("RemovePeer: %v", err)
}
peers, err = relay.Dev.Peers()
if err != nil {
t.Fatalf("Peers after remove: %v", err)
}
if len(peers) != 1 {
t.Fatalf("expected 1 peer after remove, got %d", len(peers))
}
if peers[0].PublicKey != peer2.PubKey {
t.Fatal("wrong peer remained after remove")
}
}
// checkAllowedIP asserts that a peer has exactly one AllowedIP matching addr/bits.
func checkAllowedIP(t *testing.T, p wgtypes.Peer, addr netip.Addr, bits int) {
t.Helper()
if len(p.AllowedIPs) != 1 {
t.Fatalf("expected 1 AllowedIP, got %d", len(p.AllowedIPs))
}
ones, _ := p.AllowedIPs[0].Mask.Size()
if ones != bits {
t.Fatalf("expected /%d, got /%d", bits, ones)
}
got := netip.AddrFrom4([4]byte(p.AllowedIPs[0].IP.To4()))
if got != addr {
t.Fatalf("expected AllowedIP %v, got %v", addr, got)
}
}