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 } // amneziawg-go IpcSet does not resolve hostnames reliably (Windows WSAHOST_NOT_FOUND). if err := cfg.ResolveEndpoint(); 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 } // Close device first so tun/netstack dials fail and relays exit promptly. if e.dev != nil { e.dev.Close() e.dev = nil } e.tnet = nil if e.proxies != nil { _ = e.proxies.Close() e.proxies = 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 }