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

View File

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

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

View File

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

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

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

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
import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer
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
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 {

View File

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

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
import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package peer
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 (
"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

View File

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

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

View File

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

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

View File

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

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
import (