Files
vppn/peer/app.go
2026-06-15 18:45:11 +02:00

157 lines
3.5 KiB
Go

package peer
import (
"fmt"
"log"
"net/netip"
"os"
"os/signal"
"sort"
"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
TickInterval = 2 * 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
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()
stateTicker := time.NewTicker(TickInterval)
pingTicker := time.NewTicker(PingInterval)
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 <-stateTicker.C:
a.onStateTick()
case <-pingTicker.C:
a.onPingTick()
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\n", a.vpnNet)
fmt.Fprintf(&b, " IPv4: %v\n", a.selfV4)
fmt.Fprintf(&b, " IPv6: %v\n", a.selfV6)
b.WriteString("Peers:\n")
//
peers := make([]*Peer, 0, len(a.peersByIP))
for _, p := range a.peersByIP {
peers = append(peers, p)
}
sort.Slice(peers, func(i, j int) bool {
return peers[i].VPNIP.As4()[3] < peers[j].VPNIP.As4()[3]
})
for _, p := range peers {
ip := p.VPNIP.As4()[3]
up := "DOWN"
if p.Up() {
up = "UP "
}
endpoint := p.WGEndpoint()
if endpoint.IsValid() {
fmt.Fprintf(&b, " %24s %03d %s %s seen=%s @ %s\n",
p.Name, ip, p.State, up, time.Since(p.LastPing).Round(time.Millisecond), endpoint)
} else {
fmt.Fprintf(&b, " %24s %03d %s %s seen=%s\n",
p.Name, ip, p.State, up, time.Since(p.LastPing).Round(time.Millisecond))
}
}
log.Print(b.String())
}