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()
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package awg
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseINI_AWG20(t *testing.T) {
|
||||
raw := `[Interface]
|
||||
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
Address = 10.8.0.2/32
|
||||
DNS = 1.1.1.1
|
||||
Jc = 4
|
||||
Jmin = 40
|
||||
Jmax = 70
|
||||
H1 = 1
|
||||
H2 = 2
|
||||
H3 = 3
|
||||
H4 = 4
|
||||
I1 = <r 128>
|
||||
|
||||
[Peer]
|
||||
PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
|
||||
Endpoint = 203.0.113.10:51820
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
PersistentKeepalive = 25
|
||||
`
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Jc != 4 || c.I1 != "<r 128>" || c.Endpoint != "203.0.113.10:51820" {
|
||||
t.Fatalf("unexpected conf: %+v", c)
|
||||
}
|
||||
if !c.IsAWG20() {
|
||||
t.Fatal("expected AWG2 markers")
|
||||
}
|
||||
ipc, err := c.ToIPC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsAll(ipc, "jc=4", "i1=<r 128>", "endpoint=203.0.113.10:51820") {
|
||||
t.Fatalf("bad ipc: %s", ipc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetect(t *testing.T) {
|
||||
if !Detect("[Interface]\nPrivateKey = x") {
|
||||
t.Fatal("expected detect")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func containsAll(s string, parts ...string) bool {
|
||||
for _, p := range parts {
|
||||
if !stringsContains(s, p) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func stringsContains(s, sub string) bool {
|
||||
return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, sub string) int {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package awg
|
||||
|
||||
import "strings"
|
||||
|
||||
// Detect is also used by linknorm / netcheck.
|
||||
func LooksLike(raw string) bool { return Detect(raw) }
|
||||
|
||||
// IsAWG20 returns true when obfuscation params typical of AWG 1.5/2.0 are present.
|
||||
func (c Conf) IsAWG20() bool {
|
||||
if c.I1 != "" || c.I2 != "" || c.I3 != "" || c.I4 != "" || c.I5 != "" {
|
||||
return true
|
||||
}
|
||||
if c.Jc != 0 || c.Jmin != 0 || c.Jmax != 0 {
|
||||
return true
|
||||
}
|
||||
if c.S1 != 0 || c.S2 != 0 || c.S3 != 0 || c.S4 != 0 {
|
||||
return true
|
||||
}
|
||||
if c.H1 != "" || c.H2 != "" || c.H3 != "" || c.H4 != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StripBOM removes a UTF-8 BOM if present.
|
||||
func StripBOM(s string) string {
|
||||
return strings.TrimPrefix(s, "\ufeff")
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package awg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/amnezia-vpn/amneziawg-go/conn"
|
||||
"github.com/amnezia-vpn/amneziawg-go/device"
|
||||
"github.com/amnezia-vpn/amneziawg-go/tun/netstack"
|
||||
|
||||
"vpnclient/internal/config"
|
||||
)
|
||||
|
||||
// Engine runs AmneziaWG 2.0 in userspace (gvisor netstack) and exposes local SOCKS/HTTP.
|
||||
type Engine struct {
|
||||
mu sync.Mutex
|
||||
dev *device.Device
|
||||
tnet *netstack.Net
|
||||
proxies *proxyServer
|
||||
profile config.Profile
|
||||
stderr io.Writer
|
||||
running bool
|
||||
}
|
||||
|
||||
func New(stderr io.Writer) *Engine {
|
||||
if stderr == nil {
|
||||
stderr = os.Stderr
|
||||
}
|
||||
return &Engine{stderr: stderr}
|
||||
}
|
||||
|
||||
func (e *Engine) Protocol() config.Protocol { return config.ProtocolAWG }
|
||||
|
||||
func (e *Engine) Start(ctx context.Context, profile config.Profile, _ string) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.running {
|
||||
return fmt.Errorf("awg: already running")
|
||||
}
|
||||
|
||||
cfg, err := Parse(profile.Proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipc, err := cfg.ToIPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addrs, err := cfg.LocalAddrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunDev, tnet, err := netstack.CreateNetTUN(addrs, cfg.DNSAddrs(), cfg.EffectiveMTU())
|
||||
if err != nil {
|
||||
return fmt.Errorf("awg netstack: %w", err)
|
||||
}
|
||||
logger := &device.Logger{
|
||||
Verbosef: func(format string, args ...any) {},
|
||||
Errorf: func(format string, args ...any) {
|
||||
fmt.Fprintf(e.stderr, "awg: "+format+"\n", args...)
|
||||
},
|
||||
}
|
||||
dev := device.NewDevice(tunDev, conn.NewDefaultBind(), logger)
|
||||
if err := dev.IpcSet(ipc); err != nil {
|
||||
dev.Close()
|
||||
return fmt.Errorf("awg configure: %w", err)
|
||||
}
|
||||
if err := dev.Up(); err != nil {
|
||||
dev.Close()
|
||||
return fmt.Errorf("awg up: %w", err)
|
||||
}
|
||||
|
||||
socksHP, _ := profile.SOCKSListenHostPort()
|
||||
httpHP, _ := profile.HTTPListenHostPort()
|
||||
if socksHP == "" {
|
||||
socksHP = "127.0.0.1:1080"
|
||||
}
|
||||
if httpHP == "" {
|
||||
httpHP = "127.0.0.1:1081"
|
||||
}
|
||||
|
||||
proxies, err := startProxies(socksHP, httpHP, tnet.DialContext)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
e.dev = dev
|
||||
e.tnet = tnet
|
||||
e.proxies = proxies
|
||||
e.profile = profile
|
||||
e.running = true
|
||||
|
||||
// Brief handshake grace; don't fail hard if peer is slow.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = e.stopLocked()
|
||||
return ctx.Err()
|
||||
case <-time.After(400 * time.Millisecond):
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Stop() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.stopLocked()
|
||||
}
|
||||
|
||||
func (e *Engine) stopLocked() error {
|
||||
if !e.running {
|
||||
return nil
|
||||
}
|
||||
if e.proxies != nil {
|
||||
_ = e.proxies.Close()
|
||||
e.proxies = nil
|
||||
}
|
||||
if e.dev != nil {
|
||||
e.dev.Close()
|
||||
e.dev = nil
|
||||
}
|
||||
e.tnet = nil
|
||||
e.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Running() bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.running
|
||||
}
|
||||
|
||||
func (e *Engine) LocalHTTPProxy() (string, bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if !e.running {
|
||||
return "", false
|
||||
}
|
||||
if e.proxies != nil && e.proxies.httpAddr != "" {
|
||||
return e.proxies.httpAddr, true
|
||||
}
|
||||
return e.profile.HTTPListenHostPort()
|
||||
}
|
||||
|
||||
func (e *Engine) LocalSOCKSProxy() (string, bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if !e.running {
|
||||
return "", false
|
||||
}
|
||||
if e.proxies != nil && e.proxies.socksAddr != "" {
|
||||
return e.proxies.socksAddr, true
|
||||
}
|
||||
return e.profile.SOCKSListenHostPort()
|
||||
}
|
||||
|
||||
// EnsureBinary is a no-op: AWG is embedded via amneziawg-go.
|
||||
func EnsureBinary(_ string) (string, error) {
|
||||
return "embedded-amneziawg-go", nil
|
||||
}
|
||||
|
||||
// ResolveBinary reports the embedded core.
|
||||
func ResolveBinary(_ string) (string, error) {
|
||||
return "embedded-amneziawg-go", nil
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package awg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DialFunc dials TCP through the AWG userspace stack.
|
||||
type DialFunc func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
type proxyServer struct {
|
||||
mu sync.Mutex
|
||||
socksLn net.Listener
|
||||
httpLn net.Listener
|
||||
dial DialFunc
|
||||
wg sync.WaitGroup
|
||||
closed bool
|
||||
socksAddr string
|
||||
httpAddr string
|
||||
}
|
||||
|
||||
func startProxies(socksHostPort, httpHostPort string, dial DialFunc) (*proxyServer, error) {
|
||||
s := &proxyServer{dial: dial}
|
||||
if socksHostPort != "" {
|
||||
ln, err := net.Listen("tcp", socksHostPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("awg socks listen %s: %w", socksHostPort, err)
|
||||
}
|
||||
s.socksLn = ln
|
||||
s.socksAddr = ln.Addr().String()
|
||||
s.wg.Add(1)
|
||||
go s.serveSOCKS()
|
||||
}
|
||||
if httpHostPort != "" {
|
||||
ln, err := net.Listen("tcp", httpHostPort)
|
||||
if err != nil {
|
||||
_ = s.Close()
|
||||
return nil, fmt.Errorf("awg http listen %s: %w", httpHostPort, err)
|
||||
}
|
||||
s.httpLn = ln
|
||||
s.httpAddr = ln.Addr().String()
|
||||
s.wg.Add(1)
|
||||
go s.serveHTTP()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *proxyServer) Close() error {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
s.closed = true
|
||||
if s.socksLn != nil {
|
||||
_ = s.socksLn.Close()
|
||||
}
|
||||
if s.httpLn != nil {
|
||||
_ = s.httpLn.Close()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *proxyServer) serveSOCKS() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
c, err := s.socksLn.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.wg.Add(1)
|
||||
go func(conn net.Conn) {
|
||||
defer s.wg.Done()
|
||||
_ = handleSOCKS(conn, s.dial)
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *proxyServer) serveHTTP() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
c, err := s.httpLn.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.wg.Add(1)
|
||||
go func(conn net.Conn) {
|
||||
defer s.wg.Done()
|
||||
_ = handleHTTPProxy(conn, s.dial)
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSOCKS(client net.Conn, dial DialFunc) error {
|
||||
defer client.Close()
|
||||
_ = client.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
buf := make([]byte, 258)
|
||||
if _, err := io.ReadFull(client, buf[:2]); err != nil {
|
||||
return err
|
||||
}
|
||||
if buf[0] != 0x05 {
|
||||
return fmt.Errorf("socks: not v5")
|
||||
}
|
||||
nmethods := int(buf[1])
|
||||
if _, err := io.ReadFull(client, buf[:nmethods]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := client.Write([]byte{0x05, 0x00}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.ReadFull(client, buf[:4]); err != nil {
|
||||
return err
|
||||
}
|
||||
if buf[0] != 0x05 || buf[1] != 0x01 {
|
||||
_, _ = client.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
|
||||
return fmt.Errorf("socks: only CONNECT supported")
|
||||
}
|
||||
var host string
|
||||
switch buf[3] {
|
||||
case 0x01: // IPv4
|
||||
if _, err := io.ReadFull(client, buf[:4]); err != nil {
|
||||
return err
|
||||
}
|
||||
host = net.IP(buf[:4]).String()
|
||||
case 0x03: // domain
|
||||
if _, err := io.ReadFull(client, buf[:1]); err != nil {
|
||||
return err
|
||||
}
|
||||
l := int(buf[0])
|
||||
if _, err := io.ReadFull(client, buf[:l]); err != nil {
|
||||
return err
|
||||
}
|
||||
host = string(buf[:l])
|
||||
case 0x04: // IPv6
|
||||
if _, err := io.ReadFull(client, buf[:16]); err != nil {
|
||||
return err
|
||||
}
|
||||
host = net.IP(buf[:16]).String()
|
||||
default:
|
||||
_, _ = client.Write([]byte{0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
|
||||
return fmt.Errorf("socks: bad atyp")
|
||||
}
|
||||
if _, err := io.ReadFull(client, buf[:2]); err != nil {
|
||||
return err
|
||||
}
|
||||
port := binary.BigEndian.Uint16(buf[:2])
|
||||
_ = client.SetDeadline(time.Time{})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
remote, err := dial(ctx, "tcp", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
|
||||
if err != nil {
|
||||
_, _ = client.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
|
||||
return err
|
||||
}
|
||||
defer remote.Close()
|
||||
if _, err := client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
return relay(client, remote)
|
||||
}
|
||||
|
||||
func handleHTTPProxy(client net.Conn, dial DialFunc) error {
|
||||
defer client.Close()
|
||||
_ = client.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
br := bufio.NewReader(client)
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = client.SetDeadline(time.Time{})
|
||||
|
||||
if strings.EqualFold(req.Method, http.MethodConnect) {
|
||||
host := req.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host += ":443"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
remote, err := dial(ctx, "tcp", host)
|
||||
if err != nil {
|
||||
_, _ = client.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
|
||||
return err
|
||||
}
|
||||
defer remote.Close()
|
||||
if _, err := client.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
return relay(client, remote)
|
||||
}
|
||||
|
||||
// Absolute-form HTTP proxy request.
|
||||
u := req.URL
|
||||
host := u.Host
|
||||
if host == "" {
|
||||
host = req.Host
|
||||
}
|
||||
if host == "" {
|
||||
_, _ = client.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||
return fmt.Errorf("http proxy: no host")
|
||||
}
|
||||
if !strings.Contains(host, ":") {
|
||||
host += ":80"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
remote, err := dial(ctx, "tcp", host)
|
||||
if err != nil {
|
||||
_, _ = client.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
|
||||
return err
|
||||
}
|
||||
defer remote.Close()
|
||||
req.RequestURI = ""
|
||||
if err := req.Write(remote); err != nil {
|
||||
return err
|
||||
}
|
||||
return relay(client, remote)
|
||||
}
|
||||
|
||||
func relay(a, b net.Conn) error {
|
||||
errc := make(chan error, 2)
|
||||
go func() {
|
||||
_, err := io.Copy(a, b)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(b, a)
|
||||
errc <- err
|
||||
}()
|
||||
err := <-errc
|
||||
_ = a.Close()
|
||||
_ = b.Close()
|
||||
<-errc
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user