|
|
|
@@ -3,6 +3,7 @@ package awg
|
|
|
|
|
import (
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"net/netip"
|
|
|
|
@@ -12,22 +13,22 @@ import (
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
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
|
|
|
|
|
PublicKey string
|
|
|
|
|
PresharedKey string
|
|
|
|
|
Endpoint string
|
|
|
|
|
AllowedIPs []string
|
|
|
|
|
Keepalive int
|
|
|
|
|
Name string
|
|
|
|
|
Endpoint string
|
|
|
|
|
AllowedIPs []string
|
|
|
|
|
Keepalive int
|
|
|
|
|
Name string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect reports whether raw looks like an AWG/WireGuard config or share link.
|
|
|
|
@@ -38,26 +39,43 @@ func Detect(raw string) bool {
|
|
|
|
|
}
|
|
|
|
|
lower := strings.ToLower(raw)
|
|
|
|
|
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
|
|
|
|
|
strings.HasPrefix(lower, "wireguard://") {
|
|
|
|
|
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey") {
|
|
|
|
|
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 or awg:// URI and returns Conf.
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
@@ -65,78 +83,27 @@ func Parse(raw string) (Conf, error) {
|
|
|
|
|
|
|
|
|
|
func parseINI(raw string) (Conf, error) {
|
|
|
|
|
var c Conf
|
|
|
|
|
section := ""
|
|
|
|
|
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 = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
|
|
|
|
|
section = normalizeKey(line[1 : len(line)-1])
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
key, val, ok := splitKV(line)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
k := strings.ToLower(key)
|
|
|
|
|
k := normalizeKey(key)
|
|
|
|
|
val = unwrapQuotes(val)
|
|
|
|
|
switch section {
|
|
|
|
|
case "interface":
|
|
|
|
|
switch k {
|
|
|
|
|
case "privatekey":
|
|
|
|
|
c.PrivateKey = val
|
|
|
|
|
case "address":
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
case "interface", "":
|
|
|
|
|
applyInterfaceField(&c, k, val)
|
|
|
|
|
case "peer":
|
|
|
|
|
switch k {
|
|
|
|
|
case "publickey":
|
|
|
|
|
c.PublicKey = val
|
|
|
|
|
case "presharedkey":
|
|
|
|
|
c.PresharedKey = val
|
|
|
|
|
case "endpoint":
|
|
|
|
|
c.Endpoint = val
|
|
|
|
|
case "allowedips":
|
|
|
|
|
c.AllowedIPs = appendCSV(c.AllowedIPs, val)
|
|
|
|
|
case "persistentkeepalive":
|
|
|
|
|
c.Keepalive, _ = strconv.Atoi(val)
|
|
|
|
|
}
|
|
|
|
|
applyPeerField(&c, k, val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := c.validate(); err != nil {
|
|
|
|
@@ -145,9 +112,77 @@ func parseINI(raw string) (Conf, error) {
|
|
|
|
|
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) {
|
|
|
|
|
// awg://host:port/?public_key=...&private_key=...&address=10.8.0.2/32&jc=4&...
|
|
|
|
|
// 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")
|
|
|
|
@@ -160,7 +195,22 @@ func parseURI(raw string) (Conf, error) {
|
|
|
|
|
query = rest[i+1:]
|
|
|
|
|
}
|
|
|
|
|
hostPort = strings.TrimSuffix(hostPort, "/")
|
|
|
|
|
c := Conf{Endpoint: 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
|
|
|
|
@@ -169,21 +219,21 @@ func parseURI(raw string) (Conf, error) {
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
k = strings.ToLower(strings.TrimSpace(k))
|
|
|
|
|
v = strings.TrimSpace(v)
|
|
|
|
|
k = normalizeKey(k)
|
|
|
|
|
v = unwrapQuotes(strings.TrimSpace(v))
|
|
|
|
|
v, _ = urlDecode(v)
|
|
|
|
|
switch k {
|
|
|
|
|
case "private_key", "privatekey":
|
|
|
|
|
case "privatekey", "clientprivkey", "clientprivatekey":
|
|
|
|
|
c.PrivateKey = v
|
|
|
|
|
case "public_key", "publickey":
|
|
|
|
|
case "publickey", "serverpubkey", "serverpublickey", "peerpublickey", "peerkey":
|
|
|
|
|
c.PublicKey = v
|
|
|
|
|
case "preshared_key", "presharedkey", "psk":
|
|
|
|
|
case "presharedkey", "psk", "pskkey":
|
|
|
|
|
c.PresharedKey = v
|
|
|
|
|
case "address", "addr":
|
|
|
|
|
case "address", "addr", "clientip":
|
|
|
|
|
c.Address = appendCSV(c.Address, v)
|
|
|
|
|
case "dns":
|
|
|
|
|
c.DNS = appendCSV(c.DNS, v)
|
|
|
|
|
case "allowed_ips", "allowedips":
|
|
|
|
|
case "allowedips", "allowedip":
|
|
|
|
|
c.AllowedIPs = appendCSV(c.AllowedIPs, v)
|
|
|
|
|
case "mtu":
|
|
|
|
|
c.MTU, _ = strconv.Atoi(v)
|
|
|
|
@@ -223,8 +273,12 @@ func parseURI(raw string) (Conf, error) {
|
|
|
|
|
c.I5 = v
|
|
|
|
|
case "name", "remark":
|
|
|
|
|
c.Name = v
|
|
|
|
|
case "endpoint":
|
|
|
|
|
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 {
|
|
|
|
@@ -233,12 +287,137 @@ func parseURI(raw string) (Conf, error) {
|
|
|
|
|
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")
|
|
|
|
|
return fmt.Errorf("awg: нет PrivateKey (приватный ключ клиента)")
|
|
|
|
|
}
|
|
|
|
|
if c.PublicKey == "" {
|
|
|
|
|
return fmt.Errorf("awg: нет PublicKey")
|
|
|
|
|
return fmt.Errorf("awg: нет PublicKey (публичный ключ сервера)")
|
|
|
|
|
}
|
|
|
|
|
if c.Endpoint == "" {
|
|
|
|
|
return fmt.Errorf("awg: нет Endpoint")
|
|
|
|
@@ -260,7 +439,6 @@ func HostPort(raw string) (host, port string, err error) {
|
|
|
|
|
}
|
|
|
|
|
h, p, err := net.SplitHostPort(c.Endpoint)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// bare host without port
|
|
|
|
|
return c.Endpoint, "51820", nil
|
|
|
|
|
}
|
|
|
|
|
return h, p, nil
|
|
|
|
@@ -368,14 +546,37 @@ func (c Conf) EffectiveMTU() int {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeKey(s string) ([]byte, error) {
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
if b, err := base64.StdEncoding.DecodeString(s); err == nil && len(b) == 32 {
|
|
|
|
|
return b, nil
|
|
|
|
|
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("expected 32-byte base64/hex key")
|
|
|
|
|
return nil, fmt.Errorf("ожидался ключ WireGuard (32 байта base64/hex)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeInt(b *strings.Builder, key string, v int) {
|
|
|
|
@@ -417,6 +618,58 @@ func appendCSV(dst []string, raw string) []string {
|
|
|
|
|
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); {
|
|
|
|
@@ -465,10 +718,10 @@ func NormalizeShareLink(raw string) (normalized string, remark string, err error
|
|
|
|
|
} else {
|
|
|
|
|
remark = c.Endpoint
|
|
|
|
|
}
|
|
|
|
|
// Prefer keeping original INI when pasted; re-serialize only for URI imports.
|
|
|
|
|
lower := strings.ToLower(strings.TrimSpace(raw))
|
|
|
|
|
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
|
|
|
|
|
strings.HasPrefix(lower, "wireguard://") {
|
|
|
|
|
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") ||
|
|
|
|
|
strings.HasPrefix(strings.TrimSpace(raw), "{") {
|
|
|
|
|
return serializeINI(c), remark, nil
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(raw), remark, nil
|
|
|
|
|