Files
awg-profiler-golang/awg.go
T
2026-07-18 10:02:43 +03:00

242 lines
7.7 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// ─── service helpers ─────────────────────────────────────────────────────────────
func serviceEnable(os *OSInfo, iface string) {
switch os.Init {
case "systemd":
runOrDie("systemctl", "enable", "awg-quick@"+iface)
case "openrc":
if err := run("rc-update", "add", "awg-quick."+iface, "default"); err != nil {
warn("Could not register awg-quick.%s — may need manual setup", iface)
}
}
}
func serviceStart(os *OSInfo, iface string) {
switch os.Init {
case "systemd":
runOrDie("systemctl", "start", "awg-quick@"+iface)
case "openrc":
runOrDie("awg-quick", "up", iface)
}
}
func serviceStop(os *OSInfo, iface string) {
switch os.Init {
case "systemd":
runOrDie("systemctl", "stop", "awg-quick@"+iface)
case "openrc":
runOrDie("awg-quick", "down", iface)
}
}
func serviceRestart(os *OSInfo, iface string) {
serviceStop(os, iface)
serviceStart(os, iface)
}
func serviceStatus(os *OSInfo, iface string) {
switch os.Init {
case "systemd":
// Best-effort; a non-zero status is not an error here.
run("systemctl", "status", "awg-quick@"+iface, "--no-pager")
case "openrc":
run("rc-service", "awg-quick."+iface, "status")
}
}
// ─── live peer management ────────────────────────────────────────────────────────
// awgIfaceUp reports whether the AmneziaWG interface is currently up.
func awgIfaceUp(iface string) bool {
return silent("awg", "show", iface)
}
// awgPeerAdd hot-adds a peer to the running interface (no restart needed).
func awgPeerAdd(iface, pubkey, psk, ip string) {
if !awgIfaceUp(iface) {
warn("Interface %s is down — peer will be active on next start", iface)
return
}
// awg reads the preshared key from a file; use a short-lived temp file.
pskFile, err := os.CreateTemp("", "awg-psk-*")
if err != nil {
die("Failed to create temp psk file: %v", err)
}
defer os.Remove(pskFile.Name())
if _, err := pskFile.WriteString(psk); err != nil {
pskFile.Close()
die("Failed to write temp psk file: %v", err)
}
pskFile.Close()
if err := run("awg", "set", iface, "peer", pubkey,
"preshared-key", pskFile.Name(),
"allowed-ips", ip+"/32"); err != nil {
// Non-fatal: by this point the client is already saved in the registry
// and appended to <iface>.conf (createClient calls confAppendPeer first),
// so the peer will pick up on the next restart/sync-config even if the
// live hot-add fails. Dying here would report client creation as failed
// when it actually succeeded, just without taking effect immediately.
warn("awg set (peer add) failed: %v — peer saved, will apply on next restart/sync", err)
return
}
info("Peer added live to %s (%s)", iface, ip)
}
// awgPeerRemove hot-removes a peer from the running interface.
func awgPeerRemove(iface, pubkey string) {
if !awgIfaceUp(iface) {
return
}
if err := run("awg", "set", iface, "peer", pubkey, "remove"); err != nil {
die("awg set (peer remove) failed: %v", err)
}
info("Peer removed live from %s", iface)
}
// ─── config-file assembly ────────────────────────────────────────────────────────
func awgConfPath(iface string) string {
return filepath.Join(awgConfDir, iface+".conf")
}
// awgConfHeader builds the [Interface] block including obfuscation parameters.
// PostUp loads the pre-generated nft ruleset; PostDown drops the table.
func awgConfHeader(c *Config, priv, srvIP, port, mtu string) string {
return fmt.Sprintf(`[Interface]
PrivateKey = %s
Address = %s/24
ListenPort = %s
MTU = %s
SaveConfig = false
Jc = %s
Jmin = %s
Jmax = %s
S1 = %s
S2 = %s
H1 = %s
H2 = %s
H3 = %s
H4 = %s
PostUp = nft delete table inet awg_%%i 2>/dev/null || true
PostUp = nft -f %s/%%i-rules.nft
PostDown = nft delete table inet awg_%%i
`, priv, srvIP, port, mtu,
c.Jc, c.Jmin, c.Jmax, c.S1, c.S2, c.H1, c.H2, c.H3, c.H4,
awgConfDir)
}
// peerBlock renders a single [Peer] section for the interface config.
func peerBlock(name, pubkey, psk, ip string) string {
return "\n[Peer]\n# " + name +
"\nPublicKey = " + pubkey +
"\nPresharedKey = " + psk +
"\nAllowedIPs = " + ip + "/32\n"
}
// confAppendPeer appends a single [Peer] block to <conf-dir>/<iface>.conf.
func confAppendPeer(iface, name, pubkey, psk, ip string) {
confPath := awgConfPath(iface)
if _, err := os.Stat(confPath); err != nil {
warn("%s not found — skipping conf update", confPath)
return
}
f, err := os.OpenFile(confPath, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
warn("%s not found — skipping conf update", confPath)
return
}
defer f.Close()
if _, err := f.WriteString(peerBlock(name, pubkey, psk, ip)); err != nil {
die("Failed to append peer to %s: %v", confPath, err)
}
info("Peer appended to %s", confPath)
}
// confRebuild silently regenerates <conf-dir>/<iface>.conf from the registry
// (ACTIVE peers only) and rewrites the companion nft ruleset. Used by
// delete/enable/disable to keep the conf in sync without a restart prompt.
func confRebuild(c *Config, osInfo *OSInfo, clients []Client) {
iface := c.Interface
confPath := awgConfPath(iface)
if _, err := os.Stat(confPath); err != nil {
warn("%s not found — skipping conf rebuild", confPath)
return
}
srvIP := serverIP(c.Network)
defIface := defaultRouteIface()
mtu := c.mtuOr("1420")
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(iface, defIface, mtu)
}
// serverIP derives the server's .1 host address from the network CIDR.
func serverIP(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.1", parts[0], parts[1], parts[2])
}
// ─── nftables ruleset ────────────────────────────────────────────────────────────
// writeNftRules writes <conf-dir>/<iface>-rules.nft, loaded by PostUp via `nft -f`.
func writeNftRules(iface, defIface, mtu string) {
mss := atoiOrDie(mtu) - 40
nftPath := filepath.Join(awgConfDir, iface+"-rules.nft")
content := fmt.Sprintf(`table inet awg_%s {
# Allow forwarded traffic through the WG tunnel in both directions
chain forward {
type filter hook forward priority 0; policy accept;
iif "%s" accept
oif "%s" accept
}
# Masquerade only WG→external flows (more targeted than a blanket POSTROUTING rule)
chain postrouting {
type nat hook postrouting priority 100;
iif "%s" oif "%s" masquerade
}
# MSS clamping keeps TCP segments within the WG tunnel MTU;
# TTL normalisation hides the extra forwarding hop from remote hosts.
chain mangle {
type filter hook forward priority -150;
iif "%s" tcp flags syn / syn,rst tcp option maxseg size set %d
oif "%s" tcp flags syn / syn,rst tcp option maxseg size set %d
iif "%s" oif "%s" ip ttl set 64
}
}
`, iface, iface, iface, iface, defIface, iface, mss, iface, mss, iface, defIface)
if err := os.WriteFile(nftPath, []byte(content), 0600); err != nil {
die("Failed to write nft ruleset: %v", err)
}
info("NFT ruleset written: %s", nftPath)
}