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) } }