feat(tls,reality): port xray TLS/REALITY fields, cert-hash helpers, fallback UX

TLS: add verifyPeerCertByName (vcn) to inbound settings + emit in both share-link generators (frontend + Go sub) and outbound parser; the allowInsecure replacement xray removed after 2026-06-01. Add server-side curvePreferences, masterKeyLog, echSockopt (passthrough + form) at tlsSettings top-level so they survive the panel-only settings strip.

REALITY: add limitFallbackUpload/Download (afterBytes/bytesPerSec/burstBytesPerSec) with per-field tooltips, plus masterKeyLog. Verified field names/semantics against pinned xray v1.260327.1 (bytesPerSec=0 disables).

Hosts: fix verify_peer_cert_by_name column bool->string (xray expects comma-separated names) with an idempotent, history-gate-free migration (SQLite typeof blank; Postgres ALTER once); emit vcn for hosts/external proxies.

Server: add getCertHash (local cert DER SHA-256) and getRemoteCertHash (xray tls ping) endpoints + api-docs; wire pinned-cert field buttons. Drop the meaningless random-hash button.

Xray UI: metrics endpoint (listen/tag) config in Basics; import/export for routing rules and outbounds.

Fallbacks card: compact empty state, header-aligned actions, responsive labeled grid rows.

i18n: add all new keys to every locale; drop unused generateRandomPin.
This commit is contained in:
MHSanaei
2026-06-21 15:51:50 +02:00
parent 315ecc2588
commit 7c8889466b
48 changed files with 1316 additions and 173 deletions
@@ -100,17 +100,66 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
};
const generateRandomPinHash = () => {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
const hash = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
const current = (form.getFieldValue(
['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
) as string[] | undefined) ?? [];
form.setFieldValue(
['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
[...current, hash],
);
// Fill the pinned-cert field from the inbound's own certificate: read the
// first configured cert (file path or inline content) and ask the server for
// its hex SHA-256, then merge the hash(es) into pinnedPeerCertSha256.
const pinFromCert = async () => {
const certs = (form.getFieldValue(['streamSettings', 'tlsSettings', 'certificates']) ?? []) as Array<{
certificateFile?: string;
certificate?: string[];
}>;
const first = certs[0];
const certFile = first?.certificateFile?.trim() ?? '';
const certContent = Array.isArray(first?.certificate) ? first.certificate.join('\n').trim() : '';
if (!certFile && !certContent) {
messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
return;
}
setSaving(true);
try {
const msg = await HttpUtil.post('/panel/api/server/getCertHash', { certFile, certContent });
if (!msg?.success) {
messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
return;
}
const hashes = (msg.obj as string[] | undefined) ?? [];
if (hashes.length === 0) return;
const current = (form.getFieldValue(
['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
) as string[] | undefined) ?? [];
const merged = Array.from(new Set([...current, ...hashes]));
form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'], merged);
} finally {
setSaving(false);
}
};
// Fill the pinned-cert field by pinging the configured SNI: fetches the live
// remote certificate hash via `xray tls ping`. Useful when the panel doesn't
// hold the cert file (a CDN front / external endpoint).
const pinFromRemote = async () => {
const server = ((form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']) as string | undefined) ?? '').trim();
if (!server) {
messageApi.warning(t('pages.inbounds.form.pinFromRemoteNoSni'));
return;
}
setSaving(true);
try {
const msg = await HttpUtil.post('/panel/api/server/getRemoteCertHash', { server });
if (!msg?.success) {
messageApi.warning(msg?.msg || t('pages.inbounds.form.pinFromRemoteFailed'));
return;
}
const hashes = (msg.obj as string[] | undefined) ?? [];
if (hashes.length === 0) return;
const current = (form.getFieldValue(
['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
) as string[] | undefined) ?? [];
const merged = Array.from(new Set([...current, ...hashes]));
form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'], merged);
} finally {
setSaving(false);
}
};
const setCertFromPanel = async (certName: number) => {
@@ -194,7 +243,8 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
randomizeShortIds,
getNewEchCert,
clearEchCert,
generateRandomPinHash,
pinFromCert,
pinFromRemote,
setCertFromPanel,
clearCertFiles,
onSecurityChange,