From db8253421ad76b5edda1a5a80f3b21c8a1e4c1e7 Mon Sep 17 00:00:00 2001 From: Kuzz007 Date: Sat, 25 Jul 2026 22:37:08 +0300 Subject: [PATCH] refactor(amneziawg): route via Xray through the stock Routing page, not custom toggles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifies RouteViaXray after realizing the panel already has everything needed: the Routing page already lets an admin pick a source inbound tag and a target outbound (plus, if they want it, a specific source IP) for any protocol. Bolting a parallel routeThroughXray/routeOutboundTag pair onto both the client and inbound forms duplicated that mechanism instead of using it. Removed entirely: Client/ClientRecord/ServerSettings/Peer's RouteThroughXray + RouteOutboundTag fields, the effective-routing OR/ fallback logic in InstanceFromInbound, and the Switch+Select UI on both forms. Nothing configures "route via Xray" as a setting anymore. In its place, every enabled AmneziaWG inbound now gets its own Xray TPROXY bridge unconditionally, by default, no toggle: - internal/amneziawg: every peer's traffic is always TPROXY'd into that instance's own bridge (defaultPostUpDown, port derived from the inbound's id via EgressPortForInbound so the kernel side and the Xray-config side never need to negotiate a runtime value). Since the TPROXY rule is now tied to a peer's mere presence rather than an opt-in flag, hostRulesFingerprint now covers every peer unconditionally (add/remove/re-IP forces a restart, the same way ForwardedPorts always did) instead of skipping peers with nothing to opt into. - internal/web/service/xray.go's injectAmneziawgEgress creates one dokodemo-door bridge per qualifying inbound, tagged with that inbound's own real tag — the same trick injectMtprotoEgress already uses (reusing a real inbound's tag), which is why it's already selectable in the panel's Routing page: InboundService.GetInboundTags() is a plain, protocol-blind SELECT over every inbound row's tag, no dedicated UI plumbing needed. The function never generates a routing rule itself anymore — where (if anywhere) that traffic goes is entirely up to whatever rules the admin adds through the existing Routing UI. Frontend: no new UI at all. Tests rewritten to match — one bridge per inbound with its own tag/port, no rule generation, no opt-in gating. Co-Authored-By: Claude Sonnet 5 --- frontend/public/openapi.json | 23 -- frontend/src/generated/examples.ts | 6 - frontend/src/generated/schemas.ts | 23 -- frontend/src/generated/types.ts | 6 - frontend/src/generated/zod.ts | 6 - frontend/src/lib/xray/inbound-defaults.ts | 2 - .../src/pages/clients/ClientFormModal.tsx | 40 +-- .../inbounds/form/protocols/amneziawg.tsx | 29 +- frontend/src/schemas/client.ts | 2 - .../schemas/protocols/inbound/amneziawg.ts | 13 +- internal/amneziawg/manager.go | 80 +++-- internal/amneziawg/manager_test.go | 139 +++----- internal/amneziawg/route_egress.go | 54 ++-- internal/amneziawg/types.go | 30 -- internal/database/model/model.go | 158 ++++----- internal/web/service/xray.go | 137 +++----- .../web/service/xray_config_inject_test.go | 303 ++++-------------- internal/web/translation/en-US.json | 10 - internal/web/translation/ru-RU.json | 10 - 19 files changed, 289 insertions(+), 782 deletions(-) diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index d93045714..9bcb3a494 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -1095,14 +1095,6 @@ "description": "VLESS simple reverse proxy settings", "nullable": true }, - "routeOutboundTag": { - "description": "Xray outbound/balancer tag this peer's TPROXY'd traffic routes to; empty uses Xray's default routing", - "type": "string" - }, - "routeThroughXray": { - "description": "AmneziaWG: TPROXY this peer's traffic into Xray", - "type": "boolean" - }, "secret": { "example": "ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d", "type": "string" @@ -1231,12 +1223,6 @@ "type": "integer" }, "reverse": {}, - "routeOutboundTag": { - "type": "string" - }, - "routeThroughXray": { - "type": "boolean" - }, "secret": { "type": "string" }, @@ -1283,8 +1269,6 @@ "publicKey", "reset", "reverse", - "routeOutboundTag", - "routeThroughXray", "secret", "security", "subId", @@ -2878,13 +2862,6 @@ "publicKey": { "type": "string" }, - "routeOutboundTag": { - "type": "string" - }, - "routeThroughXray": { - "description": "RouteThroughXray, when true, is the inbound-wide default: every peer\nTPROXYs into Xray unless it explicitly turns its own RouteThroughXray\noff... except a plain bool can't distinguish \"peer left it unset\" from\n\"peer explicitly opted out\", so in practice this ORs with each peer's\nown flag (see Peer.RouteThroughXray) — turning this on routes every\npeer, turning it off still lets individual peers opt in on their own.\nRouteOutboundTag is the default outbound/balancer tag used when a\nrouted peer didn't set its own; empty means Xray's default routing.", - "type": "boolean" - }, "s1": { "type": "integer" }, diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 084b0a387..0d0391323 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -252,8 +252,6 @@ export const EXAMPLES: Record = { "publicKey": "", "reset": 0, "reverse": null, - "routeOutboundTag": "", - "routeThroughXray": false, "secret": "ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d", "security": "", "subId": "", @@ -288,8 +286,6 @@ export const EXAMPLES: Record = { "publicKey": "", "reset": 0, "reverse": null, - "routeOutboundTag": "", - "routeThroughXray": false, "secret": "", "security": "", "subId": "", @@ -670,8 +666,6 @@ export const EXAMPLES: Record = { "primaryDns": "", "privateKey": "", "publicKey": "", - "routeOutboundTag": "", - "routeThroughXray": false, "s1": 0, "s2": 0, "s3": 0, diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index a7caa22cc..b55c533f8 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1069,14 +1069,6 @@ export const SCHEMAS: Record = { "description": "VLESS simple reverse proxy settings", "nullable": true }, - "routeOutboundTag": { - "description": "Xray outbound/balancer tag this peer's TPROXY'd traffic routes to; empty uses Xray's default routing", - "type": "string" - }, - "routeThroughXray": { - "description": "AmneziaWG: TPROXY this peer's traffic into Xray", - "type": "boolean" - }, "secret": { "example": "ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d", "type": "string" @@ -1205,12 +1197,6 @@ export const SCHEMAS: Record = { "type": "integer" }, "reverse": {}, - "routeOutboundTag": { - "type": "string" - }, - "routeThroughXray": { - "type": "boolean" - }, "secret": { "type": "string" }, @@ -1257,8 +1243,6 @@ export const SCHEMAS: Record = { "publicKey", "reset", "reverse", - "routeOutboundTag", - "routeThroughXray", "secret", "security", "subId", @@ -2852,13 +2836,6 @@ export const SCHEMAS: Record = { "publicKey": { "type": "string" }, - "routeOutboundTag": { - "type": "string" - }, - "routeThroughXray": { - "description": "RouteThroughXray, when true, is the inbound-wide default: every peer\nTPROXYs into Xray unless it explicitly turns its own RouteThroughXray\noff... except a plain bool can't distinguish \"peer left it unset\" from\n\"peer explicitly opted out\", so in practice this ORs with each peer's\nown flag (see Peer.RouteThroughXray) — turning this on routes every\npeer, turning it off still lets individual peers opt in on their own.\nRouteOutboundTag is the default outbound/balancer tag used when a\nrouted peer didn't set its own; empty means Xray's default routing.", - "type": "boolean" - }, "s1": { "type": "integer" }, diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index ff71907f1..dd06334ed 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -261,8 +261,6 @@ export interface Client { publicKey?: string; reset: number; reverse?: ClientReverse | null; - routeOutboundTag?: string; - routeThroughXray?: boolean; secret?: string; security: string; subId: string; @@ -299,8 +297,6 @@ export interface ClientRecord { publicKey: string; reset: number; reverse: unknown; - routeOutboundTag: string; - routeThroughXray: boolean; secret: string; security: string; subId: string; @@ -653,8 +649,6 @@ export interface ServerSettings { primaryDns?: string; privateKey: string; publicKey: string; - routeOutboundTag?: string; - routeThroughXray?: boolean; s1: number; s2: number; s3: number; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index fac01f57c..a54af8da7 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -279,8 +279,6 @@ export const ClientSchema = z.object({ publicKey: z.string().optional(), reset: z.number().int(), reverse: z.lazy(() => ClientReverseSchema).nullable().optional(), - routeOutboundTag: z.string().optional(), - routeThroughXray: z.boolean().optional(), secret: z.string().optional(), security: z.string(), subId: z.string(), @@ -319,8 +317,6 @@ export const ClientRecordSchema = z.object({ publicKey: z.string(), reset: z.number().int(), reverse: z.unknown(), - routeOutboundTag: z.string(), - routeThroughXray: z.boolean(), secret: z.string(), security: z.string(), subId: z.string(), @@ -692,8 +688,6 @@ export const ServerSettingsSchema = z.object({ primaryDns: z.string().optional(), privateKey: z.string(), publicKey: z.string(), - routeOutboundTag: z.string().optional(), - routeThroughXray: z.boolean().optional(), s1: z.number().int(), s2: z.number().int(), s3: z.number().int(), diff --git a/frontend/src/lib/xray/inbound-defaults.ts b/frontend/src/lib/xray/inbound-defaults.ts index e34b72921..0c5fabc0b 100644 --- a/frontend/src/lib/xray/inbound-defaults.ts +++ b/frontend/src/lib/xray/inbound-defaults.ts @@ -298,8 +298,6 @@ export function createDefaultAmneziawgInboundSettings(): AmneziawgInboundSetting ipv6Enabled: false, ipv6Subnet: '', ipv6ExternalInterface: '', - routeThroughXray: false, - routeOutboundTag: '', jc: 5, jmin: 10, jmax: 50, diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index a912957c4..5a05522fb 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -33,7 +33,6 @@ import { FormField } from '@/components/form/rhf'; import { TLS_FLOW_CONTROL } from '@/schemas/primitives'; import type { ClientRecord, InboundOption, ExternalLink, ExternalLinkInput } from '@/hooks/useClients'; import { useFail2banStatusQuery, getLimitIpNotice } from '@/api/queries/useFail2banStatusQuery'; -import { useOutboundTags } from '@/api/queries/useOutboundTags'; import { ClientFormSchema, ClientCreateFormSchema, type ClientFormValues } from '@/schemas/client'; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); @@ -104,8 +103,6 @@ type Values = ClientFormValues & { wgPreSharedKey: string; wgAllowedIPs: string; awgForwardedPorts: string; - awgRouteThroughXray: boolean; - awgRouteOutboundTag: string; secret: string; adTag: string; }; @@ -136,8 +133,6 @@ const EMPTY: Values = { wgPreSharedKey: '', wgAllowedIPs: '', awgForwardedPorts: '', - awgRouteThroughXray: false, - awgRouteOutboundTag: '', secret: '', adTag: '', }; @@ -198,8 +193,6 @@ export default function ClientFormModal({ const subId = useWatch({ control: methods.control, name: 'subId' }); const auth = useWatch({ control: methods.control, name: 'auth' }); const wgPrivateKey = useWatch({ control: methods.control, name: 'wgPrivateKey' }); - const awgRouteThroughXray = useWatch({ control: methods.control, name: 'awgRouteThroughXray' }); - const { data: outboundTags } = useOutboundTags(); const limitIp = useWatch({ control: methods.control, name: 'limitIp' }); const { fields: externalLinkFields, @@ -253,8 +246,6 @@ export default function ClientFormModal({ wgPreSharedKey: client.preSharedKey || '', wgAllowedIPs: client.allowedIPs || '', awgForwardedPorts: client.forwardedPorts || '', - awgRouteThroughXray: !!client.routeThroughXray, - awgRouteOutboundTag: client.routeOutboundTag || '', secret: client.secret || '', adTag: client.adTag || '', }; @@ -570,13 +561,10 @@ export default function ClientFormModal({ if (allowedIPs.length > 0) { clientPayload.allowedIPs = allowedIPs; } - // Port-forwarding and RouteViaXray have no WireGuard equivalent — - // Xray-native WireGuard has no host-level iptables layer to hang - // per-client DNAT/TPROXY off of. + // Port-forwarding has no WireGuard equivalent — Xray-native WireGuard + // has no host-level iptables layer to hang per-client DNAT off of. if (showAmneziawg) { clientPayload.forwardedPorts = values.awgForwardedPorts.trim(); - clientPayload.routeThroughXray = values.awgRouteThroughXray; - clientPayload.routeOutboundTag = values.awgRouteThroughXray ? values.awgRouteOutboundTag.trim() : ''; } } @@ -929,30 +917,6 @@ export default function ClientFormModal({ )} - {showAmneziawg && ( - - - - )} - {showAmneziawg && awgRouteThroughXray && ( - - - - - - {routeThroughXray && ( - -