From 991fcffa68c136aad6df8909c57e78b499f86ad7 Mon Sep 17 00:00:00 2001 From: jdl Date: Fri, 12 Jun 2026 13:37:05 +0200 Subject: [PATCH] Separate multicast package. --- peer/multicast/broadcaster.go | 62 +++++++++++++++++++++++++++++++++++ peer/multicast/global.go | 9 +++++ peer/multicast/packet.go | 54 ++++++++++++++++++++++++++++++ peer/multicast/packet_test.go | 38 +++++++++++++++++++++ peer/multicast/receiver.go | 61 ++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+) create mode 100644 peer/multicast/broadcaster.go create mode 100644 peer/multicast/global.go create mode 100644 peer/multicast/packet.go create mode 100644 peer/multicast/packet_test.go create mode 100644 peer/multicast/receiver.go diff --git a/peer/multicast/broadcaster.go b/peer/multicast/broadcaster.go new file mode 100644 index 0000000..c349d6b --- /dev/null +++ b/peer/multicast/broadcaster.go @@ -0,0 +1,62 @@ +package multicast + +import ( + "log" + "net" + "net/netip" + "time" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +var addr = net.UDPAddrFromAddrPort(netip.AddrPortFrom( + netip.AddrFrom4([4]byte{224, 0, 0, 157}), + 4560)) + +func Broadcast( + selfVPNIP netip.Addr, + pubKey wgtypes.Key, + wgPort uint16, + signKey *[64]byte, +) { + for { + broadcastInner(selfVPNIP, pubKey, wgPort, signKey) + time.Sleep(errorTimeout) + } +} + +func broadcastInner(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, signKey *[64]byte) { + conn, err := net.ListenMulticastUDP("udp", nil, addr) + if err != nil { + log.Printf("[MCBroadcast] bind: %v", err) + return + } + defer conn.Close() + + buf := make([]byte, BufferSize) + packet := Packet{ + PeerIP: selfVPNIP.As4()[3], + WGPubKey: pubKey, + WGPort: wgPort, + } + + // Re-sign on each send so the timestamp is fresh; a stale timestamp would be + // dropped by receivers' freshness gate. + send := func() error { + packet.Timestamp = time.Now().Unix() + payload := packet.Marshal(buf, signKey) + _, err := conn.WriteToUDP(payload, addr) + return err + } + + if err := send(); err != nil { + log.Printf("[MCBroadcast] write: %v", err) + } + + for range time.Tick(broadcastInterval) { + if err := send(); err != nil { + log.Printf("[MCBroadcast] write: %v", err) + return + } + } +} diff --git a/peer/multicast/global.go b/peer/multicast/global.go new file mode 100644 index 0000000..00ea4b7 --- /dev/null +++ b/peer/multicast/global.go @@ -0,0 +1,9 @@ +package multicast + +import "time" + +const ( + errorTimeout = 16 * time.Second + broadcastInterval = 16 * time.Second + maxPacketAge = time.Minute +) diff --git a/peer/multicast/packet.go b/peer/multicast/packet.go new file mode 100644 index 0000000..ed3db7c --- /dev/null +++ b/peer/multicast/packet.go @@ -0,0 +1,54 @@ +package multicast + +import ( + "encoding/binary" + "net/netip" + + "golang.org/x/crypto/nacl/sign" +) + +const ( + BufferSize = packetSize + SignedPacketSize + SignedPacketSize = packetSize + signSize + packetSize = 43 + signSize = 64 +) + +// Layout: +// +// [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 +type Packet struct { + PeerIP byte // Final octet of the sender's VPN IP. + WGPubKey [32]byte // WG public key. + WGPort uint16 // WG listen port. + Timestamp int64 // Unix timestamp. + Src netip.Addr // Source of packet. + Signed []byte // Raw signed message for verification (incoming packet). +} + +// Marshal the packet into a buffer with prefixed signature. +func (p Packet) Marshal(buf []byte, signKey *[64]byte) []byte { + buf[0] = p.PeerIP + copy(buf[1:33], p.WGPubKey[:]) + binary.BigEndian.PutUint16(buf[33:35], p.WGPort) + binary.BigEndian.PutUint64(buf[35:43], uint64(p.Timestamp)) + return sign.Sign(buf[packetSize:packetSize], buf[:packetSize], signKey) +} + +func (p Packet) Verify(buf []byte, pubKey *[32]byte) bool { + _, ok := sign.Open(buf, p.Signed, pubKey) + return ok +} + +func Unmarshal(signed []byte) (p Packet) { + buf := signed[signSize:] + p.PeerIP = buf[0] + copy(p.WGPubKey[:], buf[1:33]) + p.WGPort = binary.BigEndian.Uint16(buf[33:35]) + p.Timestamp = int64(binary.BigEndian.Uint64(buf[35:43])) + p.Signed = signed + return +} diff --git a/peer/multicast/packet_test.go b/peer/multicast/packet_test.go new file mode 100644 index 0000000..6aed8d9 --- /dev/null +++ b/peer/multicast/packet_test.go @@ -0,0 +1,38 @@ +package multicast + +import ( + "crypto/rand" + "testing" + + "golang.org/x/crypto/nacl/sign" +) + +func TestPacket(t *testing.T) { + pub, priv, err := sign.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + p := Packet{ + PeerIP: 10, + WGPubKey: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2}, + WGPort: 44, + Timestamp: 12948893, + } + + buf := make([]byte, BufferSize) + signed := p.Marshal(buf, priv) + if len(signed) != SignedPacketSize { + t.Fatalf("signed length = %d, want %d", len(signed), SignedPacketSize) + } + + got := Unmarshal(signed) + if got.PeerIP != p.PeerIP || got.WGPubKey != p.WGPubKey || + got.WGPort != p.WGPort || got.Timestamp != p.Timestamp { + t.Fatalf("round-trip mismatch:\n got %+v\nwant %+v", got, p) + } + + if !got.Verify(nil, pub) { + t.Error("signature did not verify") + } +} diff --git a/peer/multicast/receiver.go b/peer/multicast/receiver.go new file mode 100644 index 0000000..2d1bfad --- /dev/null +++ b/peer/multicast/receiver.go @@ -0,0 +1,61 @@ +package multicast + +import ( + "bytes" + "fmt" + "log" + "net" + "net/netip" + "time" +) + +func Receiver(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- Packet) { + for { + if err := receiver(vpnNet, selfVPNIP, ch); err != nil { + log.Printf("[MCReader] %v", err) + } + time.Sleep(errorTimeout) + } +} + +func receiver(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- Packet) error { + selfIP := selfVPNIP.As4()[3] + + conn, err := net.ListenMulticastUDP("udp", nil, addr) + if err != nil { + return fmt.Errorf("bind: %w", err) + } + defer conn.Close() + + buf := make([]byte, BufferSize+1) // +1 to detect oversized packets + + 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 != SignedPacketSize { + continue + } + + packet := Unmarshal(buf[:n]) + + if packet.PeerIP == selfIP { + continue + } + + age := time.Since(time.Unix(packet.Timestamp, 0)) + if age > maxPacketAge || age < -maxPacketAge { + continue + } + + packet.Signed = bytes.Clone(packet.Signed) + packet.Src = src.Addr().Unmap() + ch <- packet + } +}