From d5ab84e8d503785d7f00c43a68c6ab08ef41b10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:20:48 +0330 Subject: [PATCH] feat(amneziawg): add AmneziaWG as an outbound protocol (#6320) * feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code --------- Co-authored-by: rqzbeh Co-authored-by: Claude Code Co-authored-by: Sanaei --- docs/architecture.md | 27 + frontend/.storybook/preview-head.html | 3 + frontend/src/hooks/useXraySetting.ts | 9 +- .../src/lib/xray/outbound-form-adapter.ts | 106 +++ .../xray/outbounds/OutboundFormModal.tsx | 2 + .../xray/outbounds/protocols/amneziawg.tsx | 228 ++++++ .../pages/xray/outbounds/protocols/index.ts | 1 + frontend/src/schemas/forms/outbound-form.ts | 7 + .../schemas/primitives/outbound-protocol.ts | 1 + .../schemas/protocols/outbound/amneziawg.ts | 49 ++ .../src/schemas/protocols/outbound/index.ts | 3 + .../test/amneziawg-outbound-adapter.test.ts | 95 +++ internal/amneziawg/outbound.go | 323 ++++++++ internal/amneziawg/outbound_test.go | 312 ++++++++ internal/amneziawgnet/client_device.go | 139 ++++ internal/amneziawgnet/client_device_test.go | 117 +++ internal/amneziawgnet/device.go | 12 +- internal/amneziawgnet/dns.go | 216 +++++ internal/amneziawgnet/egress.go | 652 +++++++++++++++ internal/amneziawgnet/egress_domain_test.go | 741 ++++++++++++++++++ internal/amneziawgnet/outbound_manager.go | 194 +++++ .../amneziawgnet/outbound_manager_test.go | 145 ++++ internal/amneziawgnet/resolving_bind.go | 67 ++ internal/amneziawgnet/resolving_bind_test.go | 70 ++ internal/amneziawgnet/socks_bridge.go | 33 + internal/amneziawgnet/socks_bridge_test.go | 52 ++ internal/web/job/amneziawg_job.go | 78 +- internal/web/service/outbound/outbound.go | 2 +- internal/web/service/outbound/probe_http.go | 28 + .../web/service/outbound/probe_http_test.go | 27 + internal/web/service/port_conflict.go | 12 + internal/web/service/port_conflict_test.go | 25 +- internal/web/service/xray.go | 5 + .../web/service/xray_amneziawg_outbound.go | 51 ++ .../service/xray_amneziawg_outbound_test.go | 289 +++++++ internal/web/service/xray_setting.go | 15 + internal/web/translation/ar-EG.json | 5 +- internal/web/translation/en-US.json | 5 +- internal/web/translation/es-ES.json | 5 +- internal/web/translation/fa-IR.json | 5 +- internal/web/translation/id-ID.json | 5 +- internal/web/translation/ja-JP.json | 5 +- internal/web/translation/pt-BR.json | 5 +- internal/web/translation/ru-RU.json | 5 +- internal/web/translation/tr-TR.json | 5 +- internal/web/translation/uk-UA.json | 5 +- internal/web/translation/vi-VN.json | 5 +- internal/web/translation/zh-CN.json | 5 +- internal/web/translation/zh-TW.json | 5 +- internal/web/web.go | 1 + 50 files changed, 4169 insertions(+), 33 deletions(-) create mode 100644 frontend/src/pages/xray/outbounds/protocols/amneziawg.tsx create mode 100644 frontend/src/schemas/protocols/outbound/amneziawg.ts create mode 100644 frontend/src/test/amneziawg-outbound-adapter.test.ts create mode 100644 internal/amneziawg/outbound.go create mode 100644 internal/amneziawg/outbound_test.go create mode 100644 internal/amneziawgnet/client_device.go create mode 100644 internal/amneziawgnet/client_device_test.go create mode 100644 internal/amneziawgnet/dns.go create mode 100644 internal/amneziawgnet/egress.go create mode 100644 internal/amneziawgnet/egress_domain_test.go create mode 100644 internal/amneziawgnet/outbound_manager.go create mode 100644 internal/amneziawgnet/outbound_manager_test.go create mode 100644 internal/amneziawgnet/resolving_bind.go create mode 100644 internal/amneziawgnet/resolving_bind_test.go create mode 100644 internal/amneziawgnet/socks_bridge.go create mode 100644 internal/amneziawgnet/socks_bridge_test.go create mode 100644 internal/web/service/xray_amneziawg_outbound.go create mode 100644 internal/web/service/xray_amneziawg_outbound_test.go diff --git a/docs/architecture.md b/docs/architecture.md index 231b7f79b..6808aad87 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -600,3 +600,30 @@ only - it changes no code), `claude-issue-analyst.yml` (issue triage). - **Tests live next to code** (`foo.go` ↔ `foo_test.go`), plus golden snapshots in `frontend/src/test/golden/fixtures/` for config generation — update fixtures intentionally, not blindly, when output changes. + +## AmneziaWG outbound pseudo-protocol + +The template stores `protocol: "amneziawg"` rows verbatim; Xray-core has no +such proxy. At config generation (`GetXrayConfig` and the outbound latency +probe's batch config) each row is swapped by `amneziawgnet.BuildSocksBridge` +into a loopback socks outbound pointed at the panel's egress server (port +`EgressBasePort`), authenticating with the row's tag as username. Sibling keys +(`mux`, `sendThrough`, `targetStrategy`, `streamSettings.sockopt`) survive the +swap. The embedded amneziawg-go client device lives in the panel process; an +unbridgeable entry (unreadable settings, empty/non-string tag) fails config +generation instead of skipping, because a skipped entry leaves +`protocol: "amneziawg"` behind -- which makes Xray refuse the whole config. + +Traffic flow: Xray socks client -> egress SOCKS5 server (tag = username) -> +per-tag device netstack -> amneziawg-go tunnel. Domain targets are resolved by +a DNS exchange through that same netstack (`resolveTunnelVia`, default server +`DefaultTunnelDNSServer`), so names never leak to the panel host's resolver and +answers are valid at the tunnel's location; results cache for 60s. UDP flows +key sessions on the resolved address:port. Peer endpoints may be hostnames: +`resolvingBind.ParseEndpoint` resolves once at configure time (kernel +`wg setconf` semantics); a hostname whose DNS dies later needs a template +re-save or job restart to re-resolve. + +`randomTrailers` defaults to false wherever the panel does not control the +peer (outbound form/schema): a receiver without 3.1 trailers silently drops +oversized packets from a sender with it enabled. diff --git a/frontend/.storybook/preview-head.html b/frontend/.storybook/preview-head.html index 3cfb32a6a..095ce5391 100644 --- a/frontend/.storybook/preview-head.html +++ b/frontend/.storybook/preview-head.html @@ -1,4 +1,7 @@