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 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(time.Second) // TODO: Const. 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.onTick() case <-pingTicker.C: a.onPingTicker() tickCount++ if tickCount == 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()) }