package peer import ( "encoding/json" "io" "log" "net/http" "net/netip" "net/url" "time" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "vppn/m" ) const hubPollInterval = 64 * time.Second type HubPoller struct { selfVPNIP netip.Addr vpnNet netip.Prefix hubURL string apiKey string addCh chan<- HubPeer removeCh chan<- wgtypes.Key known map[wgtypes.Key]int64 // pubKey → last seen version } func NewHubPoller( selfVPNIP netip.Addr, vpnNet netip.Prefix, hubURL, apiKey string, addCh chan<- HubPeer, removeCh chan<- wgtypes.Key, ) (*HubPoller, error) { u, err := url.Parse(hubURL) if err != nil { return nil, err } u.Path = "/peer/fetch-state/" return &HubPoller{ selfVPNIP: selfVPNIP, vpnNet: vpnNet, hubURL: u.String(), apiKey: apiKey, addCh: addCh, removeCh: removeCh, known: make(map[wgtypes.Key]int64), }, nil } func (hp *HubPoller) Run() { hp.poll() for range time.Tick(hubPollInterval) { hp.poll() } } func (hp *HubPoller) poll() { req, err := http.NewRequest(http.MethodGet, hp.hubURL, nil) if err != nil { log.Printf("[HubPoller] build request: %v", err) return } req.SetBasicAuth("", hp.apiKey) client := &http.Client{Timeout: 32 * time.Second} resp, err := client.Do(req) if err != nil { log.Printf("[HubPoller] fetch: %v", err) return } body, err := io.ReadAll(resp.Body) _ = resp.Body.Close() if err != nil { log.Printf("[HubPoller] read body: %v", err) return } var state m.NetworkState if err := json.Unmarshal(body, &state); err != nil { log.Printf("[HubPoller] unmarshal: %v", err) return } hp.apply(state) } func (hp *HubPoller) apply(state m.NetworkState) { seen := make(map[wgtypes.Key]struct{}, len(hp.known)) netAddr := hp.vpnNet.Addr().As4() for _, p := range state.Peers { if p == nil || len(p.WGPubKey) != wgtypes.KeyLen { continue } pubKey, err := wgtypes.NewKey(p.WGPubKey) if err != nil { continue } octets := netAddr octets[3] = p.PeerIP vpnIP := netip.AddrFrom4(octets) if vpnIP == hp.selfVPNIP { continue } seen[pubKey] = struct{}{} if v, ok := hp.known[pubKey]; ok && v == p.Version { continue } hp.known[pubKey] = p.Version hp.addCh <- hubPeerFrom(pubKey, vpnIP, p) } for key := range hp.known { if _, ok := seen[key]; !ok { delete(hp.known, key) hp.removeCh <- key } } } func hubPeerFrom(pubKey wgtypes.Key, vpnIP netip.Addr, p *m.Peer) HubPeer { var ep4, ep6 netip.AddrPort if len(p.Addr4) > 0 { if addr, ok := netip.AddrFromSlice(p.Addr4); ok { ep4 = netip.AddrPortFrom(addr.Unmap(), p.Port) } } if len(p.Addr6) > 0 { if addr, ok := netip.AddrFromSlice(p.Addr6); ok { ep6 = netip.AddrPortFrom(addr, p.Port) } } return HubPeer{ PubKey: pubKey, VPNIP: vpnIP, IsRelay: p.Relay, IsPublic: ep4.IsValid() || ep6.IsValid(), EndpointV4: ep4, EndpointV6: ep6, } }