diff --git a/frontend/src/lib/xray/inbound-defaults.ts b/frontend/src/lib/xray/inbound-defaults.ts index 6098e5b16..3aa491294 100644 --- a/frontend/src/lib/xray/inbound-defaults.ts +++ b/frontend/src/lib/xray/inbound-defaults.ts @@ -257,12 +257,20 @@ export interface WireguardInboundSeed { mtu?: number; secretKey?: string; noKernelTun?: boolean; + subnetIp?: string; + subnetCidr?: number; } // WireGuard is multi-client now: a new inbound holds only the server identity // (secretKey/mtu) and starts with no clients. Clients (peers) are added later // through the client modal, which generates each one's keypair and a unique // tunnel address. peers stays empty for backward-compatible parsing. +// +// subnetIp/subnetCidr default to 10.0.0.0/24 here — the same value the Go +// backend has always fallen back to for an inbound with no clients yet — so +// a freshly created inbound shows an explicit, editable value from the +// start (matching AmneziaWG's own subnet field), rather than an empty one +// that silently relies on server-side inference until an admin fills it in. export function createDefaultWireguardInboundSettings( seed: WireguardInboundSeed = {}, ): WireguardInboundSettings { @@ -272,6 +280,8 @@ export function createDefaultWireguardInboundSettings( peers: [], clients: [], noKernelTun: seed.noKernelTun ?? false, + subnetIp: seed.subnetIp ?? '10.0.0.0', + subnetCidr: seed.subnetCidr ?? 24, }; } diff --git a/frontend/src/pages/inbounds/form/protocols/wireguard.tsx b/frontend/src/pages/inbounds/form/protocols/wireguard.tsx index 595a5bfb4..39eb2eda6 100644 --- a/frontend/src/pages/inbounds/form/protocols/wireguard.tsx +++ b/frontend/src/pages/inbounds/form/protocols/wireguard.tsx @@ -24,6 +24,12 @@ export default function WireguardFields({ wgPubKey, regenInboundWg }: WireguardF + + + + + + diff --git a/frontend/src/schemas/protocols/inbound/wireguard.ts b/frontend/src/schemas/protocols/inbound/wireguard.ts index 033716619..b984cd694 100644 --- a/frontend/src/schemas/protocols/inbound/wireguard.ts +++ b/frontend/src/schemas/protocols/inbound/wireguard.ts @@ -66,5 +66,12 @@ export const WireguardInboundSettingsSchema = z.object({ clients: z.array(WireguardClientSchema).default([]), noKernelTun: z.boolean().default(false), domainStrategy: WireguardDomainStrategySchema.optional(), + // Admin-configurable base subnet new clients are auto-allocated from — + // mirrors AmneziaWG's settings.server.subnetIp/subnetCidr. Optional and + // left blank by default: an inbound that never sets this keeps the + // pre-existing behavior (infer from existing clients' own addresses, else + // fall back to 10.0.0.0/24 server-side). + subnetIp: z.string().default(''), + subnetCidr: optionalClearedInt(z.number().int().min(1).max(32)), }); export type WireguardInboundSettings = z.infer; diff --git a/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap b/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap index 9766a4f5b..4e36cb1e4 100644 --- a/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap +++ b/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap @@ -54,6 +54,8 @@ exports[`createDefault*InboundSettings factories > wireguard 1`] = ` "noKernelTun": false, "peers": [], "secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=", + "subnetCidr": 24, + "subnetIp": "10.0.0.0", } `; diff --git a/frontend/src/test/__snapshots__/inbound-full.test.ts.snap b/frontend/src/test/__snapshots__/inbound-full.test.ts.snap index 8f2cd9538..453873ed0 100644 --- a/frontend/src/test/__snapshots__/inbound-full.test.ts.snap +++ b/frontend/src/test/__snapshots__/inbound-full.test.ts.snap @@ -622,6 +622,7 @@ exports[`InboundSchema (full) fixtures > parses wireguard-server byte-stably 1`] }, ], "secretKey": "iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=", + "subnetIp": "", }, "shareAddr": "", "shareAddrStrategy": "node", diff --git a/frontend/src/test/__snapshots__/protocols.test.ts.snap b/frontend/src/test/__snapshots__/protocols.test.ts.snap index c2eddb307..7ffc28223 100644 --- a/frontend/src/test/__snapshots__/protocols.test.ts.snap +++ b/frontend/src/test/__snapshots__/protocols.test.ts.snap @@ -248,6 +248,7 @@ exports[`InboundSettingsSchema fixtures > parses wireguard-basic byte-stably 1`] }, ], "secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=", + "subnetIp": "", }, } `; diff --git a/internal/web/service/client_inbound_apply.go b/internal/web/service/client_inbound_apply.go index fa5da1ecc..9ea26fa84 100644 --- a/internal/web/service/client_inbound_apply.go +++ b/internal/web/service/client_inbound_apply.go @@ -422,7 +422,7 @@ func (s *ClientService) addInboundClient(inboundSvc *InboundService, data *model return false, cErr } if oldInbound.Protocol == model.WireGuard { - if dErr := defaultWireguardClients(existingClients, clients, interfaceClients, crossUsed); dErr != nil { + if dErr := defaultWireguardClients(oldInbound.Settings, existingClients, clients, interfaceClients, crossUsed); dErr != nil { return false, dErr } } diff --git a/internal/web/service/client_wireguard.go b/internal/web/service/client_wireguard.go index cda8d4842..71f38e51f 100644 --- a/internal/web/service/client_wireguard.go +++ b/internal/web/service/client_wireguard.go @@ -1,6 +1,8 @@ package service import ( + "encoding/json" + "fmt" "net/netip" "strconv" "strings" @@ -12,6 +14,41 @@ import ( const defaultWireguardBase = "10.0.0.0/24" +// wireguardSubnetSettings is the subset of a WireGuard inbound's top-level +// settings JSON this package cares about for subnet resolution. Unlike +// AmneziaWG (whose whole settings shape is a typed struct in +// internal/amneziawg), plain WireGuard has no dedicated Go struct on this +// fork's side at all -- everything else is handled as untyped +// map[string]any -- so this stays a narrow, local decode rather than +// introducing a full struct just for two fields. +type wireguardSubnetSettings struct { + SubnetIP string `json:"subnetIp"` + SubnetCIDR int `json:"subnetCidr"` +} + +// explicitWireguardSubnetBase resolves an admin-configured subnet base out +// of settingsJSON's own subnetIp/subnetCidr fields, mirroring AmneziaWG's +// defaultAmneziaWGSubnetBases. Returns "" when either field is unset/empty +// or doesn't parse as a valid prefix -- callers fall back to +// wireguardAllocationBase's existing infer-from-clients behavior in that +// case, so an inbound saved before this field existed (or one that simply +// never set it) keeps behaving exactly as it always has. +func explicitWireguardSubnetBase(settingsJSON string) string { + var parsed wireguardSubnetSettings + if err := json.Unmarshal([]byte(settingsJSON), &parsed); err != nil { + return "" + } + ip := strings.TrimSpace(parsed.SubnetIP) + if ip == "" || parsed.SubnetCIDR <= 0 { + return "" + } + base := fmt.Sprintf("%s/%d", ip, parsed.SubnetCIDR) + if _, err := netip.ParsePrefix(base); err != nil { + return "" + } + return base +} + func keepAliveStr(seconds int) string { if seconds <= 0 { return "" @@ -149,16 +186,25 @@ func wireguardAllowedIPsCollision(entries, used []string) string { // crossInboundUsed maps AllowedIPs already claimed by clients on every OTHER // WireGuard/AmneziaWG inbound on this panel to a human-readable description // of which inbound holds it (see otherTunnelAllowedIPs). It is folded into -// used only AFTER wireguardAllocationBase runs, so an unrelated inbound's -// subnet can never skew this inbound's own base-subnet inference — it only +// used only AFTER the base subnet is resolved, so an unrelated inbound's +// subnet can never skew this inbound's own base-subnet resolution — it only // ever narrows which addresses are free to hand out or accept, and lets a // manual-entry collision name the other inbound instead of just the address. -func defaultWireguardClients(existing, clients []model.Client, interfaceClients []any, crossInboundUsed map[string]string) error { +// +// settingsJSON is checked first for an admin-configured subnetIp/subnetCidr +// (see explicitWireguardSubnetBase) — set explicitly, that always wins. +// Only when it's unset does base fall back to inferring from existing +// clients' own addresses, and finally to defaultWireguardBase, exactly as +// before this field existed. +func defaultWireguardClients(settingsJSON string, existing, clients []model.Client, interfaceClients []any, crossInboundUsed map[string]string) error { used := make([]string, 0) for i := range existing { used = append(used, existing[i].AllowedIPs...) } - base := wireguardAllocationBase(used, defaultWireguardBase) + base := explicitWireguardSubnetBase(settingsJSON) + if base == "" { + base = wireguardAllocationBase(used, defaultWireguardBase) + } for addr := range crossInboundUsed { used = append(used, addr) } diff --git a/internal/web/service/client_wireguard_test.go b/internal/web/service/client_wireguard_test.go index c1ea136a7..2a1ff5020 100644 --- a/internal/web/service/client_wireguard_test.go +++ b/internal/web/service/client_wireguard_test.go @@ -47,7 +47,7 @@ func TestAllocateWireguardAddress(t *testing.T) { func TestDefaultWireguardClientsGeneratesKeypair(t *testing.T) { clients := []model.Client{{Email: "a@wg"}} ifaces := []any{map[string]any{"email": "a@wg"}} - if err := defaultWireguardClients(nil, clients, ifaces, nil); err != nil { + if err := defaultWireguardClients("", nil, clients, ifaces, nil); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } c := clients[0] @@ -74,7 +74,7 @@ func TestDefaultWireguardClientsDerivesPublicKey(t *testing.T) { } clients := []model.Client{{Email: "b@wg", PrivateKey: priv}} ifaces := []any{map[string]any{"email": "b@wg"}} - if err := defaultWireguardClients(nil, clients, ifaces, nil); err != nil { + if err := defaultWireguardClients("", nil, clients, ifaces, nil); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } if clients[0].PublicKey != wantPub { @@ -90,7 +90,7 @@ func TestDefaultWireguardClientsPreservesProvided(t *testing.T) { AllowedIPs: []string{"10.0.0.50/32"}, }} ifaces := []any{map[string]any{"email": "c@wg"}} - if err := defaultWireguardClients(nil, clients, ifaces, nil); err != nil { + if err := defaultWireguardClients("", nil, clients, ifaces, nil); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } if clients[0].PrivateKey != "keep-priv" || clients[0].PublicKey != "keep-pub" { @@ -125,7 +125,7 @@ func TestDefaultWireguardClientsHonorsExistingSubnet(t *testing.T) { existing := []model.Client{{Email: "old@wg", AllowedIPs: []string{"172.16.0.2/32"}}} clients := []model.Client{{Email: "new@wg"}} ifaces := []any{map[string]any{"email": "new@wg"}} - if err := defaultWireguardClients(existing, clients, ifaces, nil); err != nil { + if err := defaultWireguardClients("", existing, clients, ifaces, nil); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } if got := clients[0].AllowedIPs[0]; got != "172.16.0.3/32" { @@ -184,7 +184,7 @@ func TestAllocateWireguardAddressWithoutWideningFailsClosed(t *testing.T) { func TestDefaultWireguardClientsAllocatesDistinctIPs(t *testing.T) { clients := []model.Client{{Email: "x@wg"}, {Email: "y@wg"}} ifaces := []any{map[string]any{"email": "x@wg"}, map[string]any{"email": "y@wg"}} - if err := defaultWireguardClients(nil, clients, ifaces, nil); err != nil { + if err := defaultWireguardClients("", nil, clients, ifaces, nil); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } if clients[0].AllowedIPs[0] == clients[1].AllowedIPs[0] { @@ -237,7 +237,7 @@ func TestDefaultWireguardClientsHonorsAndValidatesSuppliedAllowedIPs(t *testing. clients := []model.Client{{Email: "c@wg", AllowedIPs: []string{"10.0.0.9"}}} ifaces := []any{map[string]any{"email": "c@wg"}} - if err := defaultWireguardClients(existing, clients, ifaces, nil); err != nil { + if err := defaultWireguardClients("", existing, clients, ifaces, nil); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } if len(clients[0].AllowedIPs) != 1 || clients[0].AllowedIPs[0] != "10.0.0.9/32" { @@ -245,13 +245,13 @@ func TestDefaultWireguardClientsHonorsAndValidatesSuppliedAllowedIPs(t *testing. } dup := []model.Client{{Email: "d@wg", AllowedIPs: []string{"10.0.0.2/32"}}} - err := defaultWireguardClients(existing, dup, []any{map[string]any{"email": "d@wg"}}, nil) + err := defaultWireguardClients("", existing, dup, []any{map[string]any{"email": "d@wg"}}, nil) if err == nil { t.Fatal("duplicate allowedIPs across clients must be rejected") } bad := []model.Client{{Email: "e@wg", AllowedIPs: []string{"not-an-ip"}}} - if err := defaultWireguardClients(existing, bad, []any{map[string]any{"email": "e@wg"}}, nil); err == nil { + if err := defaultWireguardClients("", existing, bad, []any{map[string]any{"email": "e@wg"}}, nil); err == nil { t.Fatal("invalid allowedIPs entry must be rejected") } } @@ -264,7 +264,7 @@ func TestDefaultWireguardClientsHonorsAndValidatesSuppliedAllowedIPs(t *testing. func TestDefaultWireguardClientsRejectsCrossInboundDuplicate(t *testing.T) { crossUsed := map[string]string{"10.8.1.21/32": "inbound 'awg' (#10)"} dup := []model.Client{{Email: "d@wg", AllowedIPs: []string{"10.8.1.21/32"}}} - err := defaultWireguardClients(nil, dup, []any{map[string]any{"email": "d@wg"}}, crossUsed) + err := defaultWireguardClients("", nil, dup, []any{map[string]any{"email": "d@wg"}}, crossUsed) if err == nil { t.Fatal("allowedIPs already used on another inbound must be rejected") } @@ -279,7 +279,7 @@ func TestDefaultWireguardClientsAutoAllocateSkipsCrossInboundUsed(t *testing.T) crossUsed := map[string]string{"10.0.0.2/32": "inbound 'other-wg' (#7)"} clients := []model.Client{{Email: "f@wg"}} ifaces := []any{map[string]any{"email": "f@wg"}} - if err := defaultWireguardClients(nil, clients, ifaces, crossUsed); err != nil { + if err := defaultWireguardClients("", nil, clients, ifaces, crossUsed); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } if clients[0].AllowedIPs[0] != "10.0.0.3/32" { @@ -296,10 +296,70 @@ func TestDefaultWireguardClientsCrossInboundUsedDoesNotSkewSubnetInference(t *te crossUsed := map[string]string{"10.8.1.21/32": "inbound 'awg' (#10)"} clients := []model.Client{{Email: "g@wg"}} ifaces := []any{map[string]any{"email": "g@wg"}} - if err := defaultWireguardClients(nil, clients, ifaces, crossUsed); err != nil { + if err := defaultWireguardClients("", nil, clients, ifaces, crossUsed); err != nil { t.Fatalf("defaultWireguardClients: %v", err) } if got := clients[0].AllowedIPs[0]; got != "10.0.0.2/32" { t.Fatalf("base subnet must stay the default 10.0.0.0/24, not be skewed by a cross-inbound address; got %v", got) } } + +func TestExplicitWireguardSubnetBase(t *testing.T) { + tests := []struct { + name string + settingsJSON string + want string + }{ + {name: "unset settings", settingsJSON: `{"secretKey":"x"}`, want: ""}, + {name: "empty subnetIp", settingsJSON: `{"subnetIp":"","subnetCidr":24}`, want: ""}, + {name: "zero cidr", settingsJSON: `{"subnetIp":"10.8.1.0","subnetCidr":0}`, want: ""}, + {name: "invalid ip", settingsJSON: `{"subnetIp":"not-an-ip","subnetCidr":24}`, want: ""}, + {name: "invalid json", settingsJSON: `not json`, want: ""}, + {name: "configured subnet", settingsJSON: `{"subnetIp":"10.8.1.0","subnetCidr":24}`, want: "10.8.1.0/24"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := explicitWireguardSubnetBase(tt.settingsJSON); got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} + +// TestDefaultWireguardClientsPrefersExplicitSubnetOverInference is the +// backend half of a user-requested feature: WireGuard previously had no +// admin-configurable subnet at all, only an implicit one (inferred from +// existing clients' own addresses, or a hardcoded 10.0.0.0/24 fallback when +// none exist yet) -- unlike AmneziaWG, which has always had a real +// server.subnetIp/subnetCidr field. An explicit subnetIp/subnetCidr in the +// inbound's own settings must now win outright, even when existing clients +// would otherwise suggest a different base via wireguardAllocationBase. +func TestDefaultWireguardClientsPrefersExplicitSubnetOverInference(t *testing.T) { + existing := []model.Client{{Email: "old@wg", AllowedIPs: []string{"172.16.0.2/32"}}} + clients := []model.Client{{Email: "new@wg"}} + ifaces := []any{map[string]any{"email": "new@wg"}} + settingsJSON := `{"subnetIp":"10.8.1.0","subnetCidr":24}` + if err := defaultWireguardClients(settingsJSON, existing, clients, ifaces, nil); err != nil { + t.Fatalf("defaultWireguardClients: %v", err) + } + if got := clients[0].AllowedIPs[0]; got != "10.8.1.2/32" { + t.Fatalf("explicit subnet must win over inference from existing clients (172.16.0.0/24); got %v", got) + } +} + +// TestDefaultWireguardClientsFallsBackWhenNoExplicitSubnet locks in the +// backward-compat half of the same feature: an inbound saved before this +// field existed (settingsJSON carries no subnetIp/subnetCidr at all) must +// keep allocating exactly as it always has. +func TestDefaultWireguardClientsFallsBackWhenNoExplicitSubnet(t *testing.T) { + existing := []model.Client{{Email: "old@wg", AllowedIPs: []string{"172.16.0.2/32"}}} + clients := []model.Client{{Email: "new@wg"}} + ifaces := []any{map[string]any{"email": "new@wg"}} + settingsJSON := `{"secretKey":"x","peers":[],"clients":[]}` + if err := defaultWireguardClients(settingsJSON, existing, clients, ifaces, nil); err != nil { + t.Fatalf("defaultWireguardClients: %v", err) + } + if got := clients[0].AllowedIPs[0]; got != "172.16.0.3/32" { + t.Fatalf("with no explicit subnet, inference from existing clients must still apply; got %v", got) + } +} diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index b6634e7f0..ae6b5abb5 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "المفتاح السري", "publicKey": "المفتاح العام", + "subnetIp": "الشبكة الفرعية", + "subnetCidr": "بادئة الشبكة الفرعية (CIDR)", "allowedIPs": "عناوين IP المسموح بها", "endpoint": "النهاية", "domainStrategy": "استراتيجية الدومين" diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index f89e4cec9..d1de2f533 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -1752,6 +1752,8 @@ "wireguard": { "secretKey": "Secret Key", "publicKey": "Public Key", + "subnetIp": "Subnet", + "subnetCidr": "Subnet CIDR", "allowedIPs": "Allowed IPs", "endpoint": "Endpoint", "domainStrategy": "Domain Strategy" diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index ffc241017..d519e567c 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "Llave secreta", "publicKey": "Llave pública", + "subnetIp": "Subred", + "subnetCidr": "CIDR de la subred", "allowedIPs": "IP permitidas", "endpoint": "Punto final", "domainStrategy": "Estrategia de dominio" diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 2272d1add..32ec2a022 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "کلید شخصی", "publicKey": "کلید عمومی", + "subnetIp": "زیرشبکه", + "subnetCidr": "پیشوند زیرشبکه (CIDR)", "allowedIPs": "آی‌پی‌های مجاز", "endpoint": "نقطه پایانی", "domainStrategy": "استراتژی حل دامنه" diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index fc40a1f5e..da74f95b6 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "Kunci Rahasia", "publicKey": "Kunci Publik", + "subnetIp": "Subnet", + "subnetCidr": "CIDR Subnet", "allowedIPs": "IP yang Diizinkan", "endpoint": "Titik Akhir", "domainStrategy": "Strategi Domain" diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index c3137077f..278cd1909 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "シークレットキー", "publicKey": "公開鍵", + "subnetIp": "サブネット", + "subnetCidr": "サブネットCIDR", "allowedIPs": "許可されたIP", "endpoint": "エンドポイント", "domainStrategy": "ドメイン戦略" diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index c456efb6a..5efa59b28 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "Chave Secreta", "publicKey": "Chave Pública", + "subnetIp": "Sub-rede", + "subnetCidr": "CIDR da Sub-rede", "allowedIPs": "IPs Permitidos", "endpoint": "Ponto Final", "domainStrategy": "Estratégia de Domínio" diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 6a153008d..6b4bb4e5b 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "Секретный ключ", "publicKey": "Публичный ключ", + "subnetIp": "Подсеть", + "subnetCidr": "Маска подсети (CIDR)", "allowedIPs": "Разрешенные IP-адреса", "endpoint": "Конечная точка", "domainStrategy": "Стратегия домена" diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 82bf0998d..fc571fe74 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "Gizli Anahtar", "publicKey": "Genel Anahtar", + "subnetIp": "Alt Ağ", + "subnetCidr": "Alt Ağ CIDR", "allowedIPs": "İzin Verilen IP'ler", "endpoint": "Uç Nokta", "domainStrategy": "Alan Adı Stratejisi" diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 29f9c6cc6..b96cd72a8 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "Приватний ключ", "publicKey": "Публічний ключ", + "subnetIp": "Підмережа", + "subnetCidr": "CIDR підмережі", "allowedIPs": "Дозволені IP-адреси", "endpoint": "Кінцева точка", "domainStrategy": "Стратегія домену" diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index e15ffb7e2..874084d0d 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "Khoá bí mật", "publicKey": "Khóa công khai", + "subnetIp": "Mạng con", + "subnetCidr": "CIDR mạng con", "allowedIPs": "IP được phép", "endpoint": "Điểm cuối", "domainStrategy": "Chiến lược tên miền" diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index a65d541d7..8de788f43 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "密钥", "publicKey": "公钥", + "subnetIp": "子网", + "subnetCidr": "子网 CIDR", "allowedIPs": "允许的 IP", "endpoint": "端点", "domainStrategy": "域策略" diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index f396f1bc3..08020688a 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -1635,6 +1635,8 @@ "wireguard": { "secretKey": "金鑰", "publicKey": "公鑰", + "subnetIp": "子網路", + "subnetCidr": "子網路 CIDR", "allowedIPs": "允許的 IP", "endpoint": "端點", "domainStrategy": "域策略"