Files
navi/internal/protocols/awg/conf.go
T

941 lines
23 KiB
Go

package awg
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"time"
)
// Conf is a parsed AmneziaWG / WireGuard client configuration (AWG 2.0 fields included).
type Conf struct {
PrivateKey string
Address []string
DNS []string
MTU int
Jc int
Jmin int
Jmax int
S1, S2, S3, S4 int
H1, H2, H3, H4 string
I1, I2, I3, I4, I5 string
PublicKey string
PresharedKey string
Endpoint string
AllowedIPs []string
Keepalive int
Name string
}
// Detect reports whether raw looks like an AWG/WireGuard config or share link.
func Detect(raw string) bool {
raw = sanitizeShare(raw)
if raw == "" {
return false
}
lower := strings.ToLower(raw)
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") {
return true
}
if strings.Contains(lower, "[interface]") &&
(strings.Contains(lower, "privatekey") || strings.Contains(lower, "private_key") || strings.Contains(lower, "private key")) {
return true
}
if strings.HasPrefix(strings.TrimSpace(raw), "{") &&
(strings.Contains(lower, "client_priv_key") || strings.Contains(lower, "privatekey") ||
strings.Contains(lower, "server_pub_key")) {
return true
}
return false
}
// Parse accepts a WireGuard/AWG .conf body, awg:// URI, vpn:// blob, or Amnezia JSON.
func Parse(raw string) (Conf, error) {
raw = sanitizeShare(raw)
if raw == "" {
return Conf{}, fmt.Errorf("пустой AWG конфиг")
}
// Strip UTF-8 BOM
raw = strings.TrimPrefix(raw, "\ufeff")
lower := strings.ToLower(raw)
switch {
case strings.HasPrefix(lower, "vpn://"):
return parseVPNScheme(raw)
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
strings.HasPrefix(lower, "wireguard://"):
return parseURI(raw)
case strings.HasPrefix(raw, "{") || strings.HasPrefix(raw, "["):
if c, err := parseAmneziaJSON(raw); err == nil {
return c, nil
}
// fall through to INI if JSON didn't work (rare)
return parseINI(raw)
default:
return parseINI(raw)
}
}
func sanitizeShare(raw string) string {
raw = strings.TrimPrefix(raw, "\ufeff")
var b strings.Builder
b.Grow(len(raw))
for _, r := range raw {
switch r {
case '\u200b', '\u200c', '\u200d', '\u2060', '\ufeff',
'\u2066', '\u2067', '\u2068', '\u2069',
'\u200e', '\u200f',
'\u202a', '\u202b', '\u202c', '\u202d', '\u202e':
continue
}
if r < 0x20 && r != '\n' && r != '\r' && r != '\t' {
continue
}
b.WriteRune(r)
}
return strings.TrimSpace(b.String())
}
func parseINI(raw string) (Conf, error) {
var c Conf
section := "interface" // treat leading keys before [Interface] as Interface
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
section = normalizeKey(line[1 : len(line)-1])
continue
}
key, val, ok := splitKV(line)
if !ok {
continue
}
k := normalizeKey(key)
val = unwrapQuotes(val)
switch section {
case "interface", "":
applyInterfaceField(&c, k, val)
case "peer":
applyPeerField(&c, k, val)
}
}
if err := c.validate(); err != nil {
return Conf{}, err
}
return c, nil
}
func applyInterfaceField(c *Conf, k, val string) {
switch k {
case "privatekey", "clientprivkey", "clientprivatekey":
c.PrivateKey = val
case "address", "addr", "clientip":
c.Address = appendCSV(c.Address, val)
case "dns":
c.DNS = appendCSV(c.DNS, val)
case "mtu":
c.MTU, _ = strconv.Atoi(val)
case "jc":
c.Jc, _ = strconv.Atoi(val)
case "jmin":
c.Jmin, _ = strconv.Atoi(val)
case "jmax":
c.Jmax, _ = strconv.Atoi(val)
case "s1":
c.S1, _ = strconv.Atoi(val)
case "s2":
c.S2, _ = strconv.Atoi(val)
case "s3":
c.S3, _ = strconv.Atoi(val)
case "s4":
c.S4, _ = strconv.Atoi(val)
case "h1":
c.H1 = val
case "h2":
c.H2 = val
case "h3":
c.H3 = val
case "h4":
c.H4 = val
case "i1":
c.I1 = val
case "i2":
c.I2 = val
case "i3":
c.I3 = val
case "i4":
c.I4 = val
case "i5":
c.I5 = val
}
}
func applyPeerField(c *Conf, k, val string) {
switch k {
case "publickey", "serverpubkey", "serverpublickey", "peerpublickey", "peerkey":
c.PublicKey = val
case "presharedkey", "psk", "pskkey":
c.PresharedKey = val
case "endpoint", "host", "hostname":
c.Endpoint = val
case "allowedips", "allowedip":
c.AllowedIPs = appendCSV(c.AllowedIPs, val)
case "persistentkeepalive", "keepalive":
c.Keepalive, _ = strconv.Atoi(val)
}
}
func parseURI(raw string) (Conf, error) {
// Formats:
// awg://host:port/?private_key=...&public_key=...&address=10.8.0.2/32
// awg://PRIVATE_KEY@host:port/?public_key=...&address=...
// awg://host:port/#name (query keys)
u := raw
fragment := ""
if i := strings.Index(u, "#"); i >= 0 {
fragment = u[i+1:]
u = u[:i]
}
schemeIdx := strings.Index(u, "://")
if schemeIdx < 0 {
return Conf{}, fmt.Errorf("bad awg uri")
}
rest := u[schemeIdx+3:]
hostPort := rest
query := ""
if i := strings.Index(rest, "?"); i >= 0 {
hostPort = rest[:i]
query = rest[i+1:]
}
hostPort = strings.TrimSuffix(hostPort, "/")
c := Conf{}
if frag, err := urlDecode(fragment); err == nil && frag != "" {
c.Name = frag
}
// userinfo = private key (WireGuard share-link style)
if at := strings.LastIndex(hostPort, "@"); at >= 0 {
pk, _ := urlDecode(hostPort[:at])
c.PrivateKey = unwrapQuotes(pk)
hostPort = hostPort[at+1:]
}
if hostPort != "" {
c.Endpoint = hostPort
}
for _, part := range strings.Split(query, "&") {
if part == "" {
continue
}
k, v, ok := strings.Cut(part, "=")
if !ok {
continue
}
k = normalizeKey(k)
v = unwrapQuotes(strings.TrimSpace(v))
v, _ = urlDecode(v)
switch k {
case "privatekey", "clientprivkey", "clientprivatekey":
c.PrivateKey = v
case "publickey", "serverpubkey", "serverpublickey", "peerpublickey", "peerkey":
c.PublicKey = v
case "presharedkey", "psk", "pskkey":
c.PresharedKey = v
case "address", "addr", "clientip":
c.Address = appendCSV(c.Address, v)
case "dns":
c.DNS = appendCSV(c.DNS, v)
case "allowedips", "allowedip":
c.AllowedIPs = appendCSV(c.AllowedIPs, v)
case "mtu":
c.MTU, _ = strconv.Atoi(v)
case "keepalive", "persistentkeepalive":
c.Keepalive, _ = strconv.Atoi(v)
case "jc":
c.Jc, _ = strconv.Atoi(v)
case "jmin":
c.Jmin, _ = strconv.Atoi(v)
case "jmax":
c.Jmax, _ = strconv.Atoi(v)
case "s1":
c.S1, _ = strconv.Atoi(v)
case "s2":
c.S2, _ = strconv.Atoi(v)
case "s3":
c.S3, _ = strconv.Atoi(v)
case "s4":
c.S4, _ = strconv.Atoi(v)
case "h1":
c.H1 = v
case "h2":
c.H2 = v
case "h3":
c.H3 = v
case "h4":
c.H4 = v
case "i1":
c.I1 = v
case "i2":
c.I2 = v
case "i3":
c.I3 = v
case "i4":
c.I4 = v
case "i5":
c.I5 = v
case "name", "remark":
c.Name = v
case "endpoint", "host", "hostname":
c.Endpoint = v
case "port":
if c.Endpoint != "" && !strings.Contains(c.Endpoint, ":") {
c.Endpoint = c.Endpoint + ":" + v
}
}
}
if err := c.validate(); err != nil {
return Conf{}, err
}
return c, nil
}
func parseVPNScheme(raw string) (Conf, error) {
payload := strings.TrimSpace(raw[len("vpn://"):])
payload = strings.TrimSuffix(payload, "/")
decoded, err := decodeMaybeBase64(payload)
if err != nil {
return Conf{}, fmt.Errorf("awg vpn://: %w", err)
}
if c, err := parseAmneziaJSON(decoded); err == nil {
return c, nil
}
// nested: sometimes JSON has last_config string with INI or JSON
var wrap map[string]any
if err := json.Unmarshal([]byte(decoded), &wrap); err == nil {
for _, key := range []string{"last_config", "config", "cfg", "awg_config", "wireguard"} {
if s, ok := wrap[key].(string); ok && strings.TrimSpace(s) != "" {
if c, err := Parse(s); err == nil {
return c, nil
}
}
}
}
return Parse(decoded)
}
func parseAmneziaJSON(raw string) (Conf, error) {
raw = strings.TrimSpace(raw)
var m map[string]any
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return Conf{}, err
}
// Flatten nested last_config JSON string.
if lc, ok := m["last_config"].(string); ok && strings.TrimSpace(lc) != "" {
var nested map[string]any
if json.Unmarshal([]byte(lc), &nested) == nil {
for k, v := range nested {
if _, exists := m[k]; !exists {
m[k] = v
}
}
} else if c, err := Parse(lc); err == nil {
return c, nil
}
}
str := func(keys ...string) string {
for _, k := range keys {
for mk, mv := range m {
if normalizeKey(mk) != normalizeKey(k) {
continue
}
switch t := mv.(type) {
case string:
return unwrapQuotes(strings.TrimSpace(t))
case float64:
return strconv.FormatInt(int64(t), 10)
case json.Number:
return t.String()
}
}
}
return ""
}
atoi := func(keys ...string) int {
s := str(keys...)
n, _ := strconv.Atoi(s)
return n
}
c := Conf{
PrivateKey: str("client_priv_key", "privateKey", "private_key", "PrivateKey"),
PublicKey: str("server_pub_key", "publicKey", "public_key", "PublicKey", "peer_public_key"),
PresharedKey: str("psk_key", "presharedKey", "preshared_key", "PresharedKey"),
Jc: atoi("Jc", "jc"),
Jmin: atoi("Jmin", "jmin"),
Jmax: atoi("Jmax", "jmax"),
S1: atoi("S1", "s1"),
S2: atoi("S2", "s2"),
S3: atoi("S3", "s3"),
S4: atoi("S4", "s4"),
H1: str("H1", "h1"),
H2: str("H2", "h2"),
H3: str("H3", "h3"),
H4: str("H4", "h4"),
I1: str("I1", "i1"),
I2: str("I2", "i2"),
I3: str("I3", "i3"),
I4: str("I4", "i4"),
I5: str("I5", "i5"),
MTU: atoi("mtu", "MTU"),
Keepalive: atoi("persistent_keep_alive", "PersistentKeepalive", "keepalive"),
Name: str("name", "description", "hostName"),
}
if addr := str("client_ip", "address", "Address"); addr != "" {
c.Address = appendCSV(nil, addr)
}
if dns := str("dns1", "dns", "DNS"); dns != "" {
c.DNS = appendCSV(nil, dns)
}
if dns2 := str("dns2"); dns2 != "" {
c.DNS = appendCSV(c.DNS, dns2)
}
host := str("hostName", "host", "endpoint_host", "server")
port := str("port", "endpoint_port")
endpoint := str("endpoint", "Endpoint")
switch {
case endpoint != "":
c.Endpoint = endpoint
case host != "" && port != "":
c.Endpoint = net.JoinHostPort(trimBrackets(host), port)
case host != "":
c.Endpoint = host
}
if allowed := str("allowed_ips", "AllowedIPs"); allowed != "" {
c.AllowedIPs = appendCSV(nil, allowed)
}
if err := c.validate(); err != nil {
return Conf{}, err
}
return c, nil
}
func (c *Conf) validate() error {
c.PrivateKey = unwrapQuotes(strings.TrimSpace(c.PrivateKey))
c.PublicKey = unwrapQuotes(strings.TrimSpace(c.PublicKey))
c.PresharedKey = unwrapQuotes(strings.TrimSpace(c.PresharedKey))
c.Endpoint = strings.TrimSpace(c.Endpoint)
if c.PrivateKey == "" {
return fmt.Errorf("awg: нет PrivateKey (приватный ключ клиента)")
}
if c.PublicKey == "" {
return fmt.Errorf("awg: нет PublicKey (публичный ключ сервера)")
}
if c.Endpoint == "" {
return fmt.Errorf("awg: нет Endpoint")
}
if len(c.Address) == 0 {
return fmt.Errorf("awg: нет Address")
}
if len(c.AllowedIPs) == 0 {
c.AllowedIPs = []string{"0.0.0.0/0", "::/0"}
}
return nil
}
// HostPort returns UDP endpoint host and port for ping.
func HostPort(raw string) (host, port string, err error) {
c, err := Parse(raw)
if err != nil {
return "", "", err
}
h, p, err := net.SplitHostPort(c.Endpoint)
if err != nil {
return c.Endpoint, "51820", nil
}
return h, p, nil
}
// lookupIP is overridable in tests.
var lookupIP = defaultLookupIP
func defaultLookupIP(host string) ([]net.IP, error) {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
try := func(r *net.Resolver) ([]net.IP, error) {
addrs, err := r.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
out := make([]net.IP, 0, len(addrs))
for _, a := range addrs {
if a.IP != nil {
out = append(out, a.IP)
}
}
if len(out) == 0 {
return nil, fmt.Errorf("нет IP-адресов")
}
return out, nil
}
// PreferGo avoids flaky Windows WinDNS "No such host" for some A/AAAA shapes.
if ips, err := try(&net.Resolver{PreferGo: true, Dial: resolverDial}); err == nil {
return ips, nil
}
if ips, err := try(net.DefaultResolver); err == nil {
return ips, nil
}
// Last resort: classic LookupIP (system).
return net.LookupIP(host)
}
func resolverDial(ctx context.Context, network, _ string) (net.Conn, error) {
d := net.Dialer{Timeout: 3 * time.Second}
var last error
for _, dns := range []string{"1.1.1.1:53", "8.8.8.8:53", "9.9.9.9:53"} {
c, err := d.DialContext(ctx, network, dns)
if err == nil {
return c, nil
}
last = err
}
return nil, last
}
// ResolveEndpoint turns "hostname:port" into "ip:port" for amneziawg-go IpcSet
// (which fails on bare hostnames with "No such host is known"). Prefers IPv4.
func ResolveEndpoint(endpoint string) (string, error) {
return resolveEndpointWith(endpoint, lookupIP)
}
// ResolveEndpoint rewrites c.Endpoint to a resolved IP:port (IPv4 preferred).
func (c *Conf) ResolveEndpoint() error {
resolved, err := ResolveEndpoint(c.Endpoint)
if err != nil {
return err
}
c.Endpoint = resolved
return nil
}
func resolveEndpointWith(endpoint string, lookup func(string) ([]net.IP, error)) (string, error) {
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
return "", fmt.Errorf("awg: нет Endpoint")
}
host, port, err := net.SplitHostPort(endpoint)
if err != nil {
host = trimBrackets(endpoint)
port = "51820"
} else {
host = trimBrackets(host)
}
if host == "" {
return "", fmt.Errorf("awg: нет Endpoint")
}
if ip := net.ParseIP(host); ip != nil {
return net.JoinHostPort(ip.String(), port), nil
}
ips, err := lookup(host)
if err != nil {
return "", fmt.Errorf("awg: не удалось разрешить DNS для Endpoint %q: %v", host, err)
}
var v4, v6 net.IP
for _, ip := range ips {
if ip == nil {
continue
}
if ip4 := ip.To4(); ip4 != nil {
v4 = ip4
break
}
if v6 == nil {
v6 = ip
}
}
chosen := v4
if chosen == nil {
chosen = v6
}
if chosen == nil {
return "", fmt.Errorf("awg: не удалось разрешить DNS для Endpoint %q: нет IP-адресов", host)
}
return net.JoinHostPort(chosen.String(), port), nil
}
// ToIPC builds the amneziawg-go UAPI configuration string.
// Endpoint hostnames are resolved to IP first (IPv4 preferred).
func (c Conf) ToIPC() (string, error) {
priv, err := decodeKey(c.PrivateKey)
if err != nil {
return "", fmt.Errorf("private key: %w", err)
}
pub, err := decodeKey(c.PublicKey)
if err != nil {
return "", fmt.Errorf("public key: %w", err)
}
endpoint, err := ResolveEndpoint(c.Endpoint)
if err != nil {
return "", err
}
var b strings.Builder
b.WriteString("private_key=")
b.WriteString(hex.EncodeToString(priv))
writeInt(&b, "jc", c.Jc)
writeInt(&b, "jmin", c.Jmin)
writeInt(&b, "jmax", c.Jmax)
writeInt(&b, "s1", c.S1)
writeInt(&b, "s2", c.S2)
writeInt(&b, "s3", c.S3)
writeInt(&b, "s4", c.S4)
writeStr(&b, "h1", c.H1)
writeStr(&b, "h2", c.H2)
writeStr(&b, "h3", c.H3)
writeStr(&b, "h4", c.H4)
writeStr(&b, "i1", c.I1)
writeStr(&b, "i2", c.I2)
writeStr(&b, "i3", c.I3)
writeStr(&b, "i4", c.I4)
writeStr(&b, "i5", c.I5)
b.WriteString("\npublic_key=")
b.WriteString(hex.EncodeToString(pub))
b.WriteString("\nendpoint=")
b.WriteString(endpoint)
for _, ip := range c.AllowedIPs {
b.WriteString("\nallowed_ip=")
b.WriteString(strings.TrimSpace(ip))
}
if c.PresharedKey != "" {
psk, err := decodeKey(c.PresharedKey)
if err != nil {
return "", fmt.Errorf("preshared key: %w", err)
}
b.WriteString("\npreshared_key=")
b.WriteString(hex.EncodeToString(psk))
}
if c.Keepalive > 0 {
writeInt(&b, "persistent_keepalive_interval", c.Keepalive)
}
return b.String(), nil
}
// LocalAddrs parses Interface Address values.
func (c Conf) LocalAddrs() ([]netip.Addr, error) {
var out []netip.Addr
for _, a := range c.Address {
a = strings.TrimSpace(a)
if a == "" {
continue
}
if p, err := netip.ParsePrefix(a); err == nil {
out = append(out, p.Addr())
continue
}
addr, err := netip.ParseAddr(a)
if err != nil {
return nil, fmt.Errorf("address %q: %w", a, err)
}
out = append(out, addr)
}
if len(out) == 0 {
return nil, fmt.Errorf("no local addresses")
}
return out, nil
}
// DNSAddrs parses DNS servers (defaults to 1.1.1.1).
func (c Conf) DNSAddrs() []netip.Addr {
var out []netip.Addr
for _, d := range c.DNS {
d = strings.TrimSpace(d)
if d == "" {
continue
}
if addr, err := netip.ParseAddr(d); err == nil {
out = append(out, addr)
}
}
if len(out) == 0 {
out = append(out, netip.MustParseAddr("1.1.1.1"))
}
return out
}
func (c Conf) EffectiveMTU() int {
if c.MTU > 0 {
return c.MTU
}
return 1420
}
func decodeKey(s string) ([]byte, error) {
s = unwrapQuotes(strings.TrimSpace(s))
if s == "" {
return nil, fmt.Errorf("пустой ключ")
}
encodings := []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
}
for _, enc := range encodings {
if b, err := enc.DecodeString(s); err == nil && len(b) == 32 {
return b, nil
}
}
// pad std / url
pad := func(in string) string {
if m := len(in) % 4; m != 0 {
return in + strings.Repeat("=", 4-m)
}
return in
}
for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.URLEncoding} {
if b, err := enc.DecodeString(pad(s)); err == nil && len(b) == 32 {
return b, nil
}
}
if b, err := hex.DecodeString(s); err == nil && len(b) == 32 {
return b, nil
}
return nil, fmt.Errorf("ожидался ключ WireGuard (32 байта base64/hex)")
}
func writeInt(b *strings.Builder, key string, v int) {
if v == 0 {
return
}
b.WriteByte('\n')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(strconv.Itoa(v))
}
func writeStr(b *strings.Builder, key, v string) {
v = strings.TrimSpace(v)
if v == "" {
return
}
b.WriteByte('\n')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(v)
}
func splitKV(line string) (key, val string, ok bool) {
i := strings.IndexAny(line, "=:")
if i < 0 {
return "", "", false
}
return strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]), true
}
func appendCSV(dst []string, raw string) []string {
for _, p := range strings.Split(raw, ",") {
p = strings.TrimSpace(p)
if p != "" {
dst = append(dst, p)
}
}
return dst
}
func normalizeKey(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
for _, r := range s {
if r == '_' || r == '-' || r == ' ' || r == '\t' {
continue
}
b.WriteRune(r)
}
return b.String()
}
func unwrapQuotes(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 strings.TrimSpace(s[1 : len(s)-1])
}
}
return s
}
func trimBrackets(host string) string {
host = strings.TrimSpace(host)
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
return host[1 : len(host)-1]
}
return host
}
func decodeMaybeBase64(s string) (string, error) {
s = strings.TrimSpace(s)
encodings := []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
}
pad := func(in string) string {
if m := len(in) % 4; m != 0 {
return in + strings.Repeat("=", 4-m)
}
return in
}
for _, enc := range encodings {
if b, err := enc.DecodeString(pad(s)); err == nil && len(b) > 0 {
return string(b), nil
}
}
return "", fmt.Errorf("не base64")
}
func urlDecode(s string) (string, error) {
var b strings.Builder
for i := 0; i < len(s); {
switch s[i] {
case '+':
b.WriteByte(' ')
i++
case '%':
if i+2 >= len(s) {
return s, nil
}
var v byte
for _, c := range []byte{s[i+1], s[i+2]} {
v <<= 4
switch {
case c >= '0' && c <= '9':
v |= c - '0'
case c >= 'a' && c <= 'f':
v |= c - 'a' + 10
case c >= 'A' && c <= 'F':
v |= c - 'A' + 10
default:
return s, nil
}
}
b.WriteByte(v)
i += 3
default:
b.WriteByte(s[i])
i++
}
}
return b.String(), nil
}
// NormalizeShareLink stores a canonical conf body (or passes through awg URI).
func NormalizeShareLink(raw string) (normalized string, remark string, err error) {
c, err := Parse(raw)
if err != nil {
return "", "", err
}
if c.Name != "" {
remark = c.Name
} else if h, _, e := net.SplitHostPort(c.Endpoint); e == nil {
remark = h
} else {
remark = c.Endpoint
}
lower := strings.ToLower(strings.TrimSpace(raw))
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") ||
strings.HasPrefix(strings.TrimSpace(raw), "{") {
return serializeINI(c), remark, nil
}
return strings.TrimSpace(raw), remark, nil
}
func serializeINI(c Conf) string {
var b strings.Builder
b.WriteString("[Interface]\n")
b.WriteString("PrivateKey = " + c.PrivateKey + "\n")
if len(c.Address) > 0 {
b.WriteString("Address = " + strings.Join(c.Address, ", ") + "\n")
}
if len(c.DNS) > 0 {
b.WriteString("DNS = " + strings.Join(c.DNS, ", ") + "\n")
}
if c.MTU > 0 {
b.WriteString("MTU = " + strconv.Itoa(c.MTU) + "\n")
}
if c.Jc != 0 {
b.WriteString("Jc = " + strconv.Itoa(c.Jc) + "\n")
}
if c.Jmin != 0 {
b.WriteString("Jmin = " + strconv.Itoa(c.Jmin) + "\n")
}
if c.Jmax != 0 {
b.WriteString("Jmax = " + strconv.Itoa(c.Jmax) + "\n")
}
if c.S1 != 0 {
b.WriteString("S1 = " + strconv.Itoa(c.S1) + "\n")
}
if c.S2 != 0 {
b.WriteString("S2 = " + strconv.Itoa(c.S2) + "\n")
}
if c.S3 != 0 {
b.WriteString("S3 = " + strconv.Itoa(c.S3) + "\n")
}
if c.S4 != 0 {
b.WriteString("S4 = " + strconv.Itoa(c.S4) + "\n")
}
if c.H1 != "" {
b.WriteString("H1 = " + c.H1 + "\n")
}
if c.H2 != "" {
b.WriteString("H2 = " + c.H2 + "\n")
}
if c.H3 != "" {
b.WriteString("H3 = " + c.H3 + "\n")
}
if c.H4 != "" {
b.WriteString("H4 = " + c.H4 + "\n")
}
if c.I1 != "" {
b.WriteString("I1 = " + c.I1 + "\n")
}
if c.I2 != "" {
b.WriteString("I2 = " + c.I2 + "\n")
}
if c.I3 != "" {
b.WriteString("I3 = " + c.I3 + "\n")
}
if c.I4 != "" {
b.WriteString("I4 = " + c.I4 + "\n")
}
if c.I5 != "" {
b.WriteString("I5 = " + c.I5 + "\n")
}
b.WriteString("\n[Peer]\n")
b.WriteString("PublicKey = " + c.PublicKey + "\n")
if c.PresharedKey != "" {
b.WriteString("PresharedKey = " + c.PresharedKey + "\n")
}
b.WriteString("Endpoint = " + c.Endpoint + "\n")
if len(c.AllowedIPs) > 0 {
b.WriteString("AllowedIPs = " + strings.Join(c.AllowedIPs, ", ") + "\n")
}
if c.Keepalive > 0 {
b.WriteString("PersistentKeepalive = " + strconv.Itoa(c.Keepalive) + "\n")
}
return b.String()
}