142 lines
3.4 KiB
Go
142 lines
3.4 KiB
Go
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)
|
|
}
|
|
}
|