feat(amneziawg): make the Xray TPROXY bridge a per-inbound opt-in

Addresses Finding 10 from the automated PR review: an always-on TPROXY
bridge makes every AmneziaWG tunnel hard-depend on Xray being up (all
traffic, including DNS, drops whenever Xray restarts), and forces a full
awg-quick down+up bounce on any client add/remove/re-IP, permanently
losing the syncconf fast path.

Adds ServerSettings.RouteThroughXray (off by default):

- defaultPostUpDown only emits the TPROXY/policy-route rules when it's
  on; a plain AmneziaWG tunnel now has zero Xray dependency out of the
  box.
- structuralFingerprint covers it (toggling it changes whether PostUp/
  PostDown contain any TPROXY rules at all -- structural, not a
  per-peer host-rule). hostRulesFingerprint's IPv4 tracking is now
  itself conditional on RouteThroughXray (and IPv6 tracking on
  IPv6Enabled), so an instance that never uses either keeps the
  syncconf fast path for a plain peer re-IP.
- injectAmneziawgEgress only creates a bridge for inbounds that opted
  in; checkAmneziawgEgressConflict (the Finding-7 fix) now parses each
  candidate through InstanceFromInbound so a non-routed inbound's port
  is correctly never treated as reserved.
- New inbound-level Switch in the AmneziaWG form; the actual outbound
  decision is still made entirely through the panel's stock Routing
  page, same as before -- only whether the bridge exists at all is now
  a choice.

