Files
vppn/peer/app.go
2026-06-13 19:34:18 +02:00

142 lines
3.2 KiB
Go

package peer
import (
"fmt"
"log"
"net/netip"
"os"
"os/signal"
"strings"
"syscall"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"vppn/m"
"vppn/peer/control"
"vppn/peer/multicast"
"vppn/peer/wginterface"
)
var _ WGDevice = (*wginterface.Device)(nil) // compile-time check: Device satisfies WGDevice
const (
ControlPort = 4561
PingInterval = 8 * time.Second
TimeoutInterval = 30 * time.Second
)
// scratchSize is large enough for the biggest buffer either the ping or the
// multicast path serializes through the shared App scratch.
const scratchSize = max(control.Size, multicast.SignedPacketSize)
type PingEvent struct {
srcVPNIP netip.Addr
ping control.Ping
}
// App is the peer application. All mutable state lives here and is
// accessed only from the Run goroutine.
type App struct {
// Identity
vpnIP netip.Addr
vpnNet netip.Prefix
privKey wgtypes.Key
pubKey wgtypes.Key
isRelay bool
isPublic bool
localDomain string
// Infrastructure
dev WGDevice
controlConn ControlConn
// Peer state
relay *Peer
peersByKey map[wgtypes.Key]*Peer
peersByIP map[netip.Addr]*Peer
// Our own external endpoints, learned from Dst fields in incoming pings
selfV4 netip.AddrPort
selfV6 netip.AddrPort
// Reusable serialization scratch for outgoing pings and multicast signature
// verification. Only touched from the Run goroutine.
scratch []byte
// Event channels fed by background goroutines
hubAddCh <-chan m.Peer
hubRemoveCh <-chan wgtypes.Key
pingCh <-chan PingEvent
multicastCh <-chan multicast.Packet
}
// Run is the main event loop. It runs until SIGTERM/SIGINT.
func (a *App) Run() error {
// Establish a clean hosts section before the first poll lands, clearing
// any stale entries left by a prior run (e.g. crash, or peers removed
// while we were down).
a.updateHosts()
ticker := time.NewTicker(PingInterval)
defer ticker.Stop()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(sig)
tickCount := 0
for {
select {
case p := <-a.hubAddCh:
a.onAddPeer(p)
case key := <-a.hubRemoveCh:
a.onRemovePeer(key)
case e := <-a.pingCh:
a.onPing(e)
case e := <-a.multicastCh:
a.onMulticastDiscovery(e)
case <-ticker.C:
a.onTick()
tickCount++
if tickCount%8 == 0 {
a.logNetworkState()
}
case <-sig:
return a.onShutdown()
}
}
}
func (a *App) onShutdown() error {
return wginterface.Delete(a.dev.Name())
}
func (a *App) logNetworkState() {
var b strings.Builder
fmt.Fprintf(&b, "Network state (self: %s public=%v):\n", a.vpnIP, a.isPublic)
fmt.Fprintf(&b, " Network: %v", a.vpnNet)
fmt.Fprintf(&b, " IPv4: %v\n", a.selfV4)
fmt.Fprintf(&b, " IPv6: %v\n", a.selfV6)
b.WriteString("Peers:\n")
for _, p := range a.peersByIP {
ip := p.VPNIP.As4()[3]
switch p.State {
case StateDirect:
fmt.Fprintf(&b, " %24s %03d DIRECT @ %s rtt=%s\n",
p.Name, ip, p.WGEndpoint(), p.RTT.Round(time.Millisecond))
case StateProbing:
fmt.Fprintf(&b, " %24s %03d PROBING @ %s\n",
p.Name, ip, p.PreferredEndpoint())
case StateRelayed:
fmt.Fprintf(&b, " %24s %03d RELAYED\n", p.Name, ip)
}
}
log.Print(b.String())
}