mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 13:24:33 +00:00
fix: default hysteria tls to no utls fingerprint
This commit is contained in:
34
frontend/src/lib/xray/inbound-tls-defaults.ts
Normal file
34
frontend/src/lib/xray/inbound-tls-defaults.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
|
||||
|
||||
function defaultCertificate(): Record<string, unknown> {
|
||||
return {
|
||||
useFile: true,
|
||||
certificateFile: '',
|
||||
keyFile: '',
|
||||
certificate: [],
|
||||
key: [],
|
||||
ocspStapling: 3600,
|
||||
oneTimeLoading: false,
|
||||
usage: 'encipherment',
|
||||
buildChain: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTlsSettingsWithDefaultCert(): Record<string, unknown> {
|
||||
const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
|
||||
tls.certificates = [defaultCertificate()];
|
||||
return tls;
|
||||
}
|
||||
|
||||
export function createHysteriaTlsSettingsWithDefaultCert(): Record<string, unknown> {
|
||||
const tls = createTlsSettingsWithDefaultCert();
|
||||
tls.alpn = ['h3'];
|
||||
|
||||
const settings = tls.settings && typeof tls.settings === 'object' && !Array.isArray(tls.settings)
|
||||
? { ...(tls.settings as Record<string, unknown>) }
|
||||
: {};
|
||||
settings.fingerprint = '';
|
||||
tls.settings = settings;
|
||||
|
||||
return tls;
|
||||
}
|
||||
@@ -95,6 +95,20 @@ function dropZeroNumbers(obj: Record<string, unknown>, keys: readonly string[]):
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTlsForWire(raw: Record<string, unknown>): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
if (out.fingerprint === '') delete out.fingerprint;
|
||||
|
||||
const settings = out.settings;
|
||||
if (isRecord(settings)) {
|
||||
const settingsOut: Record<string, unknown> = { ...settings };
|
||||
if (settingsOut.fingerprint === '') delete settingsOut.fingerprint;
|
||||
out.settings = settingsOut;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function normalizeXhttpForWire(
|
||||
raw: Record<string, unknown>,
|
||||
side: StreamWireSide,
|
||||
@@ -211,6 +225,11 @@ export function normalizeStreamSettingsForWire(
|
||||
out.xhttpSettings = normalizeXhttpForWire(xhttp, opts.side);
|
||||
}
|
||||
|
||||
const tls = out.tlsSettings;
|
||||
if (isRecord(tls)) {
|
||||
out.tlsSettings = normalizeTlsForWire(tls);
|
||||
}
|
||||
|
||||
const sockopt = out.sockopt;
|
||||
if (isRecord(sockopt)) {
|
||||
const normalized = normalizeSockoptForWire(sockopt);
|
||||
|
||||
@@ -36,7 +36,7 @@ import { antdRule } from '@/utils/zodForm';
|
||||
import { Protocols } from '@/schemas/primitives';
|
||||
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
||||
import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
|
||||
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
|
||||
import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
|
||||
import { SniffingSchema } from '@/schemas/primitives/sniffing';
|
||||
import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
|
||||
import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp';
|
||||
@@ -351,22 +351,11 @@ export default function InboundFormModal({
|
||||
// snap back to TCP so the standard network selector has a valid
|
||||
// starting point.
|
||||
if (next === Protocols.HYSTERIA) {
|
||||
const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
|
||||
tls.certificates = [{
|
||||
useFile: true,
|
||||
certificateFile: '',
|
||||
keyFile: '',
|
||||
certificate: [],
|
||||
key: [],
|
||||
oneTimeLoading: false,
|
||||
usage: 'encipherment',
|
||||
buildChain: false,
|
||||
}];
|
||||
form.setFieldValue('streamSettings', {
|
||||
network: 'hysteria',
|
||||
security: 'tls',
|
||||
hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
|
||||
tlsSettings: tls,
|
||||
tlsSettings: createHysteriaTlsSettingsWithDefaultCert(),
|
||||
// Hysteria2 needs an obfs wrapper on the FinalMask side; seed
|
||||
// it with salamander + a random password so the listener boots
|
||||
// with a usable default. Re-selecting Hysteria from another
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { MessageInstance } from 'antd/es/message/interface';
|
||||
|
||||
import { HttpUtil, RandomUtil } from '@/utils';
|
||||
import { getRandomRealityTarget } from '@/models/reality-targets';
|
||||
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
|
||||
import { createTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
|
||||
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
|
||||
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
||||
|
||||
@@ -160,19 +160,7 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
|
||||
delete cleaned.tlsSettings;
|
||||
delete cleaned.realitySettings;
|
||||
if (next === 'tls') {
|
||||
const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
|
||||
tls.certificates = [{
|
||||
useFile: true,
|
||||
certificateFile: '',
|
||||
keyFile: '',
|
||||
certificate: [],
|
||||
key: [],
|
||||
ocspStapling: 3600,
|
||||
oneTimeLoading: false,
|
||||
usage: 'encipherment',
|
||||
buildChain: false,
|
||||
}];
|
||||
cleaned.tlsSettings = tls;
|
||||
cleaned.tlsSettings = createTlsSettingsWithDefaultCert();
|
||||
}
|
||||
if (next === 'reality') {
|
||||
const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
|
||||
|
||||
@@ -22,6 +22,9 @@ export const UtlsFingerprintSchema = z.enum([
|
||||
]);
|
||||
export type UtlsFingerprint = z.infer<typeof UtlsFingerprintSchema>;
|
||||
|
||||
export const TlsFingerprintSchema = z.union([UtlsFingerprintSchema, z.literal('')]);
|
||||
export type TlsFingerprint = z.infer<typeof TlsFingerprintSchema>;
|
||||
|
||||
export const AlpnSchema = z.enum(['h3', 'h2', 'http/1.1']);
|
||||
export type Alpn = z.infer<typeof AlpnSchema>;
|
||||
|
||||
@@ -51,7 +54,7 @@ export const TlsCertSchema = z.union([TlsCertFileSchema, TlsCertInlineSchema]);
|
||||
export type TlsCert = z.infer<typeof TlsCertSchema>;
|
||||
|
||||
export const TlsClientSettingsSchema = z.object({
|
||||
fingerprint: UtlsFingerprintSchema.default('chrome'),
|
||||
fingerprint: TlsFingerprintSchema.default('chrome'),
|
||||
echConfigList: z.string().default(''),
|
||||
pinnedPeerCertSha256: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
createDefaultVmessInboundSettings,
|
||||
createDefaultWireguardInboundSettings,
|
||||
} from '@/lib/xray/inbound-defaults';
|
||||
import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
|
||||
import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
|
||||
import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
|
||||
import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
|
||||
@@ -147,3 +148,18 @@ describe('createDefault*InboundSettings factories', () => {
|
||||
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createHysteriaTlsSettingsWithDefaultCert', () => {
|
||||
it('defaults Hysteria TLS to uTLS None and h3 ALPN', () => {
|
||||
const tls = createHysteriaTlsSettingsWithDefaultCert();
|
||||
expect(tls.alpn).toEqual(['h3']);
|
||||
expect((tls.settings as Record<string, unknown>).fingerprint).toBe('');
|
||||
expect(tls.certificates).toEqual([
|
||||
expect.objectContaining({
|
||||
useFile: true,
|
||||
certificateFile: '',
|
||||
keyFile: '',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
normalizeXhttpForWire,
|
||||
validateRealityTarget,
|
||||
} from '@/lib/xray/stream-wire-normalize';
|
||||
import { InboundFormSchema } from '@/schemas/forms/inbound-form';
|
||||
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
||||
|
||||
describe('validateRealityTarget', () => {
|
||||
@@ -150,6 +151,28 @@ describe('normalizeStreamSettingsForWire reality', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeStreamSettingsForWire tls', () => {
|
||||
it('drops empty uTLS fingerprints from inbound and outbound TLS shapes', () => {
|
||||
const out = normalizeStreamSettingsForWire({
|
||||
network: 'hysteria',
|
||||
security: 'tls',
|
||||
tlsSettings: {
|
||||
fingerprint: '',
|
||||
settings: {
|
||||
fingerprint: '',
|
||||
echConfigList: '',
|
||||
},
|
||||
},
|
||||
}, { side: 'inbound' });
|
||||
|
||||
const tls = out.tlsSettings as Record<string, unknown>;
|
||||
const settings = tls.settings as Record<string, unknown>;
|
||||
expect(tls).not.toHaveProperty('fingerprint');
|
||||
expect(settings).not.toHaveProperty('fingerprint');
|
||||
expect(settings.echConfigList).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inbound formValuesToWirePayload integration', () => {
|
||||
it('emits lean stream-one xhttp + sockopt on save', () => {
|
||||
const values = {
|
||||
@@ -209,6 +232,51 @@ describe('inbound formValuesToWirePayload integration', () => {
|
||||
const realitySettings = reality.settings as Record<string, unknown>;
|
||||
expect(realitySettings.publicKey).toBe('pub');
|
||||
});
|
||||
|
||||
it('accepts Hysteria TLS with uTLS None and omits fingerprint on save', () => {
|
||||
const values = {
|
||||
remark: 'hy2',
|
||||
enable: true,
|
||||
port: 443,
|
||||
listen: '',
|
||||
tag: 'hy2-443',
|
||||
expiryTime: 0,
|
||||
sniffing: { enabled: false },
|
||||
up: 0,
|
||||
down: 0,
|
||||
total: 0,
|
||||
trafficReset: 'never',
|
||||
lastTrafficResetTime: 0,
|
||||
nodeId: null,
|
||||
protocol: 'hysteria',
|
||||
settings: { version: 2, clients: [] },
|
||||
streamSettings: {
|
||||
network: 'hysteria',
|
||||
security: 'tls',
|
||||
hysteriaSettings: {
|
||||
version: 2,
|
||||
auth: 'auth',
|
||||
udpIdleTimeout: 60,
|
||||
},
|
||||
tlsSettings: {
|
||||
alpn: ['h3'],
|
||||
settings: {
|
||||
fingerprint: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const parsed = InboundFormSchema.safeParse(values);
|
||||
expect(parsed.success).toBe(true);
|
||||
if (!parsed.success) throw parsed.error;
|
||||
|
||||
const payload = formValuesToWirePayload(parsed.data);
|
||||
const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
|
||||
const tls = stream.tlsSettings as Record<string, unknown>;
|
||||
const settings = tls.settings as Record<string, unknown>;
|
||||
expect(settings).not.toHaveProperty('fingerprint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('freedom outbound sockopt wire payload', () => {
|
||||
|
||||
Reference in New Issue
Block a user