mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-20 18:11:00 +00:00
fix: default hysteria tls to no utls fingerprint
This commit is contained in:
@@ -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(
|
export function normalizeXhttpForWire(
|
||||||
raw: Record<string, unknown>,
|
raw: Record<string, unknown>,
|
||||||
side: StreamWireSide,
|
side: StreamWireSide,
|
||||||
@@ -211,6 +225,11 @@ export function normalizeStreamSettingsForWire(
|
|||||||
out.xhttpSettings = normalizeXhttpForWire(xhttp, opts.side);
|
out.xhttpSettings = normalizeXhttpForWire(xhttp, opts.side);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tls = out.tlsSettings;
|
||||||
|
if (isRecord(tls)) {
|
||||||
|
out.tlsSettings = normalizeTlsForWire(tls);
|
||||||
|
}
|
||||||
|
|
||||||
const sockopt = out.sockopt;
|
const sockopt = out.sockopt;
|
||||||
if (isRecord(sockopt)) {
|
if (isRecord(sockopt)) {
|
||||||
const normalized = normalizeSockoptForWire(sockopt);
|
const normalized = normalizeSockoptForWire(sockopt);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { antdRule } from '@/utils/zodForm';
|
|||||||
import { Protocols } from '@/schemas/primitives';
|
import { Protocols } from '@/schemas/primitives';
|
||||||
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
||||||
import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
|
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 { SniffingSchema } from '@/schemas/primitives/sniffing';
|
||||||
import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
|
import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
|
||||||
import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp';
|
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
|
// snap back to TCP so the standard network selector has a valid
|
||||||
// starting point.
|
// starting point.
|
||||||
if (next === Protocols.HYSTERIA) {
|
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', {
|
form.setFieldValue('streamSettings', {
|
||||||
network: 'hysteria',
|
network: 'hysteria',
|
||||||
security: 'tls',
|
security: 'tls',
|
||||||
hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
|
hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
|
||||||
tlsSettings: tls,
|
tlsSettings: createHysteriaTlsSettingsWithDefaultCert(),
|
||||||
// Hysteria2 needs an obfs wrapper on the FinalMask side; seed
|
// Hysteria2 needs an obfs wrapper on the FinalMask side; seed
|
||||||
// it with salamander + a random password so the listener boots
|
// it with salamander + a random password so the listener boots
|
||||||
// with a usable default. Re-selecting Hysteria from another
|
// 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 { HttpUtil, RandomUtil } from '@/utils';
|
||||||
import { getRandomRealityTarget } from '@/models/reality-targets';
|
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 { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
|
||||||
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
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.tlsSettings;
|
||||||
delete cleaned.realitySettings;
|
delete cleaned.realitySettings;
|
||||||
if (next === 'tls') {
|
if (next === 'tls') {
|
||||||
const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
|
cleaned.tlsSettings = createTlsSettingsWithDefaultCert();
|
||||||
tls.certificates = [{
|
|
||||||
useFile: true,
|
|
||||||
certificateFile: '',
|
|
||||||
keyFile: '',
|
|
||||||
certificate: [],
|
|
||||||
key: [],
|
|
||||||
ocspStapling: 3600,
|
|
||||||
oneTimeLoading: false,
|
|
||||||
usage: 'encipherment',
|
|
||||||
buildChain: false,
|
|
||||||
}];
|
|
||||||
cleaned.tlsSettings = tls;
|
|
||||||
}
|
}
|
||||||
if (next === 'reality') {
|
if (next === 'reality') {
|
||||||
const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
|
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 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 const AlpnSchema = z.enum(['h3', 'h2', 'http/1.1']);
|
||||||
export type Alpn = z.infer<typeof AlpnSchema>;
|
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 type TlsCert = z.infer<typeof TlsCertSchema>;
|
||||||
|
|
||||||
export const TlsClientSettingsSchema = z.object({
|
export const TlsClientSettingsSchema = z.object({
|
||||||
fingerprint: UtlsFingerprintSchema.default('chrome'),
|
fingerprint: TlsFingerprintSchema.default('chrome'),
|
||||||
echConfigList: z.string().default(''),
|
echConfigList: z.string().default(''),
|
||||||
pinnedPeerCertSha256: z.array(z.string()).default([]),
|
pinnedPeerCertSha256: z.array(z.string()).default([]),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
createDefaultVmessInboundSettings,
|
createDefaultVmessInboundSettings,
|
||||||
createDefaultWireguardInboundSettings,
|
createDefaultWireguardInboundSettings,
|
||||||
} from '@/lib/xray/inbound-defaults';
|
} from '@/lib/xray/inbound-defaults';
|
||||||
|
import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
|
||||||
import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
|
import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
|
||||||
import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
|
import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
|
||||||
import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
|
import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
|
||||||
@@ -147,3 +148,18 @@ describe('createDefault*InboundSettings factories', () => {
|
|||||||
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
|
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,
|
normalizeXhttpForWire,
|
||||||
validateRealityTarget,
|
validateRealityTarget,
|
||||||
} from '@/lib/xray/stream-wire-normalize';
|
} from '@/lib/xray/stream-wire-normalize';
|
||||||
|
import { InboundFormSchema } from '@/schemas/forms/inbound-form';
|
||||||
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
||||||
|
|
||||||
describe('validateRealityTarget', () => {
|
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', () => {
|
describe('inbound formValuesToWirePayload integration', () => {
|
||||||
it('emits lean stream-one xhttp + sockopt on save', () => {
|
it('emits lean stream-one xhttp + sockopt on save', () => {
|
||||||
const values = {
|
const values = {
|
||||||
@@ -209,6 +232,51 @@ describe('inbound formValuesToWirePayload integration', () => {
|
|||||||
const realitySettings = reality.settings as Record<string, unknown>;
|
const realitySettings = reality.settings as Record<string, unknown>;
|
||||||
expect(realitySettings.publicKey).toBe('pub');
|
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', () => {
|
describe('freedom outbound sockopt wire payload', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user