mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 18:07:14 +00:00
feat(amneziawg): replace the TPROXY bridge with a SOCKS5 relay inbound (Phase 3 start)
Hard cutover, part 1: injectAmneziawgnetSocks replaces injectAmneziawgEgress as the AmneziaWG-side Xray config injector. Every enabled AmneziaWG inbound now gets an always-on loopback SOCKS5 inbound (built by amneziawgnet.SocksInboundSettings) instead of an opt-in dokodemo-door TPROXY bridge -- there's no RouteThroughXray gate anymore since the embedded path has no alternative datapath once traffic is decapsulated in gVisor. Reuses the real inbound's own tag, same as before, so per-inbound stats totals keep matching. internal/amneziawgnet gains SOCKSPortForInbound (deterministic port derivation, its own range distinct from the kernel-module bridge's) and SocksPassword (a process-wide, lazily-generated, not-persisted password -- this traffic never leaves loopback). port_conflict.go's port-reservation check is updated to match: the new SOCKS5 relay port is reserved unconditionally for every qualifying AmneziaWG inbound, not gated on RouteThroughXray. Not yet done (tracked in the migration plan): swapping the actual manager call sites (cron job, immediate-apply CRUD, shutdown) from the kernel-module Manager to amneziawgnet's, and deleting the now-dead TPROXY/awg-quick code. This commit could not be locally verified beyond internal/amneziawgnet itself (this machine has no C compiler, so internal/database and anything that imports it -- including internal/web/service -- can't be built or vetted here); pushing for real CI feedback before continuing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package amneziawgnet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SOCKSBasePort is the first loopback port used for an AmneziaWG inbound's
|
||||
// own Xray SOCKS5 relay inbound (see relay.go/SocksInboundSettings). Its own
|
||||
// range, distinct from amneziawg.EgressBasePort (63100, the kernel-module
|
||||
// path's TPROXY bridge port) so the two can never collide even if both
|
||||
// happen to be reachable during a transition.
|
||||
const SOCKSBasePort = 65100
|
||||
|
||||
// SOCKSPortForInbound returns the loopback port of one AmneziaWG inbound's
|
||||
// own Xray SOCKS5 relay inbound, derived deterministically from its id so
|
||||
// the config-generation code (which builds the inbound) and the relay code
|
||||
// (which dials it) never have to agree on a runtime-negotiated value --
|
||||
// mirrors amneziawg.EgressPortForInbound's own reasoning exactly.
|
||||
func SOCKSPortForInbound(inboundID int) int {
|
||||
return SOCKSBasePort + inboundID
|
||||
}
|
||||
|
||||
var (
|
||||
socksPasswordOnce sync.Once
|
||||
socksPassword string
|
||||
)
|
||||
|
||||
// SocksPassword returns the process-wide password used to authenticate into
|
||||
// every AmneziaWG SOCKS5 relay inbound, generating and caching it once
|
||||
// (lazily, on first use) rather than persisting it anywhere: this traffic
|
||||
// never leaves loopback, both the config generator (SocksInboundSettings'
|
||||
// caller) and the relay dialer (SocksRelay/UDPRelay) live in this same
|
||||
// process, and Xray's own generated config is already rebuilt from scratch
|
||||
// on every reconcile -- there is nothing for a stored value to survive
|
||||
// across that a fresh one wouldn't equally satisfy. Not a real secret (see
|
||||
// SocksRelay's own doc comment); this only needs to be unpredictable enough
|
||||
// that nothing outside this process could plausibly guess it and dial in
|
||||
// over loopback.
|
||||
func SocksPassword() string {
|
||||
socksPasswordOnce.Do(func() {
|
||||
var b [24]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
// crypto/rand failing is effectively unrecoverable for a
|
||||
// process that generates real WireGuard keys elsewhere too;
|
||||
// a fixed fallback keeps this from panicking outright.
|
||||
socksPassword = fmt.Sprintf("amneziawgnet-fallback-%x", b)
|
||||
return
|
||||
}
|
||||
socksPassword = base64.RawURLEncoding.EncodeToString(b[:])
|
||||
})
|
||||
return socksPassword
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
@@ -177,15 +178,15 @@ func (s *InboundService) checkPortConflict(inbound *model.Inbound, ignoreId int)
|
||||
}
|
||||
|
||||
// Every enabled local AmneziaWG inbound gets its own automatic Xray
|
||||
// bridge (see injectAmneziawgEgress) on 127.0.0.1 at a port derived
|
||||
// purely from its id (amneziawg.EgressPortForInbound) -- like the
|
||||
// internal Xray API inbound above, that bridge is not itself a database
|
||||
// row, so the ordinary DB-backed query below can never see it. Without
|
||||
// this check, an unrelated inbound saved onto that exact port silently
|
||||
// fails at the next Xray start, taking every other protocol down with
|
||||
// it, not just AmneziaWG.
|
||||
// SOCKS5 relay inbound (see injectAmneziawgnetSocks) on 127.0.0.1 at a
|
||||
// port derived purely from its id (amneziawgnet.SOCKSPortForInbound) --
|
||||
// like the internal Xray API inbound above, that relay inbound is not
|
||||
// itself a database row, so the ordinary DB-backed query below can never
|
||||
// see it. Without this check, an unrelated inbound saved onto that exact
|
||||
// port silently fails at the next Xray start, taking every other
|
||||
// protocol down with it, not just AmneziaWG.
|
||||
if inbound.NodeID == nil && listenOverlaps("127.0.0.1", inbound.Listen) {
|
||||
conflict, err := s.checkAmneziawgEgressConflict(inbound, ignoreId, newBits)
|
||||
conflict, err := s.checkAmneziawgnetSocksConflict(inbound, ignoreId, newBits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -229,15 +230,16 @@ func (s *InboundService) checkPortConflict(inbound *model.Inbound, ignoreId int)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// checkAmneziawgEgressConflict reports whether inbound's own port collides
|
||||
// with an existing, enabled local AmneziaWG inbound's automatic Xray bridge
|
||||
// port. Only inbounds that actually have RouteThroughXray on ever get a
|
||||
// bridge (see injectAmneziawgEgress); the others' "reserved" port isn't
|
||||
// really reserved, so they must not be flagged. ignoreId excludes one
|
||||
// inbound id from the AmneziaWG candidates, the same way the general
|
||||
// DB-backed conflict query above excludes the inbound being edited from
|
||||
// matching itself.
|
||||
func (s *InboundService) checkAmneziawgEgressConflict(inbound *model.Inbound, ignoreId int, newBits transportBits) (*portConflictDetail, error) {
|
||||
// checkAmneziawgnetSocksConflict reports whether inbound's own port
|
||||
// collides with an existing, enabled local AmneziaWG inbound's automatic
|
||||
// Xray SOCKS5 relay port. Unlike the retired kernel-module bridge this
|
||||
// checks every qualifying AmneziaWG inbound unconditionally: the embedded
|
||||
// relay has no RouteThroughXray-style opt-in, every one of them gets a
|
||||
// relay inbound (see injectAmneziawgnetSocks). ignoreId excludes one inbound
|
||||
// id from the AmneziaWG candidates, the same way the general DB-backed
|
||||
// conflict query above excludes the inbound being edited from matching
|
||||
// itself.
|
||||
func (s *InboundService) checkAmneziawgnetSocksConflict(inbound *model.Inbound, ignoreId int, newBits transportBits) (*portConflictDetail, error) {
|
||||
db := database.GetDB()
|
||||
var candidates []*model.Inbound
|
||||
q := db.Model(model.Inbound{}).Where("protocol = ? AND enable = ? AND node_id IS NULL", model.AmneziaWG, true)
|
||||
@@ -248,11 +250,10 @@ func (s *InboundService) checkAmneziawgEgressConflict(inbound *model.Inbound, ig
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range candidates {
|
||||
inst, ok := amneziawg.InstanceFromInbound(c)
|
||||
if !ok || !inst.RouteThroughXray {
|
||||
if _, ok := amneziawg.InstanceFromInbound(c); !ok {
|
||||
continue
|
||||
}
|
||||
if amneziawg.EgressPortForInbound(c.Id) != inbound.Port {
|
||||
if amneziawgnet.SOCKSPortForInbound(c.Id) != inbound.Port {
|
||||
continue
|
||||
}
|
||||
return &portConflictDetail{
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/op/go-logging"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
@@ -732,16 +732,18 @@ func TestCheckPortConflict_ReservedAPIPortUDPCoexists(t *testing.T) {
|
||||
}
|
||||
|
||||
// amneziawgRoutedSettings builds a minimal but complete AmneziaWG settings
|
||||
// blob with one qualifying, enabled peer and RouteThroughXray on -- the
|
||||
// shape that actually makes injectAmneziawgEgress (and therefore
|
||||
// checkAmneziawgEgressConflict) create a bridge at all.
|
||||
// blob with one qualifying, enabled peer -- the shape that makes
|
||||
// injectAmneziawgnetSocks (and therefore checkAmneziawgnetSocksConflict)
|
||||
// create a relay inbound at all. The routeThroughXray field is kept in the
|
||||
// JSON (a stale value from a pre-cutover install) specifically to prove
|
||||
// it's now ignored -- see the "RouteThroughXrayOff" test below.
|
||||
const amneziawgRoutedSettings = `{"server":{"privateKey":"priv","publicKey":"pub","subnetIp":"10.8.1.0","subnetCidr":24,"routeThroughXray":true},"clients":[{"email":"a@x","enable":true,"publicKey":"pub-a","allowedIPs":["10.8.1.2/32"]}]}`
|
||||
|
||||
// An enabled AmneziaWG inbound's automatic Xray bridge (injectAmneziawgEgress)
|
||||
// is a synthetic loopback dokodemo-door inbound, not a database row, so
|
||||
// checkPortConflict needs its own check to catch a collision -- exactly the
|
||||
// same shape of problem as the reserved API port above.
|
||||
func TestCheckPortConflict_AmneziawgEgressBridgeBlockedLocal(t *testing.T) {
|
||||
// An enabled AmneziaWG inbound's automatic Xray SOCKS5 relay inbound
|
||||
// (injectAmneziawgnetSocks) is a synthetic loopback inbound, not a database
|
||||
// row, so checkPortConflict needs its own check to catch a collision --
|
||||
// exactly the same shape of problem as the reserved API port above.
|
||||
func TestCheckPortConflict_AmneziawgnetSocksRelayBlockedLocal(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
|
||||
|
||||
@@ -749,13 +751,13 @@ func TestCheckPortConflict_AmneziawgEgressBridgeBlockedLocal(t *testing.T) {
|
||||
if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
|
||||
t.Fatalf("read seeded row: %v", err)
|
||||
}
|
||||
bridgePort := amneziawg.EgressPortForInbound(awgInbound.Id)
|
||||
relayPort := amneziawgnet.SOCKSPortForInbound(awgInbound.Id)
|
||||
|
||||
svc := &InboundService{}
|
||||
candidate := &model.Inbound{
|
||||
Tag: "vless-bridge",
|
||||
Listen: "0.0.0.0",
|
||||
Port: bridgePort,
|
||||
Port: relayPort,
|
||||
Protocol: model.VLESS,
|
||||
}
|
||||
got, err := svc.checkPortConflict(candidate, 0)
|
||||
@@ -763,7 +765,7 @@ func TestCheckPortConflict_AmneziawgEgressBridgeBlockedLocal(t *testing.T) {
|
||||
t.Fatalf("checkPortConflict: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatalf("a local inbound on the AmneziaWG bridge port %d must conflict", bridgePort)
|
||||
t.Fatalf("a local inbound on the AmneziaWG relay port %d must conflict", relayPort)
|
||||
}
|
||||
if msg := got.String(); !strings.Contains(msg, "awg-1") {
|
||||
t.Fatalf("conflict message should name the owning AmneziaWG inbound; got %q", msg)
|
||||
@@ -771,9 +773,9 @@ func TestCheckPortConflict_AmneziawgEgressBridgeBlockedLocal(t *testing.T) {
|
||||
}
|
||||
|
||||
// Nodes run their own Xray, so a node inbound landing on the central panel's
|
||||
// AmneziaWG bridge port must be allowed -- the bridge only ever binds
|
||||
// AmneziaWG relay port must be allowed -- the relay inbound only ever binds
|
||||
// 127.0.0.1 on the local panel's own Xray.
|
||||
func TestCheckPortConflict_AmneziawgEgressBridgeAllowedOnNode(t *testing.T) {
|
||||
func TestCheckPortConflict_AmneziawgnetSocksRelayAllowedOnNode(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
|
||||
|
||||
@@ -781,37 +783,37 @@ func TestCheckPortConflict_AmneziawgEgressBridgeAllowedOnNode(t *testing.T) {
|
||||
if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
|
||||
t.Fatalf("read seeded row: %v", err)
|
||||
}
|
||||
bridgePort := amneziawg.EgressPortForInbound(awgInbound.Id)
|
||||
relayPort := amneziawgnet.SOCKSPortForInbound(awgInbound.Id)
|
||||
|
||||
svc := &InboundService{}
|
||||
candidate := &model.Inbound{
|
||||
Tag: "node-bridge",
|
||||
Listen: "0.0.0.0",
|
||||
Port: bridgePort,
|
||||
Port: relayPort,
|
||||
Protocol: model.VLESS,
|
||||
NodeID: new(1),
|
||||
}
|
||||
if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil {
|
||||
t.Fatalf("a node inbound on the local AmneziaWG bridge port must be allowed; got=%v err=%v", got, err)
|
||||
t.Fatalf("a node inbound on the local AmneziaWG relay port must be allowed; got=%v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A disabled AmneziaWG inbound never gets a bridge injected
|
||||
// (injectAmneziawgEgress skips !inbound.Enable), so its "reserved" port must
|
||||
// not block anything.
|
||||
func TestCheckPortConflict_AmneziawgEgressBridgeIgnoredWhenDisabled(t *testing.T) {
|
||||
// A disabled AmneziaWG inbound never gets a relay inbound injected
|
||||
// (injectAmneziawgnetSocks skips !inbound.Enable), so its "reserved" port
|
||||
// must not block anything.
|
||||
func TestCheckPortConflict_AmneziawgnetSocksRelayIgnoredWhenDisabled(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
awg := &model.Inbound{Tag: "awg-1", Enable: false, Listen: "0.0.0.0", Port: 51820, Protocol: model.AmneziaWG, Settings: `{}`}
|
||||
if err := database.GetDB().Create(awg).Error; err != nil {
|
||||
t.Fatalf("seed disabled awg inbound: %v", err)
|
||||
}
|
||||
bridgePort := amneziawg.EgressPortForInbound(awg.Id)
|
||||
relayPort := amneziawgnet.SOCKSPortForInbound(awg.Id)
|
||||
|
||||
svc := &InboundService{}
|
||||
candidate := &model.Inbound{
|
||||
Tag: "vless-bridge",
|
||||
Listen: "0.0.0.0",
|
||||
Port: bridgePort,
|
||||
Port: relayPort,
|
||||
Protocol: model.VLESS,
|
||||
}
|
||||
if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil {
|
||||
@@ -819,11 +821,41 @@ func TestCheckPortConflict_AmneziawgEgressBridgeIgnoredWhenDisabled(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
// An enabled AmneziaWG inbound with RouteThroughXray off never gets a bridge
|
||||
// injected either (injectAmneziawgEgress requires it), so its port isn't
|
||||
// reserved -- an inbound created with the default settings, not just an
|
||||
// explicitly disabled one, must not block anything.
|
||||
func TestCheckPortConflict_AmneziawgEgressBridgeIgnoredWhenRouteThroughXrayOff(t *testing.T) {
|
||||
// Unlike the retired kernel-module bridge, the embedded relay has no
|
||||
// RouteThroughXray-style opt-in -- every qualifying AmneziaWG inbound
|
||||
// reserves its relay port regardless of that (now-vestigial) field's value,
|
||||
// including a stale routeThroughXray:true left over from a pre-cutover
|
||||
// install (amneziawgRoutedSettings).
|
||||
func TestCheckPortConflict_AmneziawgnetSocksRelayReservedRegardlessOfLegacyRouteThroughXrayField(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{"server":{"privateKey":"priv","publicKey":"pub","subnetIp":"10.8.1.0","subnetCidr":24},"clients":[{"email":"a@x","enable":true,"publicKey":"pub-a","allowedIPs":["10.8.1.2/32"]}]}`)
|
||||
|
||||
var awgInbound model.Inbound
|
||||
if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
|
||||
t.Fatalf("read seeded row: %v", err)
|
||||
}
|
||||
relayPort := amneziawgnet.SOCKSPortForInbound(awgInbound.Id)
|
||||
|
||||
svc := &InboundService{}
|
||||
candidate := &model.Inbound{
|
||||
Tag: "vless-bridge",
|
||||
Listen: "0.0.0.0",
|
||||
Port: relayPort,
|
||||
Protocol: model.VLESS,
|
||||
}
|
||||
got, err := svc.checkPortConflict(candidate, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("checkPortConflict: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatalf("an enabled, qualifying AmneziaWG inbound must reserve its relay port even with RouteThroughXray left at its default")
|
||||
}
|
||||
}
|
||||
|
||||
// A qualifying AmneziaWG inbound with no enabled/valid peer at all never
|
||||
// gets a relay inbound (amneziawg.InstanceFromInbound returns ok=false), so
|
||||
// its port isn't reserved.
|
||||
func TestCheckPortConflict_AmneziawgnetSocksRelayIgnoredWhenNoQualifyingPeer(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`)
|
||||
|
||||
@@ -831,24 +863,24 @@ func TestCheckPortConflict_AmneziawgEgressBridgeIgnoredWhenRouteThroughXrayOff(t
|
||||
if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
|
||||
t.Fatalf("read seeded row: %v", err)
|
||||
}
|
||||
bridgePort := amneziawg.EgressPortForInbound(awgInbound.Id)
|
||||
relayPort := amneziawgnet.SOCKSPortForInbound(awgInbound.Id)
|
||||
|
||||
svc := &InboundService{}
|
||||
candidate := &model.Inbound{
|
||||
Tag: "vless-bridge",
|
||||
Listen: "0.0.0.0",
|
||||
Port: bridgePort,
|
||||
Port: relayPort,
|
||||
Protocol: model.VLESS,
|
||||
}
|
||||
if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil {
|
||||
t.Fatalf("an AmneziaWG inbound with RouteThroughXray off must not reserve its bridge port; got=%v err=%v", got, err)
|
||||
t.Fatalf("an AmneziaWG inbound with no qualifying peer must not reserve its relay port; got=%v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// An unrelated port never conflicts with the bridge.
|
||||
func TestCheckPortConflict_AmneziawgEgressBridgeDifferentPortAllowed(t *testing.T) {
|
||||
// An unrelated port never conflicts with the relay inbound.
|
||||
func TestCheckPortConflict_AmneziawgnetSocksRelayDifferentPortAllowed(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`)
|
||||
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
|
||||
|
||||
svc := &InboundService{}
|
||||
candidate := &model.Inbound{
|
||||
@@ -858,6 +890,6 @@ func TestCheckPortConflict_AmneziawgEgressBridgeDifferentPortAllowed(t *testing.
|
||||
Protocol: model.VLESS,
|
||||
}
|
||||
if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil {
|
||||
t.Fatalf("an unrelated port must not conflict with the AmneziaWG bridge; got=%v err=%v", got, err)
|
||||
t.Fatalf("an unrelated port must not conflict with the AmneziaWG relay inbound; got=%v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
@@ -367,14 +368,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
injectMtprotoEgress(xrayConfig, inbound)
|
||||
}
|
||||
|
||||
// Route opted-in AmneziaWG peers through the core's router. Unlike mtg,
|
||||
// AmneziaWG has no sidecar process of its own making outbound connections
|
||||
// to dial through a bridge — it's a kernel tunnel interface, so the host
|
||||
// side (internal/amneziawg's defaultPostUpDown) TPROXYs each opted-in
|
||||
// peer's traffic to one loopback bridge shared by every AmneziaWG
|
||||
// instance; this call is what creates that bridge and, per peer, the
|
||||
// routing rule matching its preserved source IP to its chosen outbound.
|
||||
injectAmneziawgEgress(xrayConfig, inbounds)
|
||||
// Every AmneziaWG inbound is embedded (internal/amneziawgnet: amneziawg-go
|
||||
// over a gVisor netstack, no kernel module) and relays every peer's
|
||||
// decapsulated traffic into its own loopback SOCKS5 inbound, always on —
|
||||
// unlike mtproto's bridge above, there's no opt-in gate here: once
|
||||
// traffic is decapsulated in gVisor, Xray's own freedom outbound is the
|
||||
// only way it reaches the real internet at all, not an optional extra
|
||||
// hop. Whether it goes anywhere beyond Xray's default routing is up to
|
||||
// whatever rules the admin adds through the stock Routing page, exactly
|
||||
// like routing any other protocol.
|
||||
injectAmneziawgnetSocks(xrayConfig, inbounds)
|
||||
|
||||
// Wire the panel's own HTTP traffic through the configured outbound, after
|
||||
// the subscription merge so subscription outbound tags are valid targets.
|
||||
@@ -670,61 +673,40 @@ func injectMtprotoEgress(cfg *xray.Config, inbound *model.Inbound) {
|
||||
})
|
||||
}
|
||||
|
||||
// amneziawgEgressDokodemoSettings is the dokodemo-door settings block for the
|
||||
// shared AmneziaWG TPROXY bridge: accept both TCP and UDP, and (per this
|
||||
// fork's existing "Tunnel" protocol convention — see
|
||||
// frontend/src/lib/xray/inbound-tag.ts) use followRedirect mode so the
|
||||
// destination comes from the TPROXY-preserved original address rather than a
|
||||
// fixed port/address pair.
|
||||
const amneziawgEgressDokodemoSettings = `{"allowedNetwork":"tcp,udp","followRedirect":true}`
|
||||
|
||||
// amneziawgEgressStreamSettings turns the bridge's listening socket into a
|
||||
// TPROXY target, matching internal/amneziawg's iptables `-j TPROXY` rules —
|
||||
// without this, the kernel-redirected packets never reach a listening
|
||||
// socket.
|
||||
const amneziawgEgressStreamSettings = `{"sockopt":{"tproxy":"tproxy"}}`
|
||||
|
||||
// amneziawgEgressSniffingSettings enables sniffing on the bridge, matching
|
||||
// this fork's own normal per-inbound default (see default.json's "mixed"
|
||||
// inbound). Without this, domain-based Routing rules can never match a
|
||||
// single byte of RouteThroughXray traffic: an AmneziaWG peer resolves DNS
|
||||
// amneziawgEgressSniffingSettings enables sniffing on the AmneziaWG SOCKS5
|
||||
// relay inbound, matching this fork's own normal per-inbound default (see
|
||||
// default.json's "mixed" inbound). Without this, domain-based Routing rules
|
||||
// can never match a single byte of AmneziaWG traffic: a peer resolves DNS
|
||||
// itself, through the tunnel, before ever sending a packet — by the time
|
||||
// TPROXY hands the decapsulated traffic to this bridge, the destination is
|
||||
// already a bare IP, with no domain name attached at the network layer at
|
||||
// all. Sniffing recovers it from the payload itself (TLS SNI / HTTP Host /
|
||||
// QUIC) the same way it already does for every other inbound; without it,
|
||||
// only tag/IP/network-based rules can ever match this bridge's traffic,
|
||||
// and any domain rule above it in the list is silently unreachable.
|
||||
// the embedded forwarder recovers the decapsulated traffic, the destination
|
||||
// is already a bare IP, with no domain name attached at the network layer
|
||||
// at all. Sniffing recovers it from the payload itself (TLS SNI / HTTP Host
|
||||
// / QUIC) the same way it already does for every other inbound; without
|
||||
// it, only tag/IP/network-based rules can ever match this traffic, and any
|
||||
// domain rule above it in the list is silently unreachable.
|
||||
const amneziawgEgressSniffingSettings = `{"enabled":true,"destOverride":["http","tls","quic","fakedns"]}`
|
||||
|
||||
// injectAmneziawgEgress gives every enabled, RouteThroughXray-opted-in
|
||||
// AmneziaWG inbound with at least one qualifying peer its own loopback
|
||||
// dokodemo-door bridge — tagged with that inbound's own real tag, so it's
|
||||
// already selectable in the panel's stock Routing page's inbound-tag
|
||||
// picker, exactly the way an mtproto inbound's own bridge already is (see
|
||||
// injectMtprotoEgress): the picker's tag list comes from
|
||||
// InboundService.GetInboundTags(), a plain,
|
||||
// protocol-blind SELECT over every inbound row's tag, so reusing a real
|
||||
// inbound's own tag needs no dedicated UI plumbing at all.
|
||||
// injectAmneziawgnetSocks gives every enabled AmneziaWG inbound with at
|
||||
// least one qualifying peer its own loopback SOCKS5 inbound for the
|
||||
// embedded (amneziawg-go) relay path (internal/amneziawgnet) -- always on,
|
||||
// unlike injectAmneziawgEgress's opt-in RouteThroughXray bridge above, since
|
||||
// there is no alternative datapath once traffic is decapsulated in gVisor:
|
||||
// Xray's own freedom outbound is how it reaches the real internet at all
|
||||
// (see internal/amneziawgnet/relay.go's doc comment, Finding 3 of the
|
||||
// migration plan). Tagged with the inbound's own real tag, for the same two
|
||||
// reasons injectAmneziawgEgress already is: it's already selectable in the
|
||||
// panel's stock Routing page (InboundService.GetInboundTags is
|
||||
// protocol-blind), and per-inbound traffic totals
|
||||
// (internal/web/service/inbound_traffic.go's addClientTraffic) match by
|
||||
// exact tag -- reusing it isn't a style choice.
|
||||
//
|
||||
// RouteThroughXray is a per-inbound opt-in, off by default: when it's off,
|
||||
// no bridge is created at all and the tunnel has no Xray dependency
|
||||
// whatsoever. When it's on, every peer's traffic lands on the bridge —
|
||||
// internal/amneziawg's defaultPostUpDown TPROXYs it there, there is no
|
||||
// further per-peer opt-in — but this function never generates a routing
|
||||
// rule of its own. Whether that traffic goes anywhere beyond Xray's default
|
||||
// routing is entirely up to whatever rules the admin adds through that same
|
||||
// stock Routing page (inboundTag + an optional sourceIP to target one
|
||||
// specific peer + outboundTag, exactly like routing any other protocol).
|
||||
//
|
||||
// An inbound is skipped, individually, when its own tag is already taken by
|
||||
// another config entry — mirroring injectMtprotoEgress/injectPanelEgress's
|
||||
// own defensive check, even though a real collision shouldn't be possible
|
||||
// (inbound tags are unique, and the main GenXrayInboundConfig loop already
|
||||
// excludes mtproto/amneziawg inbounds from ever claiming their own tag
|
||||
// there). Generated state is hot-appliable and never modifies the stored
|
||||
// template or restarts the core.
|
||||
func injectAmneziawgEgress(cfg *xray.Config, inbounds []*model.Inbound) {
|
||||
// No RouteThroughXray gate, no qualifying-peer IPv4 check the way
|
||||
// injectAmneziawgEgress needs one: amneziawg.InstanceFromInbound already
|
||||
// returns ok=false for zero qualifying peers (Enable && PublicKey != "" &&
|
||||
// len(AllowedIPs) > 0), and peer identity here comes from Email directly,
|
||||
// not an IPv4 lookup, so a v6-only peer is just as valid an account as any
|
||||
// other.
|
||||
func injectAmneziawgnetSocks(cfg *xray.Config, inbounds []*model.Inbound) {
|
||||
existingTags := make(map[string]struct{}, len(cfg.InboundConfigs))
|
||||
for i := range cfg.InboundConfigs {
|
||||
existingTags[cfg.InboundConfigs[i].Tag] = struct{}{}
|
||||
@@ -735,32 +717,38 @@ func injectAmneziawgEgress(cfg *xray.Config, inbounds []*model.Inbound) {
|
||||
continue
|
||||
}
|
||||
inst, ok := amneziawg.InstanceFromInbound(inbound)
|
||||
if !ok || !inst.RouteThroughXray {
|
||||
continue
|
||||
}
|
||||
hasQualifyingPeer := false
|
||||
for _, p := range inst.Peers {
|
||||
if amneziawg.FirstIPv4(p.AllowedIPs) != "" {
|
||||
hasQualifyingPeer = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasQualifyingPeer {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, taken := existingTags[inbound.Tag]; taken {
|
||||
logger.Warning("amneziawg egress: inbound tag [", inbound.Tag, "] already present in generated config, skipping its bridge")
|
||||
logger.Warning("amneziawgnet socks: inbound tag [", inbound.Tag, "] already present in generated config, skipping its relay inbound")
|
||||
continue
|
||||
}
|
||||
|
||||
emails := make([]string, 0, len(inst.Peers))
|
||||
for _, p := range inst.Peers {
|
||||
if p.Email != "" {
|
||||
emails = append(emails, p.Email)
|
||||
}
|
||||
}
|
||||
if len(emails) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
settings, err := amneziawgnet.SocksInboundSettings(emails, amneziawgnet.SocksPassword())
|
||||
if err != nil {
|
||||
logger.Warning("amneziawgnet socks: building settings for inbound [", inbound.Tag, "]: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
existingTags[inbound.Tag] = struct{}{}
|
||||
cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
|
||||
Listen: json_util.RawMessage(`"127.0.0.1"`),
|
||||
Port: amneziawg.EgressPortForInbound(inbound.Id),
|
||||
Protocol: "dokodemo-door",
|
||||
Settings: json_util.RawMessage(amneziawgEgressDokodemoSettings),
|
||||
StreamSettings: json_util.RawMessage(amneziawgEgressStreamSettings),
|
||||
Sniffing: json_util.RawMessage(amneziawgEgressSniffingSettings),
|
||||
Tag: inbound.Tag,
|
||||
Listen: json_util.RawMessage(`"127.0.0.1"`),
|
||||
Port: amneziawgnet.SOCKSPortForInbound(inbound.Id),
|
||||
Protocol: "socks",
|
||||
Settings: json_util.RawMessage(settings),
|
||||
Sniffing: json_util.RawMessage(amneziawgEgressSniffingSettings),
|
||||
Tag: inbound.Tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||
@@ -561,46 +562,46 @@ func TestInjectMtprotoEgress_BadRoutingSkips(t *testing.T) {
|
||||
}
|
||||
|
||||
func amneziawgInbound(id int, tag string, clients []model.Client) *model.Inbound {
|
||||
server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24, RouteThroughXray: true}
|
||||
server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24}
|
||||
settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients})
|
||||
return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgEgress_CreatesBridgeTaggedWithInboundsOwnTag(t *testing.T) {
|
||||
func TestInjectAmneziawgnetSocks_CreatesRelayTaggedWithInboundsOwnTag(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
before := string(cfg.RouterConfig)
|
||||
inbound := amneziawgInbound(7, "awg-7", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
|
||||
})
|
||||
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
|
||||
injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound})
|
||||
|
||||
if len(cfg.InboundConfigs) != 2 {
|
||||
t.Fatalf("expected the bridge to be appended, got %d inbounds", len(cfg.InboundConfigs))
|
||||
t.Fatalf("expected the relay inbound to be appended, got %d inbounds", len(cfg.InboundConfigs))
|
||||
}
|
||||
ib := cfg.InboundConfigs[1]
|
||||
if ib.Tag != "awg-7" || ib.Protocol != "dokodemo-door" || ib.Port != amneziawg.EgressPortForInbound(7) {
|
||||
t.Fatalf("bridge must reuse the inbound's own tag (so it's already selectable in the stock Routing page) and this instance's own derived port, got %+v", ib)
|
||||
if ib.Tag != "awg-7" || ib.Protocol != "socks" || ib.Port != amneziawgnet.SOCKSPortForInbound(7) {
|
||||
t.Fatalf("relay inbound must reuse the inbound's own tag (so per-inbound stats totals keep matching, and it's already selectable in the stock Routing page) and this instance's own derived port, got %+v", ib)
|
||||
}
|
||||
if string(ib.Listen) != `"127.0.0.1"` {
|
||||
t.Fatalf("bridge must listen on loopback, got %s", ib.Listen)
|
||||
t.Fatalf("relay inbound must listen on loopback, got %s", ib.Listen)
|
||||
}
|
||||
if !strings.Contains(string(ib.StreamSettings), `"tproxy":"tproxy"`) {
|
||||
t.Fatalf("bridge must set sockopt.tproxy, got %s", ib.StreamSettings)
|
||||
if !strings.Contains(string(ib.Settings), `"auth":"password"`) || !strings.Contains(string(ib.Settings), `"udp":true`) {
|
||||
t.Fatalf("relay inbound must require password auth and allow UDP ASSOCIATE, got %s", ib.Settings)
|
||||
}
|
||||
if !strings.Contains(string(ib.Settings), `"followRedirect":true`) {
|
||||
t.Fatalf("bridge must set followRedirect, got %s", ib.Settings)
|
||||
if !strings.Contains(string(ib.Settings), `"a@x"`) {
|
||||
t.Fatalf("relay inbound must have an account for the peer's email, got %s", ib.Settings)
|
||||
}
|
||||
if !strings.Contains(string(ib.Sniffing), `"enabled":true`) {
|
||||
t.Fatalf("bridge must enable sniffing -- a peer's own DNS resolution means the decapsulated traffic never carries a domain at the network layer, so domain-based Routing rules can only ever match via sniffing the payload, got %s", ib.Sniffing)
|
||||
t.Fatalf("relay inbound must enable sniffing -- a peer's own DNS resolution means the decapsulated traffic never carries a domain at the network layer, so domain-based Routing rules can only ever match via sniffing the payload, got %s", ib.Sniffing)
|
||||
}
|
||||
// No auto-generated routing rule: it's entirely up to the admin's own
|
||||
// Routing-page rules, same as any other protocol's inbound tag.
|
||||
if string(cfg.RouterConfig) != before {
|
||||
t.Fatalf("injectAmneziawgEgress must never touch the routing section, got %s", cfg.RouterConfig)
|
||||
t.Fatalf("injectAmneziawgnetSocks must never touch the routing section, got %s", cfg.RouterConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgEgress_MultipleInboundsEachGetOwnBridge(t *testing.T) {
|
||||
func TestInjectAmneziawgnetSocks_MultipleInboundsEachGetOwnRelay(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
inbound1 := amneziawgInbound(1, "awg-1", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
|
||||
@@ -608,21 +609,21 @@ func TestInjectAmneziawgEgress_MultipleInboundsEachGetOwnBridge(t *testing.T) {
|
||||
inbound2 := amneziawgInbound(2, "awg-2", []model.Client{
|
||||
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.9.1.2/32"}},
|
||||
})
|
||||
injectAmneziawgEgress(cfg, []*model.Inbound{inbound1, inbound2})
|
||||
injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound1, inbound2})
|
||||
|
||||
if len(cfg.InboundConfigs) != 3 {
|
||||
t.Fatalf("expected one bridge per inbound (plus the pre-existing one), got %d inbounds: %+v", len(cfg.InboundConfigs), cfg.InboundConfigs)
|
||||
t.Fatalf("expected one relay inbound per inbound (plus the pre-existing one), got %d inbounds: %+v", len(cfg.InboundConfigs), cfg.InboundConfigs)
|
||||
}
|
||||
byTag := map[string]int{}
|
||||
for _, ib := range cfg.InboundConfigs[1:] {
|
||||
byTag[ib.Tag] = ib.Port
|
||||
}
|
||||
if byTag["awg-1"] != amneziawg.EgressPortForInbound(1) || byTag["awg-2"] != amneziawg.EgressPortForInbound(2) {
|
||||
if byTag["awg-1"] != amneziawgnet.SOCKSPortForInbound(1) || byTag["awg-2"] != amneziawgnet.SOCKSPortForInbound(2) {
|
||||
t.Fatalf("each inbound must get its own tag and its own derived port, got %+v", byTag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgEgress_NoQualifyingPeerSkipsBridge(t *testing.T) {
|
||||
func TestInjectAmneziawgnetSocks_NoQualifyingPeerSkipsRelay(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
client model.Client
|
||||
@@ -632,13 +633,14 @@ func TestInjectAmneziawgEgress_NoQualifyingPeerSkipsBridge(t *testing.T) {
|
||||
{"no PublicKey", model.Client{Email: "a@x", Enable: true, AllowedIPs: []string{"10.8.1.2/32"}}, true},
|
||||
{"no AllowedIPs", model.Client{Email: "a@x", Enable: true, PublicKey: "pub-a"}, true},
|
||||
{"inbound disabled", model.Client{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, false},
|
||||
{"no Email", model.Client{Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
inbound := amneziawgInbound(1, "awg-1", []model.Client{c.client})
|
||||
inbound.Enable = c.enable
|
||||
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
|
||||
injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound})
|
||||
if len(cfg.InboundConfigs) != 1 {
|
||||
t.Fatalf("%s must be a no-op, got %d inbounds", c.name, len(cfg.InboundConfigs))
|
||||
}
|
||||
@@ -646,9 +648,13 @@ func TestInjectAmneziawgEgress_NoQualifyingPeerSkipsBridge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgEgress_RouteThroughXrayOffSkipsBridge(t *testing.T) {
|
||||
func TestInjectAmneziawgnetSocks_AlwaysOnRegardlessOfLegacyRouteThroughXrayField(t *testing.T) {
|
||||
// Unlike the retired kernel-module bridge, the embedded relay has no
|
||||
// opt-in gate: there is no alternative datapath once traffic is
|
||||
// decapsulated in gVisor. A stale RouteThroughXray=false left over from
|
||||
// a pre-cutover install must not suppress the relay inbound.
|
||||
cfg := egressTestConfig()
|
||||
server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24} // RouteThroughXray left false
|
||||
server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24, RouteThroughXray: false}
|
||||
settings, _ := json.Marshal(amneziawg.InboundSettings{
|
||||
Server: &server,
|
||||
Clients: []model.Client{
|
||||
@@ -656,13 +662,13 @@ func TestInjectAmneziawgEgress_RouteThroughXrayOffSkipsBridge(t *testing.T) {
|
||||
},
|
||||
})
|
||||
inbound := &model.Inbound{Id: 1, Tag: "awg-1", Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
|
||||
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
|
||||
if len(cfg.InboundConfigs) != 1 {
|
||||
t.Fatalf("an inbound with RouteThroughXray off must never get a bridge, got %+v", cfg.InboundConfigs)
|
||||
injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound})
|
||||
if len(cfg.InboundConfigs) != 2 {
|
||||
t.Fatalf("the relay inbound must always be created regardless of RouteThroughXray, got %+v", cfg.InboundConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgEgress_WrongProtocolOrNodeSkipped(t *testing.T) {
|
||||
func TestInjectAmneziawgnetSocks_WrongProtocolOrNodeSkipped(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
vless := &model.Inbound{Id: 1, Tag: "in-1", Protocol: model.VLESS, Enable: true}
|
||||
nodeID := 5
|
||||
@@ -670,13 +676,13 @@ func TestInjectAmneziawgEgress_WrongProtocolOrNodeSkipped(t *testing.T) {
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
|
||||
})
|
||||
nodeHosted.NodeID = &nodeID
|
||||
injectAmneziawgEgress(cfg, []*model.Inbound{vless, nodeHosted})
|
||||
injectAmneziawgnetSocks(cfg, []*model.Inbound{vless, nodeHosted})
|
||||
if len(cfg.InboundConfigs) != 1 {
|
||||
t.Fatalf("a non-AmneziaWG or node-hosted inbound must never get a bridge, got %+v", cfg.InboundConfigs)
|
||||
t.Fatalf("a non-AmneziaWG or node-hosted inbound must never get a relay inbound, got %+v", cfg.InboundConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgEgress_TagCollisionSkipsThatInboundOnly(t *testing.T) {
|
||||
func TestInjectAmneziawgnetSocks_TagCollisionSkipsThatInboundOnly(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
cfg.InboundConfigs = append(cfg.InboundConfigs,
|
||||
xray.InboundConfig{Port: 1234, Protocol: "vless", Tag: "awg-1"})
|
||||
@@ -686,20 +692,20 @@ func TestInjectAmneziawgEgress_TagCollisionSkipsThatInboundOnly(t *testing.T) {
|
||||
inbound2 := amneziawgInbound(2, "awg-2", []model.Client{
|
||||
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.9.1.2/32"}},
|
||||
})
|
||||
injectAmneziawgEgress(cfg, []*model.Inbound{inbound1, inbound2})
|
||||
injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound1, inbound2})
|
||||
|
||||
// Started with 2 (api + the colliding vless entry); only awg-2's bridge
|
||||
// should have been added, awg-1's skipped since its tag is taken.
|
||||
// Started with 2 (api + the colliding vless entry); only awg-2's relay
|
||||
// inbound should have been added, awg-1's skipped since its tag is taken.
|
||||
if len(cfg.InboundConfigs) != 3 {
|
||||
t.Fatalf("expected only the non-colliding inbound's bridge to be added, got %+v", cfg.InboundConfigs)
|
||||
t.Fatalf("expected only the non-colliding inbound's relay inbound to be added, got %+v", cfg.InboundConfigs)
|
||||
}
|
||||
found := false
|
||||
for _, ib := range cfg.InboundConfigs {
|
||||
if ib.Tag == "awg-2" && ib.Protocol == "dokodemo-door" {
|
||||
if ib.Tag == "awg-2" && ib.Protocol == "socks" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("awg-2's bridge must still be created despite awg-1's tag collision")
|
||||
t.Fatal("awg-2's relay inbound must still be created despite awg-1's tag collision")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user