Release 2.6.0: fix AmneziaWG key parsing for awg://, JSON and vpn://.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -725,7 +725,7 @@
|
||||
<div class="brand-wrap">
|
||||
<div class="brand-row">
|
||||
<h1 class="brand">Navis</h1>
|
||||
<span class="badge-ver">2.2.1</span>
|
||||
<span class="badge-ver">2.6</span>
|
||||
</div>
|
||||
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
||||
</div>
|
||||
@@ -756,7 +756,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<label class="auto-best">
|
||||
<input id="autoBest" type="checkbox" checked />
|
||||
<input id="autoBest" type="checkbox" />
|
||||
Авто: пинг + подключение к лучшему
|
||||
</label>
|
||||
<div class="server-list" id="serverList"></div>
|
||||
@@ -923,8 +923,7 @@
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem(AUTO_BEST_KEY);
|
||||
if (saved === "0") autoBest.checked = false;
|
||||
if (saved === "1") autoBest.checked = true;
|
||||
autoBest.checked = saved === "1";
|
||||
} catch (_) {}
|
||||
autoBest.addEventListener("change", () => {
|
||||
try { localStorage.setItem(AUTO_BEST_KEY, autoBest.checked ? "1" : "0"); } catch (_) {}
|
||||
|
||||
@@ -15,9 +15,11 @@ func DetectProtocol(raw string) Protocol {
|
||||
case strings.HasPrefix(lower, "trojan://"):
|
||||
return ProtocolTrojan
|
||||
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
|
||||
strings.HasPrefix(lower, "wireguard://"):
|
||||
strings.HasPrefix(lower, "wireguard://"), strings.HasPrefix(lower, "vpn://"):
|
||||
return ProtocolAWG
|
||||
case strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey"):
|
||||
case strings.Contains(lower, "[interface]") && (strings.Contains(lower, "privatekey") || strings.Contains(lower, "private_key") || strings.Contains(lower, "private key")):
|
||||
return ProtocolAWG
|
||||
case strings.HasPrefix(strings.TrimSpace(raw), "{") && (strings.Contains(lower, "client_priv_key") || strings.Contains(lower, "server_pub_key")):
|
||||
return ProtocolAWG
|
||||
case strings.HasPrefix(lower, "naive+"), strings.HasPrefix(lower, "naive://"),
|
||||
strings.HasPrefix(lower, "https://"), strings.HasPrefix(lower, "quic://"),
|
||||
|
||||
+345
-92
@@ -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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package awg
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseINI_AWG20(t *testing.T) {
|
||||
raw := `[Interface]
|
||||
@@ -41,6 +44,80 @@ PersistentKeepalive = 25
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseURI_UserInfoPrivateKey(t *testing.T) {
|
||||
priv := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
pub := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE="
|
||||
raw := "awg://" + priv + "@203.0.113.10:51820/?public_key=" + pub + "&address=10.8.0.2/32&jc=4"
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.PrivateKey != priv {
|
||||
t.Fatalf("private key: %q", c.PrivateKey)
|
||||
}
|
||||
if c.PublicKey != pub {
|
||||
t.Fatalf("public key: %q", c.PublicKey)
|
||||
}
|
||||
if c.Endpoint != "203.0.113.10:51820" {
|
||||
t.Fatalf("endpoint: %q", c.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseINI_SpacedKeysAndQuotes(t *testing.T) {
|
||||
raw := `Private Key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
Address = 10.8.0.2/32
|
||||
[Peer]
|
||||
Public Key = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE='
|
||||
Endpoint = 1.2.3.4:51820
|
||||
`
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.PrivateKey == "" || c.PublicKey == "" {
|
||||
t.Fatalf("keys missing: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAmneziaJSON(t *testing.T) {
|
||||
raw := `{
|
||||
"client_priv_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||
"server_pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=",
|
||||
"client_ip": "10.8.0.2/32",
|
||||
"hostName": "203.0.113.10",
|
||||
"port": "51820",
|
||||
"Jc": 4,
|
||||
"Jmin": 10,
|
||||
"Jmax": 50,
|
||||
"H1": "1",
|
||||
"H2": "2",
|
||||
"H3": "3",
|
||||
"H4": "4"
|
||||
}`
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Endpoint != "203.0.113.10:51820" {
|
||||
t.Fatalf("endpoint %q", c.Endpoint)
|
||||
}
|
||||
if c.Jc != 4 {
|
||||
t.Fatalf("jc %d", c.Jc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVPNScheme(t *testing.T) {
|
||||
inner := `{"client_priv_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","server_pub_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=","client_ip":"10.8.0.2/32","hostName":"9.9.9.9","port":"51820"}`
|
||||
raw := "vpn://" + base64.StdEncoding.EncodeToString([]byte(inner))
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Endpoint != "9.9.9.9:51820" {
|
||||
t.Fatalf("endpoint %q", c.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetect(t *testing.T) {
|
||||
if !Detect("[Interface]\nPrivateKey = x") {
|
||||
t.Fatal("expected detect")
|
||||
@@ -48,6 +125,18 @@ func TestDetect(t *testing.T) {
|
||||
if !Detect("awg://1.2.3.4:51820/?private_key=a&public_key=b&address=10.0.0.2/32") {
|
||||
t.Fatal("expected awg uri detect")
|
||||
}
|
||||
if !Detect(`{"client_priv_key":"x","server_pub_key":"y"}`) {
|
||||
t.Fatal("expected json detect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeKeyURLSafe(t *testing.T) {
|
||||
// 32 zero bytes, URL-safe without padding
|
||||
raw := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
|
||||
b, err := decodeKey(raw)
|
||||
if err != nil || len(b) != 32 {
|
||||
t.Fatalf("url-safe: %v %d", err, len(b))
|
||||
}
|
||||
}
|
||||
|
||||
func containsAll(s string, parts ...string) bool {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
// CurrentVersion is the shipped client version.
|
||||
const CurrentVersion = "2.2.1"
|
||||
const CurrentVersion = "2.6.0"
|
||||
|
||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||
|
||||
Reference in New Issue
Block a user