package handlers import ( "log" "nano-router/clients" "nano-router/config" "nano-router/firewall" "nano-router/nat" "nano-router/network" ) // resolveClientPolicy returns the effective routing policy for a device. // Explicit per-device Policy takes priority; then legacy Blocked flag; then default. func resolveClientPolicy(kd config.KnownDevice, defaultPolicy string) string { if kd.Policy != "" { return kd.Policy } if kd.Blocked { return "disabled" } if defaultPolicy != "" { return defaultPolicy } return "direct" } // applyAllRules rebuilds the complete nftables ruleset from the current config: // NAT masquerade + tproxy for VPN clients + disabled client drops + // user firewall rules + VLAN isolation. func applyAllRules(cfg *config.AppConfig) error { if !nat.IsInstalled() { return nil } defaultPolicy := cfg.ClientPolicy.Default if defaultPolicy == "" { defaultPolicy = "direct" } // Classify each known device into disabled or vpn buckets. // For devices connected on multiple interfaces (same MAC, different IPs) // we also include all live ARP IPs so every interface gets the same policy. arpByMAC := clients.GetARPIPsByMAC() seenIP := make(map[string]bool) var disabledIPs, vpnIPs []string addIP := func(ip, policy string) { if ip == "" || seenIP[ip] { return } seenIP[ip] = true switch policy { case "disabled": disabledIPs = append(disabledIPs, ip) case "vpn": vpnIPs = append(vpnIPs, ip) } } for _, kd := range cfg.KnownDevices { policy := resolveClientPolicy(kd, defaultPolicy) // Primary stored IP. ip := kd.IP if kd.StaticIP != "" { ip = kd.StaticIP } addIP(ip, policy) // All other IPs this MAC currently has in the ARP table. for _, arpIP := range arpByMAC[kd.MAC] { addIP(arpIP, policy) } } // Build the LAN interface set for isolation: // all NAT interfaces + all VLAN interfaces (active + pending). seen := map[string]bool{} var lanIfaces []string addLAN := func(name string) { if name != "" && !seen[name] { lanIfaces = append(lanIfaces, name) seen[name] = true } } for _, name := range cfg.NAT.Interfaces { addLAN(name) } names, _ := network.GetInterfaces() for _, name := range names { if network.IsVLAN(name) { addLAN(name) addLAN(network.VLANParent(name)) } } for name := range network.GetAllPending() { if network.IsVLAN(name) { addLAN(name) addLAN(network.VLANParent(name)) } } // Convert config.FirewallRule → firewall.Rule. fwRules := make([]firewall.Rule, len(cfg.Firewall.Rules)) for i, r := range cfg.Firewall.Rules { fwRules[i] = firewall.Rule{ ID: r.ID, Enabled: r.Enabled, Action: r.Action, Protocol: r.Protocol, SrcAddr: r.SrcAddr, SrcPort: r.SrcPort, DstAddr: r.DstAddr, DstPort: r.DstPort, InIface: r.InIface, OutIface: r.OutIface, Comment: r.Comment, } } return firewall.ApplyAll( firewall.NATConfig{Interfaces: cfg.NAT.Interfaces}, firewall.Config{Rules: fwRules, VLANIsolation: cfg.Firewall.VLANIsolation}, lanIfaces, firewall.ClientPolicies{ DisabledIPs: disabledIPs, VPNIPs: vpnIPs, }, ) } // applyBlockedFirewall is the async helper called after client or policy updates. func applyBlockedFirewall() { cfg, err := config.Load() if err != nil { log.Printf("Warning: load config for firewall: %v", err) return } if err := applyAllRules(cfg); err != nil { log.Printf("Warning: apply firewall rules: %v", err) } }