mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-15 00:56:08 +00:00
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:
@@ -13,6 +13,7 @@ export type HostLinkInput = Pick<
|
||||
| 'alpn'
|
||||
| 'fingerprint'
|
||||
| 'pinnedPeerCertSha256'
|
||||
| 'verifyPeerCertByName'
|
||||
| 'echConfigList'
|
||||
| 'overrideSniFromAddress'
|
||||
| 'keepSniBlank'
|
||||
@@ -45,6 +46,7 @@ export function hostToExternalProxyEntry(host: HostLinkInput): ExternalProxyEntr
|
||||
alpn: host.alpn && host.alpn.length > 0 ? host.alpn : undefined,
|
||||
pinnedPeerCertSha256:
|
||||
host.pinnedPeerCertSha256 && host.pinnedPeerCertSha256.length > 0 ? host.pinnedPeerCertSha256 : undefined,
|
||||
verifyPeerCertByName: host.verifyPeerCertByName || undefined,
|
||||
echConfigList: host.echConfigList || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,6 +157,9 @@ function applyExternalProxyTLSObj(
|
||||
if (alpn.length > 0) obj.alpn = alpn;
|
||||
const pins = externalProxyPins(externalProxy.pinnedPeerCertSha256);
|
||||
if (pins.length > 0) obj.pcs = pins;
|
||||
if (externalProxy.verifyPeerCertByName && externalProxy.verifyPeerCertByName.length > 0) {
|
||||
obj.vcn = externalProxy.verifyPeerCertByName;
|
||||
}
|
||||
if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) obj.ech = externalProxy.echConfigList;
|
||||
}
|
||||
|
||||
@@ -254,6 +257,9 @@ export function genVmessLink(input: GenVmessLinkInput): string {
|
||||
if (tlsSettings.settings.fingerprint.length > 0) obj.fp = tlsSettings.settings.fingerprint;
|
||||
if (tlsSettings.alpn.length > 0) obj.alpn = tlsSettings.alpn.join(',');
|
||||
if (tlsSettings.settings.echConfigList.length > 0) obj.ech = tlsSettings.settings.echConfigList;
|
||||
if (tlsSettings.settings.verifyPeerCertByName.length > 0) {
|
||||
obj.vcn = tlsSettings.settings.verifyPeerCertByName;
|
||||
}
|
||||
if (tlsSettings.settings.pinnedPeerCertSha256.length > 0) {
|
||||
obj.pcs = tlsSettings.settings.pinnedPeerCertSha256.join(',');
|
||||
}
|
||||
@@ -301,6 +307,9 @@ function applyExternalProxyTLSParams(
|
||||
if (alpn.length > 0) params.set('alpn', alpn);
|
||||
const pins = externalProxyPins(externalProxy.pinnedPeerCertSha256);
|
||||
if (pins.length > 0) params.set('pcs', pins);
|
||||
if (externalProxy.verifyPeerCertByName && externalProxy.verifyPeerCertByName.length > 0) {
|
||||
params.set('vcn', externalProxy.verifyPeerCertByName);
|
||||
}
|
||||
if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) params.set('ech', externalProxy.echConfigList);
|
||||
}
|
||||
|
||||
@@ -384,6 +393,9 @@ export function genVlessLink(input: GenVlessLinkInput): string {
|
||||
params.set('alpn', tls.alpn.join(','));
|
||||
if (tls.serverName.length > 0) params.set('sni', tls.serverName);
|
||||
if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
|
||||
if (tls.settings.verifyPeerCertByName.length > 0) {
|
||||
params.set('vcn', tls.settings.verifyPeerCertByName);
|
||||
}
|
||||
if (tls.settings.pinnedPeerCertSha256.length > 0) {
|
||||
params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
|
||||
}
|
||||
@@ -476,6 +488,9 @@ function writeTlsParams(stream: NonNullable<Inbound['streamSettings']>, params:
|
||||
params.set('alpn', tls.alpn.join(','));
|
||||
if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
|
||||
if (tls.serverName.length > 0) params.set('sni', tls.serverName);
|
||||
if (tls.settings.verifyPeerCertByName.length > 0) {
|
||||
params.set('vcn', tls.settings.verifyPeerCertByName);
|
||||
}
|
||||
if (tls.settings.pinnedPeerCertSha256.length > 0) {
|
||||
params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
|
||||
}
|
||||
@@ -701,6 +716,9 @@ export function genHysteriaLink(input: GenHysteriaLinkInput): string {
|
||||
if (tls.alpn.length > 0) params.set('alpn', tls.alpn.join(','));
|
||||
if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
|
||||
if (tls.serverName.length > 0) params.set('sni', tls.serverName);
|
||||
if (tls.settings.verifyPeerCertByName.length > 0) {
|
||||
params.set('vcn', tls.settings.verifyPeerCertByName);
|
||||
}
|
||||
if (tls.settings.pinnedPeerCertSha256.length > 0) {
|
||||
params.set('pinSHA256', tls.settings.pinnedPeerCertSha256.map(hysteriaPinHex).join(','));
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@ function applySecurityParams(stream: Raw, params: URLSearchParams): void {
|
||||
const alpn = params.get('alpn');
|
||||
if (alpn) tls.alpn = alpn.split(',');
|
||||
tls.echConfigList = params.get('ech') ?? '';
|
||||
tls.verifyPeerCertByName = params.get('vcn') ?? '';
|
||||
tls.pinnedPeerCertSha256 = params.get('pcs') ?? '';
|
||||
} else if (stream.security === 'reality') {
|
||||
const reality = stream.realitySettings as Raw;
|
||||
@@ -434,7 +435,7 @@ export function parseHysteria2Link(link: string): Raw | null {
|
||||
alpn: alpn ? alpn.split(',') : ['h3'],
|
||||
fingerprint: params.get('fp') ?? '',
|
||||
echConfigList: params.get('ech') ?? '',
|
||||
verifyPeerCertByName: '',
|
||||
verifyPeerCertByName: params.get('vcn') ?? '',
|
||||
pinnedPeerCertSha256: params.get('pinSHA256') ?? '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -129,6 +129,20 @@ function normalizeTlsForWire(raw: Record<string, unknown>): Record<string, unkno
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
if (out.fingerprint === '') delete out.fingerprint;
|
||||
|
||||
// Empty server-side tuning fields mean "use xray-core's default" — never emit them.
|
||||
if (Array.isArray(out.curvePreferences) && out.curvePreferences.length === 0) {
|
||||
delete out.curvePreferences;
|
||||
}
|
||||
if (out.masterKeyLog === '' || out.masterKeyLog == null) delete out.masterKeyLog;
|
||||
if (isRecord(out.echSockopt)) {
|
||||
const echSock = normalizeSockoptForWire(out.echSockopt);
|
||||
if (echSock) {
|
||||
out.echSockopt = echSock;
|
||||
} else {
|
||||
delete out.echSockopt;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = out.settings;
|
||||
if (isRecord(settings)) {
|
||||
const settingsOut: Record<string, unknown> = { ...settings };
|
||||
|
||||
Reference in New Issue
Block a user