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.
132 lines
4.3 KiB
TypeScript
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';
|
|
}
|