275 lines
8.2 KiB
Go
275 lines
8.2 KiB
Go
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")
|
||
|
|
}
|
||
|
|
}
|