package main import ( "embed" "io/fs" "log" "net/http" "os" "path/filepath" "strings" "alpine-router/config" "alpine-router/dhcp" "alpine-router/handlers" "alpine-router/mihomo" "alpine-router/nat" "alpine-router/network" "alpine-router/traffic" ) //go:embed public var publicFS embed.FS func main() { cfg, err := config.Load() if err != nil { log.Fatalf("load config.yaml: %v", err) } firstRun := len(cfg.Interfaces) == 0 && len(cfg.NAT.Interfaces) == 0 && len(cfg.DHCP.Pools) == 0 mihomo.SetConfigDir(filepath.Join(filepath.Dir(config.GetPath()), "mihomo")) if err := mihomo.EnsureDefaultConfig(); err != nil { log.Printf("Warning: ensure default mihomo config: %v", err) } if firstRun { log.Printf("First run — importing current system state into %s", config.GetPath()) cfg = importSystemState() if err := config.Save(cfg); err != nil { log.Printf("Warning: save initial config.yaml: %v", err) } else { log.Printf("Saved initial config.yaml with %d interfaces, %d NAT, %d DHCP pools", len(cfg.Interfaces), len(cfg.NAT.Interfaces), len(cfg.DHCP.Pools)) } } else { applyConfig(cfg) } mux := http.NewServeMux() 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/") if strings.Contains(suffix, "/") { handlers.HandleInterfaceAction(w, r) } else { handlers.HandleInterfaceSingle(w, r) } }) mux.HandleFunc("/api/config/", handlers.HandleConfig) mux.HandleFunc("/api/apply", handlers.HandleApply) mux.HandleFunc("/api/pending", handlers.HandlePending) mux.HandleFunc("/api/clients", handlers.HandleClients) mux.HandleFunc("/api/clients/update/", handlers.HandleClientUpdate) mux.HandleFunc("/api/config.yaml", handlers.HandleConfigYAML) mux.HandleFunc("/api/nat", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": handlers.HandleNATGet(w, r) case "POST": handlers.HandleNATSave(w, r) default: w.WriteHeader(http.StatusMethodNotAllowed) } }) mux.HandleFunc("/api/dhcp/status", handlers.HandleDHCPStatus) mux.HandleFunc("/api/dhcp/config", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": handlers.HandleDHCPConfigGet(w, r) case "POST": handlers.HandleDHCPConfigSave(w, r) default: w.WriteHeader(http.StatusMethodNotAllowed) } }) mux.HandleFunc("/api/dhcp/apply", handlers.HandleDHCPApply) mux.HandleFunc("/api/mihomo/status", handlers.HandleMihomoStatus) mux.HandleFunc("/api/mihomo/start", handlers.HandleMihomoStart) mux.HandleFunc("/api/mihomo/stop", handlers.HandleMihomoStop) mux.HandleFunc("/api/mihomo/restart", handlers.HandleMihomoRestart) mux.HandleFunc("/api/mihomo/config", handlers.HandleMihomoConfig) mux.HandleFunc("/api/mihomo/config.yaml", handlers.HandleMihomoConfigYAML) mux.HandleFunc("/api/mihomo/logs", handlers.HandleMihomoLogs) mux.HandleFunc("/api/mihomo/upload-core", handlers.HandleMihomoUploadCore) sub, err := fs.Sub(publicFS, "public") if err != nil { log.Fatal(err) } mux.Handle("/", http.FileServer(http.FS(sub))) port := "8080" if p := os.Getenv("PORT"); p != "" { port = p } traffic.Start() 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)) } func importSystemState() *config.AppConfig { cfg := &config.AppConfig{ Interfaces: map[string]*config.InterfaceConfig{}, NAT: config.NATConfig{Interfaces: []string{}}, DHCP: config.DHCPConfig{Pools: []config.DHCPPool{}}, KnownDevices: []config.KnownDevice{}, } ifaceConfigs, err := network.ParseConfig() if err != nil { log.Printf("Warning: parse /etc/network/interfaces: %v", err) } else { for name, ic := range ifaceConfigs { cfg.Interfaces[name] = &config.InterfaceConfig{ Auto: ic.Auto, Mode: ic.Mode, Address: ic.Address, Netmask: ic.Netmask, Gateway: ic.Gateway, DNS: ic.DNS, Extra: ic.Extra, } } log.Printf("Imported %d interfaces from /etc/network/interfaces", len(cfg.Interfaces)) } if nat.IsInstalled() { natCfg, err := nat.Load() if err != nil { log.Printf("Warning: load NAT state: %v", err) } else { cfg.NAT.Interfaces = natCfg.Interfaces log.Printf("Imported %d NAT interfaces", len(cfg.NAT.Interfaces)) } } if dhcp.IsInstalled() { dhcpCfg, err := dhcp.Load() if err != nil { log.Printf("Warning: load DHCP state: %v", err) } else { cfg.DHCP.Enabled = dhcpCfg.Enabled for _, p := range dhcpCfg.Pools { cfg.DHCP.Pools = append(cfg.DHCP.Pools, config.DHCPPool{ Interface: p.Interface, Enabled: p.Enabled, Subnet: p.Subnet, Netmask: p.Netmask, RangeStart: p.RangeStart, RangeEnd: p.RangeEnd, Router: p.Router, DNS: p.DNS, LeaseTime: p.LeaseTime, }) } log.Printf("Imported DHCP config (enabled=%v, %d pools)", cfg.DHCP.Enabled, len(cfg.DHCP.Pools)) } } return cfg } func applyConfig(cfg *config.AppConfig) { if len(cfg.Interfaces) > 0 { curConfigs, err := network.ParseConfig() if err != nil { log.Printf("Warning: parse current interfaces: %v", err) curConfigs = map[string]*network.InterfaceConfig{} } for name, iface := range cfg.Interfaces { ic := &network.InterfaceConfig{ Name: name, Auto: iface.Auto, Mode: iface.Mode, Address: iface.Address, Netmask: iface.Netmask, Gateway: iface.Gateway, DNS: iface.DNS, Extra: iface.Extra, } curConfigs[name] = ic } if err := network.WriteConfig(curConfigs); err != nil { log.Printf("Warning: write interfaces: %v", err) } else { log.Printf("Applied %d interface configs", len(cfg.Interfaces)) } } if nat.IsInstalled() { natCfg := &nat.Config{Interfaces: cfg.NAT.Interfaces} if err := nat.Save(natCfg); err != nil { log.Printf("Warning: save NAT state: %v", err) } var blockedIPs []string for _, kd := range cfg.KnownDevices { if kd.Blocked { ip := kd.IP if kd.StaticIP != "" { ip = kd.StaticIP } if ip != "" { blockedIPs = append(blockedIPs, ip) } } } if err := nat.ApplyRulesWithBlocked(natCfg, blockedIPs); err != nil { log.Printf("Warning: apply NAT: %v", err) } else { log.Printf("NAT rules applied (%d interfaces, %d blocked clients)", len(cfg.NAT.Interfaces), len(blockedIPs)) } } else { log.Printf("nftables not installed — NAT unavailable (install with: apk add nftables)") } if dhcp.IsInstalled() { dhcpCfg := &dhcp.Config{ Enabled: cfg.DHCP.Enabled, Pools: make([]dhcp.Pool, len(cfg.DHCP.Pools)), } for i, p := range cfg.DHCP.Pools { dhcpCfg.Pools[i] = dhcp.Pool{ Interface: p.Interface, Enabled: p.Enabled, Subnet: p.Subnet, Netmask: p.Netmask, RangeStart: p.RangeStart, RangeEnd: p.RangeEnd, Router: p.Router, DNS: p.DNS, LeaseTime: p.LeaseTime, } } var dhcpBindings []dhcp.StaticBinding for _, kd := range cfg.KnownDevices { if kd.StaticIP != "" && kd.MAC != "" { dhcpBindings = append(dhcpBindings, dhcp.StaticBinding{ MAC: kd.MAC, Host: kd.Hostname, IP: kd.StaticIP, }) } } if err := dhcp.Save(dhcpCfg); err != nil { log.Printf("Warning: save DHCP state: %v", err) } if err := dhcp.WriteConfigsWithBindings(dhcpCfg, dhcpBindings); err != nil { log.Printf("Warning: write dnsmasq config: %v", err) } else { log.Printf("DHCP config written (%d pools, %d static bindings)", len(dhcpCfg.Pools), len(dhcpBindings)) } if dhcpCfg.Enabled { if err := dhcp.ServiceRestart(); err != nil { log.Printf("Warning: start dnsmasq: %v", err) } else { log.Printf("dnsmasq started") } } } else { log.Printf("dnsmasq not installed — DHCP unavailable (install with: apk add dnsmasq)") } }