From 2b4cbae49c3ffdd6bdd47d02db61e866c2b8a585 Mon Sep 17 00:00:00 2001 From: jdl Date: Thu, 11 Jun 2026 18:47:49 +0200 Subject: [PATCH] WIP --- peer/control/ping_test.go | 18 +++++++++--------- peer/multicast.go | 39 +++++++++++++++++++++++++++++++-------- peer/on_hub.go | 1 + peer/on_multicast.go | 3 ++- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/peer/control/ping_test.go b/peer/control/ping_test.go index 9193cdf..62ed3b2 100644 --- a/peer/control/ping_test.go +++ b/peer/control/ping_test.go @@ -20,33 +20,33 @@ func TestRoundTrip(t *testing.T) { name: "client ping", ping: control.Ping{ PingTS: 1234567890, - SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"), - Dst: netip.MustParseAddrPort("5.6.7.8:51820"), + SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"), + Dst: netip.MustParseAddrPort("5.6.7.8:51820"), }, }, { name: "server response", ping: control.Ping{ PingTS: 1234567890, - SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"), - Dst: netip.MustParseAddrPort("1.2.3.4:9999"), + SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"), + Dst: netip.MustParseAddrPort("1.2.3.4:9999"), }, }, { name: "IPv6 only", ping: control.Ping{ PingTS: 999, - SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"), - Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"), + SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"), + Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"), }, }, { name: "dual stack", ping: control.Ping{ 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"), + SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"), + SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"), + Dst: netip.MustParseAddrPort("5.6.7.8:9999"), }, }, { diff --git a/peer/multicast.go b/peer/multicast.go index f383480..f2e1b8e 100644 --- a/peer/multicast.go +++ b/peer/multicast.go @@ -13,10 +13,19 @@ import ( ) const ( - mcBeaconLen = 35 // 1 VPN IP byte + 32 WG pubkey + 2 WG listen port + // Beacon payload layout (after the 64-byte nacl/sign prefix): + // [0] final octet of the sender's VPN IP + // [1:33] WG public key + // [33:35] WG listen port (big-endian uint16) + // [35:43] send time, Unix seconds (big-endian int64) — freshness/replay gate + mcBeaconLen = 43 mcSignedBeaconLen = sign.Overhead + mcBeaconLen // 64-byte nacl/sign prefix + payload mcBroadcastInterval = 32 * time.Second mcErrorRetryInterval = 16 * time.Second + // mcBeaconMaxAge bounds how far a beacon's timestamp may be from now (in + // either direction) before the reader drops it: it tolerates modest clock + // skew between roughly-synchronized (NTP) hosts and caps the replay window. + mcBeaconMaxAge = 60 * time.Second ) var mcAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom( @@ -40,26 +49,33 @@ func runMCWriterInner(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, s } defer conn.Close() - payload := buildBeacon(selfVPNIP, pubKey, wgPort) - signed := sign.Sign(nil, payload, signKey) + // Re-sign on each send so the timestamp is fresh; a stale timestamp would be + // dropped by receivers' freshness gate. + send := func() error { + payload := buildBeacon(selfVPNIP, pubKey, wgPort, time.Now().Unix()) + signed := sign.Sign(nil, payload, signKey) + _, err := conn.WriteToUDP(signed, mcAddr) + return err + } - if _, err := conn.WriteToUDP(signed, mcAddr); err != nil { + if err := send(); err != nil { log.Printf("[MCWriter] write: %v", err) } for range time.Tick(mcBroadcastInterval) { - if _, err := conn.WriteToUDP(signed, mcAddr); err != nil { + if err := send(); err != nil { log.Printf("[MCWriter] write: %v", err) return } } } -func buildBeacon(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16) []byte { +func buildBeacon(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, ts int64) []byte { beacon := make([]byte, mcBeaconLen) beacon[0] = selfVPNIP.As4()[3] copy(beacon[1:33], pubKey[:]) binary.BigEndian.PutUint16(beacon[33:35], wgPort) + binary.BigEndian.PutUint64(beacon[35:43], uint64(ts)) return beacon } @@ -97,8 +113,11 @@ func runMCReaderInner(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- Multi continue } - // Peek at VPN IP byte (first byte of payload after 64-byte sig prefix) - // to skip our own beacon before verifying. + // Cheap pre-filters on the unverified payload, before the costly + // signature check in the event loop: skip our own beacon, and drop + // stale ones (replay/freshness gate). The timestamp is authenticated by + // sign.Open later, so a forged-fresh timestamp still fails there — this + // only spares us verifying old replays. octets := netAddr octets[3] = buf[sign.Overhead] vpnIP := netip.AddrFrom4(octets) @@ -106,6 +125,10 @@ func runMCReaderInner(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- Multi continue } + if age := beaconAge(buf, time.Now()); age > mcBeaconMaxAge || age < -mcBeaconMaxAge { + continue + } + signed := make([]byte, mcSignedBeaconLen) copy(signed, buf[:mcSignedBeaconLen]) diff --git a/peer/on_hub.go b/peer/on_hub.go index 0d55718..2dc6558 100644 --- a/peer/on_hub.go +++ b/peer/on_hub.go @@ -96,6 +96,7 @@ func (a *App) switchActiveRelay() { } func preferredEndpoint(v4, v6 netip.AddrPort) netip.AddrPort { + // We always prefer v4 since all peers can connect to IPv4 addresses. if v4.IsValid() { return v4 } diff --git a/peer/on_multicast.go b/peer/on_multicast.go index 1476443..9db716b 100644 --- a/peer/on_multicast.go +++ b/peer/on_multicast.go @@ -37,7 +37,8 @@ func (a *App) onMulticastDiscovery(e MulticastEvent) { return } - // payload: [1 VPN IP byte][32 WG pubkey][2 WG port] + // payload: [1 VPN IP byte][32 WG pubkey][2 WG port][8 timestamp] + // (timestamp freshness is gated in the reader before this point). wgPubKey, err := wgtypes.NewKey(payload[1:33]) if err != nil || wgPubKey != peer.PubKey() { return