awg-shim/main.go
Rama main · réplica de lectura del repositorio original.
// awg-shim: userspace AmneziaWG tunnel exposed as a local SOCKS5 proxy
// (TCP CONNECT + UDP ASSOCIATE). No TUN device, no elevated privileges.
//
// UniGate runs this next to sing-box: sing-box does all routing (TUN or
// mixed inbound) and forwards the "via VPN" traffic to this proxy, which
// speaks AmneziaWG to the server through gVisor netstack.
//
// The desktop command lives in cmd/awg-shim. This package is also bound into
// an Android AAR with gomobile, so both platforms use exactly the same parser,
// AWG 3.1 engine and SOCKS implementation.
package awgshim
import (
"encoding/base64"
"encoding/hex"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"sync"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/device"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
)
// Protector is implemented by Android's VpnService. The AmneziaWG transport
// sockets must be excluded from the system VPN, otherwise they loop back into
// sing-box. Desktop callers pass nil and use their normal direct-route rule.
type Protector interface {
Protect(fd int) bool
}
type engine struct {
device *device.Device
listener net.Listener
}
var active struct {
sync.Mutex
engine *engine
}
// Start launches the userspace AmneziaWG engine and its local SOCKS5 server.
// It returns the actual listen address after the engine is ready.
func Start(configText, listen string, protector Protector) (string, error) {
Stop()
if strings.TrimSpace(listen) == "" {
listen = "127.0.0.1:2081"
}
cfg, err := parseConf(configText)
if err != nil {
return "", fmt.Errorf("parse conf: %w", err)
}
tunDev, tnet, err := netstack.CreateNetTUN(cfg.addresses, cfg.dns, cfg.mtu)
if err != nil {
return "", fmt.Errorf("netstack: %w", err)
}
bind := conn.NewDefaultBind()
if protector != nil {
bind = &protectedBind{Bind: bind, protector: protector}
}
dev := device.NewDevice(tunDev, bind, device.NewLogger(device.LogLevelError, "awg "))
if err := dev.IpcSet(cfg.uapi); err != nil {
dev.Close()
return "", fmt.Errorf("device config: %w", err)
}
if err := dev.Up(); err != nil {
dev.Close()
return "", fmt.Errorf("device up: %w", err)
}
srv := &socksServer{tnet: tnet}
ln, err := net.Listen("tcp", listen)
if err != nil {
dev.Close()
return "", fmt.Errorf("listen %s: %w", listen, err)
}
instance := &engine{device: dev, listener: ln}
active.Lock()
active.engine = instance
active.Unlock()
go func() { _ = srv.serve(ln) }()
return ln.Addr().String(), nil
}
// Stop shuts down both SOCKS and the AWG device. It is safe to call repeatedly.
func Stop() {
active.Lock()
instance := active.engine
active.engine = nil
active.Unlock()
if instance != nil {
_ = instance.listener.Close()
instance.device.Close()
}
}
type protectedBind struct {
conn.Bind
protector Protector
}
func (b *protectedBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) {
receivers, actualPort, err := b.Bind.Open(port)
if err != nil {
return nil, 0, err
}
peek, ok := b.Bind.(conn.PeekLookAtSocketFd)
if !ok {
_ = b.Bind.Close()
return nil, 0, fmt.Errorf("AmneziaWG bind does not expose socket descriptors")
}
protected := false
for _, getFD := range []func() (int, error){peek.PeekLookAtSocketFd4, peek.PeekLookAtSocketFd6} {
fd, fdErr := getFD()
if fdErr != nil {
continue
}
if !b.protector.Protect(fd) {
_ = b.Bind.Close()
return nil, 0, fmt.Errorf("Android refused to protect AmneziaWG socket %d", fd)
}
protected = true
}
if !protected {
_ = b.Bind.Close()
return nil, 0, fmt.Errorf("AmneziaWG did not open an IPv4 or IPv6 socket")
}
return receivers, actualPort, nil
}
// ---------------------------------------------------------------------------
// wg-quick style conf → netstack params + UAPI config text
// ---------------------------------------------------------------------------
type shimConf struct {
addresses []netip.Addr
dns []netip.Addr
mtu int
uapi string
}
type awgField struct {
conf string
uapi string
}
// AmneziaWG interface fields. Config names are normalized to lower-case by
// parseConf; UAPI uses snake_case for the 3.x parameters.
var awgFields = []awgField{
{"jc", "jc"}, {"jmin", "jmin"}, {"jmax", "jmax"},
{"s1", "s1"}, {"s2", "s2"}, {"s3", "s3"}, {"s4", "s4"},
{"h1", "h1"}, {"h2", "h2"}, {"h3", "h3"}, {"h4", "h4"},
{"i1", "i1"}, {"i2", "i2"}, {"i3", "i3"}, {"i4", "i4"}, {"i5", "i5"},
{"headerprotectionkey", "header_protection_key"},
{"contentpaddingaddition", "content_padding_addition"},
{"rekeyaftertime", "rekey_after_time"},
{"rekeytimeout", "rekey_timeout"},
{"rejectaftertime", "reject_after_time"},
{"keepalivetimeout", "keepalive_timeout"},
{"maxhandshakeattempts", "max_handshake_attempts"},
{"randomtrailers", "random_trailers"},
{"disablecookies", "disable_cookies"},
}
func parseConf(text string) (*shimConf, error) {
c := &shimConf{mtu: 1420}
iface := map[string]string{}
peer := map[string]string{}
var cur map[string]string
for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
switch strings.ToLower(line) {
case "[interface]":
cur = iface
continue
case "[peer]":
cur = peer
continue
}
if cur == nil {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
cur[strings.ToLower(strings.TrimSpace(k))] = strings.TrimSpace(v)
}
// --- netstack parameters ---
for _, a := range splitList(iface["address"]) {
a, _, _ = strings.Cut(a, "/")
addr, err := netip.ParseAddr(a)
if err != nil {
return nil, fmt.Errorf("Address %q: %w", a, err)
}
c.addresses = append(c.addresses, addr)
}
if len(c.addresses) == 0 {
return nil, fmt.Errorf("no Address in [Interface]")
}
for _, d := range splitList(iface["dns"]) {
addr, err := netip.ParseAddr(d)
if err != nil {
continue // e.g. search domains — not our concern
}
c.dns = append(c.dns, addr)
}
if len(c.dns) == 0 {
c.dns = []netip.Addr{netip.MustParseAddr("8.8.8.8")}
}
if m := iface["mtu"]; m != "" {
mtu, err := strconv.Atoi(m)
if err != nil {
return nil, fmt.Errorf("MTU %q: %w", m, err)
}
c.mtu = mtu
}
// --- UAPI: device section first, then peer ---
var b strings.Builder
priv, err := b64ToHex(iface["privatekey"])
if err != nil {
return nil, fmt.Errorf("PrivateKey: %w", err)
}
fmt.Fprintf(&b, "private_key=%s\n", priv)
for _, field := range awgFields {
if v, ok := iface[field.conf]; ok && v != "" {
switch field.conf {
case "headerprotectionkey":
v, err = awgKeyToHex(v)
if err != nil {
return nil, fmt.Errorf("HeaderProtectionKey: %w", err)
}
case "randomtrailers", "disablecookies":
v, err = awgBool(v)
if err != nil {
return nil, fmt.Errorf("%s: %w", field.conf, err)
}
}
fmt.Fprintf(&b, "%s=%s\n", field.uapi, v)
}
}
pub, err := b64ToHex(peer["publickey"])
if err != nil {
return nil, fmt.Errorf("PublicKey: %w", err)
}
fmt.Fprintf(&b, "public_key=%s\n", pub)
if psk := peer["presharedkey"]; psk != "" {
h, err := b64ToHex(psk)
if err != nil {
return nil, fmt.Errorf("PresharedKey: %w", err)
}
fmt.Fprintf(&b, "preshared_key=%s\n", h)
}
ep, err := resolveEndpoint(peer["endpoint"])
if err != nil {
return nil, fmt.Errorf("Endpoint: %w", err)
}
fmt.Fprintf(&b, "endpoint=%s\n", ep)
if ka := peer["persistentkeepalive"]; ka != "" {
if strings.EqualFold(ka, "off") {
ka = "0"
}
fmt.Fprintf(&b, "persistent_keepalive_interval=%s\n", ka)
}
allowed := splitList(peer["allowedips"])
if len(allowed) == 0 {
allowed = []string{"0.0.0.0/0", "::/0"}
}
for _, a := range allowed {
fmt.Fprintf(&b, "allowed_ip=%s\n", a)
}
c.uapi = b.String()
return c, nil
}
func splitList(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func b64ToHex(s string) (string, error) {
if s == "" {
return "", fmt.Errorf("missing")
}
raw, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
if len(raw) != 32 {
return "", fmt.Errorf("bad key length %d", len(raw))
}
return hex.EncodeToString(raw), nil
}
func awgKeyToHex(s string) (string, error) {
s = strings.TrimSpace(s)
if len(s) == 64 {
if raw, err := hex.DecodeString(s); err == nil && len(raw) == 32 {
return strings.ToLower(s), nil
}
}
return b64ToHex(s)
}
func awgBool(s string) (string, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "on", "true", "1":
return "true", nil
case "off", "false", "0":
return "false", nil
default:
return "", fmt.Errorf("expected on/off, true/false or 1/0")
}
}
// UAPI wants ip:port; the conf may carry a hostname — resolve it with the
// system resolver (the shim's own UDP goes out via the physical interface;
// sing-box routing excludes the shim process / endpoint from the tunnel).
func resolveEndpoint(ep string) (string, error) {
if ep == "" {
return "", fmt.Errorf("missing")
}
host, port, err := net.SplitHostPort(ep)
if err != nil {
return "", err
}
if ip := net.ParseIP(host); ip != nil {
return ep, nil
}
ips, err := net.LookupIP(host)
if err != nil {
return "", err
}
var pick net.IP
for _, ip := range ips {
if v4 := ip.To4(); v4 != nil {
pick = v4
break
}
if pick == nil {
pick = ip
}
}
if pick == nil {
return "", fmt.Errorf("no addresses for %s", host)
}
return net.JoinHostPort(pick.String(), port), nil
}