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

View File

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

View File

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

View File

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