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:
Kuzz007
2026-07-25 22:37:08 +03:00
parent 909feefd1d
commit db8253421a
19 changed files with 289 additions and 782 deletions
+2 -38
View File
@@ -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')}