This commit is contained in:
jdl
2026-06-11 18:47:49 +02:00
parent 0902341322
commit 2b4cbae49c
4 changed files with 43 additions and 18 deletions

View File

@@ -20,33 +20,33 @@ func TestRoundTrip(t *testing.T) {
name: "client ping", name: "client ping",
ping: control.Ping{ ping: control.Ping{
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"),
}, },
}, },
{ {
name: "server response", name: "server response",
ping: control.Ping{ ping: control.Ping{
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"),
}, },
}, },
{ {
name: "IPv6 only", name: "IPv6 only",
ping: control.Ping{ ping: control.Ping{
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"),
}, },
}, },
{ {
name: "dual stack", name: "dual stack",
ping: control.Ping{ ping: control.Ping{
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"),
Dst: netip.MustParseAddrPort("5.6.7.8:9999"), Dst: netip.MustParseAddrPort("5.6.7.8:9999"),
}, },
}, },
{ {

View File

@@ -13,10 +13,19 @@ import (
) )
const ( 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 mcSignedBeaconLen = sign.Overhead + mcBeaconLen // 64-byte nacl/sign prefix + payload
mcBroadcastInterval = 32 * time.Second mcBroadcastInterval = 32 * time.Second
mcErrorRetryInterval = 16 * 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( var mcAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(
@@ -40,26 +49,33 @@ func runMCWriterInner(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, s
} }
defer conn.Close() defer conn.Close()
payload := buildBeacon(selfVPNIP, pubKey, wgPort) // Re-sign on each send so the timestamp is fresh; a stale timestamp would be
signed := sign.Sign(nil, payload, signKey) // 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) log.Printf("[MCWriter] write: %v", err)
} }
for range time.Tick(mcBroadcastInterval) { for range time.Tick(mcBroadcastInterval) {
if _, err := conn.WriteToUDP(signed, mcAddr); err != nil { if err := send(); err != nil {
log.Printf("[MCWriter] write: %v", err) log.Printf("[MCWriter] write: %v", err)
return 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 := make([]byte, mcBeaconLen)
beacon[0] = selfVPNIP.As4()[3] beacon[0] = selfVPNIP.As4()[3]
copy(beacon[1:33], pubKey[:]) copy(beacon[1:33], pubKey[:])
binary.BigEndian.PutUint16(beacon[33:35], wgPort) binary.BigEndian.PutUint16(beacon[33:35], wgPort)
binary.BigEndian.PutUint64(beacon[35:43], uint64(ts))
return beacon return beacon
} }
@@ -97,8 +113,11 @@ func runMCReaderInner(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- Multi
continue continue
} }
// Peek at VPN IP byte (first byte of payload after 64-byte sig prefix) // Cheap pre-filters on the unverified payload, before the costly
// to skip our own beacon before verifying. // 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 := netAddr
octets[3] = buf[sign.Overhead] octets[3] = buf[sign.Overhead]
vpnIP := netip.AddrFrom4(octets) vpnIP := netip.AddrFrom4(octets)
@@ -106,6 +125,10 @@ func runMCReaderInner(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- Multi
continue continue
} }
if age := beaconAge(buf, time.Now()); age > mcBeaconMaxAge || age < -mcBeaconMaxAge {
continue
}
signed := make([]byte, mcSignedBeaconLen) signed := make([]byte, mcSignedBeaconLen)
copy(signed, buf[:mcSignedBeaconLen]) copy(signed, buf[:mcSignedBeaconLen])

View File

@@ -96,6 +96,7 @@ func (a *App) switchActiveRelay() {
} }
func preferredEndpoint(v4, v6 netip.AddrPort) netip.AddrPort { func preferredEndpoint(v4, v6 netip.AddrPort) netip.AddrPort {
// We always prefer v4 since all peers can connect to IPv4 addresses.
if v4.IsValid() { if v4.IsValid() {
return v4 return v4
} }

View File

@@ -37,7 +37,8 @@ func (a *App) onMulticastDiscovery(e MulticastEvent) {
return 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]) wgPubKey, err := wgtypes.NewKey(payload[1:33])
if err != nil || wgPubKey != peer.PubKey() { if err != nil || wgPubKey != peer.PubKey() {
return return