This commit is contained in:
jdl
2026-06-07 18:12:44 +02:00
parent cad200b9cc
commit b8344a20b9
37 changed files with 568 additions and 981 deletions

View File

@@ -1,11 +1,8 @@
package main package main
import ( import "log"
"log"
"vppn/peer"
)
func main() { func main() {
log.SetFlags(0) // TODO: construct peer.App and call Run (audit C1).
peer.Main2() log.Fatal("not yet implemented")
} }

View File

@@ -344,15 +344,15 @@ type Peer struct {
Version int64 Version int64
APIKey string APIKey string
Name string Name string
PublicIP1 []byte Addr4 []byte
Port1 uint16 Port4 uint16
PublicIP2 []byte Addr6 []byte
Port2 uint16 Port6 uint16
Relay bool Relay bool
WGPubKey []byte 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( func Peer_Insert(
tx TX, tx TX,
@@ -363,7 +363,7 @@ func Peer_Insert(
return err 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 return err
} }
@@ -376,7 +376,7 @@ func Peer_Update(
return err 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 { if err != nil {
return err return err
} }
@@ -404,7 +404,7 @@ func Peer_UpdateFull(
return err 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 { if err != nil {
return err return err
} }
@@ -456,8 +456,8 @@ func Peer_Get(
err error, err error,
) { ) {
row = &Peer{} 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) 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.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 row = nil
} }
return return
@@ -473,7 +473,7 @@ func Peer_GetWhere(
) { ) {
row = &Peer{} row = &Peer{}
r := tx.QueryRow(query, args...) 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 row = nil
} }
return return
@@ -495,7 +495,7 @@ func Peer_Iterate(
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
row := &Peer{} 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) { if !yield(row, err) {
return return
} }

View File

@@ -59,39 +59,39 @@ func Network_Validate(c *Network) error {
func Peer_Sanitize(p *Peer) { func Peer_Sanitize(p *Peer) {
p.Name = strings.TrimSpace(p.Name) p.Name = strings.TrimSpace(p.Name)
if len(p.PublicIP1) != 0 { if len(p.Addr4) != 0 {
if addr, ok := netip.AddrFromSlice(p.PublicIP1); ok { if addr, ok := netip.AddrFromSlice(p.Addr4); ok {
p.PublicIP1 = addr.AsSlice() p.Addr4 = addr.AsSlice()
} }
} }
if len(p.PublicIP2) != 0 { if len(p.Addr6) != 0 {
if addr, ok := netip.AddrFromSlice(p.PublicIP2); ok { if addr, ok := netip.AddrFromSlice(p.Addr6); ok {
p.PublicIP2 = addr.AsSlice() p.Addr6 = addr.AsSlice()
} }
} }
if p.Port1 == 0 { if p.Port4 == 0 {
p.Port1 = 456 p.Port4 = 456
} }
if len(p.PublicIP2) != 0 && p.Port2 == 0 { if len(p.Addr6) != 0 && p.Port6 == 0 {
p.Port2 = 456 p.Port6 = 456
} }
} }
func Peer_Validate(p *Peer) error { func Peer_Validate(p *Peer) error {
if len(p.PublicIP1) > 0 { if len(p.Addr4) > 0 {
if _, ok := netip.AddrFromSlice(p.PublicIP1); !ok { if _, ok := netip.AddrFromSlice(p.Addr4); !ok {
return ErrInvalidIP return ErrInvalidIP
} }
} }
if len(p.PublicIP2) > 0 { if len(p.Addr6) > 0 {
if _, ok := netip.AddrFromSlice(p.PublicIP2); !ok { if _, ok := netip.AddrFromSlice(p.Addr6); !ok {
return ErrInvalidIP return ErrInvalidIP
} }
if p.Port2 == 0 { if p.Port6 == 0 {
return ErrInvalidPort return ErrInvalidPort
} }
} }
if p.Port1 == 0 { if p.Port4 == 0 {
return ErrInvalidPort return ErrInvalidPort
} }

View File

@@ -15,10 +15,10 @@ TABLE peers OF Peer (
Version int64, Version int64,
APIKey string NoUpdate, APIKey string NoUpdate,
Name string, Name string,
PublicIP1 []byte, Addr4 []byte,
Port1 uint16, Port4 uint16,
PublicIP2 []byte, Addr6 []byte,
Port2 uint16, Port6 uint16,
Relay bool, Relay bool,
WGPubKey []byte NoUpdate WGPubKey []byte NoUpdate
); );

View File

@@ -15,10 +15,10 @@ CREATE TABLE peers (
Version INTEGER NOT NULL, -- Changes when updated. Version INTEGER NOT NULL, -- Changes when updated.
APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key. APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key.
Name TEXT NOT NULL UNIQUE, -- For humans. Name TEXT NOT NULL UNIQUE, -- For humans.
PublicIP1 BLOB NOT NULL, Addr4 BLOB NOT NULL,
Port1 INTEGER NOT NULL, Port4 INTEGER NOT NULL,
PublicIP2 BLOB NOT NULL, Addr6 BLOB NOT NULL,
Port2 INTEGER NOT NULL, Port6 INTEGER NOT NULL,
Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets.
WGPubKey BLOB NOT NULL, WGPubKey BLOB NOT NULL,
PRIMARY KEY(NetworkID, PeerIP) PRIMARY KEY(NetworkID, PeerIP)

View File

@@ -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 { 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{} p := &api.Peer{}
err := webutil.NewFormScanner(r.Form). err := webutil.NewFormScanner(r.Form).
Scan("NetworkID", &p.NetworkID). Scan("NetworkID", &p.NetworkID).
Scan("IP", &p.PeerIP). Scan("IP", &p.PeerIP).
Scan("Name", &p.Name). Scan("Name", &p.Name).
Scan("PublicIP1", &ip1Str). Scan("Addr4", &addr4Str).
Scan("Port1", &p.Port1). Scan("Port4", &p.Port4).
Scan("PublicIP2", &ip2Str). Scan("Addr6", &addr6Str).
Scan("Port2", &p.Port2). Scan("Port6", &p.Port6).
Scan("Relay", &p.Relay). Scan("Relay", &p.Relay).
Error() Error()
if err != nil { if err != nil {
return err return err
} }
if p.PublicIP1, err = stringToIP(ip1Str); err != nil { if p.Addr4, err = stringToIP(addr4Str); err != nil {
return err return err
} }
if p.PublicIP2, err = stringToIP(ip2Str); err != nil { if p.Addr6, err = stringToIP(addr6Str); err != nil {
return err return err
} }
@@ -204,24 +204,24 @@ func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *htt
return err return err
} }
var ip1Str, ip2Str string var addr4Str, addr6Str string
err = webutil.NewFormScanner(r.Form). err = webutil.NewFormScanner(r.Form).
Scan("Name", &peer.Name). Scan("Name", &peer.Name).
Scan("PublicIP1", &ip1Str). Scan("Addr4", &addr4Str).
Scan("Port1", &peer.Port1). Scan("Port4", &peer.Port4).
Scan("PublicIP2", &ip2Str). Scan("Addr6", &addr6Str).
Scan("Port2", &peer.Port2). Scan("Port6", &peer.Port6).
Scan("Relay", &peer.Relay). Scan("Relay", &peer.Relay).
Error() Error()
if err != nil { if err != nil {
return err return err
} }
if peer.PublicIP1, err = stringToIP(ip1Str); err != nil { if peer.Addr4, err = stringToIP(addr4Str); err != nil {
return err return err
} }
if peer.PublicIP2, err = stringToIP(ip2Str); err != nil { if peer.Addr6, err = stringToIP(addr6Str); err != nil {
return err return err
} }
@@ -355,15 +355,15 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) {
for _, p := range l { for _, p := range l {
if len(p.WGPubKey) != 0 { if len(p.WGPubKey) != 0 {
peers[p.PeerIP] = &m.Peer{ peers[p.PeerIP] = &m.Peer{
PeerIP: p.PeerIP, PeerIP: p.PeerIP,
Version: p.Version, Version: p.Version,
Name: p.Name, Name: p.Name,
PublicIP1: p.PublicIP1, Addr4: p.Addr4,
Port1: p.Port1, Port4: p.Port4,
PublicIP2: p.PublicIP2, Addr6: p.Addr6,
Port2: p.Port2, Port6: p.Port6,
Relay: p.Relay, Relay: p.Relay,
WGPubKey: p.WGPubKey, WGPubKey: p.WGPubKey,
} }
} }
} }

View File

@@ -12,15 +12,15 @@ type PeerInitResp struct {
} }
type Peer struct { type Peer struct {
PeerIP byte PeerIP byte
Version int64 Version int64
Name string Name string
PublicIP1 []byte Addr4 []byte
Port1 uint16 Port4 uint16
PublicIP2 []byte Addr6 []byte
Port2 uint16 Port6 uint16
Relay bool Relay bool
WGPubKey []byte WGPubKey []byte
} }
type NetworkState struct { type NetworkState struct {

View File

@@ -1,7 +1,6 @@
package peer package peer
import ( import (
"net"
"net/netip" "net/netip"
"os" "os"
"os/signal" "os/signal"
@@ -56,7 +55,7 @@ type App struct {
// Infrastructure // Infrastructure
dev WGDevice dev WGDevice
controlConn *net.UDPConn controlConn ControlConn
// Peer state // Peer state
relay *Peer relay *Peer
@@ -67,9 +66,6 @@ type App struct {
selfV4 netip.AddrPort selfV4 netip.AddrPort
selfV6 netip.AddrPort selfV6 netip.AddrPort
// Monotonically increasing ID for outbound pings (client role only)
nextPingID int64 // TODO: Remove
// Event channels fed by background goroutines // Event channels fed by background goroutines
hubAddCh <-chan HubPeer hubAddCh <-chan HubPeer
hubRemoveCh <-chan wgtypes.Key hubRemoveCh <-chan wgtypes.Key

View File

@@ -25,10 +25,10 @@ func addRelayPeer(t *testing.T, a *App, vpnIP string, ep netip.AddrPort) *Peer {
return p 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"). // vpnIP is the local VPN address (e.g. "10.0.0.1").
// isPublic / isRelay describe the local node's role. // 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() t.Helper()
privKey, err := wgtypes.GeneratePrivateKey() privKey, err := wgtypes.GeneratePrivateKey()
if err != nil { if err != nil {
@@ -36,6 +36,7 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
} }
ip := netip.MustParseAddr(vpnIP) ip := netip.MustParseAddr(vpnIP)
dev := &fakeWGDevice{} dev := &fakeWGDevice{}
cc := &fakeControlConn{}
a := &App{ a := &App{
vpnIP: ip, vpnIP: ip,
vpnNet: netip.MustParsePrefix("10.0.0.0/24"), 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, isPublic: isPublic,
isRelay: isRelay, isRelay: isRelay,
dev: dev, dev: dev,
controlConn: cc,
peersByKey: make(map[wgtypes.Key]*Peer), peersByKey: make(map[wgtypes.Key]*Peer),
peersByIP: make(map[netip.Addr]*Peer), peersByIP: make(map[netip.Addr]*Peer),
hubAddCh: make(chan HubPeer), hubAddCh: make(chan HubPeer),
@@ -51,5 +53,5 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
pingCh: make(chan PingEvent), pingCh: make(chan PingEvent),
multicastCh: make(chan MulticastEvent), multicastCh: make(chan MulticastEvent),
} }
return a, dev return a, dev, cc
} }

View File

@@ -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
}

View File

@@ -11,14 +11,14 @@ import (
const ( const (
version = 1 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. // 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 // In each peer pair, the peer with the lower VPN IP is the client: it sets
// and PingTS and sends pings on a timer. The server echoes ID and PingTS back // PingTS and sends pings on a timer. The server echoes PingTS back in its
// in its response, allowing the client to compute RTT = now - PingTS. // response, allowing the client to compute RTT = now - PingTS.
// //
// Both client and server populate SrcV4, SrcV6, and Dst on every packet so // Both client and server populate SrcV4, SrcV6, and Dst on every packet so
// endpoint information flows in both directions. // endpoint information flows in both directions.
@@ -27,50 +27,47 @@ const (
// WireGuard handshake source. Zero if the sender has not observed a handshake // WireGuard handshake source. Zero if the sender has not observed a handshake
// from the recipient. // from the recipient.
type Ping struct { type Ping struct {
ID int64 // Client ping ID.
PingTS int64 // Client ping send time in nanoseconds. PingTS int64 // Client ping send time in nanoseconds.
SrcV4 netip.AddrPort // Sender's discovered IPv4 address and port. SrcV4 netip.AddrPort // Sender's discovered IPv4 address and port.
SrcV6 netip.AddrPort // Sender's discovered IPv6 address and port. SrcV6 netip.AddrPort // Sender's discovered IPv6 address and port.
Dst netip.AddrPort 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 { func (p Ping) Marshal() [Size]byte {
var buf [Size]byte var buf [Size]byte
buf[0] = version buf[0] = version
binary.BigEndian.PutUint64(buf[1:9], uint64(p.ID)) binary.BigEndian.PutUint64(buf[1:9], uint64(p.PingTS))
binary.BigEndian.PutUint64(buf[9:17], uint64(p.PingTS))
if p.SrcV4.IsValid() { if p.SrcV4.IsValid() {
a4 := p.SrcV4.Addr().As4() a4 := p.SrcV4.Addr().As4()
copy(buf[17:21], a4[:]) copy(buf[9:13], a4[:])
binary.BigEndian.PutUint16(buf[21:23], p.SrcV4.Port()) binary.BigEndian.PutUint16(buf[13:15], p.SrcV4.Port())
} }
a16 := p.SrcV6.Addr().As16() a16 := p.SrcV6.Addr().As16()
copy(buf[23:39], a16[:]) copy(buf[15:31], a16[:])
binary.BigEndian.PutUint16(buf[39:41], p.SrcV6.Port()) binary.BigEndian.PutUint16(buf[31:33], p.SrcV6.Port())
a16 = p.Dst.Addr().As16() a16 = p.Dst.Addr().As16()
copy(buf[41:57], a16[:]) copy(buf[33:49], a16[:])
binary.BigEndian.PutUint16(buf[57:59], p.Dst.Port()) binary.BigEndian.PutUint16(buf[49:51], p.Dst.Port())
return buf 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) { func Unmarshal(buf [Size]byte) (Ping, error) {
if buf[0] != version { if buf[0] != version {
return Ping{}, fmt.Errorf("unknown ping version %d", buf[0]) return Ping{}, fmt.Errorf("unknown ping version %d", buf[0])
} }
p := Ping{ p := Ping{
ID: int64(binary.BigEndian.Uint64(buf[1:9])), PingTS: int64(binary.BigEndian.Uint64(buf[1:9])),
PingTS: int64(binary.BigEndian.Uint64(buf[9:17])),
} }
if addr := netip.AddrFrom4([4]byte(buf[17:21])); !addr.IsUnspecified() { if addr := netip.AddrFrom4([4]byte(buf[9:13])); !addr.IsUnspecified() {
p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[21:23])) p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[13:15]))
} }
if addr := netip.AddrFrom16([16]byte(buf[23:39])); !addr.IsUnspecified() { if addr := netip.AddrFrom16([16]byte(buf[15:31])); !addr.IsUnspecified() {
p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[39:41])) p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[31:33]))
} }
if addr := netip.AddrFrom16([16]byte(buf[41:57])).Unmap(); !addr.IsUnspecified() { if addr := netip.AddrFrom16([16]byte(buf[33:49])).Unmap(); !addr.IsUnspecified() {
p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[57:59])) p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[49:51]))
} }
return p, nil return p, nil
} }

View File

@@ -19,7 +19,6 @@ func TestRoundTrip(t *testing.T) {
{ {
name: "client ping", name: "client ping",
ping: control.Ping{ ping: control.Ping{
ID: 42,
PingTS: 1234567890, PingTS: 1234567890,
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"), SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
Dst: netip.MustParseAddrPort("5.6.7.8:51820"), Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
@@ -28,7 +27,6 @@ func TestRoundTrip(t *testing.T) {
{ {
name: "server response", name: "server response",
ping: control.Ping{ ping: control.Ping{
ID: 42,
PingTS: 1234567890, PingTS: 1234567890,
SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"), SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"),
Dst: netip.MustParseAddrPort("1.2.3.4:9999"), Dst: netip.MustParseAddrPort("1.2.3.4:9999"),
@@ -37,7 +35,6 @@ func TestRoundTrip(t *testing.T) {
{ {
name: "IPv6 only", name: "IPv6 only",
ping: control.Ping{ ping: control.Ping{
ID: 1,
PingTS: 999, PingTS: 999,
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"), SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"), Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"),
@@ -46,7 +43,6 @@ func TestRoundTrip(t *testing.T) {
{ {
name: "dual stack", name: "dual stack",
ping: control.Ping{ ping: control.Ping{
ID: 7,
PingTS: 555, PingTS: 555,
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"), SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"), SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
@@ -56,7 +52,6 @@ func TestRoundTrip(t *testing.T) {
{ {
name: "no src known", name: "no src known",
ping: control.Ping{ ping: control.Ping{
ID: 3,
Dst: netip.MustParseAddrPort("5.6.7.8:51820"), Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
}, },
}, },

61
peer/control_conn.go Normal file
View File

@@ -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}
}
}

View File

@@ -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
}

View File

@@ -1,12 +1,34 @@
package peer package peer
import ( import (
"errors"
"log" "log"
"net/netip" "net/netip"
"syscall"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "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 { func (a *App) devPeers() []wgtypes.Peer {
peers, err := a.dev.Peers() peers, err := a.dev.Peers()
if err != nil { if err != nil {
@@ -16,37 +38,25 @@ func (a *App) devPeers() []wgtypes.Peer {
} }
func (a *App) devAddPeer(p *Peer) { func (a *App) devAddPeer(p *Peer) {
if err := a.dev.AddPeer(p.PubKey()); err != nil { devRetry(p.VPNIP, "AddPeer", func() error { return a.dev.AddPeer(p.PubKey()) })
log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err)
}
} }
func (a *App) devAddDirect(p *Peer, endpoint netip.AddrPort) { func (a *App) devAddDirect(p *Peer, endpoint netip.AddrPort) {
if err := a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP); err != nil { devRetry(p.VPNIP, "AddDirect", func() error { return a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP) })
log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err)
}
} }
func (a *App) devSetRelay(p *Peer, endpoint netip.AddrPort) { func (a *App) devSetRelay(p *Peer, endpoint netip.AddrPort) {
if err := a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet); err != nil { devRetry(p.VPNIP, "SetRelay", func() error { return a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet) })
log.Fatalf("Failed to add relay %v: %v", p.VPNIP, err)
}
} }
func (a *App) devPromote(p *Peer) { func (a *App) devPromote(p *Peer) {
if err := a.dev.Promote(p.PubKey(), p.VPNIP); err != nil { devRetry(p.VPNIP, "Promote", func() error { return a.dev.Promote(p.PubKey(), p.VPNIP) })
log.Fatalf("Failed to promote peer %v: %v", p.VPNIP, err)
}
} }
func (a *App) devAddProbe(p *Peer, endpoint netip.AddrPort) { func (a *App) devAddProbe(p *Peer, endpoint netip.AddrPort) {
if err := a.dev.AddProbe(p.PubKey(), endpoint); err != nil { devRetry(p.VPNIP, "AddProbe", func() error { return a.dev.AddProbe(p.PubKey(), endpoint) })
log.Fatalf("Failed to add probe %v: %v", p.VPNIP, err)
}
} }
func (a *App) devRemove(p *Peer) { func (a *App) devRemove(p *Peer) {
if err := a.dev.RemovePeer(p.PubKey()); err != nil { devRetry(p.VPNIP, "RemovePeer", func() error { return a.dev.RemovePeer(p.PubKey()) })
log.Fatalf("Failed to remove peer %v: %v", p.VPNIP, err)
}
} }

View File

@@ -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())
}
}

View File

@@ -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)
}
}

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer package peer
import ( import (
@@ -48,7 +50,7 @@ func storeJson(x any, outPath string) error {
return err 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 { if err != nil {
return err return err
} }
@@ -94,4 +96,3 @@ func loadPeerConfig(netName string) (pc LocalConfig, err error) {
func loadNetworkState(netName string) (ps m.NetworkState, err error) { func loadNetworkState(netName string) (ps m.NetworkState, err error) {
return ps, loadJson(peerStatePath(netName), &ps) return ps, loadJson(peerStatePath(netName), &ps)
} }

View File

@@ -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)
}
}

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer package peer
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer package peer
import ( import (

149
peer/hub_poller.go Normal file
View File

@@ -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,
}
}

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer package peer
import ( import (
@@ -14,17 +16,13 @@ import (
) )
type HubPoller struct { type HubPoller struct {
Globals client *http.Client
holePunch *HolePunch req *http.Request
client *http.Client versions [256]int64
req *http.Request netName string
versions [256]int64
netName string
} }
func NewHubPoller( func NewHubPoller(
g Globals,
hp *HolePunch,
netName, netName,
hubURL, hubURL,
apiKey string, apiKey string,
@@ -118,7 +116,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen { if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
return return
} }
if len(peer.PublicIP1) == 0 || peer.Port1 == 0 { if len(peer.Addr4) == 0 || peer.Port4 == 0 {
return return
} }
@@ -128,12 +126,12 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
return return
} }
ip, ok := netip.AddrFromSlice(peer.PublicIP1) ip, ok := netip.AddrFromSlice(peer.Addr4)
if !ok { if !ok {
hp.logf("Invalid public IP for peer %d", peer.PeerIP) hp.logf("Invalid public IP for peer %d", peer.PeerIP)
return return
} }
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port1) endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port4)
if peer.Relay { if peer.Relay {
if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil { if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil {

View File

@@ -2,6 +2,7 @@ package peer
import ( import (
"net/netip" "net/netip"
"vppn/peer/control"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
@@ -17,3 +18,9 @@ type WGDevice interface {
Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error
RemovePeer(pubKey wgtypes.Key) 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
}

View File

@@ -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
}

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer package peer
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer package peer
import ( import (

101
peer/multicast.go Normal file
View File

@@ -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,
}
}
}

View File

@@ -2,7 +2,9 @@ package peer
import ( import (
"log" "log"
"math"
"net/netip" "net/netip"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -19,6 +21,7 @@ func (a *App) onAddPeer(p HubPeer) {
IsPublic: p.IsPublic, IsPublic: p.IsPublic,
Endpoint4: p.EndpointV4, Endpoint4: p.EndpointV4,
Endpoint6: p.EndpointV6, Endpoint6: p.EndpointV6,
RTT: time.Duration(math.MaxInt64) * time.Nanosecond,
Role: roleFor(a.isPublic, a.vpnIP, p), 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. // switchActiveRelay promotes the lowest-latency relay peer to active.
func (a *App) switchActiveRelay() { func (a *App) switchActiveRelay() {
if a.relay != nil { 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 a.relay = nil
} }
@@ -68,7 +73,7 @@ func (a *App) switchActiveRelay() {
continue continue
} }
if best == nil || betterRelay(p, best) { if best == nil || p.RTT < best.RTT {
best = p best = p
} }
} }
@@ -77,23 +82,10 @@ func (a *App) switchActiveRelay() {
return return
} }
a.devSetRelay(best, best.WGEndpoint()) a.devSetRelay(best, best.PreferredEndpoint())
a.relay = best 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 { func preferredEndpoint(v4, v6 netip.AddrPort) netip.AddrPort {
if v4.IsValid() { if v4.IsValid() {
return v4 return v4

View File

@@ -29,7 +29,7 @@ func TestOnAddPeer(t *testing.T) {
check func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key) 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 { peer: func(k wgtypes.Key) HubPeer {
return HubPeer{PubKey: k, VPNIP: peerVPNIP} return HubPeer{PubKey: k, VPNIP: peerVPNIP}
}, },
@@ -41,14 +41,14 @@ func TestOnAddPeer(t *testing.T) {
if a.peersByIP[peerVPNIP] == nil { if a.peersByIP[peerVPNIP] == nil {
t.Fatal("not in peersByIP") t.Fatal("not in peersByIP")
} }
if p.State != StateRelayed { if p.State() != StateRelayed {
t.Fatalf("state = %v, want StateRelayed", p.State) 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 { peer: func(k wgtypes.Key) HubPeer {
return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1} return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1}
}, },
@@ -57,13 +57,7 @@ func TestOnAddPeer(t *testing.T) {
if p == nil { if p == nil {
t.Fatal("not in peersByKey") t.Fatal("not in peersByKey")
} }
if p.State != StateDirect { dev.AssertAddDirect(t, 0, p.PubKey(), ep1, p.VPNIP)
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)
}, },
}, },
{ {
@@ -104,7 +98,7 @@ func TestOnAddPeer(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { 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) key := mustKey(t)
if tc.setup != nil { if tc.setup != nil {
tc.setup(a, key) 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 { setup: func(t *testing.T, a *App) wgtypes.Key {
key := mustKey(t) key := mustKey(t)
a.onAddPeer(HubPeer{PubKey: key, VPNIP: netip.MustParseAddr("10.0.0.2")}) a.onAddPeer(HubPeer{PubKey: key, VPNIP: netip.MustParseAddr("10.0.0.2")})
return key return key
}, },
check: func(t *testing.T, a *App, dev *fakeWGDevice) { 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 { if len(a.peersByKey) != 0 || len(a.peersByIP) != 0 {
t.Errorf("maps should be empty after remove") 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 { setup: func(t *testing.T, a *App) wgtypes.Key {
relay := addRelayPeer(t, a, "10.0.0.10", ep1) relay := addRelayPeer(t, a, "10.0.0.10", ep1)
a.relay = relay a.relay = relay
return relay.PubKey return relay.PubKey()
}, },
check: func(t *testing.T, a *App, dev *fakeWGDevice) { check: func(t *testing.T, a *App, dev *fakeWGDevice) {
if len(dev.Calls) != 1 { if len(dev.Calls) != 1 {
@@ -191,7 +188,7 @@ func TestOnRemovePeer(t *testing.T) {
relay1 := addRelayPeer(t, a, "10.0.0.10", ep1) relay1 := addRelayPeer(t, a, "10.0.0.10", ep1)
addRelayPeer(t, a, "10.0.0.11", ep2) addRelayPeer(t, a, "10.0.0.11", ep2)
a.relay = relay1 a.relay = relay1
return relay1.PubKey return relay1.PubKey()
}, },
check: func(t *testing.T, a *App, dev *fakeWGDevice) { check: func(t *testing.T, a *App, dev *fakeWGDevice) {
if len(dev.Calls) != 2 { if len(dev.Calls) != 2 {
@@ -208,7 +205,7 @@ func TestOnRemovePeer(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { 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) key := tc.setup(t, a)
dev.Calls = nil dev.Calls = nil
a.onRemovePeer(key) a.onRemovePeer(key)
@@ -256,7 +253,7 @@ func TestSwitchActiveRelay(t *testing.T) {
setup: func(t *testing.T, a *App) { setup: func(t *testing.T, a *App) {
r1 := addRelayPeer(t, a, "10.0.0.10", ep1) r1 := addRelayPeer(t, a, "10.0.0.10", ep1)
r1.RTT = 10 * time.Millisecond 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) { check: func(t *testing.T, a *App, dev *fakeWGDevice) {
if len(dev.Calls) != 1 { if len(dev.Calls) != 1 {
@@ -284,7 +281,7 @@ func TestSwitchActiveRelay(t *testing.T) {
name: "stale relay demoted to direct before backup elected", name: "stale relay demoted to direct before backup elected",
setup: func(t *testing.T, a *App) { setup: func(t *testing.T, a *App) {
old := addRelayPeer(t, a, "10.0.0.10", ep1) 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 a.relay = old
addRelayPeer(t, a, "10.0.0.11", ep2) 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]) t.Errorf("call[0]: got %v, want AddDirect with ep1", dev.Calls[0])
} }
dev.AssertSetRelay(t, 1, dev.Calls[1].PubKey, ep2, a.vpnNet) 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") t.Error("relay should be the backup peer")
} }
}, },
@@ -305,7 +302,7 @@ func TestSwitchActiveRelay(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { 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) tc.setup(t, a)
dev.Calls = nil dev.Calls = nil
a.switchActiveRelay() a.switchActiveRelay()

View File

@@ -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 // If we're the server, respond - this is always necessary as it's used to
// know if peers are up or down. // know if peers are up or down.
if peer.Role == control.Server { 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. // 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) { func (a *App) addProbe(peer *Peer, v4, v6 netip.AddrPort) {
endpoint := preferredEndpoint(v4, v6) endpoint := preferredEndpoint(v4, v6)
if !endpoint.IsValid() || endpoint == peer.WGEndpoint() { if !endpoint.IsValid() || endpoint == peer.PreferredEndpoint() {
return return
} }
peer.UpdateEndpoints(v4, v6)
peer.Endpoint4 = v4
peer.Endpoint6 = v6
a.devAddProbe(peer, endpoint) a.devAddProbe(peer, endpoint)
} }

View File

@@ -11,19 +11,20 @@ import (
func (a *App) onTick() { func (a *App) onTick() {
wgPeers := a.devPeers() wgPeers := a.devPeers()
a.nextPingID++
now := time.Now().UnixNano() now := time.Now().UnixNano()
for _, wgPeer := range wgPeers { for _, wgPeer := range wgPeers {
p, ok := a.peersByKey[wgPeer.PublicKey] p, ok := a.peersByKey[wgPeer.PublicKey]
if !ok { 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 p.wgPeer = wgPeer
// Send pings to peers where we're the client. // Send pings to peers where we're the client.
if p.Role == control.Client { if p.Role == control.Client {
a.sendPing(p, a.nextPingID, now) a.sendPing(p, now)
} }
switch p.State() { switch p.State() {

View File

@@ -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.")
}

View File

@@ -1,9 +1,21 @@
package peer package peer
import "net/netip" import (
"log"
"net/netip"
func (a *App) sendPing(p *Peer, id, ts int64) { "vppn/peer/control"
_ = id )
_ = ts
_ = netip.AddrPort{} 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)
}
} }

View File

@@ -71,3 +71,12 @@ func (p *Peer) CanRelay() bool {
func (p *Peer) PreferredEndpoint() netip.AddrPort { func (p *Peer) PreferredEndpoint() netip.AddrPort {
return preferredEndpoint(p.Endpoint4, p.Endpoint6) 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
}
}

View File

@@ -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)
}
}

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer package peer
import ( import (