WIP
This commit is contained in:
@@ -68,7 +68,7 @@ type App struct {
|
|||||||
selfV6 netip.AddrPort
|
selfV6 netip.AddrPort
|
||||||
|
|
||||||
// Monotonically increasing ID for outbound pings (client role only)
|
// Monotonically increasing ID for outbound pings (client role only)
|
||||||
nextPingID int64
|
nextPingID int64 // TODO: Remove
|
||||||
|
|
||||||
// Event channels fed by background goroutines
|
// Event channels fed by background goroutines
|
||||||
hubAddCh <-chan HubPeer
|
hubAddCh <-chan HubPeer
|
||||||
@@ -104,7 +104,6 @@ func (a *App) Run() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (a *App) onShutdown() error {
|
func (a *App) onShutdown() error {
|
||||||
return wginterface.Delete(a.dev.Name())
|
return wginterface.Delete(a.dev.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,28 @@ package peer
|
|||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// addRelayPeer adds a public relay peer and marks it Up so it satisfies
|
||||||
|
// CanRelay. It does not set a.relay — callers do that explicitly.
|
||||||
|
func addRelayPeer(t *testing.T, a *App, vpnIP string, ep netip.AddrPort) *Peer {
|
||||||
|
t.Helper()
|
||||||
|
key := mustKey(t)
|
||||||
|
a.onAddPeer(HubPeer{
|
||||||
|
PubKey: key,
|
||||||
|
VPNIP: netip.MustParseAddr(vpnIP),
|
||||||
|
IsPublic: true,
|
||||||
|
IsRelay: true,
|
||||||
|
EndpointV4: ep,
|
||||||
|
})
|
||||||
|
p := a.peersByKey[key]
|
||||||
|
p.wgPeer.LastHandshakeTime = time.Now()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
// newTestApp returns a minimal App wired to a fakeWGDevice.
|
// newTestApp returns a minimal App wired to a fakeWGDevice.
|
||||||
// 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.
|
||||||
|
|||||||
76
peer/control/ping.go
Normal file
76
peer/control/ping.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Package control implements the VPN-internal peer control protocol.
|
||||||
|
// Peers exchange Ping packets over UDP on the VPN control port to maintain
|
||||||
|
// liveness and discover external endpoints for direct connection attempts.
|
||||||
|
package control
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
version = 1
|
||||||
|
Size = 59 // 1 version + 8 ID + 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.
|
||||||
|
//
|
||||||
|
// Both client and server populate SrcV4, SrcV6, and Dst on every packet so
|
||||||
|
// endpoint information flows in both directions.
|
||||||
|
//
|
||||||
|
// Dst is the recipient's external endpoint as observed by the sender from the
|
||||||
|
// 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.
|
||||||
|
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))
|
||||||
|
if p.SrcV4.IsValid() {
|
||||||
|
a4 := p.SrcV4.Addr().As4()
|
||||||
|
copy(buf[17:21], a4[:])
|
||||||
|
binary.BigEndian.PutUint16(buf[21:23], p.SrcV4.Port())
|
||||||
|
}
|
||||||
|
a16 := p.SrcV6.Addr().As16()
|
||||||
|
copy(buf[23:39], a16[:])
|
||||||
|
binary.BigEndian.PutUint16(buf[39:41], p.SrcV6.Port())
|
||||||
|
a16 = p.Dst.Addr().As16()
|
||||||
|
copy(buf[41:57], a16[:])
|
||||||
|
binary.BigEndian.PutUint16(buf[57:59], p.Dst.Port())
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decodes a Ping from a fixed-size 59-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])),
|
||||||
|
}
|
||||||
|
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.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[41:57])).Unmap(); !addr.IsUnspecified() {
|
||||||
|
p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[57:59]))
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
109
peer/control/ping_test.go
Normal file
109
peer/control/ping_test.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package control_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"vppn/peer/control"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundTrip(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
ping control.Ping
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero",
|
||||||
|
ping: control.Ping{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 only",
|
||||||
|
ping: control.Ping{
|
||||||
|
ID: 1,
|
||||||
|
PingTS: 999,
|
||||||
|
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
||||||
|
Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"),
|
||||||
|
Dst: netip.MustParseAddrPort("5.6.7.8:9999"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no src known",
|
||||||
|
ping: control.Ping{
|
||||||
|
ID: 3,
|
||||||
|
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
buf := tc.ping.Marshal()
|
||||||
|
got, err := control.Unmarshal(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if got != tc.ping {
|
||||||
|
t.Fatalf("round-trip mismatch:\n got %+v\n want %+v", got, tc.ping)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalBadVersion(t *testing.T) {
|
||||||
|
var buf [control.Size]byte
|
||||||
|
buf[0] = 99
|
||||||
|
if _, err := control.Unmarshal(buf); err == nil {
|
||||||
|
t.Fatal("expected error for unknown version, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZeroEncoding(t *testing.T) {
|
||||||
|
buf := (control.Ping{}).Marshal()
|
||||||
|
for i, b := range buf {
|
||||||
|
if i == 0 {
|
||||||
|
continue // version byte
|
||||||
|
}
|
||||||
|
if b != 0 {
|
||||||
|
t.Fatalf("expected zero encoding at byte %d, got %d", i, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoleFor(t *testing.T) {
|
||||||
|
lo := netip.MustParseAddr("10.0.0.1")
|
||||||
|
hi := netip.MustParseAddr("10.0.0.2")
|
||||||
|
|
||||||
|
if control.RoleFor(lo, hi) != control.Client {
|
||||||
|
t.Error("lower IP should be client")
|
||||||
|
}
|
||||||
|
if control.RoleFor(hi, lo) != control.Server {
|
||||||
|
t.Error("higher IP should be server")
|
||||||
|
}
|
||||||
|
}
|
||||||
22
peer/control/role.go
Normal file
22
peer/control/role.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package control
|
||||||
|
|
||||||
|
import "net/netip"
|
||||||
|
|
||||||
|
// Role identifies a peer's role in a ping exchange with a specific remote peer.
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Client initiates pings and measures RTT.
|
||||||
|
Client Role = "CLIENT"
|
||||||
|
// Server responds to pings.
|
||||||
|
Server Role = "SERVER"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RoleFor returns the Role of local relative to remote.
|
||||||
|
// The peer with the lower VPN IP is the client.
|
||||||
|
func RoleFor(local, remote netip.Addr) Role {
|
||||||
|
if local.Compare(remote) < 0 {
|
||||||
|
return Client
|
||||||
|
}
|
||||||
|
return Server
|
||||||
|
}
|
||||||
@@ -15,34 +15,38 @@ func (a *App) devPeers() []wgtypes.Peer {
|
|||||||
return peers
|
return peers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
if err := a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP); err != nil {
|
||||||
log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err)
|
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 {
|
if err := a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet); err != nil {
|
||||||
log.Fatalf("Failed to add relay %v: %v", p.VPNIP, err)
|
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 {
|
if err := a.dev.Promote(p.PubKey(), p.VPNIP); err != nil {
|
||||||
log.Fatalf("Failed to promot peer %v: %v", p.VPNIP, err)
|
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 {
|
if err := a.dev.AddProbe(p.PubKey(), endpoint); err != nil {
|
||||||
log.Fatalf("Failed to add probe %v: %v", p.VPNIP, err)
|
log.Fatalf("Failed to add probe %v: %v", p.VPNIP, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) devRemove(p *Peer) {
|
func (a *App) devRemove(p *Peer) {
|
||||||
if p.State != StateRelayed {
|
if err := a.dev.RemovePeer(p.PubKey()); err != nil {
|
||||||
if err := a.dev.RemovePeer(p.PubKey); err != nil {
|
log.Fatalf("Failed to remove peer %v: %v", p.VPNIP, err)
|
||||||
log.Fatalf("Failed to remove peer %v: %v", p.VPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package peer
|
|||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
@@ -41,6 +42,11 @@ func (f *fakeWGDevice) Peers() ([]wgtypes.Peer, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) AddPeer(pubKey wgtypes.Key) error {
|
||||||
|
f.record(fakeCall{Method: "AddPeer", PubKey: pubKey})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeWGDevice) AddDirect(pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error {
|
func (f *fakeWGDevice) AddDirect(pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error {
|
||||||
f.record(fakeCall{Method: "AddDirect", PubKey: pubKey, Endpoint: endpoint, VPNiP: vpnIP})
|
f.record(fakeCall{Method: "AddDirect", PubKey: pubKey, Endpoint: endpoint, VPNiP: vpnIP})
|
||||||
return nil
|
return nil
|
||||||
@@ -65,3 +71,53 @@ func (f *fakeWGDevice) RemovePeer(pubKey wgtypes.Key) error {
|
|||||||
f.record(fakeCall{Method: "RemovePeer", PubKey: pubKey})
|
f.record(fakeCall{Method: "RemovePeer", PubKey: pubKey})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertNoCalls fails the test if any dev calls were recorded.
|
||||||
|
func (f *fakeWGDevice) AssertNoCalls(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
if len(f.Calls) != 0 {
|
||||||
|
t.Fatalf("unexpected dev calls: %v", f.Calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) AssertAddPeer(t *testing.T, i int, pubKey wgtypes.Key) {
|
||||||
|
t.Helper()
|
||||||
|
f.assertCall(t, i, fakeCall{Method: "AddPeer", PubKey: pubKey})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) AssertAddDirect(t *testing.T, i int, pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) {
|
||||||
|
t.Helper()
|
||||||
|
f.assertCall(t, i, fakeCall{Method: "AddDirect", PubKey: pubKey, Endpoint: endpoint, VPNiP: vpnIP})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) AssertSetRelay(t *testing.T, i int, pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) {
|
||||||
|
t.Helper()
|
||||||
|
f.assertCall(t, i, fakeCall{Method: "SetRelay", PubKey: pubKey, Endpoint: endpoint, Network: network})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) AssertAddProbe(t *testing.T, i int, pubKey wgtypes.Key, endpoint netip.AddrPort) {
|
||||||
|
t.Helper()
|
||||||
|
f.assertCall(t, i, fakeCall{Method: "AddProbe", PubKey: pubKey, Endpoint: endpoint})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) AssertPromote(t *testing.T, i int, pubKey wgtypes.Key, vpnIP netip.Addr) {
|
||||||
|
t.Helper()
|
||||||
|
f.assertCall(t, i, fakeCall{Method: "Promote", PubKey: pubKey, VPNiP: vpnIP})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) AssertRemovePeer(t *testing.T, i int, pubKey wgtypes.Key) {
|
||||||
|
t.Helper()
|
||||||
|
f.assertCall(t, i, fakeCall{Method: "RemovePeer", PubKey: pubKey})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWGDevice) assertCall(t *testing.T, i int, c fakeCall) {
|
||||||
|
t.Helper()
|
||||||
|
if len(f.Calls) <= i {
|
||||||
|
t.Fatalf("no call at index %d: %v", i, c)
|
||||||
|
}
|
||||||
|
if c != f.Calls[i] {
|
||||||
|
t.Fatalf("call[%d]: got %v, want %v", i, f.Calls[i], c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
type WGDevice interface {
|
type WGDevice interface {
|
||||||
Name() string
|
Name() string
|
||||||
Peers() ([]wgtypes.Peer, error)
|
Peers() ([]wgtypes.Peer, error)
|
||||||
|
AddPeer(pubKey wgtypes.Key) error
|
||||||
AddDirect(pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error
|
AddDirect(pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error
|
||||||
SetRelay(pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) error
|
SetRelay(pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) error
|
||||||
AddProbe(pubKey wgtypes.Key, endpoint netip.AddrPort) error
|
AddProbe(pubKey wgtypes.Key, endpoint netip.AddrPort) error
|
||||||
|
|||||||
@@ -10,19 +10,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) onAddPeer(p HubPeer) {
|
func (a *App) onAddPeer(p HubPeer) {
|
||||||
if _, exists := a.peersByKey[p.PubKey]; exists {
|
a.onRemovePeer(p.PubKey)
|
||||||
a.onRemovePeer(p.PubKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
peer := &Peer{
|
peer := &Peer{
|
||||||
PubKey: p.PubKey,
|
wgPeer: wgtypes.Peer{PublicKey: p.PubKey},
|
||||||
VPNIP: p.VPNIP,
|
VPNIP: p.VPNIP,
|
||||||
IsRelay: p.IsRelay,
|
IsRelay: p.IsRelay,
|
||||||
IsPublic: p.IsPublic,
|
IsPublic: p.IsPublic,
|
||||||
Endpoint4: p.EndpointV4,
|
Endpoint4: p.EndpointV4,
|
||||||
Endpoint6: p.EndpointV6,
|
Endpoint6: p.EndpointV6,
|
||||||
Role: roleFor(a.isPublic, a.vpnIP, p),
|
Role: roleFor(a.isPublic, a.vpnIP, p),
|
||||||
State: StateRelayed,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint := peer.PreferredEndpoint()
|
endpoint := peer.PreferredEndpoint()
|
||||||
@@ -36,11 +33,10 @@ func (a *App) onAddPeer(p HubPeer) {
|
|||||||
a.peersByIP[peer.VPNIP] = peer
|
a.peersByIP[peer.VPNIP] = peer
|
||||||
|
|
||||||
if !peer.IsPublic {
|
if !peer.IsPublic {
|
||||||
|
a.devAddPeer(peer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.WGEndpoint = endpoint
|
|
||||||
peer.State = StateDirect
|
|
||||||
a.devAddDirect(peer, endpoint)
|
a.devAddDirect(peer, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +58,7 @@ 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)
|
a.devAddDirect(a.relay, a.relay.WGEndpoint())
|
||||||
a.relay = nil
|
a.relay = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +77,11 @@ func (a *App) switchActiveRelay() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.devSetRelay(best, best.WGEndpoint)
|
a.devSetRelay(best, best.WGEndpoint())
|
||||||
a.relay = best
|
a.relay = best
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Why not < ??
|
||||||
// betterRelay reports whether a is a better relay candidate than b.
|
// betterRelay reports whether a is a better relay candidate than b.
|
||||||
// Prefers lower RTT; treats zero RTT (no measurement yet) as worst case.
|
// Prefers lower RTT; treats zero RTT (no measurement yet) as worst case.
|
||||||
func betterRelay(a, b *Peer) bool {
|
func betterRelay(a, b *Peer) bool {
|
||||||
|
|||||||
315
peer/on_hub_test.go
Normal file
315
peer/on_hub_test.go
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustKey(t *testing.T) wgtypes.Key {
|
||||||
|
t.Helper()
|
||||||
|
k, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate key: %v", err)
|
||||||
|
}
|
||||||
|
return k.PublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOnAddPeer(t *testing.T) {
|
||||||
|
ep1 := netip.MustParseAddrPort("1.2.3.4:51820")
|
||||||
|
ep2 := netip.MustParseAddrPort("5.6.7.8:51820")
|
||||||
|
peerVPNIP := netip.MustParseAddr("10.0.0.2")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
setup func(a *App, key wgtypes.Key)
|
||||||
|
peer func(key wgtypes.Key) HubPeer
|
||||||
|
check func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non-public peer added in StateRelayed with no dev calls",
|
||||||
|
peer: func(k wgtypes.Key) HubPeer {
|
||||||
|
return HubPeer{PubKey: k, VPNIP: peerVPNIP}
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key) {
|
||||||
|
p := a.peersByKey[key]
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("not in peersByKey")
|
||||||
|
}
|
||||||
|
if a.peersByIP[peerVPNIP] == nil {
|
||||||
|
t.Fatal("not in peersByIP")
|
||||||
|
}
|
||||||
|
if p.State != StateRelayed {
|
||||||
|
t.Fatalf("state = %v, want StateRelayed", p.State)
|
||||||
|
}
|
||||||
|
dev.AssertNoCalls(t)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "public peer with endpoint goes to StateDirect via AddDirect",
|
||||||
|
peer: func(k wgtypes.Key) HubPeer {
|
||||||
|
return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1}
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key) {
|
||||||
|
p := a.peersByKey[key]
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "public peer with no endpoint is dropped",
|
||||||
|
peer: func(k wgtypes.Key) HubPeer {
|
||||||
|
return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true}
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key) {
|
||||||
|
if a.peersByKey[key] != nil {
|
||||||
|
t.Fatal("peer should not be in peersByKey")
|
||||||
|
}
|
||||||
|
if a.peersByIP[peerVPNIP] != nil {
|
||||||
|
t.Fatal("peer should not be in peersByIP")
|
||||||
|
}
|
||||||
|
dev.AssertNoCalls(t)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "re-add removes old WG entry before adding new one",
|
||||||
|
setup: func(a *App, key wgtypes.Key) {
|
||||||
|
a.onAddPeer(HubPeer{PubKey: key, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1})
|
||||||
|
},
|
||||||
|
peer: func(k wgtypes.Key) HubPeer {
|
||||||
|
return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep2}
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key) {
|
||||||
|
if len(dev.Calls) != 2 {
|
||||||
|
t.Fatalf("dev calls = %v, want [RemovePeer, AddDirect]", dev.Calls)
|
||||||
|
}
|
||||||
|
dev.AssertRemovePeer(t, 0, key)
|
||||||
|
dev.AssertAddDirect(t, 1, key, ep2, peerVPNIP)
|
||||||
|
if len(a.peersByKey) != 1 || len(a.peersByIP) != 1 {
|
||||||
|
t.Errorf("maps: peersByKey=%d peersByIP=%d, want 1 each", len(a.peersByKey), len(a.peersByIP))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
a, dev := newTestApp(t, "10.0.0.1", false, false)
|
||||||
|
key := mustKey(t)
|
||||||
|
if tc.setup != nil {
|
||||||
|
tc.setup(a, key)
|
||||||
|
dev.Calls = nil
|
||||||
|
}
|
||||||
|
a.onAddPeer(tc.peer(key))
|
||||||
|
tc.check(t, a, dev, key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOnRemovePeer(t *testing.T) {
|
||||||
|
ep1 := netip.MustParseAddrPort("1.2.3.4:51820")
|
||||||
|
ep2 := netip.MustParseAddrPort("5.6.7.8:51820")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
setup func(t *testing.T, a *App) wgtypes.Key // returns the key to remove
|
||||||
|
check func(t *testing.T, a *App, dev *fakeWGDevice)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unknown key is a no-op",
|
||||||
|
setup: func(t *testing.T, a *App) wgtypes.Key {
|
||||||
|
return mustKey(t)
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
dev.AssertNoCalls(t)
|
||||||
|
if len(a.peersByKey) != 0 {
|
||||||
|
t.Errorf("peersByKey should be empty")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "StateRelayed peer removed from maps without 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(a.peersByKey) != 0 || len(a.peersByIP) != 0 {
|
||||||
|
t.Errorf("maps should be empty after remove")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "StateDirect 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"), IsPublic: true, EndpointV4: ep1})
|
||||||
|
return key
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "removing active relay with no backup clears relay field",
|
||||||
|
setup: func(t *testing.T, a *App) wgtypes.Key {
|
||||||
|
relay := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
|
a.relay = relay
|
||||||
|
return relay.PubKey
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
if len(dev.Calls) != 1 {
|
||||||
|
t.Fatalf("dev calls = %v, want [RemovePeer]", dev.Calls)
|
||||||
|
}
|
||||||
|
dev.AssertRemovePeer(t, 0, dev.Calls[0].PubKey)
|
||||||
|
if a.relay != nil {
|
||||||
|
t.Errorf("relay should be nil after removing only relay")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "removing active relay elects backup via SetRelay",
|
||||||
|
setup: func(t *testing.T, a *App) wgtypes.Key {
|
||||||
|
relay1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
|
addRelayPeer(t, a, "10.0.0.11", ep2)
|
||||||
|
a.relay = relay1
|
||||||
|
return relay1.PubKey
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
if len(dev.Calls) != 2 {
|
||||||
|
t.Fatalf("dev calls = %v, want [RemovePeer, SetRelay]", dev.Calls)
|
||||||
|
}
|
||||||
|
dev.AssertRemovePeer(t, 0, dev.Calls[0].PubKey)
|
||||||
|
dev.AssertSetRelay(t, 1, dev.Calls[1].PubKey, ep2, a.vpnNet)
|
||||||
|
if a.relay == nil {
|
||||||
|
t.Errorf("relay should be set to backup after failover")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
a, dev := newTestApp(t, "10.0.0.1", false, false)
|
||||||
|
key := tc.setup(t, a)
|
||||||
|
dev.Calls = nil
|
||||||
|
a.onRemovePeer(key)
|
||||||
|
tc.check(t, a, dev)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchActiveRelay(t *testing.T) {
|
||||||
|
ep1 := netip.MustParseAddrPort("1.2.3.4:51820")
|
||||||
|
ep2 := netip.MustParseAddrPort("5.6.7.8:51820")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
setup func(t *testing.T, a *App)
|
||||||
|
check func(t *testing.T, a *App, dev *fakeWGDevice)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no candidates leaves relay nil",
|
||||||
|
setup: func(t *testing.T, a *App) {},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
dev.AssertNoCalls(t)
|
||||||
|
if a.relay != nil {
|
||||||
|
t.Error("relay should be nil")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single candidate elected via SetRelay",
|
||||||
|
setup: func(t *testing.T, a *App) {
|
||||||
|
addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
if len(dev.Calls) != 1 {
|
||||||
|
t.Fatalf("dev calls = %v, want [SetRelay]", dev.Calls)
|
||||||
|
}
|
||||||
|
dev.AssertSetRelay(t, 0, dev.Calls[0].PubKey, ep1, a.vpnNet)
|
||||||
|
if a.relay == nil {
|
||||||
|
t.Error("relay should be set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "measured RTT beats zero RTT",
|
||||||
|
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
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
if len(dev.Calls) != 1 {
|
||||||
|
t.Fatalf("dev calls = %v, want [SetRelay]", dev.Calls)
|
||||||
|
}
|
||||||
|
dev.AssertSetRelay(t, 0, dev.Calls[0].PubKey, ep1, a.vpnNet)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lower RTT wins",
|
||||||
|
setup: func(t *testing.T, a *App) {
|
||||||
|
r1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
|
r1.RTT = 5 * time.Millisecond
|
||||||
|
r2 := addRelayPeer(t, a, "10.0.0.11", ep2)
|
||||||
|
r2.RTT = 20 * time.Millisecond
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
if len(dev.Calls) != 1 {
|
||||||
|
t.Fatalf("dev calls = %v, want [SetRelay]", dev.Calls)
|
||||||
|
}
|
||||||
|
dev.AssertSetRelay(t, 0, dev.Calls[0].PubKey, ep1, a.vpnNet)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
a.relay = old
|
||||||
|
addRelayPeer(t, a, "10.0.0.11", ep2)
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
|
if len(dev.Calls) != 2 {
|
||||||
|
t.Fatalf("dev calls = %v, want [AddDirect, SetRelay]", dev.Calls)
|
||||||
|
}
|
||||||
|
if dev.Calls[0].Method != "AddDirect" || dev.Calls[0].Endpoint != ep1 {
|
||||||
|
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 {
|
||||||
|
t.Error("relay should be the backup peer")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
a, dev := newTestApp(t, "10.0.0.1", false, false)
|
||||||
|
tc.setup(t, a)
|
||||||
|
dev.Calls = nil
|
||||||
|
a.switchActiveRelay()
|
||||||
|
tc.check(t, a, dev)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ func (a *App) onMulticastDiscovery(e MulticastEvent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if peer.IsPublic || peer.State == StateDirect {
|
if peer.IsPublic || peer.State() == StateDirect {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ func (a *App) onPing(e PingEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
peer.LastPing = now
|
|
||||||
peer.Up = true
|
|
||||||
|
|
||||||
// If we're the server, respond.
|
// 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 {
|
if peer.Role == control.Server {
|
||||||
a.sendPing(peer, e.ping.ID, e.ping.PingTS)
|
a.sendPing(peer, e.ping.ID, e.ping.PingTS)
|
||||||
}
|
}
|
||||||
@@ -35,7 +34,7 @@ func (a *App) onPing(e PingEvent) {
|
|||||||
|
|
||||||
// We can only learn our own endpoint from directly-connected peers — Dst
|
// We can only learn our own endpoint from directly-connected peers — Dst
|
||||||
// is the sender's observation of our WG handshake source.
|
// is the sender's observation of our WG handshake source.
|
||||||
if peer.State == StateDirect {
|
if peer.State() == StateDirect {
|
||||||
if dst := e.ping.Dst; dst.IsValid() {
|
if dst := e.ping.Dst; dst.IsValid() {
|
||||||
if dst.Addr().Is4() {
|
if dst.Addr().Is4() {
|
||||||
a.selfV4 = dst
|
a.selfV4 = dst
|
||||||
@@ -51,16 +50,11 @@ 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.WGEndpoint() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.Endpoint4 = v4
|
peer.Endpoint4 = v4
|
||||||
peer.Endpoint6 = v6
|
peer.Endpoint6 = v6
|
||||||
peer.WGEndpoint = endpoint
|
|
||||||
|
|
||||||
if peer.State == StateRelayed {
|
|
||||||
peer.State = StateProbing
|
|
||||||
}
|
|
||||||
a.devAddProbe(peer, endpoint)
|
a.devAddProbe(peer, endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"vppn/peer/control"
|
"vppn/peer/control"
|
||||||
|
"vppn/peer/wginterface"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) onTick() {
|
func (a *App) onTick() {
|
||||||
@@ -13,38 +14,36 @@ func (a *App) onTick() {
|
|||||||
a.nextPingID++
|
a.nextPingID++
|
||||||
now := time.Now().UnixNano()
|
now := time.Now().UnixNano()
|
||||||
|
|
||||||
// Update Up values.
|
|
||||||
for _, p := range a.peersByKey {
|
|
||||||
p.Up = p.Alive()
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Fatalf("Wireguard peer not in index: %v", 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, a.nextPingID, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promote probing peers to direct once alive (direct path confirmed
|
switch p.State() {
|
||||||
// working).
|
case StateProbing:
|
||||||
if p.State == StateProbing && time.Since(wgPeer.LastHandshakeTime) < 2*PingInterval {
|
// Promote probing peers to direct once alive (direct path confirmed
|
||||||
p.State = StateDirect
|
// working).
|
||||||
a.devAddDirect(p, p.WGEndpoint)
|
if time.Since(p.LastHandshakeTime()) < wginterface.SessionTimeout {
|
||||||
}
|
a.devAddDirect(p, p.WGEndpoint())
|
||||||
|
}
|
||||||
|
|
||||||
// Demote stale non-public direct peers back to probing.
|
case StateDirect:
|
||||||
if p.State == StateDirect && !p.IsPublic && !p.Up {
|
// Demote stale non-public direct peers back to probing.
|
||||||
p.State = StateProbing
|
if !p.IsPublic && !p.Up() {
|
||||||
a.devAddProbe(p, p.WGEndpoint)
|
a.devAddProbe(p, p.WGEndpoint())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have a live relay.
|
// Ensure we have a live relay.
|
||||||
if a.relay == nil || !a.relay.Up {
|
if a.relay == nil || !a.relay.Up() {
|
||||||
a.switchActiveRelay()
|
a.switchActiveRelay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"vppn/peer/control"
|
"vppn/peer/control"
|
||||||
|
"vppn/peer/wginterface"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PeerState string
|
type PeerState string
|
||||||
@@ -18,27 +19,53 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Peer struct {
|
type Peer struct {
|
||||||
PubKey wgtypes.Key // WireGuard public key.
|
wgPeer wgtypes.Peer
|
||||||
VPNIP netip.Addr // VPN IP address.
|
VPNIP netip.Addr // VPN IP address.
|
||||||
IsRelay bool // Peer is a relay.
|
IsRelay bool // Peer is a relay.
|
||||||
IsPublic bool // Peer has a public IP.
|
IsPublic bool // Peer has a public IP.
|
||||||
Endpoint4 netip.AddrPort // Reported IPv4 endpoint.
|
Endpoint4 netip.AddrPort // Reported IPv4 endpoint.
|
||||||
Endpoint6 netip.AddrPort // Reported IPv6 endpoint.
|
Endpoint6 netip.AddrPort // Reported IPv6 endpoint.
|
||||||
ObservedEndpoint netip.AddrPort // If we're public: WG handshake source endpoint.
|
RTT time.Duration // Round-trip time.
|
||||||
LastPing time.Time // Time of last ping received.
|
Role control.Role // Client initiates pings; server responds.
|
||||||
RTT time.Duration // Round-trip time.
|
|
||||||
Role control.Role // Client initiates pings; server responds.
|
|
||||||
State PeerState // Current connection state.
|
|
||||||
Up bool // Is the peer alive.
|
|
||||||
WGEndpoint netip.AddrPort // Endpoint currently in WG (direct/probe).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) Alive() bool {
|
// PubKey is the wireguard public key.
|
||||||
return time.Since(p.LastPing) < TimeoutInterval
|
func (p *Peer) PubKey() wgtypes.Key {
|
||||||
|
return p.wgPeer.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Peer) State() PeerState {
|
||||||
|
if len(p.wgPeer.AllowedIPs) > 0 {
|
||||||
|
return StateDirect
|
||||||
|
}
|
||||||
|
if p.wgPeer.Endpoint == nil {
|
||||||
|
return StateRelayed
|
||||||
|
}
|
||||||
|
return StateProbing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Peer) WGEndpoint() netip.AddrPort {
|
||||||
|
ep := p.wgPeer.Endpoint
|
||||||
|
if ep == nil {
|
||||||
|
return netip.AddrPort{}
|
||||||
|
}
|
||||||
|
addr, ok := netip.AddrFromSlice(ep.IP)
|
||||||
|
if !ok {
|
||||||
|
return netip.AddrPort{}
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(addr.Unmap(), uint16(ep.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Peer) LastHandshakeTime() time.Time {
|
||||||
|
return p.wgPeer.LastHandshakeTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Peer) Up() bool {
|
||||||
|
return time.Since(p.wgPeer.LastHandshakeTime) < wginterface.SessionTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) CanRelay() bool {
|
func (p *Peer) CanRelay() bool {
|
||||||
return p.IsRelay && p.Up && p.WGEndpoint.IsValid()
|
return p.IsRelay && p.Up()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) PreferredEndpoint() netip.AddrPort {
|
func (p *Peer) PreferredEndpoint() netip.AddrPort {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
// Create creates a WireGuard interface named name, assigns vpnIP/prefixLen to
|
// Create creates a WireGuard interface named name, assigns vpnIP/prefixLen to
|
||||||
// it, and brings it up.
|
// it, and brings it up.
|
||||||
func Create(name string, vpnIP net.IP, prefixLen int) error {
|
func Create(name string, vpnIP net.IP, prefixLen int) error {
|
||||||
|
_ = Delete(name) // remove any stale interface left by a previous run
|
||||||
if err := nlNewLink(name); err != nil {
|
if err := nlNewLink(name); err != nil {
|
||||||
return fmt.Errorf("failed to create wireguard link: %w", err)
|
return fmt.Errorf("failed to create wireguard link: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,17 @@ func (d *Device) Peer(pubKey wgtypes.Key) (wgtypes.Peer, error) {
|
|||||||
return wgtypes.Peer{}, fmt.Errorf("peer %v not found in %q", pubKey, d.name)
|
return wgtypes.Peer{}, fmt.Errorf("peer %v not found in %q", pubKey, d.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddPeer registers a peer with no AllowedIPs and no endpoint. WireGuard will
|
||||||
|
// accept handshakes from this peer but route no traffic to it yet.
|
||||||
|
func (d *Device) AddPeer(pubKey wgtypes.Key) error {
|
||||||
|
return d.client.ConfigureDevice(d.name, wgtypes.Config{
|
||||||
|
Peers: []wgtypes.PeerConfig{{
|
||||||
|
PublicKey: pubKey,
|
||||||
|
ReplaceAllowedIPs: true,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetRelay configures the relay peer with AllowedIPs covering the entire VPN
|
// SetRelay configures the relay peer with AllowedIPs covering the entire VPN
|
||||||
// network prefix. This is the fallback route for all VPN traffic.
|
// network prefix. This is the fallback route for all VPN traffic.
|
||||||
func (d *Device) SetRelay(pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) error {
|
func (d *Device) SetRelay(pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user