package handlers import ( "sort" "strings" "time" "nano-router/clients" "nano-router/config" ) // StartPolicySync starts a background goroutine that re-applies nftables rules // whenever the ARP table changes for devices that have an explicit policy. // This ensures that policy (VPN / disabled) follows a device even when its IP // changes due to DHCP renewal or it connects on a different interface. func StartPolicySync(interval time.Duration) { go func() { // Give the binary time to fully start before the first check. time.Sleep(15 * time.Second) var lastSig string for { sig := policyARPSignature() if sig != lastSig { lastSig = sig applyBlockedFirewall() } time.Sleep(interval) } }() } // policyARPSignature returns a stable string that captures the current mapping // of MAC→IPs only for devices that have an explicit (non-default) policy. // If the string changes between ticks, the firewall needs to be re-applied. func policyARPSignature() string { cfg, err := config.Load() if err != nil { return "" } // Collect MACs with explicit policies. policyMACs := make(map[string]string) // mac → policy for _, kd := range cfg.KnownDevices { if kd.MAC != "" && kd.Policy != "" { policyMACs[kd.MAC] = kd.Policy } // Also treat legacy blocked=true as disabled policy. if kd.MAC != "" && kd.Blocked && kd.Policy == "" { policyMACs[kd.MAC] = "disabled" } } if len(policyMACs) == 0 { return "" } arpByMAC := clients.GetARPIPsByMAC() var parts []string for mac, policy := range policyMACs { ips := arpByMAC[mac] sort.Strings(ips) parts = append(parts, policy+":"+mac+"="+strings.Join(ips, ",")) } sort.Strings(parts) return strings.Join(parts, "|") }