15.04.2026 Update
This commit is contained in:
@@ -105,6 +105,7 @@ type AppConfig struct {
|
|||||||
ClientPolicy ClientPolicyConfig `yaml:"client_policy,omitempty"`
|
ClientPolicy ClientPolicyConfig `yaml:"client_policy,omitempty"`
|
||||||
Connectivity ConnectivityConfig `yaml:"connectivity,omitempty" json:"connectivity,omitempty"`
|
Connectivity ConnectivityConfig `yaml:"connectivity,omitempty" json:"connectivity,omitempty"`
|
||||||
Auth AuthConfig `yaml:"auth,omitempty"`
|
Auth AuthConfig `yaml:"auth,omitempty"`
|
||||||
|
ListenAddresses []string `yaml:"listen_addresses,omitempty" json:"listen_addresses,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
23
main.go
23
main.go
@@ -20,6 +20,7 @@ import (
|
|||||||
"nano-router/monitor"
|
"nano-router/monitor"
|
||||||
"nano-router/nat"
|
"nano-router/nat"
|
||||||
"nano-router/network"
|
"nano-router/network"
|
||||||
|
"nano-router/setup"
|
||||||
"nano-router/traffic"
|
"nano-router/traffic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +28,11 @@ import (
|
|||||||
var publicFS embed.FS
|
var publicFS embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "setup" {
|
||||||
|
setup.Run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("load config.yaml: %v", err)
|
log.Fatalf("load config.yaml: %v", err)
|
||||||
@@ -164,8 +170,23 @@ func main() {
|
|||||||
handlers.StartPolicySync(30 * time.Second)
|
handlers.StartPolicySync(30 * time.Second)
|
||||||
|
|
||||||
log.Printf("Config file: %s", config.GetPath())
|
log.Printf("Config file: %s", config.GetPath())
|
||||||
|
|
||||||
|
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.Printf("Network Manager listening on http://0.0.0.0:%s", port)
|
||||||
log.Fatal(http.ListenAndServe(":"+port, auth.PublicAuthMiddleware(mux)))
|
log.Fatal(http.ListenAndServe(":"+port, handler))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func importSystemState() *config.AppConfig {
|
func importSystemState() *config.AppConfig {
|
||||||
|
|||||||
BIN
nano-router
BIN
nano-router
Binary file not shown.
631
setup/setup.go
Normal file
631
setup/setup.go
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
45
setup/termios_linux.go
Normal file
45
setup/termios_linux.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type termios struct {
|
||||||
|
Iflag uint32
|
||||||
|
Oflag uint32
|
||||||
|
Cflag uint32
|
||||||
|
Lflag uint32
|
||||||
|
Cc [20]byte
|
||||||
|
Ispeed uint32
|
||||||
|
Ospeed uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
tcgetattr = 0x5401
|
||||||
|
tcsetattr = 0x5402
|
||||||
|
TCSAFLUSH = 2
|
||||||
|
ECHO = 0x8
|
||||||
|
ICANON = 0x100
|
||||||
|
)
|
||||||
|
|
||||||
|
func termiosGetState(fd int) (*termios, bool) {
|
||||||
|
var state termios
|
||||||
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(tcgetattr), uintptr(unsafe.Pointer(&state))); err != 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return &state, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func termiosDisableEcho(fd int) {
|
||||||
|
var state termios
|
||||||
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(tcgetattr), uintptr(unsafe.Pointer(&state))); err != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.Lflag &^= ECHO | ICANON
|
||||||
|
syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(tcsetattr), uintptr(unsafe.Pointer(&state)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func termiosRestore(fd int, state *termios) {
|
||||||
|
syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(tcsetattr), uintptr(unsafe.Pointer(state)))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user