package main import ( "bufio" "context" crand "crypto/rand" "encoding/binary" "fmt" "io" "net" "net/http" "os" "os/exec" "regexp" "strconv" "strings" "time" ) func itoa(n int) string { return strconv.Itoa(n) } func itoa64(n int64) string { return strconv.FormatInt(n, 10) } // atoiOrDie parses a base-10 integer, aborting on malformed input. func atoiOrDie(s string) int { n, err := strconv.Atoi(strings.TrimSpace(s)) if err != nil { die("expected integer, got %q", s) } return n } // ─── logging helpers (mirror the bash die/info/warn) ──────────────────────────── // webMode makes die() panic instead of exiting the process, so the long-running // web server can recover from an operation failure and turn it into an HTTP // error response rather than crashing. It is set only by the `web` command. var webMode bool // dieError carries a die() message across a recover() in web mode. type dieError struct{ msg string } func (e dieError) Error() string { return e.msg } // die prints an error to stderr and terminates with status 1. In web mode it // panics with a dieError instead, to be recovered by the HTTP handler wrapper. func die(format string, args ...any) { if webMode { panic(dieError{fmt.Sprintf(format, args...)}) } fmt.Fprintf(os.Stderr, "ERROR: "+format+"\n", args...) os.Exit(1) } func info(format string, args ...any) { fmt.Printf("INFO: "+format+"\n", args...) } func warn(format string, args ...any) { fmt.Fprintf(os.Stderr, "WARN: "+format+"\n", args...) } // requireBinary aborts unless the named executable is on PATH. func requireBinary(name string) { if _, err := exec.LookPath(name); err != nil { die("%s not installed", name) } } // ─── interactive input ────────────────────────────────────────────────────────── var stdinReader = bufio.NewReader(os.Stdin) // confirm asks a yes/no question; returns true only for a bare y/Y. func confirm(prompt string) bool { fmt.Printf("%s [y/N] ", prompt) answer, _ := stdinReader.ReadString('\n') answer = strings.TrimSpace(answer) return answer == "y" || answer == "Y" } // promptDefault reads a value, falling back to def when the user hits enter. func promptDefault(prompt, def string) string { fmt.Printf("%s [%s]: ", prompt, def) value, _ := stdinReader.ReadString('\n') value = strings.TrimRight(value, "\r\n") if value == "" { return def } return value } // ─── randomness (crypto/rand, replaces od < /dev/urandom) ─────────────────────── func randUint32() uint32 { var b [4]byte if _, err := crand.Read(b[:]); err != nil { die("failed to read random bytes: %v", err) } return binary.BigEndian.Uint32(b[:]) } // randMagic returns a random unsigned 32-bit integer in [5, 2^31-1]. func randMagic() int64 { const span = 2147483647 - 5 + 1 // inclusive [5, 2^31-1] n := uint64(randUint32()) return int64(n%span) + 5 } // randRange returns a uniform integer in the inclusive range [min, max]. // If max < min it warns and returns min so the range stays sane. func randRange(min, max int) int { if max < min { warn("Randomisation range [%d,%d] invalid (MTU too small?) — using %d", min, max, min) return min } span := uint64(max - min + 1) n := uint64(randUint32()) return int(n%span) + min } func minInt(a, b int) int { if a < b { return a } return b } func maxInt(a, b int) int { if a > b { return a } return b } // ─── name sanitisation ────────────────────────────────────────────────────────── var sanitizeRe = regexp.MustCompile(`[^a-zA-Z0-9._-]`) func sanitize(v string) string { return sanitizeRe.ReplaceAllString(v, "_") } // ─── IP helpers ───────────────────────────────────────────────────────────────── // networkBase strips the /CIDR suffix, returning the bare address portion. func networkBase(network string) string { if i := strings.Index(network, "/"); i >= 0 { return network[:i] } return network } // requireSlash24 validates that network is an IPv4 CIDR with a /24 prefix. // serverIP/getFirstClientIP/incrementIP only ever vary the last octet, and // every written config hardcodes "Address = /24" regardless of what was // typed in — so any other prefix would silently produce a broken, internally // inconsistent config rather than the network the operator actually asked for. func requireSlash24(network string) { ip, ipnet, err := net.ParseCIDR(network) if err != nil || ip.To4() == nil { die("Invalid network %q: expected an IPv4 CIDR, e.g. 10.0.0.0/24", network) } ones, bits := ipnet.Mask.Size() if bits != 32 || ones != 24 { die("Network %q must be a /24 (only /24 subnets are supported) — e.g. 10.0.0.0/24", network) } if ipnet.IP.String() != networkBase(network) { die("Network %q is not a valid /24 base address — did you mean %s/24?", network, ipnet.IP.String()) } } // getFirstClientIP derives the .2 host of the configured network. func getFirstClientIP(network string) string { base := networkBase(network) parts := strings.Split(base, ".") if len(parts) != 4 { die("Invalid network: %s", network) } return fmt.Sprintf("%s.%s.%s.2", parts[0], parts[1], parts[2]) } // incrementIP returns the next host IP; if current is empty it seeds from .2. func incrementIP(current, network string) string { if current == "" { return getFirstClientIP(network) } parts := strings.Split(current, ".") if len(parts) != 4 { die("Invalid IP: %s", current) } o4 := atoiOrDie(parts[3]) o4++ if o4 >= 255 { die("IP pool exhausted") } return fmt.Sprintf("%s.%s.%s.%d", parts[0], parts[1], parts[2], o4) } // ─── command execution ────────────────────────────────────────────────────────── // run executes a command with inherited stdio and returns its error. func run(name string, args ...string) error { return runEnv(nil, name, args...) } func runEnv(extraEnv []string, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if extraEnv != nil { cmd.Env = append(os.Environ(), extraEnv...) } return cmd.Run() } // runOrDie runs a command and aborts if it fails. func runOrDie(name string, args ...string) { if err := run(name, args...); err != nil { die("%s failed: %v", name, err) } } // runOrDieIn runs a command with its working directory set to dir (e.g. a // cloned source tree) and aborts if it fails. func runOrDieIn(dir, name string, args ...string) { cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { die("%s failed: %v", name, err) } } // enableIPForwarding turns on IPv4 forwarding and persists it. It is tolerant // of container environments: if `sysctl -w` fails but forwarding is already on // (e.g. set at namespace creation via `--sysctl`/compose, with /proc/sys mounted // read-only) it warns and continues instead of aborting. It only dies if // forwarding is genuinely off and cannot be enabled. func enableIPForwarding() { if err := run("sysctl", "-w", "net.ipv4.ip_forward=1"); err != nil { if ipForwardingEnabled() { warn("Could not write net.ipv4.ip_forward (%v), but it is already enabled — continuing", err) } else { die("Failed to enable IP forwarding: %v (set it on the host, e.g. --sysctl net.ipv4.ip_forward=1)", err) } } // Best-effort persistence; /etc may be read-only in some containers. if err := os.WriteFile("/etc/sysctl.d/99-amneziawg.conf", []byte("net.ipv4.ip_forward=1\n"), 0644); err != nil { warn("Could not persist sysctl config: %v", err) } run("sysctl", "-p", "/etc/sysctl.d/99-amneziawg.conf") } // ipForwardingEnabled reads the live kernel flag directly. func ipForwardingEnabled() bool { data, err := os.ReadFile("/proc/sys/net/ipv4/ip_forward") return err == nil && strings.TrimSpace(string(data)) == "1" } // output runs a command and returns its trimmed stdout. func output(name string, args ...string) (string, error) { cmd := exec.Command(name, args...) cmd.Stderr = os.Stderr out, err := cmd.Output() return strings.TrimSpace(string(out)), err } // outputWithInput runs a command feeding stdin, returning trimmed stdout. func outputWithInput(stdin, name string, args ...string) (string, error) { cmd := exec.Command(name, args...) cmd.Stdin = strings.NewReader(stdin) cmd.Stderr = os.Stderr out, err := cmd.Output() return strings.TrimSpace(string(out)), err } // silent reports whether a command succeeds, discarding all its output. func silent(name string, args ...string) bool { cmd := exec.Command(name, args...) return cmd.Run() == nil } // ─── network detection ────────────────────────────────────────────────────────── // defaultRouteIface parses `ip route` for the default outbound interface, // mirroring `ip route | awk '/default/ {print $5; exit}'`. func defaultRouteIface() string { out, err := output("ip", "route") if err != nil { return "" } for _, line := range strings.Split(out, "\n") { if strings.Contains(line, "default") { fields := strings.Fields(line) if len(fields) >= 5 { return fields[4] } } } return "" } // detectPublicIP mimics `curl -sf4 https://ifconfig.me` (IPv4 only), falling // back to the placeholder used by the original script. // // It queries several plaintext "what is my IP" services in turn and validates // each response with net.ParseIP. Validation is essential: services such as // ifconfig.me serve a full HTML landing page (not the bare IP) to clients that // don't send a curl-like User-Agent, so without the parse check the config // would end up storing an HTML fragment instead of an address. func detectPublicIP() string { dialer := &net.Dialer{Timeout: 5 * time.Second} transport := &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { // Force IPv4 so the detected address matches the udp4 listener. return dialer.DialContext(ctx, "tcp4", addr) }, } client := &http.Client{Timeout: 8 * time.Second, Transport: transport} // Plaintext IPv4 endpoints, tried in order until one yields a valid IP. for _, url := range []string{ "https://ifconfig.me/ip", "https://api.ipify.org", "https://icanhazip.com", } { if ip := fetchIPv4(client, url); ip != "" { return ip } } return "YOUR_SERVER_IP" } // fetchIPv4 requests url and returns the trimmed body only if it is a valid // IPv4 address; otherwise it returns "". A curl-like User-Agent is sent so // services that content-negotiate (e.g. ifconfig.me) reply with the bare IP. func fetchIPv4(client *http.Client, url string) string { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return "" } req.Header.Set("User-Agent", "curl/8.0.0") req.Header.Set("Accept", "text/plain") resp, err := client.Do(req) if err != nil { return "" } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "" } // A valid IPv4 string is at most 15 bytes; cap the read to reject any // unexpectedly large (e.g. HTML) response early. body, err := io.ReadAll(io.LimitReader(resp.Body, 64)) if err != nil { return "" } ip := strings.TrimSpace(string(body)) if parsed := net.ParseIP(ip); parsed == nil || parsed.To4() == nil { return "" } return ip } // unameRelease returns the running kernel release (`uname -r`). func unameRelease() string { out, err := output("uname", "-r") if err != nil { die("uname -r failed: %v", err) } return out }