805 lines
19 KiB
Go
805 lines
19 KiB
Go
package awg
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// 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 = strings.TrimSpace(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 = strings.TrimSpace(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 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
|
|
}
|
|
|
|
// ToIPC builds the amneziawg-go UAPI configuration string.
|
|
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)
|
|
}
|
|
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(c.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()
|
|
}
|