Files
alpine-router/setup/setup.go
2026-04-15 12:25:39 +03:00

631 lines
16 KiB
Go

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
}