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[] = [