14.04.2026 Update
This commit is contained in:
131
main.go
131
main.go
@@ -8,15 +8,19 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"alpine-router/config"
|
||||
"alpine-router/dhcp"
|
||||
"alpine-router/firewall"
|
||||
"alpine-router/handlers"
|
||||
"alpine-router/mihomo"
|
||||
"alpine-router/nat"
|
||||
"alpine-router/network"
|
||||
"alpine-router/traffic"
|
||||
"nano-router/auth"
|
||||
"nano-router/clients"
|
||||
"nano-router/config"
|
||||
"nano-router/dhcp"
|
||||
"nano-router/firewall"
|
||||
"nano-router/handlers"
|
||||
"nano-router/mihomo"
|
||||
"nano-router/monitor"
|
||||
"nano-router/nat"
|
||||
"nano-router/network"
|
||||
"nano-router/traffic"
|
||||
)
|
||||
|
||||
//go:embed public
|
||||
@@ -36,6 +40,13 @@ func main() {
|
||||
log.Printf("Warning: ensure default mihomo config: %v", err)
|
||||
}
|
||||
|
||||
// Always wipe stale kernel state (nftables, ip rules/routes) so that the
|
||||
// config is the single source of truth even after crashes or partial updates.
|
||||
if firewall.IsInstalled() {
|
||||
firewall.CleanupAll()
|
||||
log.Printf("Cleaned up previous nftables/routing state")
|
||||
}
|
||||
|
||||
if firstRun {
|
||||
log.Printf("First run — importing current system state into %s", config.GetPath())
|
||||
cfg = importSystemState()
|
||||
@@ -51,6 +62,12 @@ func main() {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Public auth endpoints (no auth required)
|
||||
mux.HandleFunc("/api/auth/challenge", handlers.HandleAuthChallenge)
|
||||
mux.HandleFunc("/api/auth/login", handlers.HandleAuthLogin)
|
||||
|
||||
mux.HandleFunc("/api/dashboard", handlers.HandleDashboard)
|
||||
|
||||
mux.HandleFunc("/api/interfaces", handlers.HandleInterfaces)
|
||||
mux.HandleFunc("/api/interfaces/", func(w http.ResponseWriter, r *http.Request) {
|
||||
suffix := strings.TrimPrefix(r.URL.Path, "/api/interfaces/")
|
||||
@@ -63,9 +80,12 @@ func main() {
|
||||
mux.HandleFunc("/api/config/", handlers.HandleConfig)
|
||||
mux.HandleFunc("/api/apply", handlers.HandleApply)
|
||||
mux.HandleFunc("/api/pending", handlers.HandlePending)
|
||||
mux.HandleFunc("/api/subnets", handlers.HandleSubnets)
|
||||
|
||||
mux.HandleFunc("/api/clients", handlers.HandleClients)
|
||||
mux.HandleFunc("/api/clients/update/", handlers.HandleClientUpdate)
|
||||
mux.HandleFunc("/api/clients/policy", handlers.HandleClientPolicyDefault)
|
||||
mux.HandleFunc("/api/clients/policy/apply-all", handlers.HandleClientPolicyApplyAll)
|
||||
|
||||
mux.HandleFunc("/api/config.yaml", handlers.HandleConfigYAML)
|
||||
|
||||
@@ -107,22 +127,45 @@ func main() {
|
||||
mux.HandleFunc("/api/mihomo/api/", handlers.HandleMihomoAPIProxy)
|
||||
mux.HandleFunc("/api/mihomo/ws/", handlers.HandleMihomoWSProxy)
|
||||
|
||||
// Auth-protected API endpoints
|
||||
mux.HandleFunc("/api/auth/logout", handlers.HandleAuthLogout)
|
||||
mux.HandleFunc("/api/auth/status", handlers.HandleAuthStatus)
|
||||
mux.HandleFunc("/api/auth/profile", handlers.HandleAuthProfile)
|
||||
mux.HandleFunc("/api/auth/api-key", handlers.HandleAuthAPIKey)
|
||||
|
||||
sub, err := fs.Sub(publicFS, "public")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mux.Handle("/", http.FileServer(http.FS(sub)))
|
||||
fileHandler := http.FileServer(http.FS(sub))
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/home.html", http.StatusFound)
|
||||
return
|
||||
}
|
||||
fileHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
port := "8080"
|
||||
if p := os.Getenv("PORT"); p != "" {
|
||||
port = p
|
||||
}
|
||||
|
||||
// Initialize auth from config
|
||||
auth.Global.Init(cfg.Auth.Username, cfg.Auth.PasswordHash, cfg.Auth.APIKey)
|
||||
auth.StartCleanup(5 * time.Minute)
|
||||
|
||||
traffic.Start()
|
||||
monitor.Start()
|
||||
|
||||
// Re-apply nftables rules whenever a policy device changes its IP (DHCP renewal,
|
||||
// new interface). Checks ARP every 30 seconds and re-applies only on change.
|
||||
handlers.StartPolicySync(30 * time.Second)
|
||||
|
||||
log.Printf("Config file: %s", config.GetPath())
|
||||
log.Printf("Network Manager listening on http://0.0.0.0:%s", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, mux))
|
||||
log.Fatal(http.ListenAndServe(":"+port, auth.PublicAuthMiddleware(mux)))
|
||||
}
|
||||
|
||||
func importSystemState() *config.AppConfig {
|
||||
@@ -220,17 +263,48 @@ func applyConfig(cfg *config.AppConfig) {
|
||||
log.Printf("Warning: save NAT state: %v", err)
|
||||
}
|
||||
|
||||
var blockedIPs []string
|
||||
defaultPolicy := cfg.ClientPolicy.Default
|
||||
if defaultPolicy == "" {
|
||||
defaultPolicy = "direct"
|
||||
}
|
||||
|
||||
// Classify known devices by effective routing policy.
|
||||
// For devices with multiple IPs (same MAC, different interfaces) all ARP IPs
|
||||
// are included so every interface gets the same policy in nftables.
|
||||
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 {
|
||||
if kd.Blocked {
|
||||
ip := kd.IP
|
||||
if kd.StaticIP != "" {
|
||||
ip = kd.StaticIP
|
||||
}
|
||||
if ip != "" {
|
||||
blockedIPs = append(blockedIPs, ip)
|
||||
policy := kd.Policy
|
||||
if policy == "" {
|
||||
if kd.Blocked {
|
||||
policy = "disabled"
|
||||
} else {
|
||||
policy = defaultPolicy
|
||||
}
|
||||
}
|
||||
ip := kd.IP
|
||||
if kd.StaticIP != "" {
|
||||
ip = kd.StaticIP
|
||||
}
|
||||
addIP(ip, policy)
|
||||
for _, arpIP := range arpByMAC[kd.MAC] {
|
||||
addIP(arpIP, policy)
|
||||
}
|
||||
}
|
||||
|
||||
// Build LAN interface set: NAT interfaces + all VLAN interfaces + their parents.
|
||||
@@ -267,15 +341,21 @@ func applyConfig(cfg *config.AppConfig) {
|
||||
err := firewall.ApplyAll(
|
||||
firewall.NATConfig{Interfaces: cfg.NAT.Interfaces},
|
||||
firewall.Config{Rules: fwRules, VLANIsolation: cfg.Firewall.VLANIsolation},
|
||||
blockedIPs,
|
||||
lanIfaces,
|
||||
firewall.ClientPolicies{
|
||||
DisabledIPs: disabledIPs,
|
||||
VPNIPs: vpnIPs,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Warning: apply firewall/NAT rules: %v", err)
|
||||
} else {
|
||||
log.Printf("Firewall/NAT applied (%d NAT ifaces, %d fw rules, %d blocked, vlan_isolation=%v)",
|
||||
len(cfg.NAT.Interfaces), len(fwRules), len(blockedIPs), cfg.Firewall.VLANIsolation)
|
||||
log.Printf("Firewall/NAT applied (%d NAT ifaces, %d fw rules, %d disabled, %d vpn, vlan_isolation=%v)",
|
||||
len(cfg.NAT.Interfaces), len(fwRules), len(disabledIPs), len(vpnIPs), cfg.Firewall.VLANIsolation)
|
||||
}
|
||||
|
||||
// Ensure tproxy ip routing rules are in place for marked packets.
|
||||
firewall.SetupTproxyRouting()
|
||||
} else {
|
||||
log.Printf("nftables not installed — NAT/firewall unavailable (install with: apk add nftables)")
|
||||
}
|
||||
@@ -328,4 +408,13 @@ func applyConfig(cfg *config.AppConfig) {
|
||||
} else {
|
||||
log.Printf("dnsmasq not installed — DHCP unavailable (install with: apk add dnsmasq)")
|
||||
}
|
||||
|
||||
// Auto-start Mihomo if enabled in config.
|
||||
if cfg.Mihomo.Enabled {
|
||||
if err := mihomo.Start(); err != nil {
|
||||
log.Printf("Warning: auto-start mihomo: %v", err)
|
||||
} else {
|
||||
log.Printf("Mihomo auto-started (enabled=true in config)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user