141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
package peer
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/nacl/sign"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
)
|
|
|
|
const (
|
|
// 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(
|
|
netip.AddrFrom4([4]byte{224, 0, 0, 157}),
|
|
4560))
|
|
|
|
// RunMCWriter broadcasts a signed beacon on the local multicast group every
|
|
// mcBroadcastInterval so that LAN peers can discover our WireGuard endpoint.
|
|
func RunMCWriter(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, signKey *[64]byte) {
|
|
for {
|
|
runMCWriterInner(selfVPNIP, pubKey, wgPort, signKey)
|
|
time.Sleep(mcErrorRetryInterval)
|
|
}
|
|
}
|
|
|
|
func runMCWriterInner(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, signKey *[64]byte) {
|
|
conn, err := net.ListenMulticastUDP("udp", nil, mcAddr)
|
|
if err != nil {
|
|
log.Printf("[MCWriter] bind: %v", err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
// 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 := send(); err != nil {
|
|
log.Printf("[MCWriter] write: %v", err)
|
|
}
|
|
|
|
for range time.Tick(mcBroadcastInterval) {
|
|
if err := send(); err != nil {
|
|
log.Printf("[MCWriter] write: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// RunMCReader listens for multicast beacons from LAN peers and feeds
|
|
// MulticastEvents to ch.
|
|
func RunMCReader(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- MulticastEvent) {
|
|
for {
|
|
if err := runMCReaderInner(vpnNet, selfVPNIP, ch); err != nil {
|
|
log.Printf("[MCReader] %v", err)
|
|
}
|
|
time.Sleep(mcErrorRetryInterval)
|
|
}
|
|
}
|
|
|
|
func runMCReaderInner(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- MulticastEvent) error {
|
|
conn, err := net.ListenMulticastUDP("udp", nil, mcAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("bind: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
buf := make([]byte, mcSignedBeaconLen+1) // +1 to detect oversized packets
|
|
netAddr := vpnNet.Addr().As4()
|
|
|
|
for {
|
|
conn.SetReadDeadline(time.Now().Add(32 * time.Second))
|
|
n, src, err := conn.ReadFromUDPAddrPort(buf)
|
|
if err != nil {
|
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
|
continue
|
|
}
|
|
return fmt.Errorf("read: %w", err)
|
|
}
|
|
if n != mcSignedBeaconLen {
|
|
continue
|
|
}
|
|
|
|
// 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)
|
|
if vpnIP == selfVPNIP {
|
|
continue
|
|
}
|
|
|
|
if age := beaconAge(buf, time.Now()); age > mcBeaconMaxAge || age < -mcBeaconMaxAge {
|
|
continue
|
|
}
|
|
|
|
signed := make([]byte, mcSignedBeaconLen)
|
|
copy(signed, buf[:mcSignedBeaconLen])
|
|
|
|
ch <- MulticastEvent{
|
|
signed: signed,
|
|
src: src.Addr().Unmap(),
|
|
}
|
|
}
|
|
}
|