mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-13 00:25:04 +03:00
refactor(inbounds): drop the single-user shadowsocks inbound paths
isSSMultiUser treated 2022-blake3-chacha20-poly1305 as a single-user inbound, and the inbounds page grew a row QR modal, a separate link panel in the info modal and a client-count skip for that case. The method is no longer allowed on inbounds, so every shadowsocks inbound carries clients and those paths cannot be reached: remove them along with the inbound-level QrCodeModal they alone opened.
This commit is contained in:
@@ -1387,11 +1387,8 @@ export function preferPublicHost(browserHost: string, publicHost: string): strin
|
|||||||
return publicHost && isLoopbackHost(browserHost) ? publicHost : browserHost;
|
return publicHost && isLoopbackHost(browserHost) ? publicHost : browserHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the client array for protocols that have one. SS returns its
|
// Returns the client array for protocols that have one; null for http, mixed,
|
||||||
// clients only in 2022-blake3 multi-user mode (matches the legacy
|
// tunnel, wireguard, hysteria2-without-clients, and any protocol without one.
|
||||||
// `this.clients` getter, which used isSSMultiUser to gate). Returns null
|
|
||||||
// for SS single-user, http, mixed, tunnel, wireguard, hysteria2-without-
|
|
||||||
// clients, and any protocol without a clients array.
|
|
||||||
type ClientShape = {
|
type ClientShape = {
|
||||||
id?: string;
|
id?: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Pure-function ports of the legacy Inbound class capability predicates
|
// Pure-function ports of the legacy Inbound class capability predicates
|
||||||
// (canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream,
|
// (canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream,
|
||||||
// canEnableVisionSeed, isSS2022, isSSMultiUser). Each accepts the minimal
|
// canEnableVisionSeed, isSS2022). Each accepts the minimal
|
||||||
// slice of an InboundFormValues it needs, so the same predicate can be
|
// slice of an InboundFormValues it needs, so the same predicate can be
|
||||||
// called against a partial-row, a full form value, or a hand-built test
|
// called against a partial-row, a full form value, or a hand-built test
|
||||||
// fixture without the caller projecting a whole object.
|
// fixture without the caller projecting a whole object.
|
||||||
@@ -21,7 +21,6 @@ const STREAM_PROTOCOLS = [
|
|||||||
];
|
];
|
||||||
const VISION_FLOW = 'xtls-rprx-vision';
|
const VISION_FLOW = 'xtls-rprx-vision';
|
||||||
const SS_2022_PREFIX = '2022';
|
const SS_2022_PREFIX = '2022';
|
||||||
const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
|
|
||||||
|
|
||||||
export interface CapabilityProtocolSlice {
|
export interface CapabilityProtocolSlice {
|
||||||
protocol: string;
|
protocol: string;
|
||||||
@@ -103,15 +102,6 @@ export function canEnableVisionSeed(values: CapabilityVlessSlice): boolean {
|
|||||||
return clients.some((c) => c?.flow === VISION_FLOW);
|
return clients.some((c) => c?.flow === VISION_FLOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Why: legacy returns true on non-SS protocols too (the method getter
|
|
||||||
// resolves to "" and "" !== blake3-chacha20-poly1305). Preserved for
|
|
||||||
// parity with the legacy class; in practice the callers all narrow on
|
|
||||||
// protocol === shadowsocks before checking.
|
|
||||||
export function isSSMultiUser(values: CapabilityShadowsocksSlice): boolean {
|
|
||||||
const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
|
|
||||||
return method !== SS_BLAKE3_CHACHA20;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSS2022(values: CapabilityShadowsocksSlice): boolean {
|
export function isSS2022(values: CapabilityShadowsocksSlice): boolean {
|
||||||
const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
|
const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
|
||||||
return method.substring(0, 4) === SS_2022_PREFIX;
|
return method.substring(0, 4) === SS_2022_PREFIX;
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import { LazyMount } from '@/components/utility';
|
|||||||
const InboundFormModal = lazy(() => import('./form/InboundFormModal'));
|
const InboundFormModal = lazy(() => import('./form/InboundFormModal'));
|
||||||
const CloneInboundModal = lazy(() => import('./CloneInboundModal'));
|
const CloneInboundModal = lazy(() => import('./CloneInboundModal'));
|
||||||
const InboundInfoModal = lazy(() => import('./info/InboundInfoModal'));
|
const InboundInfoModal = lazy(() => import('./info/InboundInfoModal'));
|
||||||
const QrCodeModal = lazy(() => import('./qr/QrCodeModal'));
|
|
||||||
const AttachClientsModal = lazy(() => import('./clients/AttachClientsModal'));
|
const AttachClientsModal = lazy(() => import('./clients/AttachClientsModal'));
|
||||||
const AttachExistingClientsModal = lazy(() => import('./clients/AttachExistingClientsModal'));
|
const AttachExistingClientsModal = lazy(() => import('./clients/AttachExistingClientsModal'));
|
||||||
const DetachClientsModal = lazy(() => import('./clients/DetachClientsModal'));
|
const DetachClientsModal = lazy(() => import('./clients/DetachClientsModal'));
|
||||||
@@ -60,7 +59,6 @@ const AddClientsToGroupModal = lazy(() => import('./clients/AddClientsToGroupMod
|
|||||||
type RowAction =
|
type RowAction =
|
||||||
| 'edit'
|
| 'edit'
|
||||||
| 'showInfo'
|
| 'showInfo'
|
||||||
| 'qrcode'
|
|
||||||
| 'export'
|
| 'export'
|
||||||
| 'subs'
|
| 'subs'
|
||||||
| 'clipboard'
|
| 'clipboard'
|
||||||
@@ -167,9 +165,6 @@ export default function InboundsPage() {
|
|||||||
const [infoDbInbound, setInfoDbInbound] = useState<DBInbound | null>(null);
|
const [infoDbInbound, setInfoDbInbound] = useState<DBInbound | null>(null);
|
||||||
const [infoClientIndex, setInfoClientIndex] = useState(0);
|
const [infoClientIndex, setInfoClientIndex] = useState(0);
|
||||||
|
|
||||||
const [qrOpen, setQrOpen] = useState(false);
|
|
||||||
const [qrDbInbound, setQrDbInbound] = useState<DBInbound | null>(null);
|
|
||||||
|
|
||||||
const [attachOpen, setAttachOpen] = useState(false);
|
const [attachOpen, setAttachOpen] = useState(false);
|
||||||
const [attachSource, setAttachSource] = useState<DBInbound | null>(null);
|
const [attachSource, setAttachSource] = useState<DBInbound | null>(null);
|
||||||
const [attachExistingOpen, setAttachExistingOpen] = useState(false);
|
const [attachExistingOpen, setAttachExistingOpen] = useState(false);
|
||||||
@@ -213,7 +208,6 @@ export default function InboundsPage() {
|
|||||||
() => hostOverrideFor(infoDbInbound),
|
() => hostOverrideFor(infoDbInbound),
|
||||||
[infoDbInbound, hostOverrideFor],
|
[infoDbInbound, hostOverrideFor],
|
||||||
);
|
);
|
||||||
const qrNodeAddress = useMemo(() => hostOverrideFor(qrDbInbound), [qrDbInbound, hostOverrideFor]);
|
|
||||||
|
|
||||||
const openText = useCallback(
|
const openText = useCallback(
|
||||||
(opts: {
|
(opts: {
|
||||||
@@ -637,7 +631,6 @@ export default function InboundsPage() {
|
|||||||
const hydratingKeys: RowAction[] = [
|
const hydratingKeys: RowAction[] = [
|
||||||
'edit',
|
'edit',
|
||||||
'showInfo',
|
'showInfo',
|
||||||
'qrcode',
|
|
||||||
'export',
|
'export',
|
||||||
'subs',
|
'subs',
|
||||||
'clipboard',
|
'clipboard',
|
||||||
@@ -659,10 +652,6 @@ export default function InboundsPage() {
|
|||||||
setInfoClientIndex(findClientIndex(target, null));
|
setInfoClientIndex(findClientIndex(target, null));
|
||||||
setInfoOpen(true);
|
setInfoOpen(true);
|
||||||
break;
|
break;
|
||||||
case 'qrcode':
|
|
||||||
setQrDbInbound(checkFallback(target));
|
|
||||||
setQrOpen(true);
|
|
||||||
break;
|
|
||||||
case 'export':
|
case 'export':
|
||||||
exportInboundLinks(target);
|
exportInboundLinks(target);
|
||||||
break;
|
break;
|
||||||
@@ -847,17 +836,6 @@ export default function InboundsPage() {
|
|||||||
nodeAddress={infoNodeAddress}
|
nodeAddress={infoNodeAddress}
|
||||||
/>
|
/>
|
||||||
</LazyMount>
|
</LazyMount>
|
||||||
<LazyMount when={qrOpen}>
|
|
||||||
<QrCodeModal
|
|
||||||
open={qrOpen}
|
|
||||||
onClose={() => setQrOpen(false)}
|
|
||||||
dbInbound={qrDbInbound}
|
|
||||||
client={null}
|
|
||||||
nodeAddress={qrNodeAddress}
|
|
||||||
subSettings={subSettings}
|
|
||||||
hosts={hosts}
|
|
||||||
/>
|
|
||||||
</LazyMount>
|
|
||||||
<LazyMount when={attachOpen}>
|
<LazyMount when={attachOpen}>
|
||||||
<AttachClientsModal
|
<AttachClientsModal
|
||||||
open={attachOpen}
|
open={attachOpen}
|
||||||
|
|||||||
@@ -1302,28 +1302,6 @@ export default function InboundInfoModal({
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{dbInbound.isSS && !inbound.isSSMultiUser && links.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Divider>{t('pages.inbounds.copyLink')}</Divider>
|
|
||||||
{links.map((link, idx) => (
|
|
||||||
<div key={idx} className="link-panel">
|
|
||||||
<div className="link-panel-header">
|
|
||||||
<Tag color="green">{link.remark || `Link ${idx + 1}`}</Tag>
|
|
||||||
<Tooltip title={t('copy')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<CopyOutlined />}
|
|
||||||
aria-label={t('copy')}
|
|
||||||
onClick={() => copyText(link.link, t)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<code className="link-panel-text">{link.link}</code>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ import { getMessage } from '@/utils/messageBus';
|
|||||||
import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
|
import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
|
||||||
import { Protocols } from '@/schemas/primitives';
|
import { Protocols } from '@/schemas/primitives';
|
||||||
import { coerceInboundJsonField } from '@/models/dbinbound';
|
import { coerceInboundJsonField } from '@/models/dbinbound';
|
||||||
import {
|
import { canEnableTlsFlow, isSS2022 as isSS2022Helper } from '@/lib/xray/protocol-capabilities';
|
||||||
canEnableTlsFlow,
|
|
||||||
isSS2022 as isSS2022Helper,
|
|
||||||
isSSMultiUser as isSSMultiUserHelper,
|
|
||||||
} from '@/lib/xray/protocol-capabilities';
|
|
||||||
|
|
||||||
import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
|
import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
|
||||||
|
|
||||||
@@ -123,10 +119,6 @@ export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
|
|||||||
isHttpupgrade: network === 'httpupgrade',
|
isHttpupgrade: network === 'httpupgrade',
|
||||||
isXHTTP: network === 'xhttp',
|
isXHTTP: network === 'xhttp',
|
||||||
isGrpc: network === 'grpc',
|
isGrpc: network === 'grpc',
|
||||||
isSSMultiUser: isSSMultiUserHelper({
|
|
||||||
protocol: dbInbound.protocol,
|
|
||||||
settings: settings as { method?: string },
|
|
||||||
}),
|
|
||||||
isSS2022: isSS2022Helper({
|
isSS2022: isSS2022Helper({
|
||||||
protocol: dbInbound.protocol,
|
protocol: dbInbound.protocol,
|
||||||
settings: settings as { method?: string },
|
settings: settings as { method?: string },
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export interface InboundInfo {
|
|||||||
isHttpupgrade: boolean;
|
isHttpupgrade: boolean;
|
||||||
isXHTTP: boolean;
|
isXHTTP: boolean;
|
||||||
isGrpc: boolean;
|
isGrpc: boolean;
|
||||||
isSSMultiUser: boolean;
|
|
||||||
isSS2022: boolean;
|
isSS2022: boolean;
|
||||||
isVlessTlsFlow: boolean;
|
isVlessTlsFlow: boolean;
|
||||||
host: string | null;
|
host: string | null;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Button, Dropdown, type MenuProps } from 'antd';
|
|||||||
import {
|
import {
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
QrcodeOutlined,
|
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
UsergroupDeleteOutlined,
|
UsergroupDeleteOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import { isInboundMultiUser, showQrCodeMenu } from './helpers';
|
import { isInboundMultiUser } from './helpers';
|
||||||
import type { DBInboundRecord, RowAction } from './types';
|
import type { DBInboundRecord, RowAction } from './types';
|
||||||
|
|
||||||
interface RowActionsMenuProps {
|
interface RowActionsMenuProps {
|
||||||
@@ -43,9 +42,6 @@ export function buildRowActionsMenu({
|
|||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
|
items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
|
||||||
}
|
}
|
||||||
if (showQrCodeMenu(record)) {
|
|
||||||
items.push({ key: 'qrcode', icon: <QrcodeOutlined />, label: t('qrCode') });
|
|
||||||
}
|
|
||||||
if (isInboundMultiUser(record)) {
|
if (isInboundMultiUser(record)) {
|
||||||
items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
|
items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
|
||||||
if (subEnable) {
|
if (subEnable) {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
|
|
||||||
import { coerceInboundJsonField } from '@/models/dbinbound';
|
import { coerceInboundJsonField } from '@/models/dbinbound';
|
||||||
import type { HostRecord } from '@/schemas/api/host';
|
import type { HostRecord } from '@/schemas/api/host';
|
||||||
|
|
||||||
import type { DBInboundRecord, StreamHints } from './types';
|
import type { StreamHints } from './types';
|
||||||
|
|
||||||
export function readStreamHints(streamSettings: unknown): StreamHints {
|
export function readStreamHints(streamSettings: unknown): StreamHints {
|
||||||
const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
|
const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
|
||||||
@@ -82,11 +81,12 @@ export function readSettings(settings: unknown): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
|
export function isInboundMultiUser(record: { protocol: string }): boolean {
|
||||||
switch (record.protocol) {
|
switch (record.protocol) {
|
||||||
case 'vmess':
|
case 'vmess':
|
||||||
case 'vless':
|
case 'vless':
|
||||||
case 'trojan':
|
case 'trojan':
|
||||||
|
case 'shadowsocks':
|
||||||
case 'hysteria':
|
case 'hysteria':
|
||||||
case 'mtproto':
|
case 'mtproto':
|
||||||
case 'wireguard':
|
case 'wireguard':
|
||||||
@@ -94,20 +94,11 @@ export function isInboundMultiUser(record: { protocol: string; settings: unknown
|
|||||||
case 'tuic':
|
case 'tuic':
|
||||||
case 'masque':
|
case 'masque':
|
||||||
return true;
|
return true;
|
||||||
case 'shadowsocks':
|
|
||||||
return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
|
|
||||||
if (dbInbound.isSS) {
|
|
||||||
return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Max host remarks shown inline before truncating with "+N". */
|
/** Max host remarks shown inline before truncating with "+N". */
|
||||||
export const HOST_REMARK_VISIBLE_LIMIT = 2;
|
export const HOST_REMARK_VISIBLE_LIMIT = 2;
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export interface InboundSpeedEntry {
|
|||||||
export type RowAction =
|
export type RowAction =
|
||||||
| 'edit'
|
| 'edit'
|
||||||
| 'showInfo'
|
| 'showInfo'
|
||||||
| 'qrcode'
|
|
||||||
| 'export'
|
| 'export'
|
||||||
| 'subs'
|
| 'subs'
|
||||||
| 'clipboard'
|
| 'clipboard'
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Collapse, Modal } from 'antd';
|
|
||||||
import type { CollapseProps } from 'antd';
|
|
||||||
|
|
||||||
import { Protocols } from '@/schemas/primitives';
|
|
||||||
import {
|
|
||||||
genAllLinks,
|
|
||||||
genAmneziaWGPeerConfigs,
|
|
||||||
genAmneziaWGPeerLinks,
|
|
||||||
genWireguardPeerConfigs,
|
|
||||||
genWireguardPeerLinks,
|
|
||||||
isPostQuantumLink,
|
|
||||||
preferPublicHost,
|
|
||||||
} from '@/lib/xray/inbound-link';
|
|
||||||
import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
|
|
||||||
import { withHostEndpoints } from '@/lib/hosts/host-link';
|
|
||||||
import type { HostRecord } from '@/schemas/api/host';
|
|
||||||
import QrPanel from './QrPanel';
|
|
||||||
import { peerConfFileName } from '../info/helpers';
|
|
||||||
import type { SubSettings } from '../useInbounds';
|
|
||||||
|
|
||||||
interface ClientSetting {
|
|
||||||
email?: string;
|
|
||||||
subId?: string;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QrCodeModalProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
dbInbound: (DbInboundLike & { id: number; remark?: string }) | null;
|
|
||||||
client?: ClientSetting | null;
|
|
||||||
nodeAddress?: string;
|
|
||||||
subSettings?: SubSettings;
|
|
||||||
hosts?: HostRecord[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QrItem {
|
|
||||||
key: string;
|
|
||||||
header: string;
|
|
||||||
value: string;
|
|
||||||
downloadName?: string;
|
|
||||||
showQr?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY_HOSTS: HostRecord[] = [];
|
|
||||||
|
|
||||||
export default function QrCodeModal({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
dbInbound,
|
|
||||||
client = null,
|
|
||||||
nodeAddress = '',
|
|
||||||
subSettings,
|
|
||||||
hosts = EMPTY_HOSTS,
|
|
||||||
}: QrCodeModalProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
|
|
||||||
const [wireguardConfigs, setWireguardConfigs] = useState<string[][]>([]);
|
|
||||||
const [wireguardLinks, setWireguardLinks] = useState<string[][]>([]);
|
|
||||||
const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[][]>([]);
|
|
||||||
const [amneziawgLinks, setAmneziawgLinks] = useState<string[][]>([]);
|
|
||||||
const [subLink, setSubLink] = useState('');
|
|
||||||
const [subJsonLink, setSubJsonLink] = useState('');
|
|
||||||
const [activeKey, setActiveKey] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Building the links is a pure function of the props, so it runs during
|
|
||||||
// render; an effect would paint the previous inbound's QR first.
|
|
||||||
const [syncedProps, setSyncedProps] = useState<{
|
|
||||||
dbInbound: typeof dbInbound;
|
|
||||||
client: typeof client;
|
|
||||||
nodeAddress: typeof nodeAddress;
|
|
||||||
subSettings: typeof subSettings;
|
|
||||||
hosts: typeof hosts;
|
|
||||||
} | null>(null);
|
|
||||||
if (
|
|
||||||
open &&
|
|
||||||
dbInbound &&
|
|
||||||
(syncedProps === null ||
|
|
||||||
syncedProps.dbInbound !== dbInbound ||
|
|
||||||
syncedProps.client !== client ||
|
|
||||||
syncedProps.nodeAddress !== nodeAddress ||
|
|
||||||
syncedProps.subSettings !== subSettings ||
|
|
||||||
syncedProps.hosts !== hosts)
|
|
||||||
) {
|
|
||||||
setSyncedProps({ dbInbound, client, nodeAddress, subSettings, hosts });
|
|
||||||
const fallbackHostname = preferPublicHost(
|
|
||||||
window.location.hostname,
|
|
||||||
subSettings?.publicHost ?? '',
|
|
||||||
);
|
|
||||||
const inbound = withHostEndpoints(
|
|
||||||
inboundFromDb(dbInbound),
|
|
||||||
dbInbound.id,
|
|
||||||
hosts,
|
|
||||||
nodeAddress,
|
|
||||||
fallbackHostname,
|
|
||||||
);
|
|
||||||
if (inbound.protocol === Protocols.WIREGUARD) {
|
|
||||||
const peerRemark = client?.email
|
|
||||||
? `${dbInbound.remark}-${client.email}`
|
|
||||||
: dbInbound.remark || '';
|
|
||||||
setWireguardConfigs(
|
|
||||||
genWireguardPeerConfigs({
|
|
||||||
inbound,
|
|
||||||
remark: peerRemark,
|
|
||||||
hostOverride: nodeAddress,
|
|
||||||
fallbackHostname,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setWireguardLinks(
|
|
||||||
genWireguardPeerLinks({
|
|
||||||
inbound,
|
|
||||||
remark: peerRemark,
|
|
||||||
hostOverride: nodeAddress,
|
|
||||||
fallbackHostname,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setAmneziawgConfigs([]);
|
|
||||||
setAmneziawgLinks([]);
|
|
||||||
setLinks([]);
|
|
||||||
} else if (inbound.protocol === Protocols.AMNEZIAWG) {
|
|
||||||
const peerRemark = client?.email
|
|
||||||
? `${dbInbound.remark}-${client.email}`
|
|
||||||
: dbInbound.remark || '';
|
|
||||||
setAmneziawgConfigs(
|
|
||||||
genAmneziaWGPeerConfigs({
|
|
||||||
inbound,
|
|
||||||
remark: peerRemark,
|
|
||||||
hostOverride: nodeAddress,
|
|
||||||
fallbackHostname,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setAmneziawgLinks(
|
|
||||||
genAmneziaWGPeerLinks({
|
|
||||||
inbound,
|
|
||||||
remark: peerRemark,
|
|
||||||
hostOverride: nodeAddress,
|
|
||||||
fallbackHostname,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setWireguardConfigs([]);
|
|
||||||
setWireguardLinks([]);
|
|
||||||
setLinks([]);
|
|
||||||
} else {
|
|
||||||
setLinks(
|
|
||||||
genAllLinks({
|
|
||||||
inbound,
|
|
||||||
remark: dbInbound.remark || '',
|
|
||||||
client: client ?? {},
|
|
||||||
hostOverride: nodeAddress,
|
|
||||||
fallbackHostname,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setWireguardConfigs([]);
|
|
||||||
setWireguardLinks([]);
|
|
||||||
setAmneziawgConfigs([]);
|
|
||||||
setAmneziawgLinks([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const subId = client?.subId;
|
|
||||||
let nextSub = '';
|
|
||||||
let nextSubJson = '';
|
|
||||||
if (subSettings?.enable && subId) {
|
|
||||||
nextSub = (subSettings.subURI || '') + subId;
|
|
||||||
nextSubJson = subSettings.subJsonEnable ? (subSettings.subJsonURI || '') + subId : '';
|
|
||||||
}
|
|
||||||
setSubLink(nextSub);
|
|
||||||
setSubJsonLink(nextSubJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
const qrItems = useMemo<QrItem[]>(() => {
|
|
||||||
const items: QrItem[] = [];
|
|
||||||
if (subLink) {
|
|
||||||
items.push({ key: 'sub', header: t('subscription.title'), value: subLink });
|
|
||||||
}
|
|
||||||
if (subJsonLink) {
|
|
||||||
items.push({
|
|
||||||
key: 'sub-json',
|
|
||||||
header: `${t('subscription.title')} (JSON)`,
|
|
||||||
value: subJsonLink,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
links.forEach((link, idx) => {
|
|
||||||
items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link });
|
|
||||||
});
|
|
||||||
const pushTunnelPeers = (prefix: string, configs: string[][], peerLinks: string[][]) => {
|
|
||||||
configs.forEach((peerConfigs, idx) => {
|
|
||||||
peerConfigs.forEach((cfg, j) => {
|
|
||||||
const multi = peerConfigs.length > 1 ? ` #${j + 1}` : '';
|
|
||||||
items.push({
|
|
||||||
key: `${prefix}c${idx}-${j}`,
|
|
||||||
header: `Peer ${idx + 1} config${multi}`,
|
|
||||||
value: cfg,
|
|
||||||
downloadName: peerConfFileName(idx, j, peerConfigs.length),
|
|
||||||
});
|
|
||||||
const link = peerLinks[idx]?.[j];
|
|
||||||
if (link) {
|
|
||||||
items.push({
|
|
||||||
key: `${prefix}l${idx}-${j}`,
|
|
||||||
header: `Peer ${idx + 1} link${multi}`,
|
|
||||||
value: link,
|
|
||||||
showQr: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
pushTunnelPeers('w', wireguardConfigs, wireguardLinks);
|
|
||||||
pushTunnelPeers('a', amneziawgConfigs, amneziawgLinks);
|
|
||||||
return items;
|
|
||||||
}, [
|
|
||||||
subLink,
|
|
||||||
subJsonLink,
|
|
||||||
links,
|
|
||||||
wireguardConfigs,
|
|
||||||
wireguardLinks,
|
|
||||||
amneziawgConfigs,
|
|
||||||
amneziawgLinks,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const collapseItems: CollapseProps['items'] = useMemo(
|
|
||||||
() =>
|
|
||||||
qrItems.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
label: item.header,
|
|
||||||
children: (
|
|
||||||
<QrPanel
|
|
||||||
value={item.value}
|
|
||||||
remark={item.header}
|
|
||||||
downloadName={item.downloadName || ''}
|
|
||||||
showQr={item.showQr !== false && !isPostQuantumLink(item.value)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
[qrItems],
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null;
|
|
||||||
const [syncedFirstKey, setSyncedFirstKey] = useState<string | null>(null);
|
|
||||||
if (firstKey !== syncedFirstKey) {
|
|
||||||
setSyncedFirstKey(firstKey);
|
|
||||||
setActiveKey(firstKey ? [firstKey] : []);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
onCancel={onClose}
|
|
||||||
title={t('qrCode')}
|
|
||||||
footer={null}
|
|
||||||
width={420}
|
|
||||||
destroyOnHidden
|
|
||||||
>
|
|
||||||
{dbInbound && collapseItems && collapseItems.length > 0 && (
|
|
||||||
<Collapse
|
|
||||||
ghost
|
|
||||||
activeKey={activeKey}
|
|
||||||
onChange={(keys) => setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))}
|
|
||||||
items={collapseItems}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { default as QrPanel } from './QrPanel';
|
export { default as QrPanel } from './QrPanel';
|
||||||
export { default as QrCodeModal } from './QrCodeModal';
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { parseMsg } from '@/utils/zodValidate';
|
|||||||
import { DBInbound, coerceInboundJsonField } from '@/models/dbinbound';
|
import { DBInbound, coerceInboundJsonField } from '@/models/dbinbound';
|
||||||
import type { ClientStats, DBInboundInit } from '@/models/dbinbound';
|
import type { ClientStats, DBInboundInit } from '@/models/dbinbound';
|
||||||
import { Protocols } from '@/schemas/primitives';
|
import { Protocols } from '@/schemas/primitives';
|
||||||
import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
|
|
||||||
import { setDatepicker } from '@/hooks/useDatepicker';
|
import { setDatepicker } from '@/hooks/useDatepicker';
|
||||||
import { keys } from '@/api/queryKeys';
|
import { keys } from '@/api/queryKeys';
|
||||||
import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
|
import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
|
||||||
@@ -351,10 +350,8 @@ export function useInbounds() {
|
|||||||
const protocol = dbInbound.protocol;
|
const protocol = dbInbound.protocol;
|
||||||
if (!TRACKED_PROTOCOLS.includes(protocol)) continue;
|
if (!TRACKED_PROTOCOLS.includes(protocol)) continue;
|
||||||
const settings = coerceInboundJsonField(dbInbound.settings) as {
|
const settings = coerceInboundJsonField(dbInbound.settings) as {
|
||||||
method?: string;
|
|
||||||
clients?: Array<{ email?: string; enable?: boolean; comment?: string }>;
|
clients?: Array<{ email?: string; enable?: boolean; comment?: string }>;
|
||||||
};
|
};
|
||||||
if (protocol === Protocols.SHADOWSOCKS && !isSSMultiUser({ protocol, settings })) continue;
|
|
||||||
counts[dbInbound.id] = rollupClients(dbInbound, { clients: settings.clients });
|
counts[dbInbound.id] = rollupClients(dbInbound, { clients: settings.clients });
|
||||||
}
|
}
|
||||||
return counts;
|
return counts;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import {
|
|||||||
genWireguardLinks,
|
genWireguardLinks,
|
||||||
getInboundClients,
|
getInboundClients,
|
||||||
} from '@/lib/xray/inbound-link';
|
} from '@/lib/xray/inbound-link';
|
||||||
import { canEnableTlsFlow, isSS2022, isSSMultiUser } from '@/lib/xray/protocol-capabilities';
|
import { canEnableTlsFlow, isSS2022 } from '@/lib/xray/protocol-capabilities';
|
||||||
|
|
||||||
const FALLBACK_HOST = 'panel.example.test';
|
const FALLBACK_HOST = 'panel.example.test';
|
||||||
|
|
||||||
@@ -171,24 +171,6 @@ describe('inboundFromDb', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('protocol-capability helpers with raw coerced shapes', () => {
|
describe('protocol-capability helpers with raw coerced shapes', () => {
|
||||||
it('isSSMultiUser returns true for legacy SS methods', () => {
|
|
||||||
expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
isSSMultiUser({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } }),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isSSMultiUser returns false for single-user blake3-chacha20 method', () => {
|
|
||||||
expect(
|
|
||||||
isSSMultiUser({
|
|
||||||
protocol: 'shadowsocks',
|
|
||||||
settings: { method: '2022-blake3-chacha20-poly1305' },
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isSS2022 detects 2022-blake3 family', () => {
|
it('isSS2022 detects 2022-blake3 family', () => {
|
||||||
expect(
|
expect(
|
||||||
isSS2022({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } }),
|
isSS2022({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } }),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
canEnableStream,
|
canEnableStream,
|
||||||
canEnableVisionSeed,
|
canEnableVisionSeed,
|
||||||
isSS2022,
|
isSS2022,
|
||||||
isSSMultiUser,
|
|
||||||
} from '@/lib/xray/protocol-capabilities';
|
} from '@/lib/xray/protocol-capabilities';
|
||||||
|
|
||||||
// Pure-function tests for the capability predicates. Each fixture × stream
|
// Pure-function tests for the capability predicates. Each fixture × stream
|
||||||
@@ -67,7 +66,6 @@ describe('protocol capability predicates', () => {
|
|||||||
canEnableStream: canEnableStream(values),
|
canEnableStream: canEnableStream(values),
|
||||||
canEnableVisionSeed: canEnableVisionSeed(values),
|
canEnableVisionSeed: canEnableVisionSeed(values),
|
||||||
isSS2022: isSS2022(values),
|
isSS2022: isSS2022(values),
|
||||||
isSSMultiUser: isSSMultiUser(values),
|
|
||||||
};
|
};
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user