Release 1.8.0: auto-update with restart and AmneziaWG 2.0 support.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
package awg
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"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://") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse accepts a WireGuard/AWG .conf body or awg:// URI and returns Conf.
|
||||
func Parse(raw string) (Conf, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return Conf{}, fmt.Errorf("пустой AWG конфиг")
|
||||
}
|
||||
lower := strings.ToLower(raw)
|
||||
switch {
|
||||
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
|
||||
strings.HasPrefix(lower, "wireguard://"):
|
||||
return parseURI(raw)
|
||||
default:
|
||||
return parseINI(raw)
|
||||
}
|
||||
}
|
||||
|
||||
func parseINI(raw string) (Conf, error) {
|
||||
var c Conf
|
||||
section := ""
|
||||
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]))
|
||||
continue
|
||||
}
|
||||
key, val, ok := splitKV(line)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
k := strings.ToLower(key)
|
||||
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 "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)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
return Conf{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func parseURI(raw string) (Conf, error) {
|
||||
// awg://host:port/?public_key=...&private_key=...&address=10.8.0.2/32&jc=4&...
|
||||
u := raw
|
||||
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{Endpoint: hostPort}
|
||||
for _, part := range strings.Split(query, "&") {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(part, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
k = strings.ToLower(strings.TrimSpace(k))
|
||||
v = strings.TrimSpace(v)
|
||||
v, _ = urlDecode(v)
|
||||
switch k {
|
||||
case "private_key", "privatekey":
|
||||
c.PrivateKey = v
|
||||
case "public_key", "publickey":
|
||||
c.PublicKey = v
|
||||
case "preshared_key", "presharedkey", "psk":
|
||||
c.PresharedKey = v
|
||||
case "address", "addr":
|
||||
c.Address = appendCSV(c.Address, v)
|
||||
case "dns":
|
||||
c.DNS = appendCSV(c.DNS, v)
|
||||
case "allowed_ips", "allowedips":
|
||||
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":
|
||||
c.Endpoint = v
|
||||
}
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
return Conf{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conf) validate() error {
|
||||
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 {
|
||||
// bare host without port
|
||||
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 = strings.TrimSpace(s)
|
||||
if b, err := base64.StdEncoding.DecodeString(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")
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
// 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://") {
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user