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:
MHSanaei
2026-06-12 17:58:45 +02:00
parent 5716ae5987
commit 5eec178483
24 changed files with 602 additions and 4 deletions
+23 -2
View File
@@ -32,6 +32,12 @@ type Instance struct {
FrontingIP string
FrontingPort int
FrontingProxyProtocol bool
// When RouteThroughXray is set, mtg dials Telegram through the loopback
// SOCKS bridge the panel injects into the Xray config at XrayRoutePort, so
// the egress obeys the core's routing rules instead of going out directly.
RouteThroughXray bool
XrayRoutePort int
}
func (inst Instance) bindTo() string {
@@ -54,6 +60,8 @@ func (inst Instance) fingerprint() string {
inst.FrontingIP,
strconv.Itoa(inst.FrontingPort),
strconv.FormatBool(inst.FrontingProxyProtocol),
strconv.FormatBool(inst.RouteThroughXray),
strconv.Itoa(inst.XrayRoutePort),
}, "|")
}
@@ -117,6 +125,8 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
Port int `json:"port"`
ProxyProtocol bool `json:"proxyProtocol"`
} `json:"domainFronting"`
RouteThroughXray bool `json:"routeThroughXray"`
RouteXrayPort int `json:"routeXrayPort"`
}
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
return Instance{}, false
@@ -136,6 +146,8 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
FrontingIP: parsed.DomainFronting.IP,
FrontingPort: parsed.DomainFronting.Port,
FrontingProxyProtocol: parsed.DomainFronting.ProxyProtocol,
RouteThroughXray: parsed.RouteThroughXray,
XrayRoutePort: parsed.RouteXrayPort,
}, true
}
@@ -172,7 +184,7 @@ func (m *Manager) ensureLocked(inst Instance) error {
cur.proc.Stop()
delete(m.procs, inst.Id)
}
metricsPort, err := freeLocalPort()
metricsPort, err := FreeLocalPort()
if err != nil {
return err
}
@@ -307,7 +319,10 @@ func (m *Manager) CollectTraffic() []Traffic {
return out
}
func freeLocalPort() (int, error) {
// FreeLocalPort asks the OS for an unused loopback TCP port. It is used both
// for mtg's metrics endpoint and to allocate the per-inbound SOCKS egress
// bridge port persisted into mtproto inbound settings.
func FreeLocalPort() (int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, err
@@ -345,6 +360,12 @@ func renderConfig(inst Instance, metricsPort int) string {
b.WriteString("proxy-protocol = true\n")
}
}
// When the inbound opts into Xray routing, mtg reaches Telegram through the
// loopback SOCKS bridge the panel injects into the running Xray config. mtg
// only supports SOCKS5 upstreams, which is exactly what the bridge exposes.
if inst.RouteThroughXray && inst.XrayRoutePort > 0 {
fmt.Fprintf(&b, "\n[network]\nproxies = [\"socks5://127.0.0.1:%d\"]\n", inst.XrayRoutePort)
}
fmt.Fprintf(&b, "\n[stats.prometheus]\nenabled = true\nbind-to = \"127.0.0.1:%d\"\nhttp-path = \"/metrics\"\nmetric-prefix = \"mtg\"\n", metricsPort)
return b.String()
}