14.04.2026 Update

This commit is contained in:
2026-04-15 11:38:26 +03:00
parent 6aa0349f5d
commit f50d79fab3
45 changed files with 5645 additions and 751 deletions

View File

@@ -7,7 +7,16 @@ import (
"strings"
)
const tableName = "alpine-router"
const tableName = "nano-router"
// TproxyPort is the fixed Mihomo transparent-proxy port, always enabled.
const TproxyPort = 7893
// TproxyMark is the fwmark set on packets that should be routed to tproxy.
const TproxyMark = 1
// TproxyTable is the ip routing table used for fwmark-based local delivery.
const TproxyTable = 100
// Rule is a single stateless forward-filter rule.
type Rule struct {
@@ -35,29 +44,76 @@ type NATConfig struct {
Interfaces []string
}
// ClientPolicies holds per-policy IP lists derived from known device policies.
type ClientPolicies struct {
DisabledIPs []string // drop all traffic for these IPs
VPNIPs []string // redirect TCP/UDP to tproxy for these IPs
}
// IsInstalled reports whether the nft binary is available.
func IsInstalled() bool {
_, err := exec.LookPath("nft")
return err == nil
}
// CleanupAll removes all kernel state managed by nano-router so that a fresh
// applyConfig() starts from a guaranteed clean slate:
// - All nftables tables created by nano-router (main + traffic)
// - The ip rule and route used for tproxy fwmark delivery
//
// Errors are silently ignored — most arise because entries don't exist yet.
func CleanupAll() {
// Remove nftables tables. The traffic table is managed by the traffic tracker.
exec.Command("nft", "delete", "table", "ip", tableName).Run()
exec.Command("nft", "delete", "table", "ip", "nano-router-nat").Run()
exec.Command("nft", "delete", "table", "ip", "nano-router-traffic").Run()
// Remove all ip rules pointing to our tproxy routing table.
// Loop to handle duplicate entries that can accumulate across restarts.
mark := fmt.Sprintf("%d", TproxyMark)
table := fmt.Sprintf("%d", TproxyTable)
for {
err := exec.Command("ip", "rule", "del", "fwmark", mark, "table", table).Run()
if err != nil {
break
}
}
// Remove the local default route in our tproxy routing table.
exec.Command("ip", "route", "del",
"local", "0.0.0.0/0", "dev", "lo", "table", table,
).Run()
}
// SetupTproxyRouting installs the ip rule and route needed for TPROXY to work.
// Packets marked with TproxyMark are routed to the loopback interface (local delivery),
// which allows Mihomo's tproxy socket to receive them.
// Call CleanupAll first to avoid duplicate entries.
func SetupTproxyRouting() {
mark := fmt.Sprintf("%d", TproxyMark)
table := fmt.Sprintf("%d", TproxyTable)
exec.Command("ip", "rule", "add", "fwmark", mark, "table", table).Run()
exec.Command("ip", "route", "add",
"local", "0.0.0.0/0", "dev", "lo", "table", table,
).Run()
}
// ApplyAll atomically regenerates the complete nftables ruleset:
// - Tproxy prerouting for cp.VPNIPs (mangle priority, TPROXY → Mihomo :TproxyPort)
// - NAT masquerade for natCfg.Interfaces
// - Blocked client IP drops
// - Disabled client IP drops (cp.DisabledIPs)
// - User rules from fwCfg (in order, enabled only)
// - LAN isolation (if fwCfg.VLANIsolation): blocks traffic between any two LAN interfaces
// (native + tagged VLANs). User rules placed above have priority.
// - Default accept from LAN interfaces to WAN
//
// lanIfaces is the union of NAT interfaces and all VLAN interfaces — every interface
// that serves a local subnet. Isolation prevents any two of them from talking directly.
func ApplyAll(natCfg NATConfig, fwCfg Config, blockedIPs, lanIfaces []string) error {
// lanIfaces is the union of NAT interfaces and all VLAN interfaces.
func ApplyAll(natCfg NATConfig, fwCfg Config, lanIfaces []string, cp ClientPolicies) error {
if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644); err != nil {
return fmt.Errorf("enable ip_forward: %w", err)
}
// Remove both old and new table names to ensure clean state.
exec.Command("nft", "delete", "table", "ip", "alpine-router-nat").Run()
exec.Command("nft", "delete", "table", "ip", "nano-router-nat").Run()
exec.Command("nft", "delete", "table", "ip", tableName).Run()
var activeRules []Rule
@@ -68,26 +124,48 @@ func ApplyAll(natCfg NATConfig, fwCfg Config, blockedIPs, lanIfaces []string) er
}
hasNAT := len(natCfg.Interfaces) > 0
hasBlocked := len(blockedIPs) > 0
hasDisabled := len(cp.DisabledIPs) > 0
hasVPN := len(cp.VPNIPs) > 0
hasVLANIsolation := fwCfg.VLANIsolation && len(lanIfaces) >= 2
if !hasNAT && !hasBlocked && !hasVLANIsolation && len(activeRules) == 0 {
if !hasNAT && !hasDisabled && !hasVPN && !hasVLANIsolation && len(activeRules) == 0 {
return nil
}
var sb strings.Builder
fmt.Fprintf(&sb, "table ip %s {\n", tableName)
// ── Tproxy prerouting chain (mangle priority) ────────────────────────────
// Intercepts TCP/UDP from VPN-policy clients and delivers them to Mihomo.
// Runs before the forward chain, so forwarding rules don't affect these packets.
if hasVPN {
sb.WriteString(" chain tproxy_pre {\n")
sb.WriteString(" type filter hook prerouting priority mangle; policy accept;\n")
// Never redirect traffic destined for the router itself (admin panel,
// SSH, DNS served locally, etc.). Without this, Mihomo intercepts
// connections to the router's own IPs and the admin UI becomes unreachable.
sb.WriteString(" fib daddr type local return\n")
for _, ip := range cp.VPNIPs {
fmt.Fprintf(&sb,
" ip saddr %s meta l4proto { tcp, udp } tproxy to :%d meta mark set %d\n",
ip, TproxyPort, TproxyMark,
)
}
sb.WriteString(" }\n")
}
// ── Forward chain ────────────────────────────────────────────────────────
sb.WriteString(" chain forward {\n")
sb.WriteString(" type filter hook forward priority filter; policy drop;\n")
sb.WriteString(" ct state established,related accept\n")
for _, ip := range blockedIPs {
// Drop traffic for disabled clients (both directions).
for _, ip := range cp.DisabledIPs {
fmt.Fprintf(&sb, " ip saddr %s drop\n", ip)
fmt.Fprintf(&sb, " ip daddr %s drop\n", ip)
}
// User-defined forward rules (ordered, enabled only).
for _, rule := range activeRules {
line := buildRuleLine(rule)
if line == "" {
@@ -110,7 +188,7 @@ func ApplyAll(natCfg NATConfig, fwCfg Config, blockedIPs, lanIfaces []string) er
fmt.Fprintf(&sb, " iifname %s oifname %s drop\n", set, set)
}
// Allow from LAN/VLAN interfaces to WAN (non-VLAN, non-blocked traffic falls through above).
// Allow from LAN/VLAN interfaces outbound to WAN.
for _, iface := range natCfg.Interfaces {
fmt.Fprintf(&sb, " iifname %q accept\n", iface)
}
@@ -136,24 +214,23 @@ func ApplyAll(natCfg NATConfig, fwCfg Config, blockedIPs, lanIfaces []string) er
return fmt.Errorf("nft apply: %s: %w", strings.TrimSpace(string(out)), err)
}
// Flush connection tracking table so existing sessions are re-evaluated
// against the new ruleset. Without this, traffic already tracked as
// "established/related" bypasses new drop rules until the session ends.
// Set up ip rule/route for tproxy fwmark routing when VPN clients are active.
if hasVPN {
SetupTproxyRouting()
}
// Flush connection tracking so existing sessions are re-evaluated.
flushConntrack()
return nil
}
// flushConntrack clears the kernel connection tracking table so that all traffic
// is re-evaluated against the current nftables ruleset. This is necessary when
// adding new drop/reject rules to prevent previously-established sessions from
// continuing to bypass the new rules via ct state established,related accept.
// is re-evaluated against the current nftables ruleset.
func flushConntrack() {
// Preferred: conntrack utility (part of conntrack-tools package).
if err := exec.Command("conntrack", "-F").Run(); err == nil {
return
}
// Fallback: write to /proc (available when nf_conntrack module is loaded).
_ = os.WriteFile("/proc/sys/net/netfilter/nf_conntrack_flush", []byte("1"), 0644)
}