diff --git a/frontend/src/pages/inbounds/form/protocols/mtproto.tsx b/frontend/src/pages/inbounds/form/protocols/mtproto.tsx
index 12ca391e0..b5bebf87f 100644
--- a/frontend/src/pages/inbounds/form/protocols/mtproto.tsx
+++ b/frontend/src/pages/inbounds/form/protocols/mtproto.tsx
@@ -74,6 +74,13 @@ export default function MtprotoFields() {
>
+
+
+
= 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.
diff --git a/internal/mtproto/manager_test.go b/internal/mtproto/manager_test.go
index 8114e6f09..079840507 100644
--- a/internal/mtproto/manager_test.go
+++ b/internal/mtproto/manager_test.go
@@ -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
diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json
index 16e143831..f66b4531c 100644
--- a/internal/web/translation/ar-EG.json
+++ b/internal/web/translation/ar-EG.json
@@ -505,6 +505,8 @@
"mtprotoFakeTlsDomainHint": "نطاق FakeTLS الافتراضي المستخدم لإنشاء سر عميل جديد. يمكن لكل عميل استخدام نطاقه الخاص.",
"mtgThrottleMaxConnections": "الحد الأقصى للاتصالات",
"mtgThrottleMaxConnectionsHint": "تحديد الاتصالات المتزامنة لجميع المستخدمين بتوزيع عادل. القيمة 0 تعطّل الميزة.",
+ "mtgPendingHandshakesMaxPerIp": "الحد الأقصى للمصافحات المعلقة لكل IP",
+ "mtgPendingHandshakesMaxPerIpHint": "تُغلق الاتصالات الجديدة من عنوان IP لديه هذا العدد من المصافحات غير المكتملة. يتشارك المستخدمون خلف عنوان IP مشترك (NAT لدى المشغّل) هذا الحد. القيمة 0 تعطّله.",
"mtgAdTagInvalid": "يجب أن تتكوّن العلامة الإعلانية من 32 حرفًا ست عشريًا بالضبط.",
"mtgPublicIpv4": "عنوان IPv4 العام",
"mtgPublicIpv6": "عنوان IPv6 العام",
diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json
index 97290aa34..2ceb3b546 100644
--- a/internal/web/translation/en-US.json
+++ b/internal/web/translation/en-US.json
@@ -507,6 +507,8 @@
"mtprotoFakeTlsDomainHint": "Default FakeTLS domain used to generate a new client's secret. Each client can front its own domain.",
"mtgThrottleMaxConnections": "Max connections",
"mtgThrottleMaxConnectionsHint": "Cap concurrent connections across all users with a fair-share limit. 0 disables throttling.",
+ "mtgPendingHandshakesMaxPerIp": "Max pending handshakes per IP",
+ "mtgPendingHandshakesMaxPerIpHint": "Close new connections from an IP that already has this many unfinished handshakes. Clients behind a shared IP (carrier NAT) share the limit. 0 disables it.",
"mtgAdTagInvalid": "Ad-tag must be exactly 32 hexadecimal characters.",
"mtgPublicIpv4": "Public IPv4",
"mtgPublicIpv6": "Public IPv6",
diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json
index d6953176c..6fa03153e 100644
--- a/internal/web/translation/es-ES.json
+++ b/internal/web/translation/es-ES.json
@@ -505,6 +505,8 @@
"mtprotoFakeTlsDomainHint": "Dominio FakeTLS predeterminado para generar el secreto de un nuevo cliente. Cada cliente puede usar su propio dominio.",
"mtgThrottleMaxConnections": "Conexiones máximas",
"mtgThrottleMaxConnectionsHint": "Limita las conexiones simultáneas de todos los usuarios con reparto equitativo. 0 desactiva el límite.",
+ "mtgPendingHandshakesMaxPerIp": "Máx. handshakes pendientes por IP",
+ "mtgPendingHandshakesMaxPerIpHint": "Cierra las conexiones nuevas de una IP que ya tiene esta cantidad de handshakes sin completar. Los clientes detrás de una IP compartida (NAT del operador) comparten el límite. 0 lo desactiva.",
"mtgAdTagInvalid": "El ad-tag debe tener exactamente 32 caracteres hexadecimales.",
"mtgPublicIpv4": "IPv4 pública",
"mtgPublicIpv6": "IPv6 pública",
diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json
index 3a4e934c3..3b7cf60ec 100644
--- a/internal/web/translation/fa-IR.json
+++ b/internal/web/translation/fa-IR.json
@@ -505,6 +505,8 @@
"mtprotoFakeTlsDomainHint": "دامنه پیشفرض FakeTLS برای ساخت سکرت کلاینت جدید. هر کلاینت میتواند دامنه مخصوص خود را داشته باشد.",
"mtgThrottleMaxConnections": "حداکثر اتصالات",
"mtgThrottleMaxConnectionsHint": "محدود کردن اتصالات همزمان همه کاربران با تقسیم منصفانه. مقدار ۰ غیرفعال است.",
+ "mtgPendingHandshakesMaxPerIp": "حداکثر handshake معلق برای هر IP",
+ "mtgPendingHandshakesMaxPerIpHint": "اتصالهای جدید از IPای که این تعداد handshake ناتمام دارد بسته میشوند. کاربرانی که پشت یک IP مشترک (NAT اپراتور) هستند این سقف را با هم شریکاند. مقدار ۰ غیرفعال است.",
"mtgAdTagInvalid": "برچسب تبلیغاتی باید دقیقاً ۳۲ کاراکتر هگزادسیمال باشد.",
"mtgPublicIpv4": "IPv4 عمومی",
"mtgPublicIpv6": "IPv6 عمومی",
diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json
index 1af42df6f..23895aaaf 100644
--- a/internal/web/translation/id-ID.json
+++ b/internal/web/translation/id-ID.json
@@ -505,6 +505,8 @@
"mtprotoFakeTlsDomainHint": "Domain FakeTLS default untuk membuat secret klien baru. Setiap klien bisa memakai domainnya sendiri.",
"mtgThrottleMaxConnections": "Koneksi maksimum",
"mtgThrottleMaxConnectionsHint": "Batasi koneksi bersamaan semua pengguna dengan pembagian adil. 0 menonaktifkan.",
+ "mtgPendingHandshakesMaxPerIp": "Maks. handshake tertunda per IP",
+ "mtgPendingHandshakesMaxPerIpHint": "Tutup koneksi baru dari IP yang sudah memiliki handshake belum selesai sebanyak ini. Klien di balik IP bersama (NAT operator) berbagi batas ini. 0 menonaktifkan.",
"mtgAdTagInvalid": "Ad-tag harus tepat 32 karakter heksadesimal.",
"mtgPublicIpv4": "IPv4 Publik",
"mtgPublicIpv6": "IPv6 Publik",
diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json
index d8927f102..2ba08d6ba 100644
--- a/internal/web/translation/ja-JP.json
+++ b/internal/web/translation/ja-JP.json
@@ -526,6 +526,8 @@
"mtprotoFakeTlsDomainHint": "新しいクライアントのシークレット生成に使う既定の FakeTLS ドメイン。クライアントごとに別のドメインを使用できます。",
"mtgThrottleMaxConnections": "最大接続数",
"mtgThrottleMaxConnectionsHint": "全ユーザーの同時接続数を公平配分で制限します。0 で無効。",
+ "mtgPendingHandshakesMaxPerIp": "IP あたりの未完了ハンドシェイク上限",
+ "mtgPendingHandshakesMaxPerIpHint": "未完了のハンドシェイクをこの数だけ保持している IP からの新規接続を閉じます。共有 IP(キャリア NAT)配下のクライアントはこの上限を共有します。0 で無効。",
"mtgAdTagInvalid": "広告タグは正確に 32 文字の 16 進数である必要があります。",
"mtgPublicIpv4": "パブリック IPv4",
"mtgPublicIpv6": "パブリック IPv6",
diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json
index b78050110..a4331047e 100644
--- a/internal/web/translation/pt-BR.json
+++ b/internal/web/translation/pt-BR.json
@@ -526,6 +526,8 @@
"mtprotoFakeTlsDomainHint": "Domínio FakeTLS padrão usado para gerar o segredo de um novo cliente. Cada cliente pode usar seu próprio domínio.",
"mtgThrottleMaxConnections": "Conexões máximas",
"mtgThrottleMaxConnectionsHint": "Limita conexões simultâneas de todos os usuários com distribuição justa. 0 desativa o limite.",
+ "mtgPendingHandshakesMaxPerIp": "Máx. handshakes pendentes por IP",
+ "mtgPendingHandshakesMaxPerIpHint": "Fecha novas conexões de um IP que já tem esta quantidade de handshakes não concluídos. Clientes atrás de um IP compartilhado (NAT da operadora) dividem o limite. 0 desativa.",
"mtgAdTagInvalid": "A ad-tag deve ter exatamente 32 caracteres hexadecimais.",
"mtgPublicIpv4": "IPv4 público",
"mtgPublicIpv6": "IPv6 público",
diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json
index 59c308fb4..959047619 100644
--- a/internal/web/translation/ru-RU.json
+++ b/internal/web/translation/ru-RU.json
@@ -528,6 +528,8 @@
"mtprotoFakeTlsDomainHint": "Домен FakeTLS по умолчанию для генерации секрета нового клиента. Каждый клиент может использовать свой домен.",
"mtgThrottleMaxConnections": "Макс. подключений",
"mtgThrottleMaxConnectionsHint": "Ограничение одновременных подключений всех пользователей по справедливому распределению. 0 — отключено.",
+ "mtgPendingHandshakesMaxPerIp": "Макс. незавершённых рукопожатий на IP",
+ "mtgPendingHandshakesMaxPerIpHint": "Новые подключения с IP, у которого уже столько незавершённых рукопожатий, закрываются. Клиенты за общим IP (NAT оператора) делят этот лимит. 0 — отключено.",
"mtgAdTagInvalid": "Рекламный тег должен содержать ровно 32 шестнадцатеричных символа.",
"mtgPublicIpv4": "Публичный IPv4",
"mtgPublicIpv6": "Публичный IPv6",
diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json
index 656485cf4..cefea4fd2 100644
--- a/internal/web/translation/tr-TR.json
+++ b/internal/web/translation/tr-TR.json
@@ -505,6 +505,8 @@
"mtprotoFakeTlsDomainHint": "Yeni bir istemcinin sırrını oluştururken kullanılan varsayılan FakeTLS alan adı. Her istemci kendi alan adını kullanabilir.",
"mtgThrottleMaxConnections": "Maks. bağlantı",
"mtgThrottleMaxConnectionsHint": "Tüm kullanıcıların eşzamanlı bağlantılarını adil paylaşımla sınırlar. 0 devre dışı bırakır.",
+ "mtgPendingHandshakesMaxPerIp": "IP başına maks. bekleyen el sıkışma",
+ "mtgPendingHandshakesMaxPerIpHint": "Bu sayıda tamamlanmamış el sıkışması olan bir IP'den gelen yeni bağlantıları kapatır. Paylaşılan IP arkasındaki istemciler (operatör NAT) sınırı paylaşır. 0 devre dışı bırakır.",
"mtgAdTagInvalid": "Reklam etiketi tam olarak 32 onaltılık karakter olmalıdır.",
"mtgPublicIpv4": "Genel IPv4",
"mtgPublicIpv6": "Genel IPv6",
diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json
index f365d3f08..358ec5eec 100644
--- a/internal/web/translation/uk-UA.json
+++ b/internal/web/translation/uk-UA.json
@@ -505,6 +505,8 @@
"mtprotoFakeTlsDomainHint": "Домен FakeTLS за замовчуванням для генерації секрету нового клієнта. Кожен клієнт може використовувати власний домен.",
"mtgThrottleMaxConnections": "Макс. з'єднань",
"mtgThrottleMaxConnectionsHint": "Обмеження одночасних з'єднань усіх користувачів зі справедливим розподілом. 0 — вимкнено.",
+ "mtgPendingHandshakesMaxPerIp": "Макс. незавершених рукостискань на IP",
+ "mtgPendingHandshakesMaxPerIpHint": "Нові з'єднання з IP, що вже має стільки незавершених рукостискань, закриваються. Клієнти за спільним IP (NAT оператора) ділять цей ліміт. 0 — вимкнено.",
"mtgAdTagInvalid": "Рекламний тег має містити рівно 32 шістнадцяткові символи.",
"mtgPublicIpv4": "Публічний IPv4",
"mtgPublicIpv6": "Публічний IPv6",
diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json
index 98a6ddcab..277f25272 100644
--- a/internal/web/translation/vi-VN.json
+++ b/internal/web/translation/vi-VN.json
@@ -526,6 +526,8 @@
"mtprotoFakeTlsDomainHint": "Tên miền FakeTLS mặc định dùng để tạo secret cho client mới. Mỗi client có thể dùng tên miền riêng.",
"mtgThrottleMaxConnections": "Số kết nối tối đa",
"mtgThrottleMaxConnectionsHint": "Giới hạn kết nối đồng thời của tất cả người dùng theo phân bổ công bằng. 0 để tắt.",
+ "mtgPendingHandshakesMaxPerIp": "Số bắt tay chờ tối đa mỗi IP",
+ "mtgPendingHandshakesMaxPerIpHint": "Đóng kết nối mới từ IP đang có số bắt tay chưa hoàn tất bằng mức này. Các máy khách dùng chung IP (NAT nhà mạng) chia sẻ giới hạn. 0 để tắt.",
"mtgAdTagInvalid": "Ad-tag phải có đúng 32 ký tự thập lục phân.",
"mtgPublicIpv4": "IPv4 công khai",
"mtgPublicIpv6": "IPv6 công khai",
diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json
index 816bf5ee1..c26aa4ca2 100644
--- a/internal/web/translation/zh-CN.json
+++ b/internal/web/translation/zh-CN.json
@@ -525,6 +525,8 @@
"mtprotoFakeTlsDomainHint": "生成新客户端密钥时使用的默认 FakeTLS 域名。每个客户端可使用各自的域名。",
"mtgThrottleMaxConnections": "最大连接数",
"mtgThrottleMaxConnectionsHint": "按公平分配限制所有用户的并发连接数。0 表示不限制。",
+ "mtgPendingHandshakesMaxPerIp": "每 IP 最大未完成握手数",
+ "mtgPendingHandshakesMaxPerIpHint": "当某个 IP 的未完成握手达到此数量时,关闭其新连接。共享 IP(运营商 NAT)后的客户端共用此上限。0 表示禁用。",
"mtgAdTagInvalid": "广告标签必须为 32 位十六进制字符。",
"mtgPublicIpv4": "公网 IPv4",
"mtgPublicIpv6": "公网 IPv6",
diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json
index fc1949802..ebbecebf5 100644
--- a/internal/web/translation/zh-TW.json
+++ b/internal/web/translation/zh-TW.json
@@ -505,6 +505,8 @@
"mtprotoFakeTlsDomainHint": "產生新用戶端金鑰時使用的預設 FakeTLS 網域。每個用戶端可使用各自的網域。",
"mtgThrottleMaxConnections": "最大連線數",
"mtgThrottleMaxConnectionsHint": "以公平分配限制所有使用者的並行連線數。0 表示不限制。",
+ "mtgPendingHandshakesMaxPerIp": "每 IP 最大未完成交握數",
+ "mtgPendingHandshakesMaxPerIpHint": "當某個 IP 的未完成交握達到此數量時,關閉其新連線。共用 IP(電信 NAT)後的用戶端共用此上限。0 表示停用。",
"mtgAdTagInvalid": "廣告標籤必須為 32 位十六進位字元。",
"mtgPublicIpv4": "公網 IPv4",
"mtgPublicIpv6": "公網 IPv6",