Files

441 lines
13 KiB
Go
Raw Permalink Normal View History

2026-04-13 09:46:02 +03:00
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strings"
2026-04-15 11:38:26 +03:00
"time"
"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"
2026-04-15 12:25:39 +03:00
"nano-router/setup"
2026-04-15 11:38:26 +03:00
"nano-router/traffic"
2026-04-13 09:46:02 +03:00
)
//go:embed public
var publicFS embed.FS
func main() {
2026-04-15 12:25:39 +03:00
if len(os.Args) > 1 && os.Args[1] == "setup" {
setup.Run()
return
}
2026-04-13 09:46:02 +03:00
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)
}
2026-04-15 11:38:26 +03:00
// 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")
}
2026-04-13 09:46:02 +03:00
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()
2026-04-15 11:38:26 +03:00
// 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)
2026-04-13 09:46:02 +03:00
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)
2026-04-15 11:38:26 +03:00
mux.HandleFunc("/api/subnets", handlers.HandleSubnets)
2026-04-13 09:46:02 +03:00
mux.HandleFunc("/api/clients", handlers.HandleClients)
mux.HandleFunc("/api/clients/update/", handlers.HandleClientUpdate)
2026-04-15 11:38:26 +03:00
mux.HandleFunc("/api/clients/policy", handlers.HandleClientPolicyDefault)
mux.HandleFunc("/api/clients/policy/apply-all", handlers.HandleClientPolicyApplyAll)
2026-04-13 09:46:02 +03:00
mux.HandleFunc("/api/config.yaml", handlers.HandleConfigYAML)
2026-04-13 12:40:49 +03:00
mux.HandleFunc("/api/firewall", handlers.HandleFirewall)
mux.HandleFunc("/api/firewall/apply", handlers.HandleFirewallApply)
2026-04-13 09:46:02 +03:00
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)
2026-04-13 18:56:13 +03:00
mux.HandleFunc("/api/mihomo/api/", handlers.HandleMihomoAPIProxy)
mux.HandleFunc("/api/mihomo/ws/", handlers.HandleMihomoWSProxy)
2026-04-13 09:46:02 +03:00
2026-04-15 11:38:26 +03:00
// 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)
2026-04-13 09:46:02 +03:00
sub, err := fs.Sub(publicFS, "public")
if err != nil {
log.Fatal(err)
}
2026-04-15 11:38:26 +03:00
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)
})
2026-04-13 09:46:02 +03:00
port := "8080"
if p := os.Getenv("PORT"); p != "" {
port = p
}
2026-04-15 11:38:26 +03:00
// Initialize auth from config
auth.Global.Init(cfg.Auth.Username, cfg.Auth.PasswordHash, cfg.Auth.APIKey)
auth.StartCleanup(5 * time.Minute)
2026-04-13 09:46:02 +03:00
traffic.Start()
2026-04-15 11:38:26 +03:00
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)
2026-04-13 09:46:02 +03:00
log.Printf("Config file: %s", config.GetPath())
2026-04-15 12:25:39 +03:00
handler := auth.PublicAuthMiddleware(mux)
if len(cfg.ListenAddresses) > 0 {
servers := make([]*http.Server, len(cfg.ListenAddresses))
errCh := make(chan error, len(cfg.ListenAddresses))
for i, addr := range cfg.ListenAddresses {
bind := addr + ":" + port
srv := &http.Server{Addr: bind, Handler: handler}
servers[i] = srv
log.Printf("Network Manager listening on http://%s", bind)
go func(s *http.Server) { errCh <- s.ListenAndServe() }(srv)
}
log.Fatal(<-errCh)
} else {
log.Printf("Network Manager listening on http://0.0.0.0:%s", port)
log.Fatal(http.ListenAndServe(":"+port, handler))
}
2026-04-13 09:46:02 +03:00
}
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)
}
2026-04-15 11:38:26 +03:00
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)
}
}
2026-04-13 09:46:02 +03:00
for _, kd := range cfg.KnownDevices {
2026-04-15 11:38:26 +03:00
policy := kd.Policy
if policy == "" {
if kd.Blocked {
policy = "disabled"
} else {
policy = defaultPolicy
2026-04-13 09:46:02 +03:00
}
}
2026-04-15 11:38:26 +03:00
ip := kd.IP
if kd.StaticIP != "" {
ip = kd.StaticIP
}
addIP(ip, policy)
for _, arpIP := range arpByMAC[kd.MAC] {
addIP(arpIP, policy)
}
2026-04-13 09:46:02 +03:00
}
2026-04-13 12:40:49 +03:00
// Build LAN interface set: NAT interfaces + all VLAN interfaces + their parents.
seenLAN := map[string]bool{}
var lanIfaces []string
addLAN := func(name string) {
if name != "" && !seenLAN[name] {
lanIfaces = append(lanIfaces, name)
seenLAN[name] = true
}
}
for _, name := range cfg.NAT.Interfaces {
addLAN(name)
}
if names, err := network.GetInterfaces(); err == nil {
for _, name := range names {
if network.IsVLAN(name) {
addLAN(name)
addLAN(network.VLANParent(name))
}
}
}
// Convert config firewall rules.
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,
}
}
err := firewall.ApplyAll(
firewall.NATConfig{Interfaces: cfg.NAT.Interfaces},
firewall.Config{Rules: fwRules, VLANIsolation: cfg.Firewall.VLANIsolation},
lanIfaces,
2026-04-15 11:38:26 +03:00
firewall.ClientPolicies{
DisabledIPs: disabledIPs,
VPNIPs: vpnIPs,
},
2026-04-13 12:40:49 +03:00
)
if err != nil {
log.Printf("Warning: apply firewall/NAT rules: %v", err)
2026-04-13 09:46:02 +03:00
} else {
2026-04-15 11:38:26 +03:00
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)
2026-04-13 09:46:02 +03:00
}
2026-04-15 11:38:26 +03:00
// Ensure tproxy ip routing rules are in place for marked packets.
firewall.SetupTproxyRouting()
2026-04-13 09:46:02 +03:00
} else {
2026-04-13 12:40:49 +03:00
log.Printf("nftables not installed — NAT/firewall unavailable (install with: apk add nftables)")
2026-04-13 09:46:02 +03:00
}
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)")
}
2026-04-15 11:38:26 +03:00
// 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)")
}
}
2026-04-13 09:46:02 +03:00
}