188 lines
5.4 KiB
Go
188 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|