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. // Prefers a light Endpoint=/URI extract; falls back to full Parse for JSON/vpn:// blobs. func HostPort(raw string) (host, port string, err error) { if h, p, ok := lightEndpoint(raw); ok { return h, p, nil } 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 } func lightEndpoint(raw string) (host, port string, ok bool) { raw = sanitizeShare(raw) if raw == "" { return "", "", false } lower := strings.ToLower(raw) switch { case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"), strings.HasPrefix(lower, "wireguard://"): // URI host:port only — no full config decode. rest := raw if i := strings.Index(rest, "://"); i >= 0 { rest = rest[i+3:] } if at := strings.Index(rest, "@"); at >= 0 { rest = rest[at+1:] } if i := strings.IndexAny(rest, "/?#"); i >= 0 { rest = rest[:i] } rest = strings.TrimSpace(rest) if rest == "" { return "", "", false } h, p, err := net.SplitHostPort(rest) if err != nil { return rest, "51820", true } return h, p, true } if strings.Contains(lower, "endpoint") { for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { continue } low := strings.ToLower(line) if !strings.HasPrefix(low, "endpoint") { continue } _, val, cutOK := strings.Cut(line, "=") if !cutOK { _, val, cutOK = strings.Cut(line, ":") } if !cutOK { continue } val = strings.TrimSpace(val) if val == "" { continue } h, p, err := net.SplitHostPort(val) if err != nil { return val, "51820", true } return h, p, true } } return "", "", false } // 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() }