Files
3x-ui-agent/frontend/src/pages/inbounds/list/helpers.ts
T
Muse bea7f4b2b6 feat(clients): allow attaching clients to socks/mixed inbounds
The client form, bulk-add form and the inbound-side attach entry all
filtered attachable inbounds by a protocol whitelist that lacked
socks/mixed (inherited from upstream), so SK5 inbounds could never be
picked as a client's associated inbound even though the backend
accepts the association. Add 'socks' and 'mixed' to all three gates.
The client record's existing password field is the socks credential;
no new credential shape is introduced.
2026-10-21 22:01:30 +00:00

132 lines
4.3 KiB
TypeScript

import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
import { coerceInboundJsonField } from '@/models/dbinbound';
import type { DBInboundRecord, StreamHints } from './types';
export function readStreamHints(streamSettings: unknown): StreamHints {
const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
return {
network: stream.network ?? '',
isTls: stream.security === 'tls',
isReality: stream.security === 'reality',
};
}
// 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.
export 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).
export 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.
export 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(',');
}
export function shadowsocksNetworkLabel(settings: unknown): string {
return commaNetworkLabel(readSettings(settings).network || '');
}
export 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).
export function mixedNetworkLabel(settings: unknown): string {
const st = coerceInboundJsonField(settings) as { udp?: boolean };
return st.udp ? 'TCP,UDP' : 'TCP';
}
export function readSettings(settings: unknown): {
method?: string;
network?: string;
allowedNetwork?: string;
} {
return coerceInboundJsonField(settings) as {
method?: string;
network?: string;
allowedNetwork?: string;
};
}
export function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
switch (record.protocol) {
case 'vmess':
case 'vless':
case 'trojan':
case 'hysteria':
case 'mtproto':
case 'wireguard':
case 'amneziawg':
case 'socks':
case 'mixed':
return true;
case 'shadowsocks':
return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
default:
return false;
}
}
export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
if (dbInbound.isSS) {
return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
}
return false;
}
// Landing label for the list-row forwarding chip: the managed target's
// remark (falling back to its tag), or the manual endpoint address:port.
// Returns null when the inbound does not forward, so the caller renders
// no chip. The stored spec rides the row as a JSON string.
export function forwardingLandingLabel(
record: DBInboundRecord,
inboundsById: Map<number, DBInboundRecord>,
): string | null {
const spec = coerceInboundJsonField(record.forwarding) as {
mode?: string;
landing?: string;
targetInboundId?: number;
address?: string;
port?: number;
};
if (spec.mode !== 'forward') return null;
if (spec.landing === 'manual') {
return spec.address ? `${spec.address}:${spec.port ?? 0}` : 'SK5';
}
const target = spec.targetInboundId ? inboundsById.get(spec.targetInboundId) : undefined;
return target?.remark || target?.tag || 'SK5';
}