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