feat(clients): hide QR for post-quantum links in client info modal

Post-quantum keys (mldsa65 / ML-KEM-768) blow the encoded URL past
what a single QR can hold. Detect them by the markers VLESS share
links actually carry — `pqv=<base64>` for mldsa65Verify and
`encryption=mlkem768x25519plus.*` for ML-KEM-768 — and drop the QR
button for those rows. Copy still works.
This commit is contained in:
MHSanaei 2026-05-27 03:12:05 +02:00
parent e7ac1fadaa
commit 1752702f74
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A

View file

@ -33,6 +33,20 @@ const INBOUND_PROTOCOL_COLORS: Record<string, string> = {
const INBOUND_CHIP_LIMIT = 1; const INBOUND_CHIP_LIMIT = 1;
// Post-quantum keys blow up the encoded URL past what a single QR can
// hold. In VLESS share links the algorithm names don't appear as plain
// text — they ride inside query params:
// - mldsa65Verify becomes `pqv=<base64>` (sub/subService.go:841)
// - ML-KEM-768 becomes `encryption=mlkem768x25519plus.<...>`
// We also keep the literal substrings so configs that DO embed them
// directly (e.g. wireguard config text) still match.
function isPostQuantumLink(link: string): boolean {
if (/[?&]pqv=/.test(link)) return true;
if (link.includes('mlkem768') || link.includes('mldsa65')) return true;
if (link.includes('ML-KEM-768')) return true;
return false;
}
// 3x-ui's genRemark concatenates inbound remark + client email (and an // 3x-ui's genRemark concatenates inbound remark + client email (and an
// optional extra) using a configurable separator. The email half is // optional extra) using a configurable separator. The email half is
// redundant in the row title — the modal already names the client by // redundant in the row title — the modal already names the client by
@ -356,6 +370,7 @@ export default function ClientInfoModal({
const qrRemark = meta.remark || `${t('pages.clients.link')} ${idx + 1}`; const qrRemark = meta.remark || `${t('pages.clients.link')} ${idx + 1}`;
const rowTitle = trimEmail(meta.remark, client.email) const rowTitle = trimEmail(meta.remark, client.email)
|| `${t('pages.clients.link')} ${idx + 1}`; || `${t('pages.clients.link')} ${idx + 1}`;
const canQr = !isPostQuantumLink(link);
return ( return (
<div key={idx} className="link-row"> <div key={idx} className="link-row">
<Tag color={PROTOCOL_COLORS[meta.protocol] ?? 'default'} className="link-row-tag"> <Tag color={PROTOCOL_COLORS[meta.protocol] ?? 'default'} className="link-row-tag">
@ -366,16 +381,18 @@ export default function ClientInfoModal({
<Tooltip title={t('copy')}> <Tooltip title={t('copy')}>
<Button size="small" icon={<CopyOutlined />} onClick={() => copyValue(link)} /> <Button size="small" icon={<CopyOutlined />} onClick={() => copyValue(link)} />
</Tooltip> </Tooltip>
<Popover {canQr && (
trigger="click" <Popover
placement="left" trigger="click"
destroyOnHidden placement="left"
content={<QrPanel value={link} remark={qrRemark} size={220} />} destroyOnHidden
> content={<QrPanel value={link} remark={qrRemark} size={220} />}
<Tooltip title={t('pages.clients.qrCode')}> >
<Button size="small" icon={<QrcodeOutlined />} /> <Tooltip title={t('pages.clients.qrCode')}>
</Tooltip> <Button size="small" icon={<QrcodeOutlined />} />
</Popover> </Tooltip>
</Popover>
)}
</div> </div>
</div> </div>
); );