mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 09:57:14 +00:00
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.
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ export default function WireguardFields({ wgPubKey, regenInboundWg }: WireguardF
|
||||
<Form.Item label={t('pages.xray.wireguard.publicKey')}>
|
||||
<Input value={wgPubKey} disabled />
|
||||
</Form.Item>
|
||||
<FormField name={['settings', 'subnetIp']} label={t('pages.xray.wireguard.subnetIp')}>
|
||||
<Input placeholder="10.0.0.0" />
|
||||
</FormField>
|
||||
<FormField name={['settings', 'subnetCidr']} label={t('pages.xray.wireguard.subnetCidr')}>
|
||||
<InputNumber min={1} max={32} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
<FormField name={['settings', 'mtu']} label="MTU">
|
||||
<InputNumber />
|
||||
</FormField>
|
||||
|
||||
@@ -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<typeof WireguardInboundSettingsSchema>;
|
||||
|
||||
@@ -54,6 +54,8 @@ exports[`createDefault*InboundSettings factories > wireguard 1`] = `
|
||||
"noKernelTun": false,
|
||||
"peers": [],
|
||||
"secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=",
|
||||
"subnetCidr": 24,
|
||||
"subnetIp": "10.0.0.0",
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -622,6 +622,7 @@ exports[`InboundSchema (full) fixtures > parses wireguard-server byte-stably 1`]
|
||||
},
|
||||
],
|
||||
"secretKey": "iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=",
|
||||
"subnetIp": "",
|
||||
},
|
||||
"shareAddr": "",
|
||||
"shareAddrStrategy": "node",
|
||||
|
||||
@@ -248,6 +248,7 @@ exports[`InboundSettingsSchema fixtures > parses wireguard-basic byte-stably 1`]
|
||||
},
|
||||
],
|
||||
"secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=",
|
||||
"subnetIp": "",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1635,6 +1635,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "المفتاح السري",
|
||||
"publicKey": "المفتاح العام",
|
||||
"subnetIp": "الشبكة الفرعية",
|
||||
"subnetCidr": "بادئة الشبكة الفرعية (CIDR)",
|
||||
"allowedIPs": "عناوين IP المسموح بها",
|
||||
"endpoint": "النهاية",
|
||||
"domainStrategy": "استراتيجية الدومين"
|
||||
|
||||
@@ -1752,6 +1752,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "Secret Key",
|
||||
"publicKey": "Public Key",
|
||||
"subnetIp": "Subnet",
|
||||
"subnetCidr": "Subnet CIDR",
|
||||
"allowedIPs": "Allowed IPs",
|
||||
"endpoint": "Endpoint",
|
||||
"domainStrategy": "Domain Strategy"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1635,6 +1635,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "کلید شخصی",
|
||||
"publicKey": "کلید عمومی",
|
||||
"subnetIp": "زیرشبکه",
|
||||
"subnetCidr": "پیشوند زیرشبکه (CIDR)",
|
||||
"allowedIPs": "آیپیهای مجاز",
|
||||
"endpoint": "نقطه پایانی",
|
||||
"domainStrategy": "استراتژی حل دامنه"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1635,6 +1635,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "シークレットキー",
|
||||
"publicKey": "公開鍵",
|
||||
"subnetIp": "サブネット",
|
||||
"subnetCidr": "サブネットCIDR",
|
||||
"allowedIPs": "許可されたIP",
|
||||
"endpoint": "エンドポイント",
|
||||
"domainStrategy": "ドメイン戦略"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1635,6 +1635,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "Секретный ключ",
|
||||
"publicKey": "Публичный ключ",
|
||||
"subnetIp": "Подсеть",
|
||||
"subnetCidr": "Маска подсети (CIDR)",
|
||||
"allowedIPs": "Разрешенные IP-адреса",
|
||||
"endpoint": "Конечная точка",
|
||||
"domainStrategy": "Стратегия домена"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1635,6 +1635,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "Приватний ключ",
|
||||
"publicKey": "Публічний ключ",
|
||||
"subnetIp": "Підмережа",
|
||||
"subnetCidr": "CIDR підмережі",
|
||||
"allowedIPs": "Дозволені IP-адреси",
|
||||
"endpoint": "Кінцева точка",
|
||||
"domainStrategy": "Стратегія домену"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1635,6 +1635,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "密钥",
|
||||
"publicKey": "公钥",
|
||||
"subnetIp": "子网",
|
||||
"subnetCidr": "子网 CIDR",
|
||||
"allowedIPs": "允许的 IP",
|
||||
"endpoint": "端点",
|
||||
"domainStrategy": "域策略"
|
||||
|
||||
@@ -1635,6 +1635,8 @@
|
||||
"wireguard": {
|
||||
"secretKey": "金鑰",
|
||||
"publicKey": "公鑰",
|
||||
"subnetIp": "子網路",
|
||||
"subnetCidr": "子網路 CIDR",
|
||||
"allowedIPs": "允許的 IP",
|
||||
"endpoint": "端點",
|
||||
"domainStrategy": "域策略"
|
||||
|
||||
Reference in New Issue
Block a user