mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-25 05:46:18 +00:00
feat(mtproto): add domain-fronting and essential mtg options
Expose mtg's [domain-fronting] section (ip/port/proxy-protocol) plus proxy-protocol-listener, prefer-ip, and debug on MTProto inbounds. Each key is written to the generated mtg-<id>.toml only when set, so mtg's own defaults apply otherwise. The instance fingerprint now covers these fields, so editing an option restarts the sidecar. Since MTProto is mtg-served (not Xray), sniffing does not apply: hide the Sniffing tab and the Advanced sniffing sub-editor, drop it from the Advanced "All" JSON view, and emit empty sniffing in the wire payload, all gated by a new canEnableSniffing predicate.
This commit is contained in:
+77
-12
@@ -23,6 +23,15 @@ type Instance struct {
|
||||
Listen string
|
||||
Port int
|
||||
Secret string
|
||||
|
||||
// Optional mtg tuning; each is omitted from the generated TOML when
|
||||
// zero-valued so mtg falls back to its own defaults.
|
||||
Debug bool
|
||||
ProxyProtocolListener bool
|
||||
PreferIP string
|
||||
FrontingIP string
|
||||
FrontingPort int
|
||||
FrontingProxyProtocol bool
|
||||
}
|
||||
|
||||
func (inst Instance) bindTo() string {
|
||||
@@ -33,8 +42,19 @@ func (inst Instance) bindTo() string {
|
||||
return fmt.Sprintf("%s:%d", listen, inst.Port)
|
||||
}
|
||||
|
||||
// fingerprint changes whenever any value that ends up in the generated TOML
|
||||
// changes, so ensureLocked restarts mtg when the operator edits a setting.
|
||||
func (inst Instance) fingerprint() string {
|
||||
return fmt.Sprintf("%s|%s", inst.bindTo(), inst.Secret)
|
||||
return strings.Join([]string{
|
||||
inst.bindTo(),
|
||||
inst.Secret,
|
||||
strconv.FormatBool(inst.Debug),
|
||||
strconv.FormatBool(inst.ProxyProtocolListener),
|
||||
inst.PreferIP,
|
||||
inst.FrontingIP,
|
||||
strconv.Itoa(inst.FrontingPort),
|
||||
strconv.FormatBool(inst.FrontingProxyProtocol),
|
||||
}, "|")
|
||||
}
|
||||
|
||||
// Traffic is a per-inbound traffic delta scraped from an mtg metrics endpoint.
|
||||
@@ -88,7 +108,15 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
settings = healed
|
||||
}
|
||||
var parsed struct {
|
||||
Secret string `json:"secret"`
|
||||
Secret string `json:"secret"`
|
||||
Debug bool `json:"debug"`
|
||||
ProxyProtocolListener bool `json:"proxyProtocolListener"`
|
||||
PreferIP string `json:"preferIp"`
|
||||
DomainFronting struct {
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
ProxyProtocol bool `json:"proxyProtocol"`
|
||||
} `json:"domainFronting"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
|
||||
return Instance{}, false
|
||||
@@ -97,11 +125,17 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
return Instance{}, false
|
||||
}
|
||||
return Instance{
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
Listen: ib.Listen,
|
||||
Port: ib.Port,
|
||||
Secret: parsed.Secret,
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
Listen: ib.Listen,
|
||||
Port: ib.Port,
|
||||
Secret: parsed.Secret,
|
||||
Debug: parsed.Debug,
|
||||
ProxyProtocolListener: parsed.ProxyProtocolListener,
|
||||
PreferIP: parsed.PreferIP,
|
||||
FrontingIP: parsed.DomainFronting.IP,
|
||||
FrontingPort: parsed.DomainFronting.Port,
|
||||
FrontingProxyProtocol: parsed.DomainFronting.ProxyProtocol,
|
||||
}, true
|
||||
}
|
||||
|
||||
@@ -143,7 +177,7 @@ func (m *Manager) ensureLocked(inst Instance) error {
|
||||
return err
|
||||
}
|
||||
cfgPath := configPathForID(inst.Id)
|
||||
if err := writeConfig(cfgPath, inst.Secret, inst.bindTo(), metricsPort); err != nil {
|
||||
if err := writeConfig(cfgPath, inst, metricsPort); err != nil {
|
||||
return err
|
||||
}
|
||||
proc := newProcess(cfgPath, fmt.Sprintf("inbound %d", inst.Id))
|
||||
@@ -282,13 +316,44 @@ func freeLocalPort() (int, error) {
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
func writeConfig(path, secret, bindTo string, metricsPort int) error {
|
||||
// renderConfig builds the mtg TOML for an instance. Top-level keys must precede
|
||||
// any [section] header in TOML, so the layout is: required keys, then the
|
||||
// optional scalar tuning, then [domain-fronting], and finally [stats.prometheus]
|
||||
// — which x-ui always emits and scrapes for traffic (see scrapeTraffic).
|
||||
func renderConfig(inst Instance, metricsPort int) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "secret = %q\n", inst.Secret)
|
||||
fmt.Fprintf(&b, "bind-to = %q\n", inst.bindTo())
|
||||
if inst.Debug {
|
||||
b.WriteString("debug = true\n")
|
||||
}
|
||||
if inst.ProxyProtocolListener {
|
||||
b.WriteString("proxy-protocol-listener = true\n")
|
||||
}
|
||||
if inst.PreferIP != "" {
|
||||
fmt.Fprintf(&b, "prefer-ip = %q\n", inst.PreferIP)
|
||||
}
|
||||
if inst.FrontingIP != "" || inst.FrontingPort > 0 || inst.FrontingProxyProtocol {
|
||||
b.WriteString("\n[domain-fronting]\n")
|
||||
if inst.FrontingIP != "" {
|
||||
fmt.Fprintf(&b, "ip = %q\n", inst.FrontingIP)
|
||||
}
|
||||
if inst.FrontingPort > 0 {
|
||||
fmt.Fprintf(&b, "port = %d\n", inst.FrontingPort)
|
||||
}
|
||||
if inst.FrontingProxyProtocol {
|
||||
b.WriteString("proxy-protocol = true\n")
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
func writeConfig(path string, inst Instance, metricsPort int) error {
|
||||
if err := os.MkdirAll(configDir(), 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
content := fmt.Sprintf("secret = %q\nbind-to = %q\n\n[stats.prometheus]\nenabled = true\nbind-to = \"127.0.0.1:%d\"\nhttp-path = \"/metrics\"\nmetric-prefix = \"mtg\"\n",
|
||||
secret, bindTo, metricsPort)
|
||||
return os.WriteFile(path, []byte(content), 0o640)
|
||||
return os.WriteFile(path, []byte(renderConfig(inst, metricsPort)), 0o640)
|
||||
}
|
||||
|
||||
// scrapeTraffic reads the mtg Prometheus metrics endpoint and sums byte
|
||||
|
||||
+72
-1
@@ -1,6 +1,7 @@
|
||||
package mtproto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
@@ -37,7 +38,9 @@ func TestInstanceFromInbound(t *testing.T) {
|
||||
Listen: "0.0.0.0",
|
||||
Port: 8443,
|
||||
Protocol: model.MTProto,
|
||||
Settings: `{"fakeTlsDomain":"example.com","secret":""}`,
|
||||
Settings: `{"fakeTlsDomain":"example.com","secret":"",` +
|
||||
`"debug":true,"proxyProtocolListener":true,"preferIp":"prefer-ipv4",` +
|
||||
`"domainFronting":{"ip":"127.0.0.1","port":9443,"proxyProtocol":true}}`,
|
||||
}
|
||||
inst, ok := InstanceFromInbound(ib)
|
||||
if !ok {
|
||||
@@ -49,8 +52,76 @@ func TestInstanceFromInbound(t *testing.T) {
|
||||
if inst.Port != 8443 || inst.Id != 3 {
|
||||
t.Fatalf("bad instance %+v", inst)
|
||||
}
|
||||
if !inst.Debug || !inst.ProxyProtocolListener || inst.PreferIP != "prefer-ipv4" {
|
||||
t.Fatalf("scalar options not parsed: %+v", inst)
|
||||
}
|
||||
if inst.FrontingIP != "127.0.0.1" || inst.FrontingPort != 9443 || !inst.FrontingProxyProtocol {
|
||||
t.Fatalf("domain-fronting not parsed: %+v", inst)
|
||||
}
|
||||
|
||||
if _, ok := InstanceFromInbound(&model.Inbound{Protocol: model.VLESS}); ok {
|
||||
t.Fatal("non-mtproto inbound should not produce an instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderConfig(t *testing.T) {
|
||||
// A bare instance emits only the required keys and the prometheus block,
|
||||
// with no optional keys and no [domain-fronting] section.
|
||||
bare := renderConfig(Instance{Secret: "ee00", Listen: "0.0.0.0", Port: 8443}, 5000)
|
||||
for _, unwanted := range []string{"debug", "proxy-protocol-listener", "prefer-ip", "[domain-fronting]"} {
|
||||
if strings.Contains(bare, unwanted) {
|
||||
t.Fatalf("bare config should not contain %q:\n%s", unwanted, bare)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(bare, `bind-to = "0.0.0.0:8443"`) {
|
||||
t.Fatalf("missing bind-to:\n%s", bare)
|
||||
}
|
||||
if !strings.Contains(bare, "[stats.prometheus]") || !strings.Contains(bare, "127.0.0.1:5000") {
|
||||
t.Fatalf("prometheus block must always be present:\n%s", bare)
|
||||
}
|
||||
|
||||
// A fully configured instance emits every option and the fronting section.
|
||||
full := renderConfig(Instance{
|
||||
Secret: "ee11", Listen: "0.0.0.0", Port: 443,
|
||||
Debug: true, ProxyProtocolListener: true, PreferIP: "only-ipv6",
|
||||
FrontingIP: "127.0.0.1", FrontingPort: 9443, FrontingProxyProtocol: true,
|
||||
}, 6000)
|
||||
for _, want := range []string{
|
||||
"debug = true\n",
|
||||
"proxy-protocol-listener = true\n",
|
||||
`prefer-ip = "only-ipv6"`,
|
||||
"[domain-fronting]",
|
||||
`ip = "127.0.0.1"`,
|
||||
"port = 9443",
|
||||
"proxy-protocol = true\n",
|
||||
} {
|
||||
if !strings.Contains(full, want) {
|
||||
t.Fatalf("full config missing %q:\n%s", want, full)
|
||||
}
|
||||
}
|
||||
// TOML requires top-level keys before any [section] header.
|
||||
if strings.Index(full, "prefer-ip") > strings.Index(full, "[domain-fronting]") {
|
||||
t.Fatalf("top-level keys must precede the [domain-fronting] section:\n%s", full)
|
||||
}
|
||||
if strings.LastIndex(full, "[domain-fronting]") > strings.Index(full, "[stats.prometheus]") {
|
||||
t.Fatalf("[domain-fronting] must precede [stats.prometheus]:\n%s", full)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerprintReactsToOptions(t *testing.T) {
|
||||
base := Instance{Secret: "ee", Listen: "0.0.0.0", Port: 443}
|
||||
for name, mutate := range map[string]func(*Instance){
|
||||
"debug": func(i *Instance) { i.Debug = true },
|
||||
"listener": func(i *Instance) { i.ProxyProtocolListener = true },
|
||||
"preferIp": func(i *Instance) { i.PreferIP = "only-ipv4" },
|
||||
"frontingIP": func(i *Instance) { i.FrontingIP = "127.0.0.1" },
|
||||
"frontingPort": func(i *Instance) { i.FrontingPort = 9443 },
|
||||
"frontingProxy": func(i *Instance) { i.FrontingProxyProtocol = true },
|
||||
} {
|
||||
changed := base
|
||||
mutate(&changed)
|
||||
if base.fingerprint() == changed.fingerprint() {
|
||||
t.Fatalf("fingerprint must change when %s changes", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user