Translation keys added to all 13 locales in the same commit this time,
not backfilled later (see Finding 9's lesson).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kuzz007
2026-07-26 00:39:32 +03:00
parent c41f97cf86
commit 71dc453970
28 changed files with 257 additions and 85 deletions
+4
View File
@@ -2862,6 +2862,10 @@
"publicKey": { "publicKey": {
"type": "string" "type": "string"
}, },
"routeThroughXray": {
"description": "RouteThroughXray turns on this inbound's TPROXY-into-Xray bridge; see\nInstance.RouteThroughXray for what that means. Off by default.",
"type": "boolean"
},
"s1": { "s1": {
"type": "integer" "type": "integer"
}, },
+1
View File
@@ -666,6 +666,7 @@ export const EXAMPLES: Record<string, unknown> = {
"primaryDns": "", "primaryDns": "",
"privateKey": "", "privateKey": "",
"publicKey": "", "publicKey": "",
"routeThroughXray": false,
"s1": 0, "s1": 0,
"s2": 0, "s2": 0,
"s3": 0, "s3": 0,
+4
View File
@@ -2836,6 +2836,10 @@ export const SCHEMAS: Record<string, unknown> = {
"publicKey": { "publicKey": {
"type": "string" "type": "string"
}, },
"routeThroughXray": {
"description": "RouteThroughXray turns on this inbound's TPROXY-into-Xray bridge; see\nInstance.RouteThroughXray for what that means. Off by default.",
"type": "boolean"
},
"s1": { "s1": {
"type": "integer" "type": "integer"
}, },
+1
View File
@@ -649,6 +649,7 @@ export interface ServerSettings {
primaryDns?: string; primaryDns?: string;
privateKey: string; privateKey: string;
publicKey: string; publicKey: string;
routeThroughXray?: boolean;
s1: number; s1: number;
s2: number; s2: number;
s3: number; s3: number;
+1
View File
@@ -688,6 +688,7 @@ export const ServerSettingsSchema = z.object({
primaryDns: z.string().optional(), primaryDns: z.string().optional(),
privateKey: z.string(), privateKey: z.string(),
publicKey: z.string(), publicKey: z.string(),
routeThroughXray: z.boolean().optional(),
s1: z.number().int(), s1: z.number().int(),
s2: z.number().int(), s2: z.number().int(),
s3: z.number().int(), s3: z.number().int(),
@@ -298,6 +298,7 @@ export function createDefaultAmneziawgInboundSettings(): AmneziawgInboundSetting
ipv6Enabled: false, ipv6Enabled: false,
ipv6Subnet: '', ipv6Subnet: '',
ipv6ExternalInterface: '', ipv6ExternalInterface: '',
routeThroughXray: false,
jc: 5, jc: 5,
jmin: 10, jmin: 10,
jmax: 50, jmax: 50,
@@ -68,6 +68,14 @@ export default function AmneziawgFields({ awgPubKey, regenInboundAwg, regenInbou
> >
<Input placeholder="eth0" /> <Input placeholder="eth0" />
</FormField> </FormField>
<FormField
name={['settings', 'server', 'routeThroughXray']}
label={t('pages.xray.amneziawg.routeThroughXray')}
extra={t('pages.xray.amneziawg.routeThroughXrayHint')}
valueProp="checked"
>
<Switch />
</FormField>
<Form.Item label={t('pages.xray.amneziawg.obfuscation')}> <Form.Item label={t('pages.xray.amneziawg.obfuscation')}>
<Button icon={<ReloadOutlined />} onClick={regenInboundAwgObfuscation}> <Button icon={<ReloadOutlined />} onClick={regenInboundAwgObfuscation}>
{t('pages.xray.amneziawg.regenerateObfuscation')} {t('pages.xray.amneziawg.regenerateObfuscation')}
@@ -52,6 +52,7 @@ export const AmneziawgServerSchema = z.object({
ipv6Enabled: z.boolean().default(false), ipv6Enabled: z.boolean().default(false),
ipv6Subnet: z.string().default(''), ipv6Subnet: z.string().default(''),
ipv6ExternalInterface: z.string().default(''), ipv6ExternalInterface: z.string().default(''),
routeThroughXray: z.boolean().default(false),
jc: z.number().int().min(0).default(5), jc: z.number().int().min(0).default(5),
jmin: z.number().int().min(0).default(10), jmin: z.number().int().min(0).default(10),
jmax: z.number().int().min(0).default(50), jmax: z.number().int().min(0).default(50),
+63 -53
View File
@@ -85,6 +85,7 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
ExternalInterface: server.ExternalInterface, ExternalInterface: server.ExternalInterface,
IPv6Enabled: server.IPv6Enabled, IPv6Enabled: server.IPv6Enabled,
IPv6ExternalInterface: server.IPv6ExternalInterface, IPv6ExternalInterface: server.IPv6ExternalInterface,
RouteThroughXray: server.RouteThroughXray,
}, true }, true
} }
@@ -136,6 +137,7 @@ func (inst Instance) structuralFingerprint() string {
inst.ExternalInterface, inst.ExternalInterface,
strconv.FormatBool(inst.IPv6Enabled), strconv.FormatBool(inst.IPv6Enabled),
inst.IPv6ExternalInterface, inst.IPv6ExternalInterface,
strconv.FormatBool(inst.RouteThroughXray),
} }
return strings.Join(parts, "|") return strings.Join(parts, "|")
} }
@@ -157,26 +159,30 @@ func (inst Instance) peersFingerprint() string {
} }
// hostRulesFingerprint identifies per-peer state that only ever takes effect // hostRulesFingerprint identifies per-peer state that only ever takes effect
// through PostUp/PostDown shell rules — forwarded ports, the peer's IPv6 // through PostUp/PostDown shell rules — forwarded ports; when
// address (its NDP-proxy PostUp/PostDown entry), and (unconditionally, for // RouteThroughXray is on, every peer's IPv4 address (the TPROXY rule into
// every peer with a usable IPv4 address) the TPROXY rule into this // this instance's own Xray bridge is keyed on it); and when IPv6 is enabled,
// instance's own Xray bridge — rather than the WireGuard peer table itself. // the peer's IPv6 address (its NDP-proxy PostUp/PostDown entry) — rather
// It is checked separately from peersFingerprint because `awg syncconf` // than the WireGuard peer table itself. It is checked separately from
// never re-runs PostUp/PostDown, so a change here must force a full // peersFingerprint because `awg syncconf` never re-runs PostUp/PostDown, so
// interface bounce (ensureRestart) to actually take effect, unlike a // a change here must force a full interface bounce (ensureRestart) to
// key-only change that syncconf can apply in place. Since the TPROXY rule is // actually take effect, unlike a key-only change that syncconf can apply in
// now tied to every peer's mere presence (there's no more per-peer opt-in // place. The IPv4/IPv6 components are each included only when the feature
// flag), every peer is included unconditionally: adding, removing, or // that actually reads them is on: including them unconditionally would force
// re-addressing a peer now also forces a bounce, the same way a // a full bounce on every peer add/remove/re-IP even for an instance whose
// ForwardedPorts-only change always did. The IPv6 address must be included // PostUp/PostDown text never changes as a result, permanently losing the
// here too: without it, a peer that only changes its IPv6 AllowedIPs entry // syncconf fast path for no reason.
// still matches on FirstIPv4 alone, so ensureActionFor would pick the
// syncconf reload path — which never re-runs PostUp — leaving that peer's
// NDP-proxy entry pointed at its old, now-wrong address.
func (inst Instance) hostRulesFingerprint() string { func (inst Instance) hostRulesFingerprint() string {
pairs := make([]string, 0, len(inst.Peers)) pairs := make([]string, 0, len(inst.Peers))
for _, p := range inst.Peers { for _, p := range inst.Peers {
pairs = append(pairs, fmt.Sprintf("%s=fwd:%s;ip:%s;ip6:%s", p.Email, p.ForwardedPorts, FirstIPv4(p.AllowedIPs), firstIPv6(p.AllowedIPs))) v := fmt.Sprintf("%s=fwd:%s", p.Email, p.ForwardedPorts)
if inst.RouteThroughXray {
v += ";ip:" + FirstIPv4(p.AllowedIPs)
}
if inst.IPv6Enabled {
v += ";ip6:" + firstIPv6(p.AllowedIPs)
}
pairs = append(pairs, v)
} }
slices.Sort(pairs) slices.Sort(pairs)
return strings.Join(pairs, "|") return strings.Join(pairs, "|")
@@ -612,15 +618,16 @@ func hOrDefault(v, def string) string {
// rules, proxy_ndp sysctl, and one `ip -6 neigh add proxy` entry per enabled // rules, proxy_ndp sysctl, and one `ip -6 neigh add proxy` entry per enabled
// peer with an IPv6 address, so upstream routers see each client's IPv6 as // peer with an IPv6 address, so upstream routers see each client's IPv6 as
// directly reachable on the LAN without NAT66. Also emits DNAT+FORWARD rules // directly reachable on the LAN without NAT66. Also emits DNAT+FORWARD rules
// for each enabled peer with a non-empty ForwardedPorts spec, and — // for each enabled peer with a non-empty ForwardedPorts spec, and — only
// unconditionally, for every peer — a mangle-table TPROXY rule redirecting // when the instance has RouteThroughXray enabled — a mangle-table TPROXY
// that peer's traffic into this instance's own Xray bridge (see // rule redirecting every peer's traffic into this instance's own Xray
// EgressPortForInbound), plus the one-time policy route TPROXY needs to // bridge (see EgressPortForInbound), plus the one-time policy route TPROXY
// deliver it there. There is no per-peer or per-inbound opt-in: the bridge // needs to deliver it there. RouteThroughXray is off by default: a plain
// is always present by default, and it is entirely up to the admin's own // AmneziaWG tunnel has no Xray dependency at all unless the admin opts in.
// Xray Routing rules (targeting this inbound's own tag, which // When it is on, it is entirely up to the admin's own Xray Routing rules
// injectAmneziawgEgress reuses for the bridge) whether that traffic ever // (targeting this inbound's own tag, which injectAmneziawgEgress reuses for
// actually goes anywhere beyond Xray's default routing. // the bridge) whether that traffic ever actually goes anywhere beyond
// Xray's default routing.
func defaultPostUpDown(inst Instance, ext string) (postUp, postDown string) { func defaultPostUpDown(inst Instance, ext string) (postUp, postDown string) {
iface := inst.InterfaceName iface := inst.InterfaceName
up := []string{ up := []string{
@@ -675,34 +682,37 @@ func defaultPostUpDown(inst Instance, ext string) (postUp, postDown string) {
down = append(down, portForwardLines("-D", ext, iface, clientIP, p.Email, p.ForwardedPorts)...) down = append(down, portForwardLines("-D", ext, iface, clientIP, p.Email, p.ForwardedPorts)...)
} }
egressPort := EgressPortForInbound(inst.Id) if inst.RouteThroughXray {
anyPeerTproxied := false egressPort := EgressPortForInbound(inst.Id)
for _, p := range inst.Peers { anyPeerTproxied := false
clientIP := FirstIPv4(p.AllowedIPs) for _, p := range inst.Peers {
if clientIP == "" { clientIP := FirstIPv4(p.AllowedIPs)
continue if clientIP == "" {
continue
}
up = append(up, routeEgressLines("-A", iface, clientIP, p.Email, egressPort)...)
down = append(down, routeEgressLines("-D", iface, clientIP, p.Email, egressPort)...)
anyPeerTproxied = true
}
if anyPeerTproxied {
// The fwmark->table->local-everywhere policy route is what lets TPROXY
// deliver a peer's packets to this instance's own Xray bridge even
// though their destination is never one of this host's own addresses.
// It is system-wide, not interface-specific, so — like the
// IPv6-forwarding sysctl above — it is added idempotently here and
// never torn down in PostDown; a second AmneziaWG instance must find
// it already in place, not race to remove what the first still needs.
// "ip rule add" is not itself idempotent (a second call inserts a
// duplicate rather than deduplicating), and hostRulesFingerprint keys
// on every peer's presence/IP when RouteThroughXray is on, so PostUp
// re-runs on any client add/remove/re-IP — without the existence
// check below, "ip rule show" would accumulate one duplicate entry
// per bounce forever.
up = append(up,
fmt.Sprintf("ip rule list | grep -q 'fwmark %#x lookup %d' || ip rule add fwmark %#x lookup %d", EgressFwmark, EgressTable, EgressFwmark, EgressTable),
fmt.Sprintf("ip route replace local 0.0.0.0/0 dev lo table %d", EgressTable),
)
} }
up = append(up, routeEgressLines("-A", iface, clientIP, p.Email, egressPort)...)
down = append(down, routeEgressLines("-D", iface, clientIP, p.Email, egressPort)...)
anyPeerTproxied = true
}
if anyPeerTproxied {
// The fwmark->table->local-everywhere policy route is what lets TPROXY
// deliver a peer's packets to this instance's own Xray bridge even
// though their destination is never one of this host's own addresses.
// It is system-wide, not interface-specific, so — like the
// IPv6-forwarding sysctl above — it is added idempotently here and
// never torn down in PostDown; a second AmneziaWG instance must find
// it already in place, not race to remove what the first still needs.
// "ip rule add" is not itself idempotent (a second call inserts a
// duplicate rather than deduplicating), and hostRulesFingerprint keys
// on every peer's presence/IP, so PostUp re-runs on any client
// add/remove/re-IP — without the existence check below, "ip rule
// show" would accumulate one duplicate entry per bounce forever.
up = append(up,
fmt.Sprintf("ip rule list | grep -q 'fwmark %#x lookup %d' || ip rule add fwmark %#x lookup %d", EgressFwmark, EgressTable, EgressFwmark, EgressTable),
fmt.Sprintf("ip route replace local 0.0.0.0/0 dev lo table %d", EgressTable),
)
} }
up = append(up, "sysctl -w net.ipv4.ip_forward=1") up = append(up, "sysctl -w net.ipv4.ip_forward=1")
+57 -16
View File
@@ -171,6 +171,12 @@ func TestStructuralFingerprintStableAndSensitive(t *testing.T) {
if e.structuralFingerprint() == f.structuralFingerprint() { if e.structuralFingerprint() == f.structuralFingerprint() {
t.Fatal("changing IPv6ExternalInterface must change the structural fingerprint -- otherwise the edit is a complete no-op") t.Fatal("changing IPv6ExternalInterface must change the structural fingerprint -- otherwise the edit is a complete no-op")
} }
g := baseInstance()
g.RouteThroughXray = true
if a.structuralFingerprint() == g.structuralFingerprint() {
t.Fatal("toggling RouteThroughXray must change the structural fingerprint -- it changes whether PostUp/PostDown contain any TPROXY rules at all")
}
} }
func TestPeersFingerprintOrderIndependentButContentSensitive(t *testing.T) { func TestPeersFingerprintOrderIndependentButContentSensitive(t *testing.T) {
@@ -225,9 +231,6 @@ func TestHostRulesFingerprintCoversForwardedPortsAndPeerIP(t *testing.T) {
if a.hostRulesFingerprint() != b.hostRulesFingerprint() { if a.hostRulesFingerprint() != b.hostRulesFingerprint() {
t.Fatal("identical instances must produce the same host-rules fingerprint") t.Fatal("identical instances must produce the same host-rules fingerprint")
} }
if a.hostRulesFingerprint() == "" {
t.Fatal("every peer always gets a TPROXY rule now, so the fingerprint must never be empty when peers exist")
}
forwarded := baseInstance() forwarded := baseInstance()
forwarded.Peers[0].ForwardedPorts = "80,443" forwarded.Peers[0].ForwardedPorts = "80,443"
@@ -235,22 +238,47 @@ func TestHostRulesFingerprintCoversForwardedPortsAndPeerIP(t *testing.T) {
t.Fatal("adding ForwardedPorts must change the host-rules fingerprint") t.Fatal("adding ForwardedPorts must change the host-rules fingerprint")
} }
reIPed := baseInstance()
reIPed.Peers[0].AllowedIPs = []string{"10.8.1.250/32"}
if a.hostRulesFingerprint() == reIPed.hostRulesFingerprint() {
t.Fatal("changing a peer's IP must change the host-rules fingerprint -- its TPROXY rule is keyed on that IP")
}
fewer := baseInstance() fewer := baseInstance()
fewer.Peers = fewer.Peers[:1] fewer.Peers = fewer.Peers[:1]
if a.hostRulesFingerprint() == fewer.hostRulesFingerprint() { if a.hostRulesFingerprint() == fewer.hostRulesFingerprint() {
t.Fatal("removing a peer must change the host-rules fingerprint -- one fewer TPROXY rule is needed") t.Fatal("removing a peer must change the host-rules fingerprint -- one fewer peer entry exists regardless of what's tracked per peer")
} }
// RouteThroughXray off (baseInstance's default): no TPROXY rule depends
// on a peer's IPv4 address, so re-IPing one must NOT force a bounce --
// this is the whole point of making the bridge opt-in: an instance that
// never uses it keeps the syncconf fast path for a plain re-IP.
reIPedNoRoute := baseInstance()
reIPedNoRoute.Peers[0].AllowedIPs = []string{"10.8.1.250/32"}
if a.hostRulesFingerprint() != reIPedNoRoute.hostRulesFingerprint() {
t.Fatal("with RouteThroughXray off, changing a peer's IP must NOT change the host-rules fingerprint")
}
// RouteThroughXray on: now the TPROXY rule really is keyed on the IP.
routed := baseInstance()
routed.RouteThroughXray = true
routedReIPed := baseInstance()
routedReIPed.RouteThroughXray = true
routedReIPed.Peers[0].AllowedIPs = []string{"10.8.1.250/32"}
if routed.hostRulesFingerprint() == routedReIPed.hostRulesFingerprint() {
t.Fatal("with RouteThroughXray on, changing a peer's IP must change the host-rules fingerprint -- its TPROXY rule is keyed on that IP")
}
// IPv6Enabled off (baseInstance's default): no NDP-proxy entry depends
// on a peer's IPv6 address either, so adding one must not force a bounce.
ip6AddedNoIPv6 := baseInstance()
ip6AddedNoIPv6.Peers[0].AllowedIPs = []string{"10.8.1.2/32", "fd86:ea04:1115::2/128"}
if a.hostRulesFingerprint() != ip6AddedNoIPv6.hostRulesFingerprint() {
t.Fatal("with IPv6Enabled off, adding a peer's IPv6 address must NOT change the host-rules fingerprint")
}
ip6Base := baseInstance()
ip6Base.IPv6Enabled = true
ip6Added := baseInstance() ip6Added := baseInstance()
ip6Added.IPv6Enabled = true
ip6Added.Peers[0].AllowedIPs = []string{"10.8.1.2/32", "fd86:ea04:1115::2/128"} ip6Added.Peers[0].AllowedIPs = []string{"10.8.1.2/32", "fd86:ea04:1115::2/128"}
if a.hostRulesFingerprint() == ip6Added.hostRulesFingerprint() { if ip6Base.hostRulesFingerprint() == ip6Added.hostRulesFingerprint() {
t.Fatal("adding a peer's IPv6 address must change the host-rules fingerprint -- its NDP-proxy entry is keyed on it, and a change here must force the full bounce that (re-)runs PostUp") t.Fatal("with IPv6Enabled on, adding a peer's IPv6 address must change the host-rules fingerprint -- its NDP-proxy entry is keyed on it, and a change here must force the full bounce that (re-)runs PostUp")
} }
} }
@@ -315,8 +343,21 @@ func TestEgressPortForInbound(t *testing.T) {
} }
} }
func TestDefaultPostUpDownEmitsTproxyForEveryPeer(t *testing.T) { func TestDefaultPostUpDownOmitsTproxyWhenRouteThroughXrayOff(t *testing.T) {
inst := baseInstance() // two peers, a@x and b@x, no opt-in flag exists anymore inst := baseInstance() // RouteThroughXray defaults to false
up, down := defaultPostUpDown(inst, "eth0")
if strings.Contains(up, "TPROXY") || strings.Contains(up, "ip rule add fwmark") {
t.Errorf("RouteThroughXray off must emit no TPROXY/policy-route lines in PostUp, got:\n%s", up)
}
if strings.Contains(down, "TPROXY") {
t.Errorf("RouteThroughXray off must emit no TPROXY lines in PostDown, got:\n%s", down)
}
}
func TestDefaultPostUpDownEmitsTproxyForEveryPeerWhenRouteThroughXrayOn(t *testing.T) {
inst := baseInstance() // two peers, a@x and b@x
inst.RouteThroughXray = true
up, down := defaultPostUpDown(inst, "eth0") up, down := defaultPostUpDown(inst, "eth0")
wantPort := fmt.Sprintf("--on-port %d", EgressPortForInbound(inst.Id)) wantPort := fmt.Sprintf("--on-port %d", EgressPortForInbound(inst.Id))
@@ -335,12 +376,12 @@ func TestDefaultPostUpDownEmitsTproxyForEveryPeer(t *testing.T) {
if strings.Contains(down, "ip rule") || strings.Contains(down, "ip route") { if strings.Contains(down, "ip rule") || strings.Contains(down, "ip route") {
t.Error("the shared policy route must never be removed in PostDown -- other instances may still need it") t.Error("the shared policy route must never be removed in PostDown -- other instances may still need it")
} }
// Both peers always get TPROXY'd now, no opt-in: 2 peers * 2 protocols. // Both peers get TPROXY'd once opted in: 2 peers * 2 protocols.
if got := strings.Count(up, "TPROXY"); got != 4 { if got := strings.Count(up, "TPROXY"); got != 4 {
t.Errorf("expected exactly 4 TPROXY lines (tcp+udp for each of the 2 peers), got %d in:\n%s", got, up) t.Errorf("expected exactly 4 TPROXY lines (tcp+udp for each of the 2 peers), got %d in:\n%s", got, up)
} }
none := Instance{Id: 2, InterfaceName: "awg2"} // no peers at all none := Instance{Id: 2, InterfaceName: "awg2", RouteThroughXray: true} // no peers at all
upNone, _ := defaultPostUpDown(none, "eth0") upNone, _ := defaultPostUpDown(none, "eth0")
if strings.Contains(upNone, "TPROXY") || strings.Contains(upNone, "ip rule add fwmark") { if strings.Contains(upNone, "TPROXY") || strings.Contains(upNone, "ip rule add fwmark") {
t.Errorf("an instance with no peers must not emit any TPROXY/policy-route lines, got:\n%s", upNone) t.Errorf("an instance with no peers must not emit any TPROXY/policy-route lines, got:\n%s", upNone)
+16
View File
@@ -68,6 +68,18 @@ type Instance struct {
// entries specifically; empty means reuse ExternalInterface. // entries specifically; empty means reuse ExternalInterface.
IPv6Enabled bool IPv6Enabled bool
IPv6ExternalInterface string IPv6ExternalInterface string
// RouteThroughXray gates the entire TPROXY-into-Xray bridge (see
// EgressPortForInbound / injectAmneziawgEgress) for this instance: off by
// default, so a plain AmneziaWG tunnel never depends on Xray being up at
// all. Turning it on makes every peer's traffic TPROXY'd into this
// instance's own loopback Xray bridge, tagged with the inbound's own
// tag; the actual routing decision from there is left entirely to the
// panel's stock Routing page (pick this inbound's tag as source, an
// outbound, and optionally a peer's IP), exactly like routing any other
// protocol -- only whether the bridge exists at all is a per-inbound
// choice.
RouteThroughXray bool
} }
// ServerSettings is the "server" block of an AmneziaWG inbound's Settings // ServerSettings is the "server" block of an AmneziaWG inbound's Settings
@@ -101,6 +113,10 @@ type ServerSettings struct {
IPv6Subnet string `json:"ipv6Subnet,omitempty"` IPv6Subnet string `json:"ipv6Subnet,omitempty"`
IPv6ExternalInterface string `json:"ipv6ExternalInterface,omitempty"` IPv6ExternalInterface string `json:"ipv6ExternalInterface,omitempty"`
// RouteThroughXray turns on this inbound's TPROXY-into-Xray bridge; see
// Instance.RouteThroughXray for what that means. Off by default.
RouteThroughXray bool `json:"routeThroughXray,omitempty"`
// Obfuscation20's fields, repeated flat (not embedded) rather than // Obfuscation20's fields, repeated flat (not embedded) rather than
// nested under their own key: encoding/json would happily inline an // nested under their own key: encoding/json would happily inline an
// embedded Obfuscation20 the same way, but the frontend's Go->Zod/TS // embedded Obfuscation20 the same way, but the frontend's Go->Zod/TS
+10 -3
View File
@@ -231,9 +231,12 @@ func (s *InboundService) checkPortConflict(inbound *model.Inbound, ignoreId int)
// checkAmneziawgEgressConflict reports whether inbound's own port collides // checkAmneziawgEgressConflict reports whether inbound's own port collides
// with an existing, enabled local AmneziaWG inbound's automatic Xray bridge // with an existing, enabled local AmneziaWG inbound's automatic Xray bridge
// port. ignoreId excludes one inbound id from the AmneziaWG candidates, the // port. Only inbounds that actually have RouteThroughXray on ever get a
// same way the general DB-backed conflict query above excludes the inbound // bridge (see injectAmneziawgEgress); the others' "reserved" port isn't
// being edited from matching itself. // really reserved, so they must not be flagged. ignoreId excludes one
// inbound id from the AmneziaWG candidates, the same way the general
// DB-backed conflict query above excludes the inbound being edited from
// matching itself.
func (s *InboundService) checkAmneziawgEgressConflict(inbound *model.Inbound, ignoreId int, newBits transportBits) (*portConflictDetail, error) { func (s *InboundService) checkAmneziawgEgressConflict(inbound *model.Inbound, ignoreId int, newBits transportBits) (*portConflictDetail, error) {
db := database.GetDB() db := database.GetDB()
var candidates []*model.Inbound var candidates []*model.Inbound
@@ -245,6 +248,10 @@ func (s *InboundService) checkAmneziawgEgressConflict(inbound *model.Inbound, ig
return nil, err return nil, err
} }
for _, c := range candidates { for _, c := range candidates {
inst, ok := amneziawg.InstanceFromInbound(c)
if !ok || !inst.RouteThroughXray {
continue
}
if amneziawg.EgressPortForInbound(c.Id) != inbound.Port { if amneziawg.EgressPortForInbound(c.Id) != inbound.Port {
continue continue
} }
+34 -2
View File
@@ -731,13 +731,19 @@ func TestCheckPortConflict_ReservedAPIPortUDPCoexists(t *testing.T) {
} }
} }
// amneziawgRoutedSettings builds a minimal but complete AmneziaWG settings
// blob with one qualifying, enabled peer and RouteThroughXray on -- the
// shape that actually makes injectAmneziawgEgress (and therefore
// checkAmneziawgEgressConflict) create a bridge at all.
const amneziawgRoutedSettings = `{"server":{"privateKey":"priv","publicKey":"pub","subnetIp":"10.8.1.0","subnetCidr":24,"routeThroughXray":true},"clients":[{"email":"a@x","enable":true,"publicKey":"pub-a","allowedIPs":["10.8.1.2/32"]}]}`
// An enabled AmneziaWG inbound's automatic Xray bridge (injectAmneziawgEgress) // An enabled AmneziaWG inbound's automatic Xray bridge (injectAmneziawgEgress)
// is a synthetic loopback dokodemo-door inbound, not a database row, so // is a synthetic loopback dokodemo-door inbound, not a database row, so
// checkPortConflict needs its own check to catch a collision -- exactly the // checkPortConflict needs its own check to catch a collision -- exactly the
// same shape of problem as the reserved API port above. // same shape of problem as the reserved API port above.
func TestCheckPortConflict_AmneziawgEgressBridgeBlockedLocal(t *testing.T) { func TestCheckPortConflict_AmneziawgEgressBridgeBlockedLocal(t *testing.T) {
setupConflictDB(t) setupConflictDB(t)
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`) seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
var awgInbound model.Inbound var awgInbound model.Inbound
if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil { if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
@@ -769,7 +775,7 @@ func TestCheckPortConflict_AmneziawgEgressBridgeBlockedLocal(t *testing.T) {
// 127.0.0.1 on the local panel's own Xray. // 127.0.0.1 on the local panel's own Xray.
func TestCheckPortConflict_AmneziawgEgressBridgeAllowedOnNode(t *testing.T) { func TestCheckPortConflict_AmneziawgEgressBridgeAllowedOnNode(t *testing.T) {
setupConflictDB(t) setupConflictDB(t)
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`) seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
var awgInbound model.Inbound var awgInbound model.Inbound
if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil { if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
@@ -813,6 +819,32 @@ func TestCheckPortConflict_AmneziawgEgressBridgeIgnoredWhenDisabled(t *testing.T
} }
} }
// An enabled AmneziaWG inbound with RouteThroughXray off never gets a bridge
// injected either (injectAmneziawgEgress requires it), so its port isn't
// reserved -- an inbound created with the default settings, not just an
// explicitly disabled one, must not block anything.
func TestCheckPortConflict_AmneziawgEgressBridgeIgnoredWhenRouteThroughXrayOff(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`)
var awgInbound model.Inbound
if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
t.Fatalf("read seeded row: %v", err)
}
bridgePort := amneziawg.EgressPortForInbound(awgInbound.Id)
svc := &InboundService{}
candidate := &model.Inbound{
Tag: "vless-bridge",
Listen: "0.0.0.0",
Port: bridgePort,
Protocol: model.VLESS,
}
if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil {
t.Fatalf("an AmneziaWG inbound with RouteThroughXray off must not reserve its bridge port; got=%v err=%v", got, err)
}
}
// An unrelated port never conflicts with the bridge. // An unrelated port never conflicts with the bridge.
func TestCheckPortConflict_AmneziawgEgressBridgeDifferentPortAllowed(t *testing.T) { func TestCheckPortConflict_AmneziawgEgressBridgeDifferentPortAllowed(t *testing.T) {
setupConflictDB(t) setupConflictDB(t)
+13 -10
View File
@@ -645,18 +645,21 @@ const amneziawgEgressDokodemoSettings = `{"allowedNetwork":"tcp,udp","followRedi
// socket. // socket.
const amneziawgEgressStreamSettings = `{"sockopt":{"tproxy":"tproxy"}}` const amneziawgEgressStreamSettings = `{"sockopt":{"tproxy":"tproxy"}}`
// injectAmneziawgEgress gives every enabled AmneziaWG inbound with at least // injectAmneziawgEgress gives every enabled, RouteThroughXray-opted-in
// one qualifying peer its own loopback dokodemo-door bridge — tagged with // AmneziaWG inbound with at least one qualifying peer its own loopback
// that inbound's own real tag, so it's already selectable in the panel's // dokodemo-door bridge — tagged with that inbound's own real tag, so it's
// stock Routing page's inbound-tag picker, exactly the way an mtproto // already selectable in the panel's stock Routing page's inbound-tag
// inbound's own bridge already is (see injectMtprotoEgress): the picker's // picker, exactly the way an mtproto inbound's own bridge already is (see
// tag list comes from InboundService.GetInboundTags(), a plain, // injectMtprotoEgress): the picker's tag list comes from
// InboundService.GetInboundTags(), a plain,
// protocol-blind SELECT over every inbound row's tag, so reusing a real // protocol-blind SELECT over every inbound row's tag, so reusing a real
// inbound's own tag needs no dedicated UI plumbing at all. // inbound's own tag needs no dedicated UI plumbing at all.
// //
// Every peer's traffic always lands on the bridge — internal/amneziawg's // RouteThroughXray is a per-inbound opt-in, off by default: when it's off,
// defaultPostUpDown TPROXYs it there unconditionally, there is no per-peer // no bridge is created at all and the tunnel has no Xray dependency
// or per-inbound opt-in flag — but this function never generates a routing // whatsoever. When it's on, every peer's traffic lands on the bridge —
// internal/amneziawg's defaultPostUpDown TPROXYs it there, there is no
// further per-peer opt-in — but this function never generates a routing
// rule of its own. Whether that traffic goes anywhere beyond Xray's default // rule of its own. Whether that traffic goes anywhere beyond Xray's default
// routing is entirely up to whatever rules the admin adds through that same // routing is entirely up to whatever rules the admin adds through that same
// stock Routing page (inboundTag + an optional sourceIP to target one // stock Routing page (inboundTag + an optional sourceIP to target one
@@ -680,7 +683,7 @@ func injectAmneziawgEgress(cfg *xray.Config, inbounds []*model.Inbound) {
continue continue
} }
inst, ok := amneziawg.InstanceFromInbound(inbound) inst, ok := amneziawg.InstanceFromInbound(inbound)
if !ok { if !ok || !inst.RouteThroughXray {
continue continue
} }
hasQualifyingPeer := false hasQualifyingPeer := false
@@ -561,7 +561,7 @@ func TestInjectMtprotoEgress_BadRoutingSkips(t *testing.T) {
} }
func amneziawgInbound(id int, tag string, clients []model.Client) *model.Inbound { func amneziawgInbound(id int, tag string, clients []model.Client) *model.Inbound {
server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24} server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24, RouteThroughXray: true}
settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients}) settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients})
return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)} return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
} }
@@ -643,6 +643,22 @@ func TestInjectAmneziawgEgress_NoQualifyingPeerSkipsBridge(t *testing.T) {
} }
} }
func TestInjectAmneziawgEgress_RouteThroughXrayOffSkipsBridge(t *testing.T) {
cfg := egressTestConfig()
server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24} // RouteThroughXray left false
settings, _ := json.Marshal(amneziawg.InboundSettings{
Server: &server,
Clients: []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
},
})
inbound := &model.Inbound{Id: 1, Tag: "awg-1", Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 1 {
t.Fatalf("an inbound with RouteThroughXray off must never get a bridge, got %+v", cfg.InboundConfigs)
}
}
func TestInjectAmneziawgEgress_WrongProtocolOrNodeSkipped(t *testing.T) { func TestInjectAmneziawgEgress_WrongProtocolOrNodeSkipped(t *testing.T) {
cfg := egressTestConfig() cfg := egressTestConfig()
vless := &model.Inbound{Id: 1, Tag: "in-1", Protocol: model.VLESS, Enable: true} vless := &model.Inbound{Id: 1, Tag: "in-1", Protocol: model.VLESS, Enable: true}
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "مثل fd86:ea04:1115::/64. مطلوب عند تفعيل IPv6.", "ipv6SubnetHint": "مثل fd86:ea04:1115::/64. مطلوب عند تفعيل IPv6.",
"ipv6ExternalInterface": "الواجهة الخارجية لـ IPv6", "ipv6ExternalInterface": "الواجهة الخارجية لـ IPv6",
"ipv6ExternalInterfaceHint": "واجهة الشبكة على الخادم لإدخالات وكيل NDP. اتركها فارغة لاستخدام الواجهة الخارجية.", "ipv6ExternalInterfaceHint": "واجهة الشبكة على الخادم لإدخالات وكيل NDP. اتركها فارغة لاستخدام الواجهة الخارجية.",
"routeThroughXray": "التوجيه عبر Xray",
"routeThroughXrayHint": "معطّل افتراضيًا: لا يعتمد النفق على Xray إطلاقًا. عند التفعيل، يُعاد توجيه ترافيك كل عميل إلى جسر Xray الخاص بهذا الاتصال الوارد، ويُختار الاتصال الصادر الفعلي من صفحة التوجيه تمامًا مثل أي بروتوكول آخر.",
"obfuscation": "معاملات التمويه", "obfuscation": "معاملات التمويه",
"regenerateObfuscation": "إعادة التوليد", "regenerateObfuscation": "إعادة التوليد",
"jc": "Jc (عدد الحزم العشوائية)", "jc": "Jc (عدد الحزم العشوائية)",
+2
View File
@@ -1940,6 +1940,8 @@
"ipv6SubnetHint": "e.g. fd86:ea04:1115::/64. Required when IPv6 is enabled.", "ipv6SubnetHint": "e.g. fd86:ea04:1115::/64. Required when IPv6 is enabled.",
"ipv6ExternalInterface": "IPv6 External Interface", "ipv6ExternalInterface": "IPv6 External Interface",
"ipv6ExternalInterfaceHint": "Host NIC for the NDP proxy entries. Leave empty to reuse External Interface.", "ipv6ExternalInterfaceHint": "Host NIC for the NDP proxy entries. Leave empty to reuse External Interface.",
"routeThroughXray": "Route through Xray",
"routeThroughXrayHint": "Off by default: the tunnel has no dependency on Xray at all. When on, every client's traffic is redirected into this inbound's own Xray bridge, and the actual outbound is chosen from the Routing page like any other protocol.",
"obfuscation": "Obfuscation parameters", "obfuscation": "Obfuscation parameters",
"regenerateObfuscation": "Regenerate", "regenerateObfuscation": "Regenerate",
"jc": "Jc (junk packet count)", "jc": "Jc (junk packet count)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "p. ej. fd86:ea04:1115::/64. Obligatorio cuando IPv6 está habilitado.", "ipv6SubnetHint": "p. ej. fd86:ea04:1115::/64. Obligatorio cuando IPv6 está habilitado.",
"ipv6ExternalInterface": "Interfaz externa IPv6", "ipv6ExternalInterface": "Interfaz externa IPv6",
"ipv6ExternalInterfaceHint": "Interfaz de red del host para las entradas de proxy NDP. Déjalo vacío para reutilizar la interfaz externa.", "ipv6ExternalInterfaceHint": "Interfaz de red del host para las entradas de proxy NDP. Déjalo vacío para reutilizar la interfaz externa.",
"routeThroughXray": "Enrutar a través de Xray",
"routeThroughXrayHint": "Desactivado de forma predeterminada: el túnel no depende de Xray en absoluto. Al activarlo, el tráfico de cada cliente se redirige al puente Xray propio de esta entrada, y la salida real se elige desde la página de Enrutamiento igual que con cualquier otro protocolo.",
"obfuscation": "Parámetros de ofuscación", "obfuscation": "Parámetros de ofuscación",
"regenerateObfuscation": "Regenerar", "regenerateObfuscation": "Regenerar",
"jc": "Jc (cantidad de paquetes basura)", "jc": "Jc (cantidad de paquetes basura)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "مثلاً fd86:ea04:1115::/64. هنگام فعال بودن IPv6 الزامی است.", "ipv6SubnetHint": "مثلاً fd86:ea04:1115::/64. هنگام فعال بودن IPv6 الزامی است.",
"ipv6ExternalInterface": "رابط خارجی IPv6", "ipv6ExternalInterface": "رابط خارجی IPv6",
"ipv6ExternalInterfaceHint": "رابط شبکه میزبان برای ورودی‌های پراکسی NDP. برای استفاده از رابط خارجی خالی بگذارید.", "ipv6ExternalInterfaceHint": "رابط شبکه میزبان برای ورودی‌های پراکسی NDP. برای استفاده از رابط خارجی خالی بگذارید.",
"routeThroughXray": "مسیریابی از طریق Xray",
"routeThroughXrayHint": "به‌طور پیش‌فرض خاموش است: تونل هیچ وابستگی‌ای به Xray ندارد. با روشن‌کردن آن، ترافیک هر کلاینت به پل Xray مخصوص همین اینباند هدایت می‌شود و مسیر خروجی واقعی از صفحه مسیریابی، دقیقاً مثل هر پروتکل دیگر، انتخاب می‌شود.",
"obfuscation": "پارامترهای مبهم‌سازی", "obfuscation": "پارامترهای مبهم‌سازی",
"regenerateObfuscation": "بازتولید", "regenerateObfuscation": "بازتولید",
"jc": "Jc (تعداد بسته‌های زباله)", "jc": "Jc (تعداد بسته‌های زباله)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "mis. fd86:ea04:1115::/64. Wajib diisi saat IPv6 diaktifkan.", "ipv6SubnetHint": "mis. fd86:ea04:1115::/64. Wajib diisi saat IPv6 diaktifkan.",
"ipv6ExternalInterface": "NIC Eksternal IPv6", "ipv6ExternalInterface": "NIC Eksternal IPv6",
"ipv6ExternalInterfaceHint": "NIC host untuk entri proxy NDP. Biarkan kosong untuk menggunakan NIC Eksternal.", "ipv6ExternalInterfaceHint": "NIC host untuk entri proxy NDP. Biarkan kosong untuk menggunakan NIC Eksternal.",
"routeThroughXray": "Rutekan melalui Xray",
"routeThroughXrayHint": "Nonaktif secara default: tunnel tidak bergantung pada Xray sama sekali. Jika diaktifkan, trafik setiap klien dialihkan ke bridge Xray milik inbound ini sendiri, dan outbound sebenarnya dipilih dari halaman Routing seperti protokol lainnya.",
"obfuscation": "Parameter obfuskasi", "obfuscation": "Parameter obfuskasi",
"regenerateObfuscation": "Buat ulang", "regenerateObfuscation": "Buat ulang",
"jc": "Jc (jumlah paket sampah)", "jc": "Jc (jumlah paket sampah)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "例: fd86:ea04:1115::/64。IPv6有効時は必須。", "ipv6SubnetHint": "例: fd86:ea04:1115::/64。IPv6有効時は必須。",
"ipv6ExternalInterface": "IPv6外部NIC", "ipv6ExternalInterface": "IPv6外部NIC",
"ipv6ExternalInterfaceHint": "NDPプロキシエントリに使用するホストのNIC。空欄で外部NICを使用。", "ipv6ExternalInterfaceHint": "NDPプロキシエントリに使用するホストのNIC。空欄で外部NICを使用。",
"routeThroughXray": "Xray経由でルーティング",
"routeThroughXrayHint": "デフォルトではオフです。オフの場合トンネルはXrayに一切依存しません。オンにすると、各クライアントのトラフィックはこのインバウンド専用のXrayブリッジへリダイレクトされ、実際のアウトバウンドは他のプロトコルと同様にルーティングページから選択します。",
"obfuscation": "難読化パラメータ", "obfuscation": "難読化パラメータ",
"regenerateObfuscation": "再生成", "regenerateObfuscation": "再生成",
"jc": "Jc(ジャンクパケット数)", "jc": "Jc(ジャンクパケット数)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "ex. fd86:ea04:1115::/64. Obrigatório quando o IPv6 está ativado.", "ipv6SubnetHint": "ex. fd86:ea04:1115::/64. Obrigatório quando o IPv6 está ativado.",
"ipv6ExternalInterface": "Interface externa IPv6", "ipv6ExternalInterface": "Interface externa IPv6",
"ipv6ExternalInterfaceHint": "Interface de rede do host para as entradas de proxy NDP. Deixe vazio para reutilizar a interface externa.", "ipv6ExternalInterfaceHint": "Interface de rede do host para as entradas de proxy NDP. Deixe vazio para reutilizar a interface externa.",
"routeThroughXray": "Rotear pelo Xray",
"routeThroughXrayHint": "Desativado por padrão: o túnel não depende do Xray de forma alguma. Quando ativado, o tráfego de cada cliente é redirecionado para a própria ponte Xray desta entrada, e a saída real é escolhida na página de Roteamento, como em qualquer outro protocolo.",
"obfuscation": "Parâmetros de ofuscação", "obfuscation": "Parâmetros de ofuscação",
"regenerateObfuscation": "Regenerar", "regenerateObfuscation": "Regenerar",
"jc": "Jc (quantidade de pacotes de lixo)", "jc": "Jc (quantidade de pacotes de lixo)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "Например, fd86:ea04:1115::/64. Обязательно при включённом IPv6.", "ipv6SubnetHint": "Например, fd86:ea04:1115::/64. Обязательно при включённом IPv6.",
"ipv6ExternalInterface": "Внешний интерфейс для IPv6", "ipv6ExternalInterface": "Внешний интерфейс для IPv6",
"ipv6ExternalInterfaceHint": "Сетевой интерфейс хоста для записей NDP-прокси. Оставьте пустым, чтобы использовать «Внешний интерфейс».", "ipv6ExternalInterfaceHint": "Сетевой интерфейс хоста для записей NDP-прокси. Оставьте пустым, чтобы использовать «Внешний интерфейс».",
"routeThroughXray": "Маршрутизировать через Xray",
"routeThroughXrayHint": "По умолчанию выключено: туннель никак не зависит от Xray. Если включить, трафик каждого клиента перенаправляется в собственный мост Xray этого входящего соединения, а фактический исходящий выбирается на странице «Маршрутизация», как и для любого другого протокола.",
"obfuscation": "Параметры обфускации", "obfuscation": "Параметры обфускации",
"regenerateObfuscation": "Сгенерировать заново", "regenerateObfuscation": "Сгенерировать заново",
"jc": "Jc (кол-во мусорных пакетов)", "jc": "Jc (кол-во мусорных пакетов)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "örn. fd86:ea04:1115::/64. IPv6 etkinken zorunludur.", "ipv6SubnetHint": "örn. fd86:ea04:1115::/64. IPv6 etkinken zorunludur.",
"ipv6ExternalInterface": "IPv6 Harici Arayüzü", "ipv6ExternalInterface": "IPv6 Harici Arayüzü",
"ipv6ExternalInterfaceHint": "NDP proxy girişleri için sunucu ağ arayüzü. Harici Arayüzü kullanmak için boş bırakın.", "ipv6ExternalInterfaceHint": "NDP proxy girişleri için sunucu ağ arayüzü. Harici Arayüzü kullanmak için boş bırakın.",
"routeThroughXray": "Xray üzerinden yönlendir",
"routeThroughXrayHint": "Varsayılan olarak kapalıdır: tünelin Xray'e hiçbir bağımlılığı yoktur. Açıldığında, her istemcinin trafiği bu gelen bağlantıya ait Xray köprüsüne yönlendirilir ve gerçek giden bağlantı, diğer tüm protokollerde olduğu gibi Yönlendirme sayfasından seçilir.",
"obfuscation": "Gizleme parametreleri", "obfuscation": "Gizleme parametreleri",
"regenerateObfuscation": "Yeniden oluştur", "regenerateObfuscation": "Yeniden oluştur",
"jc": "Jc (gereksiz paket sayısı)", "jc": "Jc (gereksiz paket sayısı)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "напр. fd86:ea04:1115::/64. Обов'язково, якщо IPv6 увімкнено.", "ipv6SubnetHint": "напр. fd86:ea04:1115::/64. Обов'язково, якщо IPv6 увімкнено.",
"ipv6ExternalInterface": "Зовнішній інтерфейс IPv6", "ipv6ExternalInterface": "Зовнішній інтерфейс IPv6",
"ipv6ExternalInterfaceHint": "Мережевий інтерфейс хоста для записів NDP-проксі. Залиште порожнім, щоб використовувати Зовнішній інтерфейс.", "ipv6ExternalInterfaceHint": "Мережевий інтерфейс хоста для записів NDP-проксі. Залиште порожнім, щоб використовувати Зовнішній інтерфейс.",
"routeThroughXray": "Маршрутизувати через Xray",
"routeThroughXrayHint": "За замовчуванням вимкнено: тунель жодним чином не залежить від Xray. Якщо увімкнено, трафік кожного клієнта перенаправляється у власний міст Xray цього вхідного з'єднання, а фактичне вихідне з'єднання обирається на сторінці Маршрутизація, як і для будь-якого іншого протоколу.",
"obfuscation": "Параметри обфускації", "obfuscation": "Параметри обфускації",
"regenerateObfuscation": "Згенерувати заново", "regenerateObfuscation": "Згенерувати заново",
"jc": "Jc (кількість сміттєвих пакетів)", "jc": "Jc (кількість сміттєвих пакетів)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "vd. fd86:ea04:1115::/64. Bắt buộc khi bật IPv6.", "ipv6SubnetHint": "vd. fd86:ea04:1115::/64. Bắt buộc khi bật IPv6.",
"ipv6ExternalInterface": "Card mạng ngoài IPv6", "ipv6ExternalInterface": "Card mạng ngoài IPv6",
"ipv6ExternalInterfaceHint": "Card mạng của host dùng cho các mục NDP proxy. Để trống để dùng lại Card mạng ngoài.", "ipv6ExternalInterfaceHint": "Card mạng của host dùng cho các mục NDP proxy. Để trống để dùng lại Card mạng ngoài.",
"routeThroughXray": "Định tuyến qua Xray",
"routeThroughXrayHint": "Mặc định tắt: tunnel hoàn toàn không phụ thuộc vào Xray. Khi bật, lưu lượng của mỗi client sẽ được chuyển hướng vào cầu nối Xray riêng của inbound này, và outbound thực tế được chọn từ trang Định tuyến giống như mọi giao thức khác.",
"obfuscation": "Tham số làm rối (obfuscation)", "obfuscation": "Tham số làm rối (obfuscation)",
"regenerateObfuscation": "Tạo lại", "regenerateObfuscation": "Tạo lại",
"jc": "Jc (số lượng gói rác)", "jc": "Jc (số lượng gói rác)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "例如 fd86:ea04:1115::/64。启用 IPv6 时必填。", "ipv6SubnetHint": "例如 fd86:ea04:1115::/64。启用 IPv6 时必填。",
"ipv6ExternalInterface": "IPv6 外部网卡", "ipv6ExternalInterface": "IPv6 外部网卡",
"ipv6ExternalInterfaceHint": "用于 NDP 代理条目的主机网卡。留空则使用外部网卡。", "ipv6ExternalInterfaceHint": "用于 NDP 代理条目的主机网卡。留空则使用外部网卡。",
"routeThroughXray": "通过 Xray 路由",
"routeThroughXrayHint": "默认关闭:隧道完全不依赖 Xray。开启后,每个客户端的流量都会被重定向到该入站自己的 Xray 网桥,实际的出站则和其他协议一样,在路由页面中选择。",
"obfuscation": "混淆参数", "obfuscation": "混淆参数",
"regenerateObfuscation": "重新生成", "regenerateObfuscation": "重新生成",
"jc": "Jc(垃圾包数量)", "jc": "Jc(垃圾包数量)",
+2
View File
@@ -1823,6 +1823,8 @@
"ipv6SubnetHint": "例如 fd86:ea04:1115::/64。啟用 IPv6 時必填。", "ipv6SubnetHint": "例如 fd86:ea04:1115::/64。啟用 IPv6 時必填。",
"ipv6ExternalInterface": "IPv6 外部網路介面", "ipv6ExternalInterface": "IPv6 外部網路介面",
"ipv6ExternalInterfaceHint": "用於 NDP 代理項目的主機網路介面。留空則使用外部網路介面。", "ipv6ExternalInterfaceHint": "用於 NDP 代理項目的主機網路介面。留空則使用外部網路介面。",
"routeThroughXray": "透過 Xray 路由",
"routeThroughXrayHint": "預設關閉:通道完全不依賴 Xray。啟用後,每個客戶端的流量都會被重新導向到該入站自己的 Xray 橋接,實際的出站則和其他協定一樣,在路由頁面中選擇。",
"obfuscation": "混淆參數", "obfuscation": "混淆參數",
"regenerateObfuscation": "重新產生", "regenerateObfuscation": "重新產生",
"jc": "Jc(垃圾封包數量)", "jc": "Jc(垃圾封包數量)",