From 96a5c73e025727aa17057c9d5524d25bf596c2ed Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 27 May 2026 12:54:26 +0200 Subject: [PATCH] refactor(inbounds): cleaner network tags and cover Mixed/Tunnel + client form select polish The InboundList protocol column had a few rough edges: raw transports rendered with mixed casing (TCP vs ws vs grpc), WireGuard never got a network tag at all, and Mixed/Tunnel rows had no L4 indication even though they listen on tcp/udp combinations through their own settings keys (settings.udp for Mixed, settings.allowedNetwork for Tunnel). Normalise the column: a small networkLabel helper upper-cases every known transport (so TCP / UDP / KCP / QUIC / WS / GRPC / HTTP all share the same visual weight, with HTTPUpgrade / SplitHTTP / XHTTP keeping a touch of casing for readability). Add an extra UDP tag beside KCP / QUIC so the user sees the underlying L4 without having to know each transport's wire shape. Add isTunnel to the dbinbound model and per-protocol branches for Mixed (TCP / TCP,UDP) and Tunnel (reads settings.allowedNetwork the same shape Shadowsocks uses for settings.network). Also polish the attached-inbounds Select in the client form: open upwards (placement="topLeft") with a 220px listHeight and maxTagCount="responsive" so a long selection doesn't push the modal's Save button below the viewport. --- frontend/src/models/dbinbound.ts | 4 + .../src/pages/clients/ClientFormModal.tsx | 3 + frontend/src/pages/inbounds/InboundList.tsx | 101 +++++++++++++++--- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/frontend/src/models/dbinbound.ts b/frontend/src/models/dbinbound.ts index 16e769e8..77f7e7de 100644 --- a/frontend/src/models/dbinbound.ts +++ b/frontend/src/models/dbinbound.ts @@ -155,6 +155,10 @@ export class DBInbound { return this.protocol === Protocols.HYSTERIA; } + get isTunnel() { + return this.protocol === Protocols.TUNNEL; + } + get address(): string { let address = location.hostname; if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") { diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index f605c137..9f469c5c 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -515,6 +515,9 @@ export default function ClientFormModal({ onChange={(v) => update('inboundIds', v)} options={inboundOptions} placeholder={t('pages.clients.selectInbound')} + maxTagCount="responsive" + placement="topLeft" + listHeight={220} showSearch={{ filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()), }} diff --git a/frontend/src/pages/inbounds/InboundList.tsx b/frontend/src/pages/inbounds/InboundList.tsx index f9775171..74a66b05 100644 --- a/frontend/src/pages/inbounds/InboundList.tsx +++ b/frontend/src/pages/inbounds/InboundList.tsx @@ -53,8 +53,58 @@ function readStreamHints(streamSettings: unknown): StreamHints { }; } -function readSettings(settings: unknown): { method?: string } { - return coerceInboundJsonField(settings) as { method?: string }; +// Display label for a network value. All known transports render in +// upper-case for visual consistency with the TCP/UDP/TLS/Reality tags +// already shown alongside; compound names (`httpupgrade`, `splithttp`, +// `xhttp`) get a tiny touch of casing so they don't read as one word. +function networkLabel(network: string): string { + const n = (network || '').toLowerCase(); + if (!n) return 'TCP'; + switch (n) { + case 'httpupgrade': return 'HTTPUpgrade'; + case 'splithttp': return 'SplitHTTP'; + case 'xhttp': return 'XHTTP'; + } + return n.toUpperCase(); +} + +// Returns the underlying L4 protocol for transports whose name isn't +// already TCP/UDP. `kcp` and `quic` both ride on UDP; everything else +// (`ws`, `grpc`, `http`, `httpupgrade`, `xhttp`) is TCP-based and gets +// no extra tag (the transport name implies TCP). +function networkL4(network: string): 'UDP' | '' { + const n = (network || '').toLowerCase(); + if (n === 'kcp' || n === 'quic') return 'UDP'; + return ''; +} + +// Shadowsocks settings.network ("tcp" / "udp" / "tcp,udp") and Tunnel +// settings.allowedNetwork (same shape, different field name) both carry +// the L4 transport list independent of streamSettings. Returns a +// comma-separated label. +function commaNetworkLabel(raw: string): string { + const parts = (raw || 'tcp').toLowerCase().split(',').map((p) => p.trim()).filter(Boolean); + if (parts.length === 0) return 'TCP'; + return parts.map(networkLabel).join(','); +} + +function shadowsocksNetworkLabel(settings: unknown): string { + return commaNetworkLabel(readSettings(settings).network || ''); +} + +function tunnelNetworkLabel(settings: unknown): string { + return commaNetworkLabel(readSettings(settings).allowedNetwork || ''); +} + +// Mixed (socks+http combo) is always TCP at L4; settings.udp=true adds +// UDP-associate support on the same port (SOCKS5 UDP). +function mixedNetworkLabel(settings: unknown): string { + const st = coerceInboundJsonField(settings) as { udp?: boolean }; + return st.udp ? 'TCP,UDP' : 'TCP'; +} + +function readSettings(settings: unknown): { method?: string; network?: string; allowedNetwork?: string } { + return coerceInboundJsonField(settings) as { method?: string; network?: string; allowedNetwork?: string }; } function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean { @@ -80,6 +130,7 @@ type ProtocolFlags = { isMixed?: boolean; isHTTP?: boolean; isWireguard?: boolean; + isTunnel?: boolean; }; interface DBInboundRecord extends ProtocolFlags { @@ -368,13 +419,21 @@ export default function InboundList({ ...sorterFor('protocol'), render: (_, record) => { const tags: ReactElement[] = [{record.protocol}]; - if (record.isVMess || record.isVLess || record.isTrojan || record.isSS || record.isHysteria) { + if (record.isWireguard || record.isHysteria) { + tags.push(UDP); + } else if (record.isSS) { const stream = readStreamHints(record.streamSettings); - tags.push( - - {record.isHysteria ? 'UDP' : stream.network} - , - ); + tags.push({shadowsocksNetworkLabel(record.settings)}); + if (stream.isTls) tags.push(TLS); + } else if (record.isTunnel) { + tags.push({tunnelNetworkLabel(record.settings)}); + } else if (record.isMixed) { + tags.push({mixedNetworkLabel(record.settings)}); + } else if (record.isVMess || record.isVLess || record.isTrojan) { + const stream = readStreamHints(record.streamSettings); + tags.push({networkLabel(stream.network)}); + const l4 = networkL4(stream.network); + if (l4) tags.push({l4}); if (stream.isTls) tags.push(TLS); if (stream.isReality) tags.push(Reality); } @@ -606,13 +665,31 @@ export default function InboundList({
{t('pages.inbounds.protocol')} {statsRecord.protocol} - {(statsRecord.isVMess || statsRecord.isVLess || statsRecord.isTrojan || statsRecord.isSS || statsRecord.isHysteria) && (() => { + {(statsRecord.isWireguard || statsRecord.isHysteria) && ( + UDP + )} + {statsRecord.isSS && (() => { const stream = readStreamHints(statsRecord.streamSettings); return ( <> - - {statsRecord.isHysteria ? 'UDP' : stream.network} - + {shadowsocksNetworkLabel(statsRecord.settings)} + {stream.isTls && TLS} + + ); + })()} + {statsRecord.isTunnel && ( + {tunnelNetworkLabel(statsRecord.settings)} + )} + {statsRecord.isMixed && ( + {mixedNetworkLabel(statsRecord.settings)} + )} + {(statsRecord.isVMess || statsRecord.isVLess || statsRecord.isTrojan) && (() => { + const stream = readStreamHints(statsRecord.streamSettings); + const l4 = networkL4(stream.network); + return ( + <> + {networkLabel(stream.network)} + {l4 && {l4}} {stream.isTls && TLS} {stream.isReality && Reality}