diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 9bcb3a494..7a1c88991 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -1095,6 +1095,14 @@ "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" @@ -1223,6 +1231,12 @@ "type": "integer" }, "reverse": {}, + "routeOutboundTag": { + "type": "string" + }, + "routeThroughXray": { + "type": "boolean" + }, "secret": { "type": "string" }, @@ -1269,6 +1283,8 @@ "publicKey", "reset", "reverse", + "routeOutboundTag", + "routeThroughXray", "secret", "security", "subId", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 0d0391323..6807b6146 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -252,6 +252,8 @@ export const EXAMPLES: Record = { "publicKey": "", "reset": 0, "reverse": null, + "routeOutboundTag": "", + "routeThroughXray": false, "secret": "ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d", "security": "", "subId": "", @@ -286,6 +288,8 @@ export const EXAMPLES: Record = { "publicKey": "", "reset": 0, "reverse": null, + "routeOutboundTag": "", + "routeThroughXray": false, "secret": "", "security": "", "subId": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index b55c533f8..05abce170 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1069,6 +1069,14 @@ 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" @@ -1197,6 +1205,12 @@ export const SCHEMAS: Record = { "type": "integer" }, "reverse": {}, + "routeOutboundTag": { + "type": "string" + }, + "routeThroughXray": { + "type": "boolean" + }, "secret": { "type": "string" }, @@ -1243,6 +1257,8 @@ export const SCHEMAS: Record = { "publicKey", "reset", "reverse", + "routeOutboundTag", + "routeThroughXray", "secret", "security", "subId", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index dd06334ed..88a409f46 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -261,6 +261,8 @@ export interface Client { publicKey?: string; reset: number; reverse?: ClientReverse | null; + routeOutboundTag?: string; + routeThroughXray?: boolean; secret?: string; security: string; subId: string; @@ -297,6 +299,8 @@ export interface ClientRecord { publicKey: string; reset: number; reverse: unknown; + routeOutboundTag: string; + routeThroughXray: boolean; secret: string; security: string; subId: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index a54af8da7..257e81374 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -279,6 +279,8 @@ 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(), @@ -317,6 +319,8 @@ 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(), diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index 5a05522fb..a912957c4 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -33,6 +33,7 @@ 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); @@ -103,6 +104,8 @@ type Values = ClientFormValues & { wgPreSharedKey: string; wgAllowedIPs: string; awgForwardedPorts: string; + awgRouteThroughXray: boolean; + awgRouteOutboundTag: string; secret: string; adTag: string; }; @@ -133,6 +136,8 @@ const EMPTY: Values = { wgPreSharedKey: '', wgAllowedIPs: '', awgForwardedPorts: '', + awgRouteThroughXray: false, + awgRouteOutboundTag: '', secret: '', adTag: '', }; @@ -193,6 +198,8 @@ 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, @@ -246,6 +253,8 @@ 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 || '', }; @@ -561,10 +570,13 @@ export default function ClientFormModal({ if (allowedIPs.length > 0) { clientPayload.allowedIPs = allowedIPs; } - // Port-forwarding has no WireGuard equivalent — Xray-native WireGuard - // has no host-level iptables layer to hang per-client DNAT off of. + // 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. if (showAmneziawg) { clientPayload.forwardedPorts = values.awgForwardedPorts.trim(); + clientPayload.routeThroughXray = values.awgRouteThroughXray; + clientPayload.routeOutboundTag = values.awgRouteThroughXray ? values.awgRouteOutboundTag.trim() : ''; } } @@ -917,6 +929,30 @@ export default function ClientFormModal({ )} + {showAmneziawg && ( + + + + )} + {showAmneziawg && awgRouteThroughXray && ( + +