mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-11 22:00:59 +00:00
feat(mtproto): route Telegram egress through Xray routing rules
Add a per-inbound "Route through Xray" toggle (off by default) plus an optional outbound picker on MTProto inbounds. mtg only supports a SOCKS5 upstream, so when enabled the panel injects a loopback SOCKS bridge into the generated Xray config — tagged with the inbound's own tag — and mtg dials Telegram through it via a [network] proxies upstream. The router then governs Telegram egress: matchable in the Routing tab, or forced to a chosen outbound/balancer via the picker. - mtproto: Instance carries RouteThroughXray + XrayRoutePort (in the fingerprint); InstanceFromInbound parses them; renderConfig emits the socks5 [network] upstream; freeLocalPort exported as FreeLocalPort. - xray.go: injectMtprotoEgress appends the loopback SOCKS bridge and prepends an optional inboundTag->outbound/balancer rule, hot-appliable like injectPanelEgress. - inbound.go: backend-owned egress port persisted in settings, allocated once and carried across edits (stored value wins); stripped with the inert outboundTag when routing is off; allocation failure fails the save; routed add/update/del force a config regen. - mtproto_job: skip folding mtg metrics for routed inbounds (the bridge, carrying the inbound tag, is metered by xray_traffic_job) to avoid double-counting. - frontend: toggle + outbound/balancer Select (useOutboundTags) on the MTProto form; i18n keys for all locales.
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/mtproto"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
@@ -451,6 +452,108 @@ func (s *InboundService) normalizeMtprotoSecret(inbound *model.Inbound) {
|
||||
}
|
||||
}
|
||||
|
||||
// mtprotoRoutesThroughXray reports whether an mtproto inbound is configured to
|
||||
// egress through the core's router (the loopback SOCKS bridge in §xray.go).
|
||||
func mtprotoRoutesThroughXray(inbound *model.Inbound) bool {
|
||||
if inbound == nil || inbound.Protocol != model.MTProto {
|
||||
return false
|
||||
}
|
||||
var parsed struct {
|
||||
RouteThroughXray bool `json:"routeThroughXray"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
|
||||
return false
|
||||
}
|
||||
return parsed.RouteThroughXray
|
||||
}
|
||||
|
||||
func settingsRouteXrayPort(parsed map[string]any) int {
|
||||
switch v := parsed["routeXrayPort"].(type) {
|
||||
case float64:
|
||||
return int(v)
|
||||
case int:
|
||||
return v
|
||||
case json.Number:
|
||||
if n, err := v.Int64(); err == nil {
|
||||
return int(n)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseRouteXrayPort(settings string) int {
|
||||
if settings == "" {
|
||||
return 0
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
|
||||
return 0
|
||||
}
|
||||
return settingsRouteXrayPort(parsed)
|
||||
}
|
||||
|
||||
// normalizeMtprotoXrayPort guarantees a routed mtproto inbound carries a stable
|
||||
// loopback egress port in its settings, so the generated Xray SOCKS bridge and
|
||||
// the mtg sidecar agree on where mtg dials out. The port is backend-owned: it is
|
||||
// allocated once when routing is first enabled and preserved across edits
|
||||
// (carried over from oldSettings, which wins over any value the client echoed
|
||||
// back). When routing is off it — together with the now-inert outbound
|
||||
// selection — is stripped so a disabled bridge leaves nothing stale behind.
|
||||
//
|
||||
// It returns an error when an egress port cannot be allocated or persisted, so
|
||||
// the caller refuses the save rather than storing a routed-but-portless inbound,
|
||||
// which would otherwise route no traffic and have its mtg metrics skipped (see
|
||||
// mtproto_job) — silently losing its accounting.
|
||||
func (s *InboundService) normalizeMtprotoXrayPort(inbound *model.Inbound, oldSettings string) error {
|
||||
if inbound.Protocol != model.MTProto {
|
||||
return nil
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed == nil {
|
||||
return nil
|
||||
}
|
||||
routed, _ := parsed["routeThroughXray"].(bool)
|
||||
if !routed {
|
||||
_, hadPort := parsed["routeXrayPort"]
|
||||
_, hadTag := parsed["outboundTag"]
|
||||
if !hadPort && !hadTag {
|
||||
return nil
|
||||
}
|
||||
delete(parsed, "routeXrayPort")
|
||||
delete(parsed, "outboundTag")
|
||||
if bs, err := json.MarshalIndent(parsed, "", " "); err == nil {
|
||||
inbound.Settings = string(bs)
|
||||
} else {
|
||||
logger.Warning("mtproto: failed to marshal settings after disabling routing:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prefer the already-stored port (carried across edits), then any value the
|
||||
// client sent, then allocate a fresh one.
|
||||
port := parseRouteXrayPort(oldSettings)
|
||||
if port <= 0 {
|
||||
port = settingsRouteXrayPort(parsed)
|
||||
}
|
||||
if port <= 0 {
|
||||
allocated, err := mtproto.FreeLocalPort()
|
||||
if err != nil {
|
||||
return common.NewError("mtproto: could not allocate an Xray egress port:", err)
|
||||
}
|
||||
port = allocated
|
||||
}
|
||||
if settingsRouteXrayPort(parsed) == port {
|
||||
return nil
|
||||
}
|
||||
parsed["routeXrayPort"] = port
|
||||
bs, err := json.MarshalIndent(parsed, "", " ")
|
||||
if err != nil {
|
||||
return common.NewError("mtproto: could not persist the Xray egress port:", err)
|
||||
}
|
||||
inbound.Settings = string(bs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddInbound creates a new inbound configuration.
|
||||
// It validates port uniqueness, client email uniqueness, and required fields,
|
||||
// then saves the inbound to the database and optionally adds it to the running Xray instance.
|
||||
@@ -459,6 +562,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
// Normalize streamSettings based on protocol
|
||||
s.normalizeStreamSettings(inbound)
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
if err := s.normalizeMtprotoXrayPort(inbound, ""); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)
|
||||
if err := normalizeInboundShareAddressStrict(inbound); err != nil {
|
||||
return inbound, false, err
|
||||
@@ -622,6 +728,13 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
}
|
||||
}
|
||||
|
||||
// A routed mtproto inbound is not an Xray inbound itself, so the runtime
|
||||
// push above only (re)starts the mtg sidecar. The egress SOCKS bridge lives
|
||||
// in the generated config, so force a regen to wire it in.
|
||||
if mtprotoRoutesThroughXray(inbound) {
|
||||
needRestart = true
|
||||
}
|
||||
|
||||
return inbound, needRestart, err
|
||||
}
|
||||
|
||||
@@ -685,6 +798,10 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Drop the egress SOCKS bridge a routed mtproto inbound left in the config.
|
||||
if mtprotoRoutesThroughXray(&ib) {
|
||||
needRestart = true
|
||||
}
|
||||
return needRestart, nil
|
||||
}
|
||||
|
||||
@@ -827,6 +944,13 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
return inbound, false, err
|
||||
}
|
||||
inbound.NodeID = oldInbound.NodeID
|
||||
// Capture the pre-edit routing state before oldInbound.Settings is replaced
|
||||
// with the new settings further down, then ensure a routed inbound keeps a
|
||||
// stable egress port (reusing the one already stored).
|
||||
oldRoutedMtproto := mtprotoRoutesThroughXray(oldInbound)
|
||||
if err := s.normalizeMtprotoXrayPort(inbound, oldInbound.Settings); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
tag := oldInbound.Tag
|
||||
oldBits := inboundTransports(oldInbound.Protocol, oldInbound.StreamSettings, oldInbound.Settings)
|
||||
@@ -1009,6 +1133,11 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
if err = s.clientService.SyncInbound(tx, oldInbound.Id, newClients); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
// (Re)generate the Xray config whenever routing was or is now enabled, so the
|
||||
// egress SOCKS bridge is added, moved, or dropped to match the new settings.
|
||||
if mtprotoRoutesThroughXray(inbound) || oldRoutedMtproto {
|
||||
needRestart = true
|
||||
}
|
||||
return inbound, needRestart, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
func TestMtprotoRoutesThroughXray(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
ib *model.Inbound
|
||||
want bool
|
||||
}{
|
||||
"routed": {&model.Inbound{Protocol: model.MTProto, Settings: `{"routeThroughXray":true}`}, true},
|
||||
"off": {&model.Inbound{Protocol: model.MTProto, Settings: `{"routeThroughXray":false}`}, false},
|
||||
"absent": {&model.Inbound{Protocol: model.MTProto, Settings: `{}`}, false},
|
||||
"non-mtproto": {&model.Inbound{Protocol: model.VLESS, Settings: `{"routeThroughXray":true}`}, false},
|
||||
"bad json": {&model.Inbound{Protocol: model.MTProto, Settings: `{nope`}, false},
|
||||
"nil": {nil, false},
|
||||
}
|
||||
for name, c := range cases {
|
||||
if got := mtprotoRoutesThroughXray(c.ib); got != c.want {
|
||||
t.Fatalf("%s: got %v want %v", name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func routeXrayPortOf(t *testing.T, settings string) int {
|
||||
t.Helper()
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
|
||||
t.Fatalf("settings not valid JSON: %v\n%s", err, settings)
|
||||
}
|
||||
return settingsRouteXrayPort(parsed)
|
||||
}
|
||||
|
||||
func TestNormalizeMtprotoXrayPort(t *testing.T) {
|
||||
s := &InboundService{}
|
||||
|
||||
// Non-mtproto inbounds are left alone.
|
||||
ib := &model.Inbound{Protocol: model.VLESS, Settings: `{"x":1}`}
|
||||
if err := s.normalizeMtprotoXrayPort(ib, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ib.Settings != `{"x":1}` {
|
||||
t.Fatalf("non-mtproto settings must be untouched, got %s", ib.Settings)
|
||||
}
|
||||
|
||||
// Routing on with no existing port allocates a fresh one.
|
||||
ib = &model.Inbound{Protocol: model.MTProto, Settings: `{"routeThroughXray":true}`}
|
||||
if err := s.normalizeMtprotoXrayPort(ib, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := routeXrayPortOf(t, ib.Settings); p <= 0 {
|
||||
t.Fatalf("a routed inbound must get a port, got %d", p)
|
||||
}
|
||||
|
||||
// On update, the stored port wins over both a missing and a client-echoed
|
||||
// value — the backend owns it, so no churn and no client override.
|
||||
ib = &model.Inbound{Protocol: model.MTProto, Settings: `{"routeThroughXray":true,"routeXrayPort":99999}`}
|
||||
if err := s.normalizeMtprotoXrayPort(ib, `{"routeThroughXray":true,"routeXrayPort":51000}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := routeXrayPortOf(t, ib.Settings); p != 51000 {
|
||||
t.Fatalf("stored port must win, got %d", p)
|
||||
}
|
||||
|
||||
// An already-present port (no old settings) is stable and not re-marshaled.
|
||||
const stable = `{"routeThroughXray":true,"routeXrayPort":52000}`
|
||||
ib = &model.Inbound{Protocol: model.MTProto, Settings: stable}
|
||||
if err := s.normalizeMtprotoXrayPort(ib, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ib.Settings != stable {
|
||||
t.Fatalf("stable settings must pass through untouched, got %s", ib.Settings)
|
||||
}
|
||||
|
||||
// Turning routing off strips both the bridge port and the inert outbound.
|
||||
ib = &model.Inbound{Protocol: model.MTProto, Settings: `{"routeThroughXray":false,"routeXrayPort":53000,"outboundTag":"warp"}`}
|
||||
if err := s.normalizeMtprotoXrayPort(ib, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := routeXrayPortOf(t, ib.Settings); p != 0 {
|
||||
t.Fatalf("disabling routing must drop the port, got %d", p)
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(ib.Settings), &parsed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := parsed["outboundTag"]; ok {
|
||||
t.Fatalf("disabling routing must drop the inert outbound tag, got %s", ib.Settings)
|
||||
}
|
||||
}
|
||||
@@ -275,6 +275,19 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
mergeSubscriptionOutbounds(xrayConfig, prepend, appendList)
|
||||
}
|
||||
|
||||
// Route opted-in local mtproto inbounds through the core's router. Each one
|
||||
// gets a loopback SOCKS bridge — tagged with the inbound's own tag so it is
|
||||
// matchable in routing rules — that its mtg sidecar dials Telegram through.
|
||||
// Done after the subscription merge so a selected subscription outbound (or
|
||||
// balancer) is a valid rule target.
|
||||
for i := range inbounds {
|
||||
inbound := inbounds[i]
|
||||
if inbound.Protocol != model.MTProto || !inbound.Enable || inbound.NodeID != nil {
|
||||
continue
|
||||
}
|
||||
injectMtprotoEgress(xrayConfig, inbound)
|
||||
}
|
||||
|
||||
// Wire the panel's own HTTP traffic through the configured outbound, after
|
||||
// the subscription merge so subscription outbound tags are valid targets.
|
||||
if egressTag, err := s.settingService.GetPanelOutbound(); err != nil {
|
||||
@@ -382,6 +395,75 @@ func routingTagIsBalancer(routing map[string]any, tag string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// mtprotoEgressSocksSettings is the loopback SOCKS server a routed mtproto
|
||||
// inbound exposes for its mtg sidecar to dial Telegram through. mtg makes plain
|
||||
// TCP connections, so UDP is left off (matching the panel egress bridge).
|
||||
const mtprotoEgressSocksSettings = `{"auth":"noauth","udp":false}`
|
||||
|
||||
// injectMtprotoEgress wires one routed mtproto inbound into the generated
|
||||
// config: it appends a loopback SOCKS inbound (tagged with the inbound's own tag,
|
||||
// on the egress port persisted in settings) and, when an outbound is selected,
|
||||
// prepends a routing rule sending that tag to it. Both live only in the generated
|
||||
// config — the stored template is untouched — and both are hot-appliable, so
|
||||
// toggling routing never forces a full Xray restart. Mirrors injectPanelEgress.
|
||||
func injectMtprotoEgress(cfg *xray.Config, inbound *model.Inbound) {
|
||||
var parsed struct {
|
||||
RouteThroughXray bool `json:"routeThroughXray"`
|
||||
RouteXrayPort int `json:"routeXrayPort"`
|
||||
OutboundTag string `json:"outboundTag"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
|
||||
return
|
||||
}
|
||||
if !parsed.RouteThroughXray || parsed.RouteXrayPort <= 0 || inbound.Tag == "" {
|
||||
return
|
||||
}
|
||||
tag := inbound.Tag
|
||||
for i := range cfg.InboundConfigs {
|
||||
if cfg.InboundConfigs[i].Tag == tag {
|
||||
logger.Warning("mtproto egress: inbound tag [", tag, "] already present in generated config, skipping bridge")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.OutboundTag != "" {
|
||||
routing := map[string]any{}
|
||||
parseOK := true
|
||||
if len(cfg.RouterConfig) > 0 {
|
||||
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
||||
logger.Warning("mtproto egress: routing section is unparsable, skipping rule:", err)
|
||||
parseOK = false
|
||||
}
|
||||
}
|
||||
if parseOK {
|
||||
rules, _ := routing["rules"].([]any)
|
||||
rule := map[string]any{
|
||||
"type": "field",
|
||||
"inboundTag": []any{tag},
|
||||
}
|
||||
if routingTagIsBalancer(routing, parsed.OutboundTag) {
|
||||
rule["balancerTag"] = parsed.OutboundTag
|
||||
} else {
|
||||
rule["outboundTag"] = parsed.OutboundTag
|
||||
}
|
||||
routing["rules"] = append([]any{rule}, rules...)
|
||||
if newRouting, err := json.Marshal(routing); err == nil {
|
||||
cfg.RouterConfig = json_util.RawMessage(newRouting)
|
||||
} else {
|
||||
logger.Warning("mtproto egress: failed to rebuild routing section, skipping rule:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
|
||||
Listen: json_util.RawMessage(`"127.0.0.1"`),
|
||||
Port: parsed.RouteXrayPort,
|
||||
Protocol: "socks",
|
||||
Settings: json_util.RawMessage(mtprotoEgressSocksSettings),
|
||||
Tag: tag,
|
||||
})
|
||||
}
|
||||
|
||||
// mergeSubscriptionOutbounds appends the subscription outbounds to the
|
||||
// OutboundConfigs array of the xray config. It works on the already-unmarshaled
|
||||
// template so that manually configured outbounds are never overwritten.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
@@ -271,3 +272,99 @@ func TestInjectPanelEgress_BadRoutingSkips(t *testing.T) {
|
||||
t.Fatal("unparsable routing must be left untouched")
|
||||
}
|
||||
}
|
||||
|
||||
func mtprotoInbound(tag string, settings string) *model.Inbound {
|
||||
return &model.Inbound{Tag: tag, Protocol: model.MTProto, Enable: true, Settings: settings}
|
||||
}
|
||||
|
||||
func TestInjectMtprotoEgress_WithOutbound(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
|
||||
`{"routeThroughXray":true,"routeXrayPort":50000,"outboundTag":"warp"}`))
|
||||
|
||||
if len(cfg.InboundConfigs) != 2 {
|
||||
t.Fatalf("expected the bridge inbound to be appended, got %d", len(cfg.InboundConfigs))
|
||||
}
|
||||
ib := cfg.InboundConfigs[1]
|
||||
if ib.Tag != "inbound-443" || ib.Protocol != "socks" || ib.Port != 50000 {
|
||||
t.Fatalf("unexpected bridge inbound: %+v", ib)
|
||||
}
|
||||
if string(ib.Listen) != `"127.0.0.1"` {
|
||||
t.Fatalf("bridge must listen on loopback, got %s", ib.Listen)
|
||||
}
|
||||
|
||||
var routing egressRouting
|
||||
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routing.Rules) != 2 {
|
||||
t.Fatalf("expected the egress rule prepended to the existing rule, got %+v", routing.Rules)
|
||||
}
|
||||
first := routing.Rules[0]
|
||||
if first.Type != "field" || first.OutboundTag != "warp" ||
|
||||
len(first.InboundTag) != 1 || first.InboundTag[0] != "inbound-443" {
|
||||
t.Fatalf("egress rule must bind the inbound tag to the outbound, got %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectMtprotoEgress_NoOutboundLeavesRouting(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
before := string(cfg.RouterConfig)
|
||||
injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
|
||||
`{"routeThroughXray":true,"routeXrayPort":50001}`))
|
||||
|
||||
if len(cfg.InboundConfigs) != 2 || cfg.InboundConfigs[1].Port != 50001 {
|
||||
t.Fatalf("bridge must still be appended without an outbound, got %+v", cfg.InboundConfigs)
|
||||
}
|
||||
if string(cfg.RouterConfig) != before {
|
||||
t.Fatalf("no outbound means no rule change, got %s", cfg.RouterConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectMtprotoEgress_BalancerTag(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
cfg.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
|
||||
injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
|
||||
`{"routeThroughXray":true,"routeXrayPort":50002,"outboundTag":"lb"}`))
|
||||
|
||||
var routing struct {
|
||||
Rules []struct {
|
||||
OutboundTag string `json:"outboundTag"`
|
||||
BalancerTag string `json:"balancerTag"`
|
||||
} `json:"rules"`
|
||||
}
|
||||
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routing.Rules) != 1 || routing.Rules[0].BalancerTag != "lb" || routing.Rules[0].OutboundTag != "" {
|
||||
t.Fatalf("a balancer tag must target balancerTag, got %+v", routing.Rules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectMtprotoEgress_Disabled(t *testing.T) {
|
||||
// Not routed, and routed-but-portless, are both no-ops.
|
||||
for _, settings := range []string{
|
||||
`{"routeThroughXray":false,"routeXrayPort":50000}`,
|
||||
`{"routeThroughXray":true}`,
|
||||
`{"routeThroughXray":true,"routeXrayPort":0}`,
|
||||
} {
|
||||
cfg := egressTestConfig()
|
||||
before := string(cfg.RouterConfig)
|
||||
injectMtprotoEgress(cfg, mtprotoInbound("inbound-443", settings))
|
||||
if len(cfg.InboundConfigs) != 1 || string(cfg.RouterConfig) != before {
|
||||
t.Fatalf("settings %s must be a no-op, got %d inbounds", settings, len(cfg.InboundConfigs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectMtprotoEgress_TagCollisionSkips(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
cfg.InboundConfigs = append(cfg.InboundConfigs,
|
||||
xray.InboundConfig{Port: 443, Protocol: "vless", Tag: "inbound-443"})
|
||||
before := string(cfg.RouterConfig)
|
||||
injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
|
||||
`{"routeThroughXray":true,"routeXrayPort":50003,"outboundTag":"warp"}`))
|
||||
if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
|
||||
t.Fatal("a real inbound already owning the tag must make the bridge a no-op")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user