mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
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 <noreply@anthropic.com> --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -221,7 +221,7 @@ func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult
|
||||
// dial neither proves reachability nor measures latency. Such outbounds
|
||||
// must go through the real xray handshake probe instead.
|
||||
func outboundTransportIsUDP(ob map[string]any) bool {
|
||||
if protocol, _ := ob["protocol"].(string); protocol == "hysteria" || protocol == "wireguard" {
|
||||
if protocol, _ := ob["protocol"].(string); protocol == "hysteria" || protocol == "wireguard" || protocol == "amneziawg" {
|
||||
return true
|
||||
}
|
||||
if stream, ok := ob["streamSettings"].(map[string]any); ok {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
@@ -384,6 +385,33 @@ func buildBatchTestConfig(items []*httpBatchItem, allOutbounds []any, ports []in
|
||||
outbounds = append(outbounds, it.outbound)
|
||||
}
|
||||
}
|
||||
// Bridge amneziawg entries like GetXrayConfig does -- one raw entry fails
|
||||
// the whole temp config; drop unbridgeable ones, not unrelated items.
|
||||
bridged := make([]any, 0, len(outbounds))
|
||||
for _, ob := range outbounds {
|
||||
m, ok := ob.(map[string]any)
|
||||
if !ok {
|
||||
bridged = append(bridged, ob)
|
||||
continue
|
||||
}
|
||||
if p, _ := m["protocol"].(string); p != "amneziawg" {
|
||||
bridged = append(bridged, ob)
|
||||
continue
|
||||
}
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
repl, ok := amneziawgnet.BuildSocksBridge(raw)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var replacement any
|
||||
if json.Unmarshal(repl, &replacement) == nil {
|
||||
bridged = append(bridged, replacement)
|
||||
}
|
||||
}
|
||||
outbounds = bridged
|
||||
for _, ob := range outbounds {
|
||||
outbound, ok := ob.(map[string]any)
|
||||
if !ok {
|
||||
|
||||
@@ -557,6 +557,33 @@ func TestTestOutboundsTCPModeForcesUDPToHTTPProbe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestOutboundsTCPModeForcesAmneziaWGToHTTPProbe(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
withStubProcess(t, func(cfg *xray.Config, configPath string) batchProcess {
|
||||
return &stubProcess{cfg: cfg, serveSocks: true}
|
||||
})
|
||||
withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
|
||||
return &TestEgressResult{IPv4: "198.51.100.2", Country: "ZZ", Warp: "off"}
|
||||
})
|
||||
|
||||
batch := mustJSON(t, []any{map[string]any{"tag": "awg", "protocol": "amneziawg"}})
|
||||
results, err := (&OutboundService{}).TestOutbounds(batch, srv.URL, "", "tcp")
|
||||
if err != nil {
|
||||
t.Fatalf("TestOutbounds: %v", err)
|
||||
}
|
||||
r := results[0]
|
||||
if !r.Success || r.Mode != "http" {
|
||||
t.Errorf("amneziawg outbound in tcp mode = %+v, want success with mode %q", r, "http")
|
||||
}
|
||||
if r.Egress == nil || r.Egress.IPv4 != "198.51.100.2" {
|
||||
t.Errorf("amneziawg outbound egress = %+v", r.Egress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeModeLabel(t *testing.T) {
|
||||
cases := []struct{ mode, want string }{
|
||||
{"tcp", "tcp"},
|
||||
|
||||
Reference in New Issue
Block a user