WIP
This commit is contained in:
@@ -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"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user