multicast cleanup
This commit is contained in:
12
peer/app.go
12
peer/app.go
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"vppn/m"
|
||||
"vppn/peer/control"
|
||||
"vppn/peer/multicast"
|
||||
"vppn/peer/wginterface"
|
||||
)
|
||||
|
||||
@@ -27,12 +28,6 @@ type PingEvent struct {
|
||||
ping control.Ping
|
||||
}
|
||||
|
||||
// MulticastEvent carries a raw signed beacon for verification in the event loop.
|
||||
type MulticastEvent struct {
|
||||
signed []byte // nacl/sign signed beacon (64-byte sig || 35-byte payload)
|
||||
src netip.Addr // physical LAN source address
|
||||
}
|
||||
|
||||
// App is the peer application. All mutable state lives here and is
|
||||
// accessed only from the Run goroutine.
|
||||
type App struct {
|
||||
@@ -58,11 +53,14 @@ type App struct {
|
||||
selfV4 netip.AddrPort
|
||||
selfV6 netip.AddrPort
|
||||
|
||||
// Reusable scratch for multicast signature verification (event loop only).
|
||||
mcVerifyBuf []byte
|
||||
|
||||
// Event channels fed by background goroutines
|
||||
hubAddCh <-chan m.Peer
|
||||
hubRemoveCh <-chan wgtypes.Key
|
||||
pingCh <-chan PingEvent
|
||||
multicastCh <-chan MulticastEvent
|
||||
multicastCh <-chan multicast.Packet
|
||||
}
|
||||
|
||||
// Run is the main event loop. It runs until SIGTERM/SIGINT.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
"vppn/m"
|
||||
"vppn/peer/multicast"
|
||||
)
|
||||
|
||||
// addRelayPeer adds a public relay peer and marks it Up so it satisfies
|
||||
@@ -54,7 +55,7 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
|
||||
hubAddCh: make(chan m.Peer),
|
||||
hubRemoveCh: make(chan wgtypes.Key),
|
||||
pingCh: make(chan PingEvent),
|
||||
multicastCh: make(chan MulticastEvent),
|
||||
multicastCh: make(chan multicast.Packet),
|
||||
}
|
||||
return a, dev, cc
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
"vppn/m"
|
||||
"vppn/peer/multicast"
|
||||
"vppn/peer/wginterface"
|
||||
)
|
||||
|
||||
@@ -60,7 +61,7 @@ func New(
|
||||
pingCh := make(chan PingEvent)
|
||||
hubAddCh := make(chan m.Peer)
|
||||
hubRemoveCh := make(chan wgtypes.Key)
|
||||
multicastCh := make(chan MulticastEvent)
|
||||
multicastCh := make(chan multicast.Packet)
|
||||
|
||||
poller, err := NewHubPoller(
|
||||
state.VPNIP,
|
||||
@@ -79,8 +80,8 @@ func New(
|
||||
go poller.Run()
|
||||
|
||||
if !state.IsPublic {
|
||||
go RunMCWriter(state.VPNIP, state.PrivKey.PublicKey(), state.WGPort, &state.SignKey)
|
||||
go RunMCReader(state.VPNNet, state.VPNIP, multicastCh)
|
||||
go multicast.Broadcast(state.VPNIP, state.PrivKey.PublicKey(), state.WGPort, &state.SignKey)
|
||||
go multicast.Receiver(state.VPNNet, state.VPNIP, multicastCh)
|
||||
}
|
||||
|
||||
return &App{
|
||||
@@ -98,6 +99,8 @@ func New(
|
||||
peersByKey: make(map[wgtypes.Key]*Peer),
|
||||
peersByIP: make(map[netip.Addr]*Peer),
|
||||
|
||||
mcVerifyBuf: make([]byte, 0, multicast.SignedPacketSize),
|
||||
|
||||
hubAddCh: hubAddCh,
|
||||
hubRemoveCh: hubRemoveCh,
|
||||
pingCh: pingCh,
|
||||
|
||||
@@ -1,57 +1,46 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
"golang.org/x/crypto/nacl/sign"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
"vppn/peer/multicast"
|
||||
)
|
||||
|
||||
func (a *App) onMulticastDiscovery(e MulticastEvent) {
|
||||
func (a *App) onMulticastDiscovery(pkt multicast.Packet) {
|
||||
if a.isPublic {
|
||||
return
|
||||
}
|
||||
|
||||
// Peek at the VPN IP byte to find the sender peer before verifying.
|
||||
// nacl/sign prepends a 64-byte signature, so payload starts at offset sign.Overhead.
|
||||
if len(e.signed) != mcSignedBeaconLen {
|
||||
return
|
||||
}
|
||||
netAddr := a.vpnNet.Addr().As4()
|
||||
octets := netAddr
|
||||
octets[3] = e.signed[sign.Overhead]
|
||||
// Locate the sender peer by its VPN IP (final octet carried in the beacon).
|
||||
octets := a.vpnNet.Addr().As4()
|
||||
octets[3] = pkt.PeerIP
|
||||
vpnIP := netip.AddrFrom4(octets)
|
||||
|
||||
peer, ok := a.peersByIP[vpnIP]
|
||||
if !ok {
|
||||
if !ok || peer.IsPublic || peer.State == StateDirect {
|
||||
return
|
||||
}
|
||||
|
||||
if peer.IsPublic || peer.State == StateDirect {
|
||||
// Authenticate the beacon against the peer's known sign key.
|
||||
if !pkt.Verify(a.mcVerifyBuf, &peer.SignPubKey) {
|
||||
return
|
||||
}
|
||||
|
||||
payload, ok := sign.Open(nil, e.signed, &peer.SignPubKey)
|
||||
if !ok {
|
||||
// The beacon is authentic but must also advertise the WG key the hub gave
|
||||
// us for this peer; otherwise it's inconsistent — drop it.
|
||||
if wgtypes.Key(pkt.WGPubKey) != peer.PubKey() {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
wgPort := binary.BigEndian.Uint16(payload[33:35])
|
||||
endpoint := netip.AddrPortFrom(e.src, wgPort)
|
||||
endpoint := netip.AddrPortFrom(pkt.Src, pkt.WGPort)
|
||||
if !endpoint.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
var v4, v6 netip.AddrPort
|
||||
if e.src.Is4() {
|
||||
if pkt.Src.Is4() {
|
||||
v4 = endpoint
|
||||
} else {
|
||||
v6 = endpoint
|
||||
|
||||
Reference in New Issue
Block a user