Pārlūkot izejas kodu

fix: default hysteria tls to no utls fingerprint

Sanaei 1 dienu atpakaļ
vecāks
revīzija
af3c808444

+ 34 - 0
frontend/src/lib/xray/inbound-tls-defaults.ts

@@ -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;
+}

+ 19 - 0
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -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);

+ 2 - 13
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -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

+ 2 - 14
frontend/src/pages/inbounds/form/useSecurityActions.ts

@@ -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>;

+ 4 - 1
frontend/src/schemas/protocols/security/tls.ts

@@ -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 - 0
frontend/src/test/inbound-defaults.test.ts

@@ -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: '',
+      }),
+    ]);
+  });
+});

+ 68 - 0
frontend/src/test/stream-wire-normalize.test.ts

@@ -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', () => {