mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-18 20:05:03 +03:00
feat(mtproto): expose mtg-multi's per-IP pending-handshake limit
mtg-multi v1.16.0 adds [defense.pending-handshakes] max-per-ip, which closes new connections from an IP already holding that many unauthenticated handshakes, so one address cannot pin the workers other clients need. The MTProto inbound form now carries it as pendingHandshakesMaxPerIp; the key is written only when set above 0, so existing inbounds render a byte-identical config and are not restarted. A change to it moves the structural fingerprint and restarts the sidecar. dry-run is left out on purpose: it only feeds the pending_handshake_limit Prometheus metric, which the panel does not surface. The hint warns that clients behind carrier NAT share the limit.
This commit is contained in:
+34
-24
@@ -58,6 +58,10 @@ type Instance struct {
|
||||
// fair-share algorithm; zero disables throttling.
|
||||
ThrottleMaxConnections int
|
||||
|
||||
// PendingHandshakesMaxPerIP caps unauthenticated handshakes held open by one
|
||||
// client IP (mtg-multi >= 1.16.0); zero leaves the limit off.
|
||||
PendingHandshakesMaxPerIP int
|
||||
|
||||
// PublicIPv4/PublicIPv6 pin the proxy's reachable address the Telegram
|
||||
// middle proxy needs when clients carry advertising tags; they are omitted
|
||||
// when empty so mtg auto-detects, and a change forces a restart.
|
||||
@@ -93,6 +97,7 @@ func (inst Instance) structuralFingerprint() string {
|
||||
strconv.Itoa(inst.FrontingPort),
|
||||
strconv.FormatBool(inst.FrontingProxyProtocol),
|
||||
strconv.Itoa(inst.ThrottleMaxConnections),
|
||||
strconv.Itoa(inst.PendingHandshakesMaxPerIP),
|
||||
strconv.FormatBool(inst.RouteThroughXray),
|
||||
strconv.Itoa(inst.XrayRoutePort),
|
||||
inst.PublicIPv4,
|
||||
@@ -179,13 +184,14 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
Port int `json:"port"`
|
||||
ProxyProtocol bool `json:"proxyProtocol"`
|
||||
} `json:"domainFronting"`
|
||||
PreferIP string `json:"preferIp"`
|
||||
ThrottleMaxConnections int `json:"throttleMaxConnections"`
|
||||
RouteThroughXray bool `json:"routeThroughXray"`
|
||||
RouteXrayPort int `json:"routeXrayPort"`
|
||||
PublicIPv4 string `json:"publicIpv4"`
|
||||
PublicIPv6 string `json:"publicIpv6"`
|
||||
Clients []struct {
|
||||
PreferIP string `json:"preferIp"`
|
||||
ThrottleMaxConnections int `json:"throttleMaxConnections"`
|
||||
PendingHandshakesMaxPerIP int `json:"pendingHandshakesMaxPerIp"`
|
||||
RouteThroughXray bool `json:"routeThroughXray"`
|
||||
RouteXrayPort int `json:"routeXrayPort"`
|
||||
PublicIPv4 string `json:"publicIpv4"`
|
||||
PublicIPv6 string `json:"publicIpv6"`
|
||||
Clients []struct {
|
||||
Email string `json:"email"`
|
||||
Secret string `json:"secret"`
|
||||
AdTag string `json:"adTag"`
|
||||
@@ -215,22 +221,23 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
return Instance{}, false
|
||||
}
|
||||
return Instance{
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
Listen: ib.Listen,
|
||||
Port: ib.Port,
|
||||
Secrets: secrets,
|
||||
Debug: parsed.Debug,
|
||||
ProxyProtocolListener: parsed.ProxyProtocolListener,
|
||||
PreferIP: parsed.PreferIP,
|
||||
FrontingIP: parsed.DomainFronting.IP,
|
||||
FrontingPort: parsed.DomainFronting.Port,
|
||||
FrontingProxyProtocol: parsed.DomainFronting.ProxyProtocol,
|
||||
ThrottleMaxConnections: parsed.ThrottleMaxConnections,
|
||||
RouteThroughXray: parsed.RouteThroughXray,
|
||||
XrayRoutePort: parsed.RouteXrayPort,
|
||||
PublicIPv4: strings.TrimSpace(parsed.PublicIPv4),
|
||||
PublicIPv6: strings.TrimSpace(parsed.PublicIPv6),
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
Listen: ib.Listen,
|
||||
Port: ib.Port,
|
||||
Secrets: secrets,
|
||||
Debug: parsed.Debug,
|
||||
ProxyProtocolListener: parsed.ProxyProtocolListener,
|
||||
PreferIP: parsed.PreferIP,
|
||||
FrontingIP: parsed.DomainFronting.IP,
|
||||
FrontingPort: parsed.DomainFronting.Port,
|
||||
FrontingProxyProtocol: parsed.DomainFronting.ProxyProtocol,
|
||||
ThrottleMaxConnections: parsed.ThrottleMaxConnections,
|
||||
PendingHandshakesMaxPerIP: parsed.PendingHandshakesMaxPerIP,
|
||||
RouteThroughXray: parsed.RouteThroughXray,
|
||||
XrayRoutePort: parsed.RouteXrayPort,
|
||||
PublicIPv4: strings.TrimSpace(parsed.PublicIPv4),
|
||||
PublicIPv6: strings.TrimSpace(parsed.PublicIPv6),
|
||||
}, true
|
||||
}
|
||||
|
||||
@@ -523,7 +530,7 @@ func FreeLocalPort() (int, error) {
|
||||
// precede any [section] header in TOML, and [secrets] must be the final section
|
||||
// so trailing keys are not swallowed by another table. The layout is therefore:
|
||||
// top-level scalars (incl. api-bind-to and api-token), then [domain-fronting],
|
||||
// [network] and [throttle], then [secret-ad-tags] for clients overriding the
|
||||
// [network], [throttle] and [defense.pending-handshakes], then [secret-ad-tags] for clients overriding the
|
||||
// global advertising tag, and finally [secrets] with one named secret per
|
||||
// active client.
|
||||
func renderConfig(inst Instance, apiPort int, apiToken string) string {
|
||||
@@ -569,6 +576,9 @@ func renderConfig(inst Instance, apiPort int, apiToken string) string {
|
||||
if inst.ThrottleMaxConnections > 0 {
|
||||
fmt.Fprintf(&b, "\n[throttle]\nmax-connections = %d\n", inst.ThrottleMaxConnections)
|
||||
}
|
||||
if inst.PendingHandshakesMaxPerIP > 0 {
|
||||
fmt.Fprintf(&b, "\n[defense.pending-handshakes]\nmax-per-ip = %d\n", inst.PendingHandshakesMaxPerIP)
|
||||
}
|
||||
// Only clients present in [secrets] may appear here: mtg rejects a config
|
||||
// whose [secret-ad-tags] names an unknown secret, so a disabled client's
|
||||
// override must vanish together with its secret.
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestInstanceFromInbound(t *testing.T) {
|
||||
Settings: `{"fakeTlsDomain":"example.com",` +
|
||||
`"debug":true,"proxyProtocolListener":true,"preferIp":"prefer-ipv4",` +
|
||||
`"domainFronting":{"ip":"127.0.0.1","port":9443,"proxyProtocol":true},` +
|
||||
`"throttleMaxConnections":5000,` +
|
||||
`"throttleMaxConnections":5000,"pendingHandshakesMaxPerIp":64,` +
|
||||
`"routeThroughXray":true,"routeXrayPort":50000,` +
|
||||
`"adTag":" 0123456789abcdef0123456789abcdef ",` +
|
||||
`"clients":[` +
|
||||
@@ -60,6 +60,9 @@ func TestInstanceFromInbound(t *testing.T) {
|
||||
if inst.ThrottleMaxConnections != 5000 {
|
||||
t.Fatalf("throttle not parsed: %+v", inst)
|
||||
}
|
||||
if inst.PendingHandshakesMaxPerIP != 64 {
|
||||
t.Fatalf("pending-handshake limit not parsed: %+v", inst)
|
||||
}
|
||||
if !inst.RouteThroughXray || inst.XrayRoutePort != 50000 {
|
||||
t.Fatalf("xray routing not parsed: %+v", inst)
|
||||
}
|
||||
@@ -90,7 +93,7 @@ func TestRenderConfig(t *testing.T) {
|
||||
Secrets: []SecretEntry{{Name: "alice", Secret: "ee00"}},
|
||||
Listen: "0.0.0.0", Port: 8443,
|
||||
}, 5000, "")
|
||||
for _, unwanted := range []string{"debug", "proxy-protocol-listener", "prefer-ip", "[domain-fronting]", "[stats.prometheus]", "[throttle]", "[secret-ad-tags]", "api-token"} {
|
||||
for _, unwanted := range []string{"debug", "proxy-protocol-listener", "prefer-ip", "[domain-fronting]", "[stats.prometheus]", "[throttle]", "[defense.pending-handshakes]", "[secret-ad-tags]", "api-token"} {
|
||||
if strings.Contains(bare, unwanted) {
|
||||
t.Fatalf("bare config should not contain %q:\n%s", unwanted, bare)
|
||||
}
|
||||
@@ -116,9 +119,10 @@ func TestRenderConfig(t *testing.T) {
|
||||
Listen: "0.0.0.0", Port: 443,
|
||||
Debug: true, ProxyProtocolListener: true, PreferIP: "only-ipv6",
|
||||
FrontingIP: "127.0.0.1", FrontingPort: 9443, FrontingProxyProtocol: true,
|
||||
ThrottleMaxConnections: 5000,
|
||||
PublicIPv4: "1.2.3.4",
|
||||
PublicIPv6: "2001:db8::1",
|
||||
ThrottleMaxConnections: 5000,
|
||||
PendingHandshakesMaxPerIP: 64,
|
||||
PublicIPv4: "1.2.3.4",
|
||||
PublicIPv6: "2001:db8::1",
|
||||
}, 6000, "sesame")
|
||||
for _, want := range []string{
|
||||
"debug = true\n",
|
||||
@@ -133,6 +137,7 @@ func TestRenderConfig(t *testing.T) {
|
||||
"proxy-protocol = true\n",
|
||||
"[throttle]",
|
||||
"max-connections = 5000",
|
||||
"[defense.pending-handshakes]\nmax-per-ip = 64\n",
|
||||
"[secret-ad-tags]",
|
||||
`"bob" = "fedcba9876543210fedcba9876543210"`,
|
||||
} {
|
||||
@@ -160,6 +165,9 @@ func TestRenderConfig(t *testing.T) {
|
||||
if strings.LastIndex(full, "[secrets]") < strings.Index(full, "[throttle]") {
|
||||
t.Fatalf("[throttle] must precede [secrets]:\n%s", full)
|
||||
}
|
||||
if strings.LastIndex(full, "[secrets]") < strings.Index(full, "[defense.pending-handshakes]") {
|
||||
t.Fatalf("[defense.pending-handshakes] must precede [secrets]:\n%s", full)
|
||||
}
|
||||
if strings.LastIndex(full, "[secrets]") < strings.Index(full, "[secret-ad-tags]") {
|
||||
t.Fatalf("[secret-ad-tags] must precede [secrets]:\n%s", full)
|
||||
}
|
||||
@@ -244,19 +252,20 @@ func TestFingerprintSplit(t *testing.T) {
|
||||
base := Instance{Secrets: []SecretEntry{{Name: "a", 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 },
|
||||
"throttle": func(i *Instance) { i.ThrottleMaxConnections = 5000 },
|
||||
"routeXray": func(i *Instance) { i.RouteThroughXray = true },
|
||||
"routeXrayPort": func(i *Instance) { i.XrayRoutePort = 50000 },
|
||||
"port": func(i *Instance) { i.Port = 8443 },
|
||||
"listen": func(i *Instance) { i.Listen = "127.0.0.1" },
|
||||
"publicIpv4": func(i *Instance) { i.PublicIPv4 = "1.2.3.4" },
|
||||
"publicIpv6": func(i *Instance) { i.PublicIPv6 = "2001:db8::1" },
|
||||
"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 },
|
||||
"throttle": func(i *Instance) { i.ThrottleMaxConnections = 5000 },
|
||||
"pendingHandshakes": func(i *Instance) { i.PendingHandshakesMaxPerIP = 64 },
|
||||
"routeXray": func(i *Instance) { i.RouteThroughXray = true },
|
||||
"routeXrayPort": func(i *Instance) { i.XrayRoutePort = 50000 },
|
||||
"port": func(i *Instance) { i.Port = 8443 },
|
||||
"listen": func(i *Instance) { i.Listen = "127.0.0.1" },
|
||||
"publicIpv4": func(i *Instance) { i.PublicIPv4 = "1.2.3.4" },
|
||||
"publicIpv6": func(i *Instance) { i.PublicIPv6 = "2001:db8::1" },
|
||||
} {
|
||||
t.Run("structural/"+name, func(t *testing.T) {
|
||||
changed := base
|
||||
|
||||
Reference in New Issue
Block a user