From 1ad483ede6096d5c2640fe78f620d28370ab636c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:05:42 +0200 Subject: [PATCH] fix: expose streamSettings for Tunnel inbounds to support TProxy (#5171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: expose streamSettings for Tunnel inbounds to support TProxy * fix(ui): hide security tab for tunnel inbounds when stream is enabled tunnel (dokodemo-door) does not support TLS or Reality, so showing the security tab only results in a fully-disabled radio group. Exclude tunnel alongside wireguard from the security tab. * fix(tunnel): restrict stream tab to sockopt-only and fix transportless schema Tunnel (dokodemo-door) only needs sockopt.tproxy for TProxy mode — no user-selectable transport. Add hasSelectableTransport flag to hide the network picker, per-network sub-forms, ExternalProxy, and FinalMask for both tunnel and wireguard, matching the pattern already used for Hysteria. Fix a pre-existing Zod schema bug where NetworkSettingsSchema was a bare discriminatedUnion requiring `network` to be present. Wireguard and tunnel submit streamSettings without a `network` key, causing "Invalid discriminator value. Expected 'tcp' | ..." on every save. Fix by adding a transportless union branch (z.never().optional()) alongside the transport DU; also add ?? 'tcp' fallback in inbound-link.ts where stream.network is now string | undefined. Three regression tests added. --------- Co-authored-by: rqzbeh Co-authored-by: MHSanaei --- frontend/src/lib/xray/inbound-link.ts | 6 +- .../src/lib/xray/protocol-capabilities.ts | 2 +- .../pages/inbounds/form/InboundFormModal.tsx | 69 ++++++++++++------- .../src/schemas/protocols/stream/index.ts | 13 +++- .../protocol-capabilities.test.ts.snap | 28 ++++---- .../src/test/inbound-form-adapter.test.ts | 50 ++++++++++++++ 6 files changed, 125 insertions(+), 43 deletions(-) diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index 4059c9abc..decc72721 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -317,7 +317,7 @@ export function genVlessLink(input: GenVlessLinkInput): string { const security = forceTls === 'same' ? stream.security : forceTls; const params = new URLSearchParams(); - params.set('type', stream.network); + params.set('type', stream.network ?? 'tcp'); params.set('encryption', inbound.settings.encryption); if (stream.network === 'tcp') { @@ -501,7 +501,7 @@ export function genTrojanLink(input: GenTrojanLinkInput): string { const security = forceTls === 'same' ? stream.security : forceTls; const params = new URLSearchParams(); - params.set('type', stream.network); + params.set('type', stream.network ?? 'tcp'); writeNetworkParams(stream, params); applyFinalMaskToParams(stream.finalmask, params); @@ -558,7 +558,7 @@ export function genShadowsocksLink(input: GenShadowsocksLinkInput): string { const security = forceTls === 'same' ? stream.security : forceTls; const params = new URLSearchParams(); - params.set('type', stream.network); + params.set('type', stream.network ?? 'tcp'); writeNetworkParams(stream, params); applyFinalMaskToParams(stream.finalmask, params); diff --git a/frontend/src/lib/xray/protocol-capabilities.ts b/frontend/src/lib/xray/protocol-capabilities.ts index 574f513ea..fced61282 100644 --- a/frontend/src/lib/xray/protocol-capabilities.ts +++ b/frontend/src/lib/xray/protocol-capabilities.ts @@ -9,7 +9,7 @@ const TLS_ELIGIBLE_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks']; const TLS_NETWORKS = ['tcp', 'ws', 'http', 'grpc', 'httpupgrade', 'xhttp']; const REALITY_ELIGIBLE_PROTOCOLS = ['vless', 'trojan']; const REALITY_NETWORKS = ['tcp', 'http', 'grpc', 'xhttp']; -const STREAM_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria', 'wireguard']; +const STREAM_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria', 'wireguard', 'tunnel']; const VISION_FLOW = 'xtls-rprx-vision'; const SS_2022_PREFIX = '2022'; const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305'; diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index 725107d69..caf7f9fd5 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx @@ -162,6 +162,15 @@ export default function InboundFormModal({ const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none'; const streamEnabled = canEnableStream({ protocol }); const sniffingSupported = canEnableSniffing({ protocol }); + // Wireguard (always a UDP listener) and Tunnel (dokodemo-door) expose no + // user-selectable transport — their stream tab is just sockopt, which is all + // Tunnel's TProxy/redirect mode needs (sockopt.tproxy). Hysteria carries its + // own dedicated transport form. For all of these the RAW/mKCP/WS/... network + // picker and the per-network sub-forms are hidden. + const hasSelectableTransport = + protocol !== Protocols.HYSTERIA + && protocol !== Protocols.WIREGUARD + && protocol !== Protocols.TUNNEL; const wPort = Form.useWatch('port', form); const wListen = (Form.useWatch('listen', form) ?? '') as string; @@ -372,11 +381,13 @@ export default function InboundFormModal({ }], }, }); - } else if (next === Protocols.WIREGUARD) { - // Wireguard has no user-selectable transport: the listener is always - // UDP and only finalmask/sockopt from streamSettings apply. Drop the - // leftover network/transport slices so the stream tab doesn't render - // a TCP sub-form and the wire payload carries no dead tcpSettings. + } else if (next === Protocols.WIREGUARD || next === Protocols.TUNNEL) { + // Wireguard and Tunnel (dokodemo-door) have no user-selectable + // transport: wireguard is always a UDP listener, and tunnel only needs + // `sockopt.tproxy` for its TProxy/redirect mode. Drop the leftover + // network/transport slices so the stream tab doesn't render a TCP + // sub-form and the wire payload carries no dead tcpSettings — the + // sockopt section (with TProxy) stays available. form.setFieldValue('streamSettings', { security: 'none' }); } else { const current = form.getFieldValue('streamSettings') as { network?: string } | undefined; @@ -651,7 +662,7 @@ export default function InboundFormModal({ const streamTab = ( <> - {protocol !== Protocols.HYSTERIA && protocol !== Protocols.WIREGUARD && ( + {hasSelectableTransport && (