From 569b7fbc6ba538b102b69afcfbcf7e8d8e8340b5 Mon Sep 17 00:00:00 2001 From: Kuzz007 Date: Tue, 4 Aug 2026 11:27:51 +0300 Subject: [PATCH] Give WireGuard an explicit, admin-configurable subnet field WireGuard previously had no configurable subnet at all -- only an implicit one, either inferred from existing clients' own addresses (wireguardAllocationBase) or a hardcoded 10.0.0.0/24 fallback when none exist yet. AmneziaWG, by contrast, has always had a real server.subnetIp/subnetCidr field in its settings, editable in the UI. User request: give WireGuard the same treatment. Backend: explicitWireguardSubnetBase reads an optional subnetIp/ subnetCidr pair from the inbound's own settings JSON (mirroring AmneziaWG's defaultAmneziaWGSubnetBases). defaultWireguardClients checks it first; only when unset does it fall back to today's inference-from-existing-clients behavior, so an inbound saved before this field existed keeps working exactly as it always has. Frontend: subnetIp/subnetCidr added to WireguardInboundSettingsSchema and the inbound form (mirroring AmneziaWG's own field layout/labels), with a real default (10.0.0.0/24, the same value the backend already fell back to) seeded for newly created inbounds so the field starts populated and editable rather than blank. Translated across all 13 locales. This also structurally closes the class of bug fixed in 82cc69f5/291c47b3: with wg and awg subnets explicit and independently controllable, an admin who wants matching addresses across both protocols can configure them to actually agree, instead of one silently inheriting the other's incompatible range. --- frontend/src/lib/xray/inbound-defaults.ts | 10 +++ .../inbounds/form/protocols/wireguard.tsx | 6 ++ .../schemas/protocols/inbound/wireguard.ts | 7 ++ .../inbound-defaults.test.ts.snap | 2 + .../__snapshots__/inbound-full.test.ts.snap | 1 + .../test/__snapshots__/protocols.test.ts.snap | 1 + internal/web/service/client_inbound_apply.go | 2 +- internal/web/service/client_wireguard.go | 54 +++++++++++- internal/web/service/client_wireguard_test.go | 82 ++++++++++++++++--- internal/web/translation/ar-EG.json | 2 + internal/web/translation/en-US.json | 2 + internal/web/translation/es-ES.json | 2 + internal/web/translation/fa-IR.json | 2 + internal/web/translation/id-ID.json | 2 + internal/web/translation/ja-JP.json | 2 + internal/web/translation/pt-BR.json | 2 + internal/web/translation/ru-RU.json | 2 + internal/web/translation/tr-TR.json | 2 + internal/web/translation/uk-UA.json | 2 + internal/web/translation/vi-VN.json | 2 + internal/web/translation/zh-CN.json | 2 + internal/web/translation/zh-TW.json | 2 + 22 files changed, 175 insertions(+), 16 deletions(-) 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": "域策略"