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
@@ -34,9 +34,11 @@ import {
CheckCircleOutlined,
WarningOutlined,
ExportOutlined,
ImportOutlined,
} from '@ant-design/icons';
import { HttpUtil } from '@/utils';
import { FileManager, HttpUtil } from '@/utils';
import PromptModal from '@/components/feedback/PromptModal';
import OutboundFormModal from './OutboundFormModal';
import { propagateOutboundTagRename } from '../basics/helpers';
@@ -223,6 +225,35 @@ export default function OutboundsTab({
});
}
const [importOpen, setImportOpen] = useState(false);
function exportOutbounds() {
FileManager.downloadTextFile(JSON.stringify(outbounds, null, 2), 'outbounds.json', {
type: 'application/json',
});
}
function importOutbounds(value: string) {
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
messageApi.error(t('pages.xray.importInvalidJson'));
return;
}
const obj = parsed as { outbounds?: unknown };
const list = Array.isArray(parsed) ? parsed : Array.isArray(obj?.outbounds) ? obj.outbounds : null;
if (!list) {
messageApi.error(t('pages.xray.importInvalidJson'));
return;
}
mutate((tt) => {
if (!Array.isArray(tt.outbounds)) tt.outbounds = [];
tt.outbounds.push(...(list as never[]));
});
setImportOpen(false);
}
// --- Subscription management (minimal inline UI) ---
async function loadSubs() {
setSubsLoading(true);
@@ -420,6 +451,9 @@ export default function OutboundsTab({
items: [
{ key: 'warp', icon: <CloudOutlined />, label: 'WARP', onClick: onShowWarp },
{ key: 'nord', icon: <ApiOutlined />, label: 'NordVPN', onClick: onShowNord },
{ type: 'divider' },
{ key: 'import', icon: <ImportOutlined />, label: t('pages.xray.importOutbounds'), onClick: () => setImportOpen(true) },
{ key: 'export', icon: <ExportOutlined />, label: t('pages.xray.exportOutbounds'), disabled: outbounds.length === 0, onClick: exportOutbounds },
],
}}
>
@@ -488,6 +522,15 @@ export default function OutboundsTab({
onClose={() => setModalOpen(false)}
onConfirm={onConfirm}
/>
<PromptModal
open={importOpen}
onClose={() => setImportOpen(false)}
title={t('pages.xray.importOutbounds')}
okText={t('pages.xray.importOutbounds')}
type="textarea"
json
onConfirm={importOutbounds}
/>
{/* Subscription outbounds (read-only, merged at runtime) */}
{Array.isArray(subscriptionOutbounds) && subscriptionOutbounds.length > 0 && (