Files

188 lines
5.4 KiB
Go
Raw Permalink Normal View History

2026-07-18 10:02:43 +03:00
package main
import (
"bufio"
"encoding/json"
"os"
"regexp"
"strings"
"time"
)
// Config mirrors the shell-sourced awg_config file. The on-disk format is kept
// byte-compatible with the original bash profiler so the two tools can share it.
type Config struct {
Network string // SERVER_NETWORK
Interface string // SERVER_INTERFACE
Port string // SERVER_PUBLIC_PORT
PublicIP string // SERVER_PUBLIC_IP
ServerPriv string // SERVER_PRIVATE_KEY
ServerPub string // SERVER_PUBLIC_KEY
DNS string // DNS_SERVER
MTU string // SERVER_MTU
// AmneziaWG obfuscation parameters (shared by server and every client).
Jc string // AWG_JC
Jmin string // AWG_JMIN
Jmax string // AWG_JMAX
S1 string // AWG_S1
S2 string // AWG_S2
H1 string // AWG_H1
H2 string // AWG_H2
H3 string // AWG_H3
H4 string // AWG_H4
}
// mtuOr returns the configured MTU or the given fallback (bash: ${SERVER_MTU:-1420}).
func (c *Config) mtuOr(def string) string {
if c.MTU == "" {
return def
}
return c.MTU
}
var shellAssignRe = regexp.MustCompile(`^([A-Za-z_][A-Za-z0-9_]*)=(.*)$`)
// loadConfig parses the profiler config file, aborting if it is missing.
func loadConfig() *Config {
f, err := os.Open(configFile)
if err != nil {
die("Config not found: run 'init-server' first")
}
defer f.Close()
vals := map[string]string{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
m := shellAssignRe.FindStringSubmatch(line)
if m == nil {
continue
}
vals[m[1]] = unquoteShell(m[2])
}
return &Config{
Network: vals["SERVER_NETWORK"],
Interface: vals["SERVER_INTERFACE"],
Port: vals["SERVER_PUBLIC_PORT"],
PublicIP: vals["SERVER_PUBLIC_IP"],
ServerPriv: vals["SERVER_PRIVATE_KEY"],
ServerPub: vals["SERVER_PUBLIC_KEY"],
DNS: vals["DNS_SERVER"],
MTU: vals["SERVER_MTU"],
Jc: vals["AWG_JC"],
Jmin: vals["AWG_JMIN"],
Jmax: vals["AWG_JMAX"],
S1: vals["AWG_S1"],
S2: vals["AWG_S2"],
H1: vals["AWG_H1"],
H2: vals["AWG_H2"],
H3: vals["AWG_H3"],
H4: vals["AWG_H4"],
}
}
// unquoteShell strips a single layer of surrounding single/double quotes.
func unquoteShell(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 {
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
return s[1 : len(s)-1]
}
}
return s
}
// writeConfig serialises the config in the original shell-sourceable format.
func writeConfig(c *Config) {
now := time.Now().UTC().Format("2006-01-02 15:04:05") + " UTC"
content := `# AmneziaWG Profiler Configuration
# Generated: ` + now + `
# Server network (CIDR) — server takes .1, clients get .2+
SERVER_NETWORK="` + c.Network + `"
# AmneziaWG interface name
SERVER_INTERFACE="` + c.Interface + `"
# Server listen port
SERVER_PUBLIC_PORT="` + c.Port + `"
# Server public IP or hostname (used in client configs)
SERVER_PUBLIC_IP="` + c.PublicIP + `"
# Server keys (generated by init-server)
SERVER_PRIVATE_KEY="` + c.ServerPriv + `"
SERVER_PUBLIC_KEY="` + c.ServerPub + `"
# Client DNS
DNS_SERVER="` + c.DNS + `"
# MTU
SERVER_MTU="` + c.MTU + `"
# ─── AmneziaWG obfuscation parameters ───────────────────────────────
# These MUST be identical on server and every client to interoperate.
# Jc : junk packet count (1-128, recommended 4-12)
# Jmin : min junk packet size (< Jmax, < 1280)
# Jmax : max junk packet size (> Jmin, <= 1280)
# S1 : init packet junk size (<= 1132)
# S2 : response packet junk size (<= 1188, and S1 + 56 != S2)
# H1-H4: magic header values (5..2147483647, all distinct)
AWG_JC="` + c.Jc + `"
AWG_JMIN="` + c.Jmin + `"
AWG_JMAX="` + c.Jmax + `"
AWG_S1="` + c.S1 + `"
AWG_S2="` + c.S2 + `"
AWG_H1="` + c.H1 + `"
AWG_H2="` + c.H2 + `"
AWG_H3="` + c.H3 + `"
AWG_H4="` + c.H4 + `"
`
if err := os.WriteFile(configFile, []byte(content), 0600); err != nil {
die("Failed to write config: %v", err)
}
info("Config written: %s", configFile)
}
// ─── deps-installed state (new: separates install-deps from init-server) ─────────
// State records whether the dependency-installation step has been completed.
// It lives in its own file so `install-deps` can run before any server config
// exists, and `init-server` can verify the flag without re-installing anything.
type State struct {
DepsInstalled bool `json:"deps_installed"`
InstalledAt string `json:"installed_at,omitempty"`
OSID string `json:"os_id,omitempty"`
OSVersion string `json:"os_version,omitempty"`
}
// loadState reads the state file, returning a zero value if it is absent.
func loadState() *State {
data, err := os.ReadFile(stateFile)
if err != nil {
return &State{}
}
var s State
if err := json.Unmarshal(data, &s); err != nil {
warn("State file %s is invalid — treating as empty", stateFile)
return &State{}
}
return &s
}
// saveState persists the state file with restrictive permissions.
func saveState(s *State) {
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
die("Failed to encode state: %v", err)
}
if err := os.WriteFile(stateFile, append(data, '\n'), 0600); err != nil {
die("Failed to write state file: %v", err)
}
}