Ctrl K
ComunidadGuías…

awg-shim/main.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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
}