mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-09 07:25:02 +03:00
refactor(amneziawg): route via Xray through the stock Routing page, not custom toggles
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 <noreply@anthropic.com>
This commit is contained in:
@@ -252,8 +252,6 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"publicKey": "",
|
||||
"reset": 0,
|
||||
"reverse": null,
|
||||
"routeOutboundTag": "",
|
||||
"routeThroughXray": false,
|
||||
"secret": "ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d",
|
||||
"security": "",
|
||||
"subId": "",
|
||||
@@ -288,8 +286,6 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"publicKey": "",
|
||||
"reset": 0,
|
||||
"reverse": null,
|
||||
"routeOutboundTag": "",
|
||||
"routeThroughXray": false,
|
||||
"secret": "",
|
||||
"security": "",
|
||||
"subId": "",
|
||||
@@ -670,8 +666,6 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"primaryDns": "",
|
||||
"privateKey": "",
|
||||
"publicKey": "",
|
||||
"routeOutboundTag": "",
|
||||
"routeThroughXray": false,
|
||||
"s1": 0,
|
||||
"s2": 0,
|
||||
"s3": 0,
|
||||
|
||||
@@ -1069,14 +1069,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"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<string, unknown> = {
|
||||
"type": "integer"
|
||||
},
|
||||
"reverse": {},
|
||||
"routeOutboundTag": {
|
||||
"type": "string"
|
||||
},
|
||||
"routeThroughXray": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1257,8 +1243,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"publicKey",
|
||||
"reset",
|
||||
"reverse",
|
||||
"routeOutboundTag",
|
||||
"routeThroughXray",
|
||||
"secret",
|
||||
"security",
|
||||
"subId",
|
||||
@@ -2852,13 +2836,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -298,8 +298,6 @@ export function createDefaultAmneziawgInboundSettings(): AmneziawgInboundSetting
|
||||
ipv6Enabled: false,
|
||||
ipv6Subnet: '',
|
||||
ipv6ExternalInterface: '',
|
||||
routeThroughXray: false,
|
||||
routeOutboundTag: '',
|
||||
jc: 5,
|
||||
jmin: 10,
|
||||
jmax: 50,
|
||||
|
||||
@@ -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({
|
||||
<Input placeholder="80, 443, 8000-8100" />
|
||||
</FormField>
|
||||
)}
|
||||
{showAmneziawg && (
|
||||
<FormField
|
||||
name="awgRouteThroughXray"
|
||||
label={t('pages.clients.amneziaWgRouteThroughXray')}
|
||||
tooltip={t('pages.clients.amneziaWgRouteThroughXrayHint')}
|
||||
valueProp="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
)}
|
||||
{showAmneziawg && awgRouteThroughXray && (
|
||||
<FormField
|
||||
name="awgRouteOutboundTag"
|
||||
label={t('pages.clients.amneziaWgRouteOutboundTag')}
|
||||
tooltip={t('pages.clients.amneziaWgRouteOutboundTagHint')}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
placeholder={t('pages.clients.amneziaWgRouteOutboundTagPlaceholder')}
|
||||
options={(outboundTags ?? []).map((tag) => ({ value: tag, label: tag }))}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showMtproto && (
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
||||
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import { FormField } from '@/components/form/rhf';
|
||||
import { useOutboundTags } from '@/api/queries/useOutboundTags';
|
||||
|
||||
interface AmneziawgFieldsProps {
|
||||
awgPubKey: string;
|
||||
@@ -14,9 +12,6 @@ interface AmneziawgFieldsProps {
|
||||
|
||||
export default function AmneziawgFields({ awgPubKey, regenInboundAwg, regenInboundAwgObfuscation }: AmneziawgFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext();
|
||||
const routeThroughXray = useWatch({ control, name: 'settings.server.routeThroughXray' }) as boolean | undefined;
|
||||
const { data: outboundTags } = useOutboundTags();
|
||||
return (
|
||||
<>
|
||||
<Form.Item label={t('pages.xray.amneziawg.privateKey')}>
|
||||
@@ -73,28 +68,6 @@ export default function AmneziawgFields({ awgPubKey, regenInboundAwg, regenInbou
|
||||
>
|
||||
<Input placeholder="eth0" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'server', 'routeThroughXray']}
|
||||
label={t('pages.xray.amneziawg.routeThroughXray')}
|
||||
tooltip={t('pages.xray.amneziawg.routeThroughXrayHint')}
|
||||
valueProp="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
{routeThroughXray && (
|
||||
<FormField
|
||||
name={['settings', 'server', 'routeOutboundTag']}
|
||||
label={t('pages.xray.amneziawg.routeOutboundTag')}
|
||||
tooltip={t('pages.xray.amneziawg.routeOutboundTagHint')}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
placeholder={t('pages.xray.amneziawg.routeOutboundTagPlaceholder')}
|
||||
options={(outboundTags ?? []).map((tag) => ({ value: tag, label: tag }))}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<Form.Item label={t('pages.xray.amneziawg.obfuscation')}>
|
||||
<Button icon={<ReloadOutlined />} onClick={regenInboundAwgObfuscation}>
|
||||
{t('pages.xray.amneziawg.regenerateObfuscation')}
|
||||
|
||||
@@ -38,8 +38,6 @@ export const ClientRecordSchema = z.object({
|
||||
preSharedKey: z.string().optional(),
|
||||
keepAlive: z.number().optional(),
|
||||
forwardedPorts: z.string().optional(),
|
||||
routeThroughXray: z.boolean().optional(),
|
||||
routeOutboundTag: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
adTag: z.string().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
|
||||
@@ -10,12 +10,9 @@ const optionalClearedInt = (schema: z.ZodNumber) =>
|
||||
// WireguardClientSchema — the panel's generic ClientRecord already has those
|
||||
// exact keys (privateKey/publicKey/preSharedKey/allowedIPs/keepAlive), so
|
||||
// bulk operations, the QR modal and subscriptions all work unmodified — plus
|
||||
// two AmneziaWG-only additions: forwardedPorts (WireGuard's Xray-native
|
||||
// inbound has no host-level iptables layer to hang per-client DNAT off of)
|
||||
// and routeThroughXray/routeOutboundTag (TPROXYs this peer's traffic into a
|
||||
// shared Xray bridge instead of NAT'ing it straight out — see
|
||||
// internal/amneziawg's EgressPort). Keys are optional on the wire — the
|
||||
// backend generates them when absent.
|
||||
// one AmneziaWG-only addition, forwardedPorts (WireGuard's Xray-native
|
||||
// inbound has no host-level iptables layer to hang per-client DNAT off of).
|
||||
// Keys are optional on the wire — the backend generates them when absent.
|
||||
export const AmneziawgClientSchema = z.object({
|
||||
privateKey: z.string().optional(),
|
||||
publicKey: z.string().optional(),
|
||||
@@ -23,8 +20,6 @@ export const AmneziawgClientSchema = z.object({
|
||||
allowedIPs: z.array(z.string()).default([]),
|
||||
keepAlive: optionalClearedInt(z.number().int().min(0)),
|
||||
forwardedPorts: z.string().default(''),
|
||||
routeThroughXray: z.boolean().default(false),
|
||||
routeOutboundTag: z.string().default(''),
|
||||
email: z.string().min(1),
|
||||
limitIp: z.number().int().min(0).default(0),
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
@@ -57,8 +52,6 @@ export const AmneziawgServerSchema = z.object({
|
||||
ipv6Enabled: z.boolean().default(false),
|
||||
ipv6Subnet: z.string().default(''),
|
||||
ipv6ExternalInterface: z.string().default(''),
|
||||
routeThroughXray: z.boolean().default(false),
|
||||
routeOutboundTag: z.string().default(''),
|
||||
jc: z.number().int().min(0).default(5),
|
||||
jmin: z.number().int().min(0).default(10),
|
||||
jmax: z.number().int().min(0).default(50),
|
||||
|
||||
Reference in New Issue
Block a user