Files

275 lines
8.2 KiB
Go
Raw Permalink Normal View History

2026-07-18 10:02:43 +03:00
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// initServer runs the interactive server setup. Unlike the original script it
// no longer installs dependencies — that is a separate, prerequisite step. It
// verifies the deps-installed flag recorded by `install-deps` and aborts if it
// is missing, then generates keys/obfuscation params, writes configs, enables
// forwarding and starts the service.
func initServer() {
osInfo := detectOS()
// New flow: dependency installation must have happened first.
st := loadState()
if !st.DepsInstalled {
die("Dependencies not installed — run 'install-deps' first, then re-run 'init-server'")
}
info("Dependency check passed (installed %s on %s %s)",
valueOr(st.InstalledAt, "unknown time"),
valueOr(st.OSID, "?"), valueOr(st.OSVersion, "?"))
// The awg binary must exist now that deps are installed.
requireBinary("awg")
fmt.Println()
fmt.Println("=== AmneziaWG Server Initialization ===")
fmt.Println()
fmt.Println("Dependencies already installed. This will:")
fmt.Println(" 1. Generate server key pair")
fmt.Println(" 2. Generate AmneziaWG obfuscation parameters")
fmt.Printf(" 3. Write %s/<interface>.conf\n", awgConfDir)
fmt.Println(" 4. Enable IP forwarding (persistent)")
fmt.Println(" 5. Start and enable the AmneziaWG service")
fmt.Println()
if !confirm("Proceed?") {
info("Aborted")
os.Exit(0)
}
fmt.Println()
c := &Config{}
// Interactive configuration.
c.Interface = promptDefault("AmneziaWG interface name", "awg0")
c.Network = promptDefault("VPN network CIDR", "10.0.0.0/24")
requireSlash24(c.Network)
c.Port = promptDefault("Listen port", "51820")
detectedIP := detectPublicIP()
c.PublicIP = promptDefault("Server public IP or hostname", detectedIP)
c.DNS = promptDefault("DNS server for clients", "1.1.1.1")
c.MTU = promptDefault("MTU", "1420")
mtu := atoiOrDie(c.MTU)
// AmneziaWG obfuscation parameters — randomised defaults, editable.
// Ceilings are derived from the interface MTU per the AmneziaWG spec:
// Jmin < Jmax <= MTU ; S1 <= MTU-148 ; S2 <= MTU-92
mtuJunkCeil := mtu
mtuS1Ceil := mtu - 148
mtuS2Ceil := mtu - 92
rndJc := randRange(4, 12)
jminHi := minInt(mtuJunkCeil-2, 32)
rndJmin := randRange(8, jminHi)
jmaxLo := maxInt(rndJmin+32, 80)
jmaxHi := minInt(mtuJunkCeil, 200)
rndJmax := randRange(jmaxLo, jmaxHi)
rndS1 := randRange(15, minInt(mtuS1Ceil, 150))
rndS2 := randRange(15, minInt(mtuS2Ceil, 150))
// S1 + 56 != S2 (spec constraint) — resample S2 until it holds.
for rndS1+56 == rndS2 {
rndS2 = randRange(15, minInt(mtuS2Ceil, 150))
}
fmt.Println()
fmt.Println("AmneziaWG obfuscation parameters (shared by server and all clients):")
c.Jc = promptDefault("Jc (junk packet count)", itoa(rndJc))
c.Jmin = promptDefault("Jmin (min junk size)", itoa(rndJmin))
c.Jmax = promptDefault("Jmax (max junk size)", itoa(rndJmax))
c.S1 = promptDefault("S1 (init junk size)", itoa(rndS1))
c.S2 = promptDefault("S2 (response junk size)", itoa(rndS2))
// Four distinct magic-header values in [5, 2^31-1].
h1 := randMagic()
h2 := randMagic()
for h2 == h1 {
h2 = randMagic()
}
h3 := randMagic()
for h3 == h1 || h3 == h2 {
h3 = randMagic()
}
h4 := randMagic()
for h4 == h1 || h4 == h2 || h4 == h3 {
h4 = randMagic()
}
c.H1 = promptDefault("H1 (magic header 1)", itoa64(h1))
c.H2 = promptDefault("H2 (magic header 2)", itoa64(h2))
c.H3 = promptDefault("H3 (magic header 3)", itoa64(h3))
c.H4 = promptDefault("H4 (magic header 4)", itoa64(h4))
fmt.Println()
// Generate server keys (awg is guaranteed present by the deps check).
info("Generating server keys...")
priv, err := output("awg", "genkey")
if err != nil {
die("awg genkey failed: %v", err)
}
pub, err := outputWithInput(priv, "awg", "pubkey")
if err != nil {
die("awg pubkey failed: %v", err)
}
c.ServerPriv = priv
c.ServerPub = pub
srvIP := serverIP(c.Network)
defIface := defaultRouteIface()
// Write the profiler config file.
writeConfig(c)
// Write <conf-dir>/<iface>.conf and companion nft ruleset.
confPath := awgConfPath(c.Interface)
info("Writing %s...", confPath)
if err := os.MkdirAll(awgConfDir, 0755); err != nil {
die("Failed to create %s: %v", awgConfDir, err)
}
if err := os.WriteFile(confPath, []byte(awgConfHeader(c, c.ServerPriv, srvIP, c.Port, c.MTU)), 0600); err != nil {
die("Failed to write %s: %v", confPath, err)
}
writeNftRules(c.Interface, defIface, c.MTU)
// IP forwarding.
info("Enabling IP forwarding...")
enableIPForwarding()
// Service.
serviceEnable(osInfo, c.Interface)
serviceStart(osInfo, c.Interface)
// Init client storage.
initStorage()
fmt.Println()
info("=== Server initialization complete ===")
info("Interface : %s", c.Interface)
info("Server IP : %s", srvIP)
info("Network : %s", c.Network)
info("Port : %s", c.Port)
info("Public IP : %s", c.PublicIP)
info("Public Key: %s", c.ServerPub)
info("Obfusc. : Jc=%s Jmin=%s Jmax=%s S1=%s S2=%s", c.Jc, c.Jmin, c.Jmax, c.S1, c.S2)
fmt.Println()
info("Next steps: use 'create <name>' to add VPN clients")
}
// ─── server management ───────────────────────────────────────────────────────────
func serverStatus(c *Config, osInfo *OSInfo) {
fmt.Println()
fmt.Println("=== AmneziaWG Server Configuration ===")
fmt.Printf(" %-12s %s\n", "Interface:", c.Interface)
fmt.Printf(" %-12s %s\n", "Network:", c.Network)
fmt.Printf(" %-12s %s\n", "Port:", c.Port)
fmt.Printf(" %-12s %s\n", "Public IP:", c.PublicIP)
fmt.Printf(" %-12s %s\n", "Public Key:", c.ServerPub)
fmt.Println()
fmt.Println("=== Interface Status ===")
if awgIfaceUp(c.Interface) {
run("awg", "show", c.Interface)
} else {
fmt.Printf(" Interface %s is DOWN\n", c.Interface)
}
fmt.Println()
fmt.Println("=== Service Status ===")
serviceStatus(osInfo, c.Interface)
}
func showConfig() {
c := loadConfig()
confPath := awgConfPath(c.Interface)
fmt.Println()
fmt.Printf("=== Profiler Config (%s) ===\n", configFile)
printFilteredConfig(configFile)
fmt.Println()
if data, err := os.ReadFile(confPath); err == nil {
fmt.Printf("=== AmneziaWG Config (%s) ===\n", confPath)
fmt.Print(hidePrivateKey(string(data)))
} else {
warn("%s not found", confPath)
}
}
// printFilteredConfig reproduces the bash grep chain: drop lines containing
// PRIVATE / PASSW / KEY, comment lines, and blank lines.
func printFilteredConfig(path string) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "PRIVATE") ||
strings.Contains(line, "PASSW") ||
strings.Contains(line, "KEY") {
continue
}
if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
continue
}
fmt.Println(line)
}
}
// hidePrivateKey masks the PrivateKey value (bash: sed s/PrivateKey.../<hidden>/).
func hidePrivateKey(content string) string {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "PrivateKey") {
if idx := strings.Index(line, "="); idx >= 0 {
lines[i] = line[:idx+1] + " <hidden>"
}
}
}
return strings.Join(lines, "\n")
}
// syncConfig regenerates the interface conf with all ACTIVE peers, optionally
// restarting the service.
func syncConfig(c *Config, osInfo *OSInfo) {
confPath := awgConfPath(c.Interface)
if _, err := os.Stat(confPath); err != nil {
die("%s not found — run init-server first", confPath)
}
srvIP := serverIP(c.Network)
defIface := defaultRouteIface()
mtu := c.mtuOr("1420")
info("Regenerating %s with all ACTIVE peers...", confPath)
clients := loadRegistry()
var b strings.Builder
b.WriteString(awgConfHeader(c, c.ServerPriv, srvIP, c.Port, mtu))
for _, cl := range clients {
if cl.IsEnabled == "ACTIVE" {
b.WriteString(peerBlock(cl.Name, cl.PublicKey, cl.PSKKey, cl.IP))
}
}
if err := os.WriteFile(confPath, []byte(b.String()), 0600); err != nil {
die("Failed to write %s: %v", confPath, err)
}
writeNftRules(c.Interface, defIface, mtu)
info("Config written: %s", confPath)
if confirm("Restart AmneziaWG to apply changes?") {
serviceRestart(osInfo, c.Interface)
info("AmneziaWG restarted")
}
}