diff --git a/cmd/vppn/main.go b/cmd/vppn/main.go index dada4cf..6a76e24 100644 --- a/cmd/vppn/main.go +++ b/cmd/vppn/main.go @@ -1,11 +1,8 @@ package main -import ( - "log" - "vppn/peer" -) +import "log" func main() { - log.SetFlags(0) - peer.Main2() + // TODO: construct peer.App and call Run (audit C1). + log.Fatal("not yet implemented") } diff --git a/hub/api/db/generated.go b/hub/api/db/generated.go index 01ad967..c02646d 100644 --- a/hub/api/db/generated.go +++ b/hub/api/db/generated.go @@ -344,15 +344,15 @@ type Peer struct { Version int64 APIKey string Name string - PublicIP1 []byte - Port1 uint16 - PublicIP2 []byte - Port2 uint16 + Addr4 []byte + Port4 uint16 + Addr6 []byte + Port6 uint16 Relay bool WGPubKey []byte } -const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey FROM peers" +const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,Relay,WGPubKey FROM peers" func Peer_Insert( tx TX, @@ -363,7 +363,7 @@ func Peer_Insert( return err } - _, 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) + _, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,Relay,WGPubKey) VALUES(?,?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.WGPubKey) return err } @@ -376,7 +376,7 @@ func Peer_Update( return err } - 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) + result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,Addr4=?,Port4=?,Addr6=?,Port6=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -404,7 +404,7 @@ func Peer_UpdateFull( return err } - 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) + result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,Addr4=?,Port4=?,Addr6=?,Port6=?,Relay=?,WGPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.WGPubKey, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -456,8 +456,8 @@ func Peer_Get( err error, ) { row = &Peer{} - 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 { + r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,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.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey); err != nil { row = nil } return @@ -473,7 +473,7 @@ func Peer_GetWhere( ) { row = &Peer{} r := tx.QueryRow(query, args...) - 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 { + if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey); err != nil { row = nil } return @@ -495,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.PublicIP1, &row.Port1, &row.PublicIP2, &row.Port2, &row.Relay, &row.WGPubKey) + err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey) if !yield(row, err) { return } diff --git a/hub/api/db/sanitize-validate.go b/hub/api/db/sanitize-validate.go index 869633d..f42dcfa 100644 --- a/hub/api/db/sanitize-validate.go +++ b/hub/api/db/sanitize-validate.go @@ -59,39 +59,39 @@ func Network_Validate(c *Network) error { func Peer_Sanitize(p *Peer) { p.Name = strings.TrimSpace(p.Name) - if len(p.PublicIP1) != 0 { - if addr, ok := netip.AddrFromSlice(p.PublicIP1); ok { - p.PublicIP1 = addr.AsSlice() + if len(p.Addr4) != 0 { + if addr, ok := netip.AddrFromSlice(p.Addr4); ok { + p.Addr4 = addr.AsSlice() } } - if len(p.PublicIP2) != 0 { - if addr, ok := netip.AddrFromSlice(p.PublicIP2); ok { - p.PublicIP2 = addr.AsSlice() + if len(p.Addr6) != 0 { + if addr, ok := netip.AddrFromSlice(p.Addr6); ok { + p.Addr6 = addr.AsSlice() } } - if p.Port1 == 0 { - p.Port1 = 456 + if p.Port4 == 0 { + p.Port4 = 456 } - if len(p.PublicIP2) != 0 && p.Port2 == 0 { - p.Port2 = 456 + if len(p.Addr6) != 0 && p.Port6 == 0 { + p.Port6 = 456 } } func Peer_Validate(p *Peer) error { - if len(p.PublicIP1) > 0 { - if _, ok := netip.AddrFromSlice(p.PublicIP1); !ok { + if len(p.Addr4) > 0 { + if _, ok := netip.AddrFromSlice(p.Addr4); !ok { return ErrInvalidIP } } - if len(p.PublicIP2) > 0 { - if _, ok := netip.AddrFromSlice(p.PublicIP2); !ok { + if len(p.Addr6) > 0 { + if _, ok := netip.AddrFromSlice(p.Addr6); !ok { return ErrInvalidIP } - if p.Port2 == 0 { + if p.Port6 == 0 { return ErrInvalidPort } } - if p.Port1 == 0 { + if p.Port4 == 0 { return ErrInvalidPort } diff --git a/hub/api/db/tables.defs b/hub/api/db/tables.defs index 7e22294..2ec6527 100644 --- a/hub/api/db/tables.defs +++ b/hub/api/db/tables.defs @@ -15,10 +15,10 @@ TABLE peers OF Peer ( Version int64, APIKey string NoUpdate, Name string, - PublicIP1 []byte, - Port1 uint16, - PublicIP2 []byte, - Port2 uint16, + Addr4 []byte, + Port4 uint16, + Addr6 []byte, + Port6 uint16, Relay bool, WGPubKey []byte NoUpdate ); diff --git a/hub/api/migrations/2024-11-30-init.sql b/hub/api/migrations/2024-11-30-init.sql index d02d02a..b5a337f 100644 --- a/hub/api/migrations/2024-11-30-init.sql +++ b/hub/api/migrations/2024-11-30-init.sql @@ -15,10 +15,10 @@ 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. - PublicIP1 BLOB NOT NULL, - Port1 INTEGER NOT NULL, - PublicIP2 BLOB NOT NULL, - Port2 INTEGER NOT NULL, + Addr4 BLOB NOT NULL, + Port4 INTEGER NOT NULL, + Addr6 BLOB NOT NULL, + Port6 INTEGER NOT NULL, Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. WGPubKey BLOB NOT NULL, PRIMARY KEY(NetworkID, PeerIP) diff --git a/hub/handlers.go b/hub/handlers.go index 565f179..bb7fcb1 100644 --- a/hub/handlers.go +++ b/hub/handlers.go @@ -142,27 +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 ip1Str, ip2Str string + var addr4Str, addr6Str string p := &api.Peer{} err := webutil.NewFormScanner(r.Form). Scan("NetworkID", &p.NetworkID). Scan("IP", &p.PeerIP). Scan("Name", &p.Name). - Scan("PublicIP1", &ip1Str). - Scan("Port1", &p.Port1). - Scan("PublicIP2", &ip2Str). - Scan("Port2", &p.Port2). + Scan("Addr4", &addr4Str). + Scan("Port4", &p.Port4). + Scan("Addr6", &addr6Str). + Scan("Port6", &p.Port6). Scan("Relay", &p.Relay). Error() if err != nil { return err } - if p.PublicIP1, err = stringToIP(ip1Str); err != nil { + if p.Addr4, err = stringToIP(addr4Str); err != nil { return err } - if p.PublicIP2, err = stringToIP(ip2Str); err != nil { + if p.Addr6, err = stringToIP(addr6Str); err != nil { return err } @@ -204,24 +204,24 @@ func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *htt return err } - var ip1Str, ip2Str string + var addr4Str, addr6Str string err = webutil.NewFormScanner(r.Form). Scan("Name", &peer.Name). - Scan("PublicIP1", &ip1Str). - Scan("Port1", &peer.Port1). - Scan("PublicIP2", &ip2Str). - Scan("Port2", &peer.Port2). + Scan("Addr4", &addr4Str). + Scan("Port4", &peer.Port4). + Scan("Addr6", &addr6Str). + Scan("Port6", &peer.Port6). Scan("Relay", &peer.Relay). Error() if err != nil { return err } - if peer.PublicIP1, err = stringToIP(ip1Str); err != nil { + if peer.Addr4, err = stringToIP(addr4Str); err != nil { return err } - if peer.PublicIP2, err = stringToIP(ip2Str); err != nil { + if peer.Addr6, err = stringToIP(addr6Str); err != nil { return err } @@ -355,15 +355,15 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) { for _, p := range l { if len(p.WGPubKey) != 0 { peers[p.PeerIP] = &m.Peer{ - PeerIP: p.PeerIP, - Version: p.Version, - Name: p.Name, - PublicIP1: p.PublicIP1, - Port1: p.Port1, - PublicIP2: p.PublicIP2, - Port2: p.Port2, - Relay: p.Relay, - WGPubKey: p.WGPubKey, + PeerIP: p.PeerIP, + Version: p.Version, + Name: p.Name, + Addr4: p.Addr4, + Port4: p.Port4, + Addr6: p.Addr6, + Port6: p.Port6, + Relay: p.Relay, + WGPubKey: p.WGPubKey, } } } diff --git a/m/models.go b/m/models.go index eabfce2..2a79d10 100644 --- a/m/models.go +++ b/m/models.go @@ -12,15 +12,15 @@ type PeerInitResp struct { } type Peer struct { - PeerIP byte - Version int64 - Name string - PublicIP1 []byte - Port1 uint16 - PublicIP2 []byte - Port2 uint16 - Relay bool - WGPubKey []byte + PeerIP byte + Version int64 + Name string + Addr4 []byte + Port4 uint16 + Addr6 []byte + Port6 uint16 + Relay bool + WGPubKey []byte } type NetworkState struct { diff --git a/peer/app.go b/peer/app.go index cd021f7..90d123d 100644 --- a/peer/app.go +++ b/peer/app.go @@ -1,7 +1,6 @@ package peer import ( - "net" "net/netip" "os" "os/signal" @@ -56,7 +55,7 @@ type App struct { // Infrastructure dev WGDevice - controlConn *net.UDPConn + controlConn ControlConn // Peer state relay *Peer @@ -67,9 +66,6 @@ type App struct { selfV4 netip.AddrPort selfV6 netip.AddrPort - // Monotonically increasing ID for outbound pings (client role only) - nextPingID int64 // TODO: Remove - // Event channels fed by background goroutines hubAddCh <-chan HubPeer hubRemoveCh <-chan wgtypes.Key diff --git a/peer/app_test.go b/peer/app_test.go index e49a35d..2825c54 100644 --- a/peer/app_test.go +++ b/peer/app_test.go @@ -25,10 +25,10 @@ func addRelayPeer(t *testing.T, a *App, vpnIP string, ep netip.AddrPort) *Peer { return p } -// newTestApp returns a minimal App wired to a fakeWGDevice. +// newTestApp returns a minimal App wired to a fakeWGDevice and fakeControlConn. // vpnIP is the local VPN address (e.g. "10.0.0.1"). // isPublic / isRelay describe the local node's role. -func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fakeWGDevice) { +func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fakeWGDevice, *fakeControlConn) { t.Helper() privKey, err := wgtypes.GeneratePrivateKey() if err != nil { @@ -36,6 +36,7 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake } ip := netip.MustParseAddr(vpnIP) dev := &fakeWGDevice{} + cc := &fakeControlConn{} a := &App{ vpnIP: ip, vpnNet: netip.MustParsePrefix("10.0.0.0/24"), @@ -44,6 +45,7 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake isPublic: isPublic, isRelay: isRelay, dev: dev, + controlConn: cc, peersByKey: make(map[wgtypes.Key]*Peer), peersByIP: make(map[netip.Addr]*Peer), hubAddCh: make(chan HubPeer), @@ -51,5 +53,5 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake pingCh: make(chan PingEvent), multicastCh: make(chan MulticastEvent), } - return a, dev + return a, dev, cc } diff --git a/peer/control.go b/peer/control.go deleted file mode 100644 index cb929a7..0000000 --- a/peer/control.go +++ /dev/null @@ -1,150 +0,0 @@ -package peer - -import ( - "encoding/binary" - "fmt" - "log" - "net" - "net/netip" - - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" -) - -const ( - controlPort = uint16(4561) - controlMsgLen = 7 // 1 type + 4 IPv4 + 2 port - msgYourEndpt = uint8(1) - msgMyEndpt = uint8(2) -) - -type ControlServer struct { - localPeerIP byte - network []byte - conn *net.UDPConn - hp *HolePunch - netName string -} - -func NewControlServer(g Globals, hp *HolePunch, netName string) (*ControlServer, error) { - vpnIP := netip.AddrFrom4([4]byte{ - g.Network[0], g.Network[1], g.Network[2], g.LocalPeerIP, - }) - listenAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(vpnIP, controlPort)) - conn, err := net.ListenUDP("udp4", listenAddr) - if err != nil { - return nil, fmt.Errorf("listen on control port: %w", err) - } - return &ControlServer{ - localPeerIP: g.LocalPeerIP, - network: g.Network, - conn: conn, - hp: hp, - netName: netName, - }, nil -} - -func (cs *ControlServer) Run() { - buf := make([]byte, 64) - for { - n, src, err := cs.conn.ReadFromUDPAddrPort(buf) - if err != nil { - log.Printf("[Control] read: %v", err) - continue - } - if n < controlMsgLen { - continue - } - cs.handle(buf[:n], src) - } -} - -func (cs *ControlServer) handle(msg []byte, src netip.AddrPort) { - msgType, ep, ok := decodeControlMsg(msg) - if !ok { - return - } - switch msgType { - case msgYourEndpt: - cs.onYourEndpoint(ep) - case msgMyEndpt: - cs.onMyEndpoint(src, ep) - } -} - -// onYourEndpoint is called when the relay tells us our external WG endpoint. -// We broadcast MsgMyEndpoint to all known peers so they can attempt direct -// connections to us. -func (cs *ControlServer) onYourEndpoint(ourEndpoint netip.AddrPort) { - log.Printf("[Control] external endpoint: %v", ourEndpoint) - - state, err := loadNetworkState(cs.netName) - if err != nil { - log.Printf("[Control] load state: %v", err) - return - } - - for _, p := range state.Peers { - if p == nil || p.PeerIP == cs.localPeerIP || len(p.WGPubKey) != wgtypes.KeyLen { - continue - } - peerVPNIP := netip.AddrFrom4([4]byte{ - cs.network[0], cs.network[1], cs.network[2], p.PeerIP, - }) - cs.sendMsg(msgMyEndpt, peerVPNIP, ourEndpoint) - } -} - -// onMyEndpoint is called when a peer tells us their external WG endpoint. -// We start a probe-before-commit attempt to reach them directly. -func (cs *ControlServer) onMyEndpoint(src netip.AddrPort, theirEndpoint netip.AddrPort) { - peerIPByte := src.Addr().As4()[3] - - state, err := loadNetworkState(cs.netName) - if err != nil { - log.Printf("[Control] load state: %v", err) - return - } - - peer := state.Peers[peerIPByte] - if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen { - return - } - - pubKey, err := wgtypes.NewKey(peer.WGPubKey) - if err != nil { - return - } - - cs.hp.OnEndpointLearned(peerIPByte, pubKey, theirEndpoint, false) -} - -// SendYourEndpoint sends MsgYourEndpoint to a peer, informing them of their -// own external WG endpoint. Used by the relay's endpoint reporter (Phase 6). -func (cs *ControlServer) SendYourEndpoint(peerVPNIP netip.Addr, theirEndpoint netip.AddrPort) { - cs.sendMsg(msgYourEndpt, peerVPNIP, theirEndpoint) -} - -func (cs *ControlServer) sendMsg(msgType uint8, peerVPNIP netip.Addr, ep netip.AddrPort) { - dst := net.UDPAddrFromAddrPort(netip.AddrPortFrom(peerVPNIP, controlPort)) - if _, err := cs.conn.WriteTo(encodeControlMsg(msgType, ep), dst); err != nil { - log.Printf("[Control] send to %v: %v", peerVPNIP, err) - } -} - -func encodeControlMsg(msgType uint8, ep netip.AddrPort) []byte { - msg := make([]byte, controlMsgLen) - msg[0] = msgType - a4 := ep.Addr().Unmap().As4() - copy(msg[1:5], a4[:]) - binary.BigEndian.PutUint16(msg[5:7], ep.Port()) - return msg -} - -func decodeControlMsg(msg []byte) (msgType uint8, ep netip.AddrPort, ok bool) { - if len(msg) < controlMsgLen { - return 0, netip.AddrPort{}, false - } - ip := netip.AddrFrom4([4]byte(msg[1:5])) - port := binary.BigEndian.Uint16(msg[5:7]) - return msg[0], netip.AddrPortFrom(ip, port), true -} diff --git a/peer/control/ping.go b/peer/control/ping.go index 1809e31..eaedb71 100644 --- a/peer/control/ping.go +++ b/peer/control/ping.go @@ -11,14 +11,14 @@ import ( const ( version = 1 - Size = 59 // 1 version + 8 ID + 8 PingTS + 6 SrcV4 + 18 SrcV6 + 18 Dst + Size = 51 // 1 version + 8 PingTS + 6 SrcV4 + 18 SrcV6 + 18 Dst ) // Ping is the single control packet type exchanged between VPN peers. // -// In each peer pair, the peer with the lower VPN IP is the client: it sets ID -// and PingTS and sends pings on a timer. The server echoes ID and PingTS back -// in its response, allowing the client to compute RTT = now - PingTS. +// In each peer pair, the peer with the lower VPN IP is the client: it sets +// PingTS and sends pings on a timer. The server echoes PingTS back in its +// response, allowing the client to compute RTT = now - PingTS. // // Both client and server populate SrcV4, SrcV6, and Dst on every packet so // endpoint information flows in both directions. @@ -27,50 +27,47 @@ const ( // WireGuard handshake source. Zero if the sender has not observed a handshake // from the recipient. type Ping struct { - ID int64 // Client ping ID. PingTS int64 // Client ping send time in nanoseconds. SrcV4 netip.AddrPort // Sender's discovered IPv4 address and port. SrcV6 netip.AddrPort // Sender's discovered IPv6 address and port. Dst netip.AddrPort } -// Marshal encodes p into a fixed-size 59-byte array. +// Marshal encodes p into a fixed-size 51-byte array. func (p Ping) Marshal() [Size]byte { var buf [Size]byte buf[0] = version - binary.BigEndian.PutUint64(buf[1:9], uint64(p.ID)) - binary.BigEndian.PutUint64(buf[9:17], uint64(p.PingTS)) + binary.BigEndian.PutUint64(buf[1:9], uint64(p.PingTS)) if p.SrcV4.IsValid() { a4 := p.SrcV4.Addr().As4() - copy(buf[17:21], a4[:]) - binary.BigEndian.PutUint16(buf[21:23], p.SrcV4.Port()) + copy(buf[9:13], a4[:]) + binary.BigEndian.PutUint16(buf[13:15], p.SrcV4.Port()) } a16 := p.SrcV6.Addr().As16() - copy(buf[23:39], a16[:]) - binary.BigEndian.PutUint16(buf[39:41], p.SrcV6.Port()) + copy(buf[15:31], a16[:]) + binary.BigEndian.PutUint16(buf[31:33], p.SrcV6.Port()) a16 = p.Dst.Addr().As16() - copy(buf[41:57], a16[:]) - binary.BigEndian.PutUint16(buf[57:59], p.Dst.Port()) + copy(buf[33:49], a16[:]) + binary.BigEndian.PutUint16(buf[49:51], p.Dst.Port()) return buf } -// Unmarshal decodes a Ping from a fixed-size 59-byte array. +// Unmarshal decodes a Ping from a fixed-size 51-byte array. func Unmarshal(buf [Size]byte) (Ping, error) { if buf[0] != version { return Ping{}, fmt.Errorf("unknown ping version %d", buf[0]) } p := Ping{ - ID: int64(binary.BigEndian.Uint64(buf[1:9])), - PingTS: int64(binary.BigEndian.Uint64(buf[9:17])), + PingTS: int64(binary.BigEndian.Uint64(buf[1:9])), } - if addr := netip.AddrFrom4([4]byte(buf[17:21])); !addr.IsUnspecified() { - p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[21:23])) + if addr := netip.AddrFrom4([4]byte(buf[9:13])); !addr.IsUnspecified() { + p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[13:15])) } - if addr := netip.AddrFrom16([16]byte(buf[23:39])); !addr.IsUnspecified() { - p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[39:41])) + if addr := netip.AddrFrom16([16]byte(buf[15:31])); !addr.IsUnspecified() { + p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[31:33])) } - if addr := netip.AddrFrom16([16]byte(buf[41:57])).Unmap(); !addr.IsUnspecified() { - p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[57:59])) + if addr := netip.AddrFrom16([16]byte(buf[33:49])).Unmap(); !addr.IsUnspecified() { + p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[49:51])) } return p, nil } diff --git a/peer/control/ping_test.go b/peer/control/ping_test.go index 5c9696a..9193cdf 100644 --- a/peer/control/ping_test.go +++ b/peer/control/ping_test.go @@ -19,7 +19,6 @@ func TestRoundTrip(t *testing.T) { { name: "client ping", ping: control.Ping{ - ID: 42, PingTS: 1234567890, SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"), Dst: netip.MustParseAddrPort("5.6.7.8:51820"), @@ -28,7 +27,6 @@ func TestRoundTrip(t *testing.T) { { name: "server response", ping: control.Ping{ - ID: 42, PingTS: 1234567890, SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"), Dst: netip.MustParseAddrPort("1.2.3.4:9999"), @@ -37,7 +35,6 @@ func TestRoundTrip(t *testing.T) { { name: "IPv6 only", ping: control.Ping{ - ID: 1, PingTS: 999, SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"), Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"), @@ -46,7 +43,6 @@ func TestRoundTrip(t *testing.T) { { name: "dual stack", ping: control.Ping{ - ID: 7, PingTS: 555, SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"), SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"), @@ -56,7 +52,6 @@ func TestRoundTrip(t *testing.T) { { name: "no src known", ping: control.Ping{ - ID: 3, Dst: netip.MustParseAddrPort("5.6.7.8:51820"), }, }, diff --git a/peer/control_conn.go b/peer/control_conn.go new file mode 100644 index 0000000..50f762a --- /dev/null +++ b/peer/control_conn.go @@ -0,0 +1,61 @@ +package peer + +import ( + "log" + "net" + "net/netip" + + "vppn/peer/control" +) + +var _ ControlConn = (*udpControlConn)(nil) + +type udpControlConn struct { + conn *net.UDPConn +} + +// newUDPControlConn opens a UDP socket bound to localIP:port. +func newUDPControlConn(localIP netip.Addr, port uint16) (*udpControlConn, error) { + addr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(localIP, port)) + conn, err := net.ListenUDP("udp4", addr) + if err != nil { + return nil, err + } + return &udpControlConn{conn: conn}, nil +} + +func (c *udpControlConn) SendPing(dst netip.AddrPort, ping control.Ping) error { + buf := ping.Marshal() + _, err := c.conn.WriteToUDP(buf[:], net.UDPAddrFromAddrPort(dst)) + return err +} + +// run reads incoming ping packets and forwards them to ch until ctx is done. +// Call this in a goroutine before starting the App event loop. +func (c *udpControlConn) run(ch chan<- PingEvent) { + var buf [control.Size]byte + for { + n, src, err := c.conn.ReadFromUDP(buf[:]) + if err != nil { + log.Printf("control read: %v", err) + continue + } + + if n != control.Size { + continue + } + + ping, err := control.Unmarshal(buf) + if err != nil { + log.Printf("control unmarshal: %v", err) + continue + } + + srcIP, ok := netip.AddrFromSlice(src.IP) + if !ok { + continue + } + + ch <- PingEvent{srcVPNIP: srcIP.Unmap(), ping: ping} + } +} diff --git a/peer/crypto.go b/peer/crypto.go deleted file mode 100644 index b68bfd4..0000000 --- a/peer/crypto.go +++ /dev/null @@ -1,15 +0,0 @@ -package peer - -import ( - "log" - - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" -) - -func generateWGKey() wgtypes.Key { - key, err := wgtypes.GeneratePrivateKey() - if err != nil { - log.Fatalf("Failed to generate WireGuard private key: %v", err) - } - return key -} diff --git a/peer/device.go b/peer/device.go index cc2d8c0..e175278 100644 --- a/peer/device.go +++ b/peer/device.go @@ -1,12 +1,34 @@ package peer import ( + "errors" "log" "net/netip" + "syscall" + "time" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +// devRetry calls fn up to 6 times with exponential backoff, retrying on EBUSY +// (transient netlink contention during WireGuard handshake/rekey). Fatal on any other error. +func devRetry(vpnIP netip.Addr, op string, fn func() error) { + const attempts = 6 + timeout := 10 * time.Millisecond + for i := range attempts { + err := fn() + if err == nil { + return + } + if errors.Is(err, syscall.EBUSY) && i < attempts-1 { + time.Sleep(timeout) + timeout *= 2 + continue + } + log.Fatalf("%s %v: %v", op, vpnIP, err) + } +} + func (a *App) devPeers() []wgtypes.Peer { peers, err := a.dev.Peers() if err != nil { @@ -16,37 +38,25 @@ func (a *App) devPeers() []wgtypes.Peer { } func (a *App) devAddPeer(p *Peer) { - if err := a.dev.AddPeer(p.PubKey()); err != nil { - log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err) - } + devRetry(p.VPNIP, "AddPeer", func() error { return a.dev.AddPeer(p.PubKey()) }) } func (a *App) devAddDirect(p *Peer, endpoint netip.AddrPort) { - if err := a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP); err != nil { - log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err) - } + devRetry(p.VPNIP, "AddDirect", func() error { return a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP) }) } func (a *App) devSetRelay(p *Peer, endpoint netip.AddrPort) { - if err := a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet); err != nil { - log.Fatalf("Failed to add relay %v: %v", p.VPNIP, err) - } + devRetry(p.VPNIP, "SetRelay", func() error { return a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet) }) } func (a *App) devPromote(p *Peer) { - if err := a.dev.Promote(p.PubKey(), p.VPNIP); err != nil { - log.Fatalf("Failed to promote peer %v: %v", p.VPNIP, err) - } + devRetry(p.VPNIP, "Promote", func() error { return a.dev.Promote(p.PubKey(), p.VPNIP) }) } func (a *App) devAddProbe(p *Peer, endpoint netip.AddrPort) { - if err := a.dev.AddProbe(p.PubKey(), endpoint); err != nil { - log.Fatalf("Failed to add probe %v: %v", p.VPNIP, err) - } + devRetry(p.VPNIP, "AddProbe", func() error { return a.dev.AddProbe(p.PubKey(), endpoint) }) } func (a *App) devRemove(p *Peer) { - if err := a.dev.RemovePeer(p.PubKey()); err != nil { - log.Fatalf("Failed to remove peer %v: %v", p.VPNIP, err) - } + devRetry(p.VPNIP, "RemovePeer", func() error { return a.dev.RemovePeer(p.PubKey()) }) } diff --git a/peer/endpointreporter.go b/peer/endpointreporter.go deleted file mode 100644 index fe626b7..0000000 --- a/peer/endpointreporter.go +++ /dev/null @@ -1,88 +0,0 @@ -package peer - -import ( - "log" - "net/netip" - "time" - - "golang.zx2c4.com/wireguard/wgctrl" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" -) - -const reporterInterval = 5 * time.Second - -// EndpointReporter runs on relay peers only. It polls wgctrl every 5s and, -// for each peer whose LastHandshakeTime has changed, sends MsgYourEndpoint to -// that peer's VPN IP so it learns its own external WG endpoint. -type EndpointReporter struct { - client *wgctrl.Client - devName string - network []byte - netName string - control *ControlServer - lastTimes map[wgtypes.Key]time.Time -} - -func NewEndpointReporter(g Globals, cs *ControlServer, netName string) *EndpointReporter { - return &EndpointReporter{ - client: g.WGClient, - devName: g.WGDevName, - network: g.Network, - netName: netName, - control: cs, - lastTimes: make(map[wgtypes.Key]time.Time), - } -} - -func (er *EndpointReporter) Run() { - for range time.Tick(reporterInterval) { - er.poll() - } -} - -func (er *EndpointReporter) poll() { - dev, err := er.client.Device(er.devName) - if err != nil { - log.Printf("[EndpointReporter] get device: %v", err) - return - } - - state, err := loadNetworkState(er.netName) - if err != nil { - log.Printf("[EndpointReporter] load state: %v", err) - return - } - - // Build WGPubKey → VPN IP byte index from current network state. - keyToIP := make(map[wgtypes.Key]byte, len(dev.Peers)) - for _, p := range state.Peers { - if p == nil || len(p.WGPubKey) != wgtypes.KeyLen { - continue - } - key, err := wgtypes.NewKey(p.WGPubKey) - if err == nil { - keyToIP[key] = p.PeerIP - } - } - - for _, p := range dev.Peers { - if p.Endpoint == nil || p.LastHandshakeTime.IsZero() { - continue - } - if p.LastHandshakeTime == er.lastTimes[p.PublicKey] { - continue - } - - peerIPByte, ok := keyToIP[p.PublicKey] - if !ok { - continue - } - - er.lastTimes[p.PublicKey] = p.LastHandshakeTime - - peerVPNIP := netip.AddrFrom4([4]byte{ - er.network[0], er.network[1], er.network[2], peerIPByte, - }) - er.control.SendYourEndpoint(peerVPNIP, p.Endpoint.AddrPort()) - } -} diff --git a/peer/fake_control_conn_test.go b/peer/fake_control_conn_test.go new file mode 100644 index 0000000..f50a67d --- /dev/null +++ b/peer/fake_control_conn_test.go @@ -0,0 +1,43 @@ +package peer + +import ( + "net/netip" + "testing" + + "vppn/peer/control" +) + +type sentPing struct { + Dst netip.AddrPort + Ping control.Ping +} + +type fakeControlConn struct { + Sent []sentPing +} + +func (f *fakeControlConn) SendPing(dst netip.AddrPort, ping control.Ping) error { + f.Sent = append(f.Sent, sentPing{Dst: dst, Ping: ping}) + return nil +} + +func (f *fakeControlConn) AssertNone(t *testing.T) { + t.Helper() + if len(f.Sent) != 0 { + t.Fatalf("expected no pings sent, got %d: %v", len(f.Sent), f.Sent) + } +} + +func (f *fakeControlConn) AssertSent(t *testing.T, i int, dst netip.AddrPort, ping control.Ping) { + t.Helper() + if i >= len(f.Sent) { + t.Fatalf("no ping at index %d (have %d)", i, len(f.Sent)) + } + got := f.Sent[i] + if got.Dst != dst { + t.Errorf("ping[%d].Dst = %v, want %v", i, got.Dst, dst) + } + if got.Ping != ping { + t.Errorf("ping[%d].Ping = %+v, want %+v", i, got.Ping, ping) + } +} diff --git a/peer/files.go b/peer/files.go index 7e18880..4d1d5c9 100644 --- a/peer/files.go +++ b/peer/files.go @@ -1,3 +1,5 @@ +//go:build ignore + package peer import ( @@ -48,7 +50,7 @@ func storeJson(x any, outPath string) error { return err } - f, err := os.Create(tmpPath) + f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } @@ -94,4 +96,3 @@ func loadPeerConfig(netName string) (pc LocalConfig, err error) { func loadNetworkState(netName string) (ps m.NetworkState, err error) { return ps, loadJson(peerStatePath(netName), &ps) } - diff --git a/peer/files_test.go b/peer/files_test.go deleted file mode 100644 index 5a7f334..0000000 --- a/peer/files_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package peer - -import ( - "path/filepath" - "reflect" - "testing" -) - -func TestFilePaths(t *testing.T) { - confDir := configDir("netName") - if filepath.Base(confDir) != "netName" { - t.Fatal(confDir) - } - if filepath.Base(filepath.Dir(confDir)) != ".vppn" { - t.Fatal(confDir) - } - - path := peerConfigPath("netName") - if path != filepath.Join(confDir, "config.json") { - t.Fatal(path) - } - - path = peerStatePath("netName") - if path != filepath.Join(confDir, "state.json") { - t.Fatal(path) - } -} - -func TestStoreLoadJson(t *testing.T) { - type Object struct { - Name string - Age int - Price float64 - } - - tmpDir := t.TempDir() - outPath := filepath.Join(tmpDir, "object.json") - - obj := Object{ - Name: "Jason", - Age: 22, - Price: 123.534, - } - - if err := storeJson(obj, outPath); err != nil { - t.Fatal(err) - } - - obj2 := Object{} - if err := loadJson(outPath, &obj2); err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(obj, obj2) { - t.Fatal(obj, obj2) - } -} diff --git a/peer/globals.go b/peer/globals.go index 52d4105..edf6dea 100644 --- a/peer/globals.go +++ b/peer/globals.go @@ -1,3 +1,5 @@ +//go:build ignore + package peer import ( diff --git a/peer/holepunch.go b/peer/holepunch.go index 2b7e96e..c2565e6 100644 --- a/peer/holepunch.go +++ b/peer/holepunch.go @@ -1,3 +1,5 @@ +//go:build ignore + package peer import ( diff --git a/peer/hub_poller.go b/peer/hub_poller.go new file mode 100644 index 0000000..3524930 --- /dev/null +++ b/peer/hub_poller.go @@ -0,0 +1,149 @@ +package peer + +import ( + "encoding/json" + "io" + "log" + "net/http" + "net/netip" + "net/url" + "time" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "vppn/m" +) + +const hubPollInterval = 64 * time.Second + +type HubPoller struct { + selfVPNIP netip.Addr + vpnNet netip.Prefix + hubURL string + apiKey string + addCh chan<- HubPeer + removeCh chan<- wgtypes.Key + known map[wgtypes.Key]int64 // pubKey → last seen version +} + +func NewHubPoller( + selfVPNIP netip.Addr, + vpnNet netip.Prefix, + hubURL, apiKey string, + addCh chan<- HubPeer, + removeCh chan<- wgtypes.Key, +) (*HubPoller, error) { + u, err := url.Parse(hubURL) + if err != nil { + return nil, err + } + u.Path = "/peer/fetch-state/" + + return &HubPoller{ + selfVPNIP: selfVPNIP, + vpnNet: vpnNet, + hubURL: u.String(), + apiKey: apiKey, + addCh: addCh, + removeCh: removeCh, + known: make(map[wgtypes.Key]int64), + }, nil +} + +func (hp *HubPoller) Run() { + hp.poll() + for range time.Tick(hubPollInterval) { + hp.poll() + } +} + +func (hp *HubPoller) poll() { + req, err := http.NewRequest(http.MethodGet, hp.hubURL, nil) + if err != nil { + log.Printf("[HubPoller] build request: %v", err) + return + } + req.SetBasicAuth("", hp.apiKey) + + client := &http.Client{Timeout: 32 * time.Second} + resp, err := client.Do(req) + if err != nil { + log.Printf("[HubPoller] fetch: %v", err) + return + } + body, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + log.Printf("[HubPoller] read body: %v", err) + return + } + + var state m.NetworkState + if err := json.Unmarshal(body, &state); err != nil { + log.Printf("[HubPoller] unmarshal: %v", err) + return + } + + hp.apply(state) +} + +func (hp *HubPoller) apply(state m.NetworkState) { + seen := make(map[wgtypes.Key]struct{}, len(hp.known)) + + netAddr := hp.vpnNet.Addr().As4() + + for _, p := range state.Peers { + if p == nil || len(p.WGPubKey) != wgtypes.KeyLen { + continue + } + + pubKey, err := wgtypes.NewKey(p.WGPubKey) + if err != nil { + continue + } + + octets := netAddr + octets[3] = p.PeerIP + vpnIP := netip.AddrFrom4(octets) + if vpnIP == hp.selfVPNIP { + continue + } + + seen[pubKey] = struct{}{} + + if v, ok := hp.known[pubKey]; ok && v == p.Version { + continue + } + hp.known[pubKey] = p.Version + hp.addCh <- hubPeerFrom(pubKey, vpnIP, p) + } + + for key := range hp.known { + if _, ok := seen[key]; !ok { + delete(hp.known, key) + hp.removeCh <- key + } + } +} + +func hubPeerFrom(pubKey wgtypes.Key, vpnIP netip.Addr, p *m.Peer) HubPeer { + var ep4, ep6 netip.AddrPort + if len(p.Addr4) > 0 { + if addr, ok := netip.AddrFromSlice(p.Addr4); ok { + ep4 = netip.AddrPortFrom(addr.Unmap(), p.Port4) + } + } + if len(p.Addr6) > 0 { + if addr, ok := netip.AddrFromSlice(p.Addr6); ok { + ep6 = netip.AddrPortFrom(addr, p.Port6) + } + } + return HubPeer{ + PubKey: pubKey, + VPNIP: vpnIP, + IsRelay: p.Relay, + IsPublic: ep4.IsValid() || ep6.IsValid(), + EndpointV4: ep4, + EndpointV6: ep6, + } +} diff --git a/peer/hubpoller.go b/peer/hubpoller.go index 9293c9b..b5bcc07 100644 --- a/peer/hubpoller.go +++ b/peer/hubpoller.go @@ -1,3 +1,5 @@ +//go:build ignore + package peer import ( @@ -14,17 +16,13 @@ import ( ) type HubPoller struct { - Globals - holePunch *HolePunch - client *http.Client - req *http.Request - versions [256]int64 - netName string + client *http.Client + req *http.Request + versions [256]int64 + netName string } func NewHubPoller( - g Globals, - hp *HolePunch, netName, hubURL, apiKey string, @@ -118,7 +116,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) { if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen { return } - if len(peer.PublicIP1) == 0 || peer.Port1 == 0 { + if len(peer.Addr4) == 0 || peer.Port4 == 0 { return } @@ -128,12 +126,12 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) { return } - ip, ok := netip.AddrFromSlice(peer.PublicIP1) + ip, ok := netip.AddrFromSlice(peer.Addr4) if !ok { hp.logf("Invalid public IP for peer %d", peer.PeerIP) return } - endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port1) + endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port4) if peer.Relay { if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil { diff --git a/peer/interfaces.go b/peer/interfaces.go index 7aceaa7..384e7a7 100644 --- a/peer/interfaces.go +++ b/peer/interfaces.go @@ -2,6 +2,7 @@ package peer import ( "net/netip" + "vppn/peer/control" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) @@ -17,3 +18,9 @@ type WGDevice interface { Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error RemovePeer(pubKey wgtypes.Key) error } + +// ControlConn sends pings to peers over the VPN control port. +// Reading is handled separately via run, which feeds the App's pingCh. +type ControlConn interface { + SendPing(dst netip.AddrPort, ping control.Ping) error +} diff --git a/peer/main.go b/peer/main.go deleted file mode 100644 index 53c1bf8..0000000 --- a/peer/main.go +++ /dev/null @@ -1,209 +0,0 @@ -package peer - -import ( - "encoding/json" - "fmt" - "log" - "net" - "net/http" - "net/netip" - "os" - "time" -) - -// Usage: -// -// vppn netName run -// vppn netName status -func Main2() { - printUsage := func() { - fmt.Fprintf(os.Stderr, `%s COMMAND [ARGUMENTS...] - -Available commands: - run - status - hosts -`, os.Args[0]) - os.Exit(1) - } - - if len(os.Args) < 2 { - printUsage() - } - - command := os.Args[1] - - switch command { - case "run": - main_run() - case "status": - main_status() - case "hosts": - main_hosts() - default: - printUsage() - } -} - -// ---------------------------------------------------------------------------- - -type mainArgs struct { - NetName string - HubAddress string - APIKey string -} - -func main_run() { - printUsage := func() { - fmt.Fprintf(os.Stderr, `Usage: %s run NETWORK_NAME HUB_ADDRESS API_KEY - - NETWORK_NAME - Unique name of the network interface created. The network name - shouldn't change between invocations of the application. - - HUB_ADDRESS - The address of the hub server. This should also contain the scheme, for - example https://hub.domain.com/. - - API_KEY - The API key assigned to this peer by the hub. - -`, os.Args[0]) - os.Exit(1) - } - - if len(os.Args) != 5 { - printUsage() - } - - args := mainArgs{ - NetName: os.Args[2], - HubAddress: os.Args[3], - APIKey: os.Args[4], - } - - newPeerMain(args).Run() -} - -// ---------------------------------------------------------------------------- - -func main_status() { - printUsage := func() { - fmt.Fprintf(os.Stderr, `Usage: %s status NETWORK_NAME - - NETWORK_NAME - Unique name of the network interface created. - -`, os.Args[0]) - os.Exit(1) - } - - if len(os.Args) != 3 { - printUsage() - } - - netName := os.Args[2] - report := fetchStatusReport(netName) - - fmt.Printf("\n%s Status\n\n", netName) - - if len(report.Network) != 4 { - fmt.Println("ERROR: Network isn't 4 bytes.") - fmt.Printf("Network: %v\n\n", report.Network) - } else { - nw := report.Network - fmt.Printf("%-8s %d.%d.%d.%d\n", "IP", nw[0], nw[1], nw[2], report.LocalPeerIP) - fmt.Printf("%-8s %d.%d.%d.%d/24\n", "Network", nw[0], nw[1], nw[2], nw[3]) - } - - if report.RelayPeerIP != 0 { - fmt.Printf("%-8s %d\n\n", "Relay", report.RelayPeerIP) - } else { - fmt.Printf("%-8s -\n\n", "Relay") - } - - for _, status := range report.Remotes { - fmt.Printf("%3d %s\n", status.PeerIP, status.Name) - fmt.Printf(" %-11s %v\n", "Up", status.Up) - - pubIP, ok := netip.AddrFromSlice(status.PublicIP) - if ok { - fmt.Printf(" %-11s %v\n", "Public IP", pubIP) - } else { - fmt.Printf(" %-11s\n", "Public IP") - } - fmt.Printf(" %-11s %d\n", "Port", status.Port) - fmt.Printf(" %-11s %v\n", "Relay", status.Relay) - fmt.Printf(" %-11s %v\n", "Server", status.Server) - fmt.Printf(" %-11s %v\n", "Direct", status.Direct) - if status.DirectAddr.IsValid() { - fmt.Printf(" %-11s %v\n", "Address", status.DirectAddr) - } - fmt.Println("") - } -} - -// ---------------------------------------------------------------------------- - -func main_hosts() { - printUsage := func() { - fmt.Fprintf(os.Stderr, `Usage: %s hosts NETWORK_NAME - - NETWORK_NAME - Unique name of the network interface created. - -`, os.Args[0]) - os.Exit(1) - } - - if len(os.Args) != 3 { - printUsage() - } - - netName := os.Args[2] - state, err := loadNetworkState(netName) - if err != nil { - log.Fatalf("Failed to load network state: %v", err) - } - - config, err := loadPeerConfig(netName) - if err != nil { - log.Fatalf("Failed to load config: %v", err) - } - - nw := config.Network - for _, peer := range state.Peers { - if peer == nil { - continue - } - fmt.Printf("%d.%d.%d.%d %s\n", - nw[0], nw[1], nw[2], peer.PeerIP, peer.Name) - } - fmt.Println("") -} - -// ---------------------------------------------------------------------------- - -func fetchStatusReport(netName string) StatusReport { - client := http.Client{ - Transport: &http.Transport{ - Dial: func(_, _ string) (net.Conn, error) { - return net.Dial("unix", statusSocketPath(netName)) - }, - }, - Timeout: 8 * time.Second, - } - - getURL := "http://unix" + statusSocketPath(netName) - resp, err := client.Get(getURL) - if err != nil { - log.Fatalf("Failed to get response: %v", err) - } - - report := StatusReport{} - if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { - log.Fatalf("Failed to decode status report: %v", err) - } - - return report -} diff --git a/peer/mcreader.go b/peer/mcreader.go index 88707ff..42c51bb 100644 --- a/peer/mcreader.go +++ b/peer/mcreader.go @@ -1,3 +1,5 @@ +//go:build ignore + package peer import ( diff --git a/peer/mcwriter.go b/peer/mcwriter.go index b039866..642240b 100644 --- a/peer/mcwriter.go +++ b/peer/mcwriter.go @@ -1,3 +1,5 @@ +//go:build ignore + package peer import ( diff --git a/peer/multicast.go b/peer/multicast.go new file mode 100644 index 0000000..d8e2561 --- /dev/null +++ b/peer/multicast.go @@ -0,0 +1,101 @@ +package peer + +import ( + "encoding/binary" + "fmt" + "log" + "net" + "net/netip" + "time" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +const ( + mcBeaconLen = 35 // 1 VPN IP byte + 32 WG pubkey + 2 WG listen port + mcBroadcastInterval = 32 * time.Second + mcErrorRetryInterval = 16 * time.Second +) + +var mcAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom( + netip.AddrFrom4([4]byte{224, 0, 0, 157}), + 4560)) + +// RunMCWriter broadcasts a beacon on the local multicast group every +// mcBroadcastInterval so that LAN peers can discover our WireGuard endpoint. +func RunMCWriter(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16) { + conn, err := net.ListenMulticastUDP("udp", nil, mcAddr) + if err != nil { + log.Fatalf("[MCWriter] bind: %v", err) + } + + beacon := buildBeacon(selfVPNIP, pubKey, wgPort) + + for range time.Tick(mcBroadcastInterval) { + if _, err := conn.WriteToUDP(beacon, mcAddr); err != nil { + log.Printf("[MCWriter] write: %v", err) + } + } +} + +func buildBeacon(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16) []byte { + beacon := make([]byte, mcBeaconLen) + beacon[0] = selfVPNIP.As4()[3] + copy(beacon[1:33], pubKey[:]) + binary.BigEndian.PutUint16(beacon[33:35], wgPort) + return beacon +} + +// RunMCReader listens for multicast beacons from LAN peers and feeds +// MulticastEvents to ch. +func RunMCReader(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- MulticastEvent) { + for { + if err := runMCReaderInner(vpnNet, selfVPNIP, ch); err != nil { + log.Printf("[MCReader] %v", err) + } + time.Sleep(mcErrorRetryInterval) + } +} + +func runMCReaderInner(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- MulticastEvent) error { + conn, err := net.ListenMulticastUDP("udp", nil, mcAddr) + if err != nil { + return fmt.Errorf("bind: %w", err) + } + defer conn.Close() + + buf := make([]byte, 64) + netAddr := vpnNet.Addr().As4() + + for { + conn.SetReadDeadline(time.Now().Add(32 * time.Second)) + n, src, err := conn.ReadFromUDPAddrPort(buf) + if err != nil { + return fmt.Errorf("read: %w", err) + } + if n != mcBeaconLen { + continue + } + + octets := netAddr + octets[3] = buf[0] + vpnIP := netip.AddrFrom4(octets) + if vpnIP == selfVPNIP { + continue + } + + pubKey, err := wgtypes.NewKey(buf[1:33]) + if err != nil { + continue + } + + wgPort := binary.BigEndian.Uint16(buf[33:35]) + endpoint := netip.AddrPortFrom(src.Addr().Unmap(), wgPort) + + ch <- MulticastEvent{ + pubKey: pubKey, + vpnIP: vpnIP, + endpoint: endpoint, + } + } +} diff --git a/peer/on_hub.go b/peer/on_hub.go index 76b1b6e..ec0de5a 100644 --- a/peer/on_hub.go +++ b/peer/on_hub.go @@ -2,7 +2,9 @@ package peer import ( "log" + "math" "net/netip" + "time" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -19,6 +21,7 @@ func (a *App) onAddPeer(p HubPeer) { IsPublic: p.IsPublic, Endpoint4: p.EndpointV4, Endpoint6: p.EndpointV6, + RTT: time.Duration(math.MaxInt64) * time.Nanosecond, Role: roleFor(a.isPublic, a.vpnIP, p), } @@ -58,7 +61,9 @@ func (a *App) onRemovePeer(key wgtypes.Key) { // switchActiveRelay promotes the lowest-latency relay peer to active. func (a *App) switchActiveRelay() { if a.relay != nil { - a.devAddDirect(a.relay, a.relay.WGEndpoint()) + // If we have a relay, it's public, so should go back to being a direct + // peer - this will convert it's /24 to a /32. + a.devAddDirect(a.relay, a.relay.PreferredEndpoint()) a.relay = nil } @@ -68,7 +73,7 @@ func (a *App) switchActiveRelay() { continue } - if best == nil || betterRelay(p, best) { + if best == nil || p.RTT < best.RTT { best = p } } @@ -77,23 +82,10 @@ func (a *App) switchActiveRelay() { return } - a.devSetRelay(best, best.WGEndpoint()) + a.devSetRelay(best, best.PreferredEndpoint()) a.relay = best } -// TODO: Why not < ?? -// betterRelay reports whether a is a better relay candidate than b. -// Prefers lower RTT; treats zero RTT (no measurement yet) as worst case. -func betterRelay(a, b *Peer) bool { - if a.RTT == 0 { - return false - } - if b.RTT == 0 { - return true - } - return a.RTT < b.RTT -} - func preferredEndpoint(v4, v6 netip.AddrPort) netip.AddrPort { if v4.IsValid() { return v4 diff --git a/peer/on_hub_test.go b/peer/on_hub_test.go index d69ab5a..b02d3db 100644 --- a/peer/on_hub_test.go +++ b/peer/on_hub_test.go @@ -29,7 +29,7 @@ func TestOnAddPeer(t *testing.T) { check func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key) }{ { - name: "non-public peer added in StateRelayed with no dev calls", + name: "non-public peer registered in WG via AddPeer", peer: func(k wgtypes.Key) HubPeer { return HubPeer{PubKey: k, VPNIP: peerVPNIP} }, @@ -41,14 +41,14 @@ func TestOnAddPeer(t *testing.T) { if a.peersByIP[peerVPNIP] == nil { t.Fatal("not in peersByIP") } - if p.State != StateRelayed { - t.Fatalf("state = %v, want StateRelayed", p.State) + if p.State() != StateRelayed { + t.Fatalf("state = %v, want StateRelayed", p.State()) } - dev.AssertNoCalls(t) + dev.AssertAddPeer(t, 0, key) }, }, { - name: "public peer with endpoint goes to StateDirect via AddDirect", + name: "public peer with endpoint registered via AddDirect", peer: func(k wgtypes.Key) HubPeer { return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1} }, @@ -57,13 +57,7 @@ func TestOnAddPeer(t *testing.T) { if p == nil { t.Fatal("not in peersByKey") } - if p.State != StateDirect { - t.Fatalf("state = %v, want StateDirect", p.State) - } - if p.WGEndpoint != ep1 { - t.Fatalf("WGEndpoint = %v, want %v", p.WGEndpoint, ep1) - } - dev.AssertAddDirect(t, 0, p.PubKey, p.WGEndpoint, p.VPNIP) + dev.AssertAddDirect(t, 0, p.PubKey(), ep1, p.VPNIP) }, }, { @@ -104,7 +98,7 @@ func TestOnAddPeer(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - a, dev := newTestApp(t, "10.0.0.1", false, false) + a, dev, _ := newTestApp(t, "10.0.0.1", false, false) key := mustKey(t) if tc.setup != nil { tc.setup(a, key) @@ -138,14 +132,17 @@ func TestOnRemovePeer(t *testing.T) { }, }, { - name: "StateRelayed peer removed from maps without RemovePeer", + name: "StateRelayed peer removed from maps with RemovePeer", setup: func(t *testing.T, a *App) wgtypes.Key { key := mustKey(t) a.onAddPeer(HubPeer{PubKey: key, VPNIP: netip.MustParseAddr("10.0.0.2")}) return key }, check: func(t *testing.T, a *App, dev *fakeWGDevice) { - dev.AssertNoCalls(t) + if len(dev.Calls) != 1 { + t.Fatalf("dev calls = %v, want [RemovePeer]", dev.Calls) + } + dev.AssertRemovePeer(t, 0, dev.Calls[0].PubKey) if len(a.peersByKey) != 0 || len(a.peersByIP) != 0 { t.Errorf("maps should be empty after remove") } @@ -173,7 +170,7 @@ func TestOnRemovePeer(t *testing.T) { setup: func(t *testing.T, a *App) wgtypes.Key { relay := addRelayPeer(t, a, "10.0.0.10", ep1) a.relay = relay - return relay.PubKey + return relay.PubKey() }, check: func(t *testing.T, a *App, dev *fakeWGDevice) { if len(dev.Calls) != 1 { @@ -191,7 +188,7 @@ func TestOnRemovePeer(t *testing.T) { relay1 := addRelayPeer(t, a, "10.0.0.10", ep1) addRelayPeer(t, a, "10.0.0.11", ep2) a.relay = relay1 - return relay1.PubKey + return relay1.PubKey() }, check: func(t *testing.T, a *App, dev *fakeWGDevice) { if len(dev.Calls) != 2 { @@ -208,7 +205,7 @@ func TestOnRemovePeer(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - a, dev := newTestApp(t, "10.0.0.1", false, false) + a, dev, _ := newTestApp(t, "10.0.0.1", false, false) key := tc.setup(t, a) dev.Calls = nil a.onRemovePeer(key) @@ -256,7 +253,7 @@ func TestSwitchActiveRelay(t *testing.T) { setup: func(t *testing.T, a *App) { r1 := addRelayPeer(t, a, "10.0.0.10", ep1) r1.RTT = 10 * time.Millisecond - addRelayPeer(t, a, "10.0.0.11", ep2) // RTT stays 0 + addRelayPeer(t, a, "10.0.0.11", ep2) // RTT stays MaxInt64 (unmeaured) }, check: func(t *testing.T, a *App, dev *fakeWGDevice) { if len(dev.Calls) != 1 { @@ -284,7 +281,7 @@ func TestSwitchActiveRelay(t *testing.T) { name: "stale relay demoted to direct before backup elected", setup: func(t *testing.T, a *App) { old := addRelayPeer(t, a, "10.0.0.10", ep1) - old.Up = false // stale — this is what triggers the switch from onTick + old.wgPeer.LastHandshakeTime = time.Time{} // stale — triggers switch from onTick a.relay = old addRelayPeer(t, a, "10.0.0.11", ep2) }, @@ -296,7 +293,7 @@ func TestSwitchActiveRelay(t *testing.T) { t.Errorf("call[0]: got %v, want AddDirect with ep1", dev.Calls[0]) } dev.AssertSetRelay(t, 1, dev.Calls[1].PubKey, ep2, a.vpnNet) - if a.relay == nil || a.relay.WGEndpoint != ep2 { + if a.relay == nil || a.relay.Endpoint4 != ep2 { t.Error("relay should be the backup peer") } }, @@ -305,7 +302,7 @@ func TestSwitchActiveRelay(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - a, dev := newTestApp(t, "10.0.0.1", false, false) + a, dev, _ := newTestApp(t, "10.0.0.1", false, false) tc.setup(t, a) dev.Calls = nil a.switchActiveRelay() diff --git a/peer/on_ping.go b/peer/on_ping.go index 7d861ac..48e35e2 100644 --- a/peer/on_ping.go +++ b/peer/on_ping.go @@ -19,7 +19,7 @@ func (a *App) onPing(e PingEvent) { // If we're the server, respond - this is always necessary as it's used to // know if peers are up or down. if peer.Role == control.Server { - a.sendPing(peer, e.ping.ID, e.ping.PingTS) + a.sendPing(peer, e.ping.PingTS) } // Compute RTT from server echo. @@ -50,11 +50,9 @@ func (a *App) onPing(e PingEvent) { func (a *App) addProbe(peer *Peer, v4, v6 netip.AddrPort) { endpoint := preferredEndpoint(v4, v6) - if !endpoint.IsValid() || endpoint == peer.WGEndpoint() { + if !endpoint.IsValid() || endpoint == peer.PreferredEndpoint() { return } - - peer.Endpoint4 = v4 - peer.Endpoint6 = v6 + peer.UpdateEndpoints(v4, v6) a.devAddProbe(peer, endpoint) } diff --git a/peer/on_tick.go b/peer/on_tick.go index 30c3d64..3786df3 100644 --- a/peer/on_tick.go +++ b/peer/on_tick.go @@ -11,19 +11,20 @@ import ( func (a *App) onTick() { wgPeers := a.devPeers() - a.nextPingID++ now := time.Now().UnixNano() for _, wgPeer := range wgPeers { p, ok := a.peersByKey[wgPeer.PublicKey] if !ok { - log.Fatalf("Wireguard peer not in index: %v", wgPeer) + log.Printf("Wireguard peer not in index, removing: %v", wgPeer) + a.devRemove(&Peer{wgPeer: wgPeer}) + continue } p.wgPeer = wgPeer // Send pings to peers where we're the client. if p.Role == control.Client { - a.sendPing(p, a.nextPingID, now) + a.sendPing(p, now) } switch p.State() { diff --git a/peer/peer.go b/peer/peer.go deleted file mode 100644 index 6362cdf..0000000 --- a/peer/peer.go +++ /dev/null @@ -1,220 +0,0 @@ -package peer - -import ( - "bytes" - "encoding/json" - "io" - "log" - "net/http" - "net/netip" - "net/url" - "os" - "vppn/m" - - "git.crumpington.com/lib/go/flock" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" -) - -type peerMain struct { - Globals - netName string - holePunch *HolePunch - controlServer *ControlServer - endpointReporter *EndpointReporter // non-nil on relay peers only - hubPoller *HubPoller - lockFile *os.File -} - -func newPeerMain(args mainArgs) *peerMain { - logf := func(s string, args ...any) { - log.Printf("[Main] "+s, args...) - } - - if err := os.MkdirAll(configDir(args.NetName), 0700); err != nil { - log.Fatalf("Failed to create config directory: %v", err) - } - - lockFile, err := flock.TryLock(lockFilePath(args.NetName)) - if err != nil { - log.Fatalf("Failed to open lock file: %v", err) - } - if lockFile == nil { - log.Fatalf("Failed to obtain file lock.") - } - - config, err := loadPeerConfig(args.NetName) - if err != nil { - logf("Failed to load configuration: %v", err) - logf("Initializing...") - initPeerWithHub(args) - - config, err = loadPeerConfig(args.NetName) - if err != nil { - log.Fatalf("Failed to load configuration: %v", err) - } - } - - state, err := loadNetworkState(args.NetName) - if err != nil { - log.Fatalf("Failed to load network state: %v", err) - } - - wgPrivKey, err := wgtypes.ParseKey(config.WGPrivKey) - if err != nil { - 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) - } - - 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 create WireGuard device: %v", err) - } - - 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 { - 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 - } - - g := NewGlobals(config, netip.AddrPort{}) - g.WGPrivKey = wgPrivKey - g.WGClient = wgClient - g.WGDevName = args.NetName - - holePunch := NewHolePunch(g) - - controlServer, err := NewControlServer(g, holePunch, args.NetName) - if err != nil { - log.Fatalf("Failed to create control server: %v", err) - } - - 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, holePunch, args.NetName, args.HubAddress, args.APIKey) - if err != nil { - log.Fatalf("Failed to create hub poller: %v", err) - } - - go runStatusServer(g, statusSocketPath(args.NetName)) - - return &peerMain{ - Globals: g, - netName: args.NetName, - holePunch: holePunch, - controlServer: controlServer, - endpointReporter: endpointReporter, - hubPoller: hubPoller, - lockFile: lockFile, - } -} - -func (p *peerMain) Run() { - go p.controlServer.Run() - if p.endpointReporter != nil { - go p.endpointReporter.Run() - } - go RunMCWriter(p.Globals) - go RunMCReader(p.Globals, p.holePunch, p.netName) - go p.hubPoller.Run() - select {} -} - -func initPeerWithHub(args mainArgs) { - privKey := generateWGKey() - pubKey := privKey.PublicKey() - - initURL, err := url.Parse(args.HubAddress) - if err != nil { - log.Fatalf("Failed to parse hub URL: %v", err) - } - initURL.Path = "/peer/init/" - - initArgs := m.PeerInitArgs{ - WGPubKey: pubKey[:], - } - - buf := &bytes.Buffer{} - if err := json.NewEncoder(buf).Encode(initArgs); err != nil { - log.Fatalf("Failed to encode init args: %v", err) - } - - req, err := http.NewRequest(http.MethodPost, initURL.String(), buf) - if err != nil { - log.Fatalf("Failed to construct request: %v", err) - } - req.SetBasicAuth("", args.APIKey) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Fatalf("Failed to init with hub: %v", err) - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - 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{ - 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) - } - - if err := storePeerConfig(args.NetName, config); err != nil { - log.Fatalf("Failed to store configuration: %v", err) - } - - log.Print("Initialization successful.") -} diff --git a/peer/ping.go b/peer/ping.go index 5126ebf..5e49290 100644 --- a/peer/ping.go +++ b/peer/ping.go @@ -1,9 +1,21 @@ package peer -import "net/netip" +import ( + "log" + "net/netip" -func (a *App) sendPing(p *Peer, id, ts int64) { - _ = id - _ = ts - _ = netip.AddrPort{} + "vppn/peer/control" +) + +func (a *App) sendPing(p *Peer, ts int64) { + ping := control.Ping{ + PingTS: ts, + SrcV4: a.selfV4, + SrcV6: a.selfV6, + Dst: p.WGEndpoint(), + } + dst := netip.AddrPortFrom(p.VPNIP, ControlPort) + if err := a.controlConn.SendPing(dst, ping); err != nil { + log.Printf("sendPing %v: %v", p.VPNIP, err) + } } diff --git a/peer/remote.go b/peer/remote.go index 69b8f53..5a6f0c5 100644 --- a/peer/remote.go +++ b/peer/remote.go @@ -71,3 +71,12 @@ func (p *Peer) CanRelay() bool { func (p *Peer) PreferredEndpoint() netip.AddrPort { return preferredEndpoint(p.Endpoint4, p.Endpoint6) } + +func (p *Peer) UpdateEndpoints(v4, v6 netip.AddrPort) { + if v4.IsValid() { + p.Endpoint4 = v4 + } + if v6.IsValid() { + p.Endpoint6 = v6 + } +} diff --git a/peer/statusserver.go b/peer/statusserver.go deleted file mode 100644 index 77fd48a..0000000 --- a/peer/statusserver.go +++ /dev/null @@ -1,50 +0,0 @@ -package peer - -import ( - "encoding/json" - "log" - "net" - "net/http" - "net/netip" - "os" -) - -type StatusReport struct { - LocalPeerIP byte - Network []byte - RelayPeerIP byte - Remotes []RemoteStatus -} - -type RemoteStatus struct { - PeerIP byte - Up bool - Name string - PublicIP []byte - Port uint16 - Relay bool - Server bool - Direct bool - DirectAddr netip.AddrPort -} - -func runStatusServer(g Globals, socketPath string) { - _ = os.RemoveAll(socketPath) - - handler := func(w http.ResponseWriter, r *http.Request) { - report := StatusReport{ - LocalPeerIP: g.LocalPeerIP, - Network: g.Network, - } - json.NewEncoder(w).Encode(report) - } - - 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) - } - if err := server.Serve(unixListener); err != nil { - log.Fatalf("Failed to serve on unix socket: %v", err) - } -} diff --git a/peer/wgdev.go b/peer/wgdev.go index b537684..395b9c8 100644 --- a/peer/wgdev.go +++ b/peer/wgdev.go @@ -1,3 +1,5 @@ +//go:build ignore + package peer import (