package setup import ( "bufio" "fmt" "net" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "nano-router/auth" "nano-router/config" "nano-router/network" ) const serviceName = "network-manager" const servicePath = "/etc/init.d/" + serviceName const binInstallPath = "/usr/local/bin/network-manager" func Run() { reader := bufio.NewReader(os.Stdin) printBanner() fmt.Println() fmt.Println(" This wizard will guide you through initial router configuration.") fmt.Println(" Press Enter to accept the default value shown in brackets.") fmt.Println() ifaces, err := network.GetInterfaces() if err != nil { fmt.Fprintf(os.Stderr, " [ERROR] Cannot list network interfaces: %v\n", err) os.Exit(1) } physicalIfaces := filterPhysicalIfaces(ifaces) if len(physicalIfaces) < 2 { fmt.Fprintf(os.Stderr, " [ERROR] At least 2 physical interfaces are required, found %d.\n", len(physicalIfaces)) os.Exit(1) } fmt.Println(" ── Step 1: WAN Interface ──────────────────────────────────────────") fmt.Println() wanIface := selectInterface(reader, physicalIfaces, "Select WAN interface") fmt.Println() fmt.Printf(" Configuring WAN interface: %s\n", wanIface) fmt.Println() wanMode := selectWANMode(reader) fmt.Println() var wanAddress, wanNetmask, wanGateway string if wanMode == "static" { wanStats, _ := network.GetInterfaceStats(wanIface) defAddr := "" if wanStats != nil && wanStats.IPv4 != "" { defAddr = wanStats.IPv4 } defMask := "255.255.255.0" if wanStats != nil && wanStats.IPv4Mask != "" { defMask = wanStats.IPv4Mask } defGW := "" if wanStats != nil && wanStats.Gateway != "" { defGW = wanStats.Gateway } wanAddress = readIP(reader, " WAN IP address", defAddr) wanNetmask = readNetmask(reader, " WAN netmask", defMask) wanGateway = readIPOrDefault(reader, " WAN gateway", defGW) } fmt.Println() fmt.Println(" ── Step 2: LAN Interface ──────────────────────────────────────────") fmt.Println() remainingIfaces := removeItem(physicalIfaces, wanIface) lanIface := selectInterface(reader, remainingIfaces, "Select LAN interface") fmt.Println() lanStats, _ := network.GetInterfaceStats(lanIface) defLanAddr := "192.168.1.1" defLanMask := "255.255.255.0" if lanStats != nil && lanStats.IPv4 != "" { defLanAddr = lanStats.IPv4 } if lanStats != nil && lanStats.IPv4Mask != "" { defLanMask = lanStats.IPv4Mask } fmt.Printf(" Configuring LAN interface: %s\n", lanIface) fmt.Println() lanAddress := readIP(reader, " LAN IP address", defLanAddr) lanNetmask := readNetmask(reader, " LAN netmask", defLanMask) lanSubnet := computeSubnet(lanAddress, lanNetmask) fmt.Println() enableDHCP := readYesNo(reader, " Enable DHCP server on LAN?", true) fmt.Println() var dhcpRangeStart, dhcpRangeEnd string var dhcpLeaseTime int enableDHCPPool := false if enableDHCP { defStart := incrementIP(lanAddress, 100) defEnd := incrementIP(lanAddress, 200) if lanStats != nil && lanStats.IPv4 != "" { defStart = incrementIP(lanStats.IPv4, 100) defEnd = incrementIP(lanStats.IPv4, 200) } fmt.Println(" ── DHCP Pool Configuration ─────────────────────────────────────────") fmt.Println() dhcpRangeStart = readIP(reader, " DHCP range start", defStart) dhcpRangeEnd = readIP(reader, " DHCP range end", defEnd) dhcpLeaseTime = readInt(reader, " Lease time (seconds)", 86400) enableDHCPPool = true fmt.Println() } fmt.Println(" ── Step 3: Administrator Account ──────────────────────────────────") fmt.Println() adminUsername := readNonEmpty(reader, " Administrator login", "admin") adminPassword := readPassword(reader, " Administrator password") fmt.Println() fmt.Println() fmt.Println(" ── Generating Configuration ────────────────────────────────────────") fmt.Println() cfg := buildConfig(wanIface, wanMode, wanAddress, wanNetmask, wanGateway, lanIface, lanAddress, lanNetmask, lanSubnet, enableDHCP, enableDHCPPool, dhcpRangeStart, dhcpRangeEnd, dhcpLeaseTime, adminUsername, adminPassword) if err := config.Save(cfg); err != nil { fmt.Fprintf(os.Stderr, " [ERROR] Cannot save config: %v\n", err) os.Exit(1) } fmt.Printf(" Configuration saved to %s\n", config.GetPath()) fmt.Println() fmt.Println(" ── Installing Service ──────────────────────────────────────────────") fmt.Println() if err := installService(); err != nil { fmt.Fprintf(os.Stderr, " [ERROR] Cannot install service: %v\n", err) os.Exit(1) } fmt.Println() fmt.Println(" ── Setup Complete ──────────────────────────────────────────────────") fmt.Println() fmt.Printf(" Admin panel: http://%s:8080\n", lanAddress) fmt.Printf(" Login: %s\n", adminUsername) fmt.Printf(" Password: %s\n", adminPassword) fmt.Println() fmt.Println(" The service has been installed and enabled for autostart.") fmt.Println(" Run 'rc-service network-manager start' or reboot to activate.") fmt.Println() } func printBanner() { fmt.Println() fmt.Println(" ┌──────────────────────────────────────────────────────────────────┐") fmt.Println(" │ Alpine Router -- Initial Setup Wizard │") fmt.Println(" └──────────────────────────────────────────────────────────────────┘") } func selectInterface(reader *bufio.Reader, ifaces []string, prompt string) string { fmt.Printf(" %s:\n", prompt) fmt.Println() for i, name := range ifaces { stats, _ := network.GetInterfaceStats(name) extra := "" if stats != nil { parts := []string{} if stats.IPv4 != "" { parts = append(parts, stats.IPv4) } if stats.State != "" { parts = append(parts, "["+stats.State+"]") } if len(parts) > 0 { extra = " " + strings.Join(parts, " ") } } fmt.Printf(" %2d) %s%s\n", i+1, name, extra) } fmt.Println() for { fmt.Print(" Enter number: ") input := strings.TrimSpace(readLine(reader)) num, err := strconv.Atoi(input) if err != nil || num < 1 || num > len(ifaces) { fmt.Printf(" [!] Please enter a number between 1 and %d\n", len(ifaces)) continue } selected := ifaces[num-1] fmt.Printf(" Selected: %s\n", selected) return selected } } func selectWANMode(reader *bufio.Reader) string { fmt.Println(" WAN addressing mode:") fmt.Println() fmt.Println(" 1) DHCP ( automatic )") fmt.Println(" 2) Static ( manual IP configuration )") fmt.Println() for { fmt.Print(" Select mode (1-2) [1]: ") input := strings.TrimSpace(readLine(reader)) if input == "" || input == "1" { fmt.Println(" Selected: DHCP") return "dhcp" } if input == "2" { fmt.Println(" Selected: Static") return "static" } fmt.Println(" [!] Enter 1 or 2") } } func readIP(reader *bufio.Reader, prompt, def string) string { for { promptStr := fmt.Sprintf(" %s", prompt) if def != "" { promptStr += fmt.Sprintf(" [%s]", def) } promptStr += ": " fmt.Print(promptStr) input := strings.TrimSpace(readLine(reader)) if input == "" && def != "" { input = def } if net.ParseIP(input) == nil { fmt.Println(" [!] Invalid IP address, try again") continue } return input } } func readIPOrDefault(reader *bufio.Reader, prompt, def string) string { for { promptStr := fmt.Sprintf(" %s", prompt) if def != "" { promptStr += fmt.Sprintf(" [%s]", def) } promptStr += " (or leave empty to skip): " fmt.Print(promptStr) input := strings.TrimSpace(readLine(reader)) if input == "" { if def != "" { return def } return "" } if net.ParseIP(input) == nil { fmt.Println(" [!] Invalid IP address, try again") continue } return input } } func readNetmask(reader *bufio.Reader, prompt, def string) string { for { promptStr := fmt.Sprintf(" %s", prompt) if def != "" { promptStr += fmt.Sprintf(" [%s]", def) } promptStr += ": " fmt.Print(promptStr) input := strings.TrimSpace(readLine(reader)) if input == "" && def != "" { input = def } if isValidNetmask(input) { return input } fmt.Println(" [!] Invalid netmask, try again (e.g. 255.255.255.0)") } } func readYesNo(reader *bufio.Reader, prompt string, def bool) bool { defStr := "Y/n" if !def { defStr = "y/N" } for { fmt.Printf(" %s [%s]: ", prompt, defStr) input := strings.TrimSpace(strings.ToLower(readLine(reader))) if input == "" { return def } switch input { case "y", "yes": return true case "n", "no": return false default: fmt.Println(" [!] Enter y or n") } } } func readNonEmpty(reader *bufio.Reader, prompt, def string) string { for { promptStr := fmt.Sprintf(" %s", prompt) if def != "" { promptStr += fmt.Sprintf(" [%s]", def) } promptStr += ": " fmt.Print(promptStr) input := strings.TrimSpace(readLine(reader)) if input == "" && def != "" { return def } if input != "" { return input } fmt.Println(" [!] Value cannot be empty") } } func readPassword(reader *bufio.Reader, prompt string) string { fd := int(os.Stdin.Fd()) oldState, canMute := termiosGetState(fd) for { fmt.Printf(" %s: ", prompt) if canMute { termiosDisableEcho(fd) } pw1 := readLineRaw() if canMute { termiosRestore(fd, oldState) fmt.Println() } if len(pw1) < 4 { fmt.Println(" [!] Password must be at least 4 characters") continue } fmt.Print(" Confirm password: ") if canMute { termiosDisableEcho(fd) } pw2 := readLineRaw() if canMute { termiosRestore(fd, oldState) fmt.Println() } if pw1 != pw2 { fmt.Println(" [!] Passwords do not match, try again") continue } return pw1 } } func readLineRaw() string { var line []byte buf := make([]byte, 1) for { n, err := os.Stdin.Read(buf) if n > 0 { if buf[0] == '\n' || buf[0] == '\r' { break } line = append(line, buf[0]) } if err != nil { break } } return string(line) } func readInt(reader *bufio.Reader, prompt string, def int) int { for { fmt.Printf(" %s [%d]: ", prompt, def) input := strings.TrimSpace(readLine(reader)) if input == "" { return def } num, err := strconv.Atoi(input) if err != nil || num <= 0 { fmt.Println(" [!] Enter a positive integer") continue } return num } } func readLine(reader *bufio.Reader) string { line, _ := reader.ReadString('\n') return strings.TrimRight(line, "\r\n") } func filterPhysicalIfaces(ifaces []string) []string { skip := map[string]bool{"lo": true} var result []string for _, name := range ifaces { if skip[name] { continue } if network.IsVLAN(name) { continue } result = append(result, name) } sort.Strings(result) return result } func removeItem(xs []string, item string) []string { var result []string for _, x := range xs { if x != item { result = append(result, x) } } return result } func isValidNetmask(mask string) bool { m := net.ParseIP(mask) if m == nil { return false } m4 := m.To4() if m4 == nil { return false } var n uint32 for _, b := range m4 { n = (n << 8) | uint32(b) } if n == 0 { return false } complement := ^n return complement&((complement)+1) == 0 } func computeSubnet(ipStr, maskStr string) string { ip := net.ParseIP(ipStr) mask := net.ParseIP(maskStr) if ip == nil || mask == nil { return ipStr } ip4 := ip.To4() mask4 := mask.To4() if ip4 == nil || mask4 == nil { return ipStr } ipMask := net.IPMask(mask4) subnet := net.IPNet{ IP: ip4.Mask(ipMask), Mask: ipMask, } return subnet.String() } func incrementIP(ipStr string, offset int) string { ip := net.ParseIP(ipStr) if ip == nil { return ipStr } ip4 := ip.To4() if ip4 == nil { return ipStr } val := uint32(ip4[0])<<24 | uint32(ip4[1])<<16 | uint32(ip4[2])<<8 | uint32(ip4[3]) val += uint32(offset) return fmt.Sprintf("%d.%d.%d.%d", (val>>24)&0xFF, (val>>16)&0xFF, (val>>8)&0xFF, val&0xFF) } func buildConfig( wanIface, wanMode, wanAddress, wanNetmask, wanGateway, lanIface, lanAddress, lanNetmask, lanSubnet string, enableDHCP, enableDHCPPool bool, dhcpRangeStart, dhcpRangeEnd string, dhcpLeaseTime int, adminUsername, adminPassword string, ) *config.AppConfig { cfg := &config.AppConfig{ Interfaces: map[string]*config.InterfaceConfig{ wanIface: { Type: "wan", Auto: true, Mode: wanMode, Address: wanAddress, Netmask: wanNetmask, Gateway: wanGateway, }, lanIface: { Type: "lan", Auto: true, Mode: "static", Address: lanAddress, Netmask: lanNetmask, }, }, NAT: config.NATConfig{ Interfaces: []string{lanIface}, }, Firewall: config.FirewallConfig{ VLANIsolation: true, Rules: []config.FirewallRule{}, }, DHCP: config.DHCPConfig{ Enabled: enableDHCP, Pools: []config.DHCPPool{}, }, KnownDevices: []config.KnownDevice{}, Mihomo: config.MihomoConfig{Enabled: false}, ClientPolicy: config.ClientPolicyConfig{Default: "direct"}, Auth: config.AuthConfig{ Username: adminUsername, PasswordHash: auth.HashPassword(adminPassword), }, ListenAddresses: []string{lanAddress}, } if enableDHCP && enableDHCPPool { parts := strings.Split(lanSubnet, "/") subnetPart := lanSubnet if len(parts) == 2 { subnetPart = parts[0] + "/" + parts[1] } dnsServers := []string{lanAddress} if wanMode == "static" && wanGateway != "" { dnsServers = []string{lanAddress, wanGateway} } cfg.DHCP.Pools = append(cfg.DHCP.Pools, config.DHCPPool{ Interface: lanIface, Enabled: true, Subnet: subnetPart, Netmask: lanNetmask, RangeStart: dhcpRangeStart, RangeEnd: dhcpRangeEnd, Router: lanAddress, DNS: dnsServers, LeaseTime: dhcpLeaseTime, }) } return cfg } func installService() error { exe, err := os.Executable() if err != nil { return fmt.Errorf("detect executable path: %w", err) } resolved, err := filepath.EvalSymlinks(exe) if err != nil { resolved = exe } if _, err := os.Stat(servicePath); err == nil { fmt.Println(" Removing existing service...") _ = exec.Command("rc-service", serviceName, "stop").Run() _ = exec.Command("rc-update", "delete", serviceName).Run() _ = os.Remove(servicePath) } fmt.Printf(" Installing binary to %s...\n", binInstallPath) if err := os.MkdirAll(filepath.Dir(binInstallPath), 0755); err != nil { return fmt.Errorf("create bin directory: %w", err) } if out, err := exec.Command("cp", resolved, binInstallPath).CombinedOutput(); err != nil { return fmt.Errorf("copy binary: %s: %w", strings.TrimSpace(string(out)), err) } if err := os.Chmod(binInstallPath, 0755); err != nil { return fmt.Errorf("chmod binary: %w", err) } fmt.Println(" Binary installed and made executable") serviceContent := `#!/sbin/openrc-run description="Network Manager Web Panel" command="` + binInstallPath + `" command_background=true pidfile="/run/${RC_SVCNAME}.pid" output_log="/var/log/network-manager.log" error_log="/var/log/network-manager.log" depend() { need net after firewall } start_pre() { checkpath --directory /var/log } ` if err := os.WriteFile(servicePath, []byte(serviceContent), 0755); err != nil { return fmt.Errorf("write service file: %w", err) } fmt.Printf(" Service file created: %s\n", servicePath) if out, err := exec.Command("rc-update", "add", serviceName, "default").CombinedOutput(); err != nil { return fmt.Errorf("enable service: %s: %w", strings.TrimSpace(string(out)), err) } fmt.Println(" Service enabled for autostart (default runlevel)") return nil }