2026-04-13 09:46:02 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"embed"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"alpine-router/config"
|
|
|
|
|
"alpine-router/dhcp"
|
2026-04-13 12:40:49 +03:00
|
|
|
"alpine-router/firewall"
|
2026-04-13 09:46:02 +03:00
|
|
|
"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)
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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},
|
|
|
|
|
blockedIPs,
|
|
|
|
|
lanIfaces,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Warning: apply firewall/NAT rules: %v", err)
|
2026-04-13 09:46:02 +03:00
|
|
|
} else {
|
2026-04-13 12:40:49 +03:00
|
|
|
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)
|
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)")
|
|
|
|
|
}
|
|
|
|
|
}
|