mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 09:29:34 +00:00
feat(frontend): stream and security Zod families with discriminated unions
Stand up the remaining Step 2 families. NetworkSettingsSchema is a 6-branch DU on `network` covering tcp/kcp/ws/grpc/httpupgrade/xhttp, with asymmetric per-network wire keys (tcpSettings, wsSettings, ...) preserved exactly so fixtures round-trip byte-identical. SecuritySettingsSchema is a 3-branch DU on `security` covering none/tls/reality. TLS certs use a file-vs-inline union; uTLS fingerprints are shared between TLS and Reality via a single primitive enum. Hysteria-as-network, finalmask, and sockopt are not in the plan's Step 2 inventory and are deferred to Step 6 (Tighten) - they're orthogonal extras on the stream root, not network-discriminated branches. Resolves a Security identifier collision in protocols/index.ts by re-exporting the type alias as SecurityKind (the `Security` name is taken by the namespace re-export).
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
export * as Inbound from './inbound';
|
||||
export * as Outbound from './outbound';
|
||||
export * as Stream from './stream';
|
||||
export * as Security from './security';
|
||||
|
||||
export { InboundSettingsSchema } from './inbound';
|
||||
export type { InboundSettings } from './inbound';
|
||||
export { OutboundSettingsSchema } from './outbound';
|
||||
export type { OutboundSettings } from './outbound';
|
||||
export { NetworkSchema, NetworkSettingsSchema } from './stream';
|
||||
export type { Network, NetworkSettings } from './stream';
|
||||
export { SecuritySchema, SecuritySettingsSchema } from './security';
|
||||
export type { Security as SecurityKind, SecuritySettings } from './security';
|
||||
|
||||
23
frontend/src/schemas/protocols/security/index.ts
Normal file
23
frontend/src/schemas/protocols/security/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { RealityStreamSettingsSchema } from './reality';
|
||||
import { TlsStreamSettingsSchema } from './tls';
|
||||
|
||||
export * from './none';
|
||||
export * from './reality';
|
||||
export * from './tls';
|
||||
|
||||
export const SecuritySchema = z.enum(['none', 'tls', 'reality']);
|
||||
export type Security = z.infer<typeof SecuritySchema>;
|
||||
|
||||
// Tagged-wrapper DU on `security`. Wire shape: when security==='tls' only
|
||||
// `tlsSettings` is present, when 'reality' only `realitySettings`, when
|
||||
// 'none' neither key appears. The Xray panel's StreamSettings class emits
|
||||
// `undefined` for the inactive branch which strips the key during JSON
|
||||
// serialization, so this DU faithfully describes what's on disk.
|
||||
export const SecuritySettingsSchema = z.discriminatedUnion('security', [
|
||||
z.object({ security: z.literal('none') }),
|
||||
z.object({ security: z.literal('tls'), tlsSettings: TlsStreamSettingsSchema }),
|
||||
z.object({ security: z.literal('reality'), realitySettings: RealityStreamSettingsSchema }),
|
||||
]);
|
||||
export type SecuritySettings = z.infer<typeof SecuritySettingsSchema>;
|
||||
7
frontend/src/schemas/protocols/security/none.ts
Normal file
7
frontend/src/schemas/protocols/security/none.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// `security: 'none'` carries no payload — the streamSettings root just omits
|
||||
// both `tlsSettings` and `realitySettings`. This empty leaf is kept for
|
||||
// symmetry so the discriminated union has a branch for every security value.
|
||||
export const NoneSecuritySettingsSchema = z.object({});
|
||||
export type NoneSecuritySettings = z.infer<typeof NoneSecuritySettingsSchema>;
|
||||
41
frontend/src/schemas/protocols/security/reality.ts
Normal file
41
frontend/src/schemas/protocols/security/reality.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { UtlsFingerprintSchema } from '@/schemas/protocols/security/tls';
|
||||
|
||||
// Reality client-side handshake config (sits under the inbound's
|
||||
// realitySettings.settings on the wire — the panel's class names the field
|
||||
// `settings` even though it's the "client" half of Reality).
|
||||
export const RealityClientSettingsSchema = z.object({
|
||||
publicKey: z.string().default(''),
|
||||
fingerprint: UtlsFingerprintSchema.default('chrome'),
|
||||
serverName: z.string().default(''),
|
||||
spiderX: z.string().default('/'),
|
||||
mldsa65Verify: z.string().default(''),
|
||||
});
|
||||
export type RealityClientSettings = z.infer<typeof RealityClientSettingsSchema>;
|
||||
|
||||
// Reality stream payload. `serverNames` and `shortIds` are stored as
|
||||
// comma-joined strings in the panel class but ship as string[] on the wire
|
||||
// — fixtures round-trip through the array form. `target` is the dest host
|
||||
// Reality piggybacks on; the panel auto-generates random target+SNI when
|
||||
// blank.
|
||||
export const RealityStreamSettingsSchema = z.object({
|
||||
show: z.boolean().default(false),
|
||||
xver: z.number().int().min(0).default(0),
|
||||
target: z.string().default(''),
|
||||
serverNames: z.array(z.string()).default([]),
|
||||
privateKey: z.string().default(''),
|
||||
minClientVer: z.string().default(''),
|
||||
maxClientVer: z.string().default(''),
|
||||
maxTimediff: z.number().int().min(0).default(0),
|
||||
shortIds: z.array(z.string()).default([]),
|
||||
mldsa65Seed: z.string().default(''),
|
||||
settings: RealityClientSettingsSchema.default({
|
||||
publicKey: '',
|
||||
fingerprint: 'chrome',
|
||||
serverName: '',
|
||||
spiderX: '/',
|
||||
mldsa65Verify: '',
|
||||
}),
|
||||
});
|
||||
export type RealityStreamSettings = z.infer<typeof RealityStreamSettingsSchema>;
|
||||
72
frontend/src/schemas/protocols/security/tls.ts
Normal file
72
frontend/src/schemas/protocols/security/tls.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const TlsVersionSchema = z.enum(['1.0', '1.1', '1.2', '1.3']);
|
||||
export type TlsVersion = z.infer<typeof TlsVersionSchema>;
|
||||
|
||||
// Xray's uTLS fingerprints — used both for TLS and Reality. Kept here (not
|
||||
// in primitives/) because the only consumer is security/tls.ts and
|
||||
// security/reality.ts via re-import.
|
||||
export const UtlsFingerprintSchema = z.enum([
|
||||
'chrome',
|
||||
'firefox',
|
||||
'safari',
|
||||
'ios',
|
||||
'android',
|
||||
'edge',
|
||||
'360',
|
||||
'qq',
|
||||
'random',
|
||||
'randomized',
|
||||
'randomizednoalpn',
|
||||
'unsafe',
|
||||
]);
|
||||
export type UtlsFingerprint = z.infer<typeof UtlsFingerprintSchema>;
|
||||
|
||||
export const AlpnSchema = z.enum(['h3', 'h2', 'http/1.1']);
|
||||
export type Alpn = z.infer<typeof AlpnSchema>;
|
||||
|
||||
export const TlsCertUsageSchema = z.enum(['encipherment', 'verify', 'issue']);
|
||||
export type TlsCertUsage = z.infer<typeof TlsCertUsageSchema>;
|
||||
|
||||
// TLS certs on the wire come in two shapes — file-backed or inline. The
|
||||
// panel class collapses them into one with a `useFile` boolean; we model
|
||||
// the wire shape as a DU so saves round-trip without the boolean leaking.
|
||||
export const TlsCertFileSchema = z.object({
|
||||
certificateFile: z.string().min(1),
|
||||
keyFile: z.string().min(1),
|
||||
oneTimeLoading: z.boolean().default(false),
|
||||
usage: TlsCertUsageSchema.default('encipherment'),
|
||||
buildChain: z.boolean().default(false),
|
||||
});
|
||||
export const TlsCertInlineSchema = z.object({
|
||||
certificate: z.array(z.string()),
|
||||
key: z.array(z.string()),
|
||||
oneTimeLoading: z.boolean().default(false),
|
||||
usage: TlsCertUsageSchema.default('encipherment'),
|
||||
buildChain: z.boolean().default(false),
|
||||
});
|
||||
export const TlsCertSchema = z.union([TlsCertFileSchema, TlsCertInlineSchema]);
|
||||
export type TlsCert = z.infer<typeof TlsCertSchema>;
|
||||
|
||||
export const TlsClientSettingsSchema = z.object({
|
||||
fingerprint: UtlsFingerprintSchema.default('chrome'),
|
||||
echConfigList: z.string().default(''),
|
||||
});
|
||||
export type TlsClientSettings = z.infer<typeof TlsClientSettingsSchema>;
|
||||
|
||||
// `serverName` is the SNI; the class field is `sni` internally but on the
|
||||
// wire stays `serverName` to match Xray's config schema.
|
||||
export const TlsStreamSettingsSchema = z.object({
|
||||
serverName: z.string().default(''),
|
||||
minVersion: TlsVersionSchema.default('1.2'),
|
||||
maxVersion: TlsVersionSchema.default('1.3'),
|
||||
cipherSuites: z.string().default(''),
|
||||
rejectUnknownSni: z.boolean().default(false),
|
||||
disableSystemRoot: z.boolean().default(false),
|
||||
enableSessionResumption: z.boolean().default(false),
|
||||
certificates: z.array(TlsCertSchema).default([]),
|
||||
alpn: z.array(AlpnSchema).default(['h2', 'http/1.1']),
|
||||
echServerKeys: z.string().default(''),
|
||||
settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '' }),
|
||||
});
|
||||
export type TlsStreamSettings = z.infer<typeof TlsStreamSettingsSchema>;
|
||||
11
frontend/src/schemas/protocols/stream/grpc.ts
Normal file
11
frontend/src/schemas/protocols/stream/grpc.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// gRPC stream is the lightest transport — three booleans/strings, no
|
||||
// header obfuscation. `multiMode` enables multi-stream gRPC (multiple
|
||||
// concurrent streams over one connection).
|
||||
export const GrpcStreamSettingsSchema = z.object({
|
||||
serviceName: z.string().default(''),
|
||||
authority: z.string().default(''),
|
||||
multiMode: z.boolean().default(false),
|
||||
});
|
||||
export type GrpcStreamSettings = z.infer<typeof GrpcStreamSettingsSchema>;
|
||||
14
frontend/src/schemas/protocols/stream/httpupgrade.ts
Normal file
14
frontend/src/schemas/protocols/stream/httpupgrade.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WsHeaderMapSchema } from '@/schemas/protocols/stream/ws';
|
||||
|
||||
// HTTP Upgrade transport reuses the flat WS-style header map (string values,
|
||||
// not arrays — toV2Headers with arr=false). No heartbeat field — that's
|
||||
// websocket-specific.
|
||||
export const HttpUpgradeStreamSettingsSchema = z.object({
|
||||
acceptProxyProtocol: z.boolean().default(false),
|
||||
path: z.string().default('/'),
|
||||
host: z.string().default(''),
|
||||
headers: WsHeaderMapSchema.default({}),
|
||||
});
|
||||
export type HttpUpgradeStreamSettings = z.infer<typeof HttpUpgradeStreamSettingsSchema>;
|
||||
33
frontend/src/schemas/protocols/stream/index.ts
Normal file
33
frontend/src/schemas/protocols/stream/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { GrpcStreamSettingsSchema } from './grpc';
|
||||
import { HttpUpgradeStreamSettingsSchema } from './httpupgrade';
|
||||
import { KcpStreamSettingsSchema } from './kcp';
|
||||
import { TcpStreamSettingsSchema } from './tcp';
|
||||
import { WsStreamSettingsSchema } from './ws';
|
||||
import { XHttpStreamSettingsSchema } from './xhttp';
|
||||
|
||||
export * from './grpc';
|
||||
export * from './httpupgrade';
|
||||
export * from './kcp';
|
||||
export * from './tcp';
|
||||
export * from './ws';
|
||||
export * from './xhttp';
|
||||
|
||||
export const NetworkSchema = z.enum(['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']);
|
||||
export type Network = z.infer<typeof NetworkSchema>;
|
||||
|
||||
// Tagged-wrapper DU on `network`. The wire shape uses an asymmetric per-
|
||||
// network key (`tcpSettings`, `wsSettings`, ...) rather than a single
|
||||
// `settings` object — same pattern Xray ships and the panel's StreamSettings
|
||||
// class flattens via toJson. Each branch carries only the matching key so
|
||||
// fixtures round-trip byte-identical.
|
||||
export const NetworkSettingsSchema = z.discriminatedUnion('network', [
|
||||
z.object({ network: z.literal('tcp'), tcpSettings: TcpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('kcp'), kcpSettings: KcpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('ws'), wsSettings: WsStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('grpc'), grpcSettings: GrpcStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('httpupgrade'), httpupgradeSettings: HttpUpgradeStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
|
||||
]);
|
||||
export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
|
||||
14
frontend/src/schemas/protocols/stream/kcp.ts
Normal file
14
frontend/src/schemas/protocols/stream/kcp.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// mKCP transport (Xray's reliable UDP). The panel renames upCap/downCap on
|
||||
// the JS side back to uplinkCapacity/downlinkCapacity on the wire. Defaults
|
||||
// match xray-core's recommended values.
|
||||
export const KcpStreamSettingsSchema = z.object({
|
||||
mtu: z.number().int().min(576).max(1460).default(1350),
|
||||
tti: z.number().int().min(10).max(100).default(20),
|
||||
uplinkCapacity: z.number().int().min(0).default(5),
|
||||
downlinkCapacity: z.number().int().min(0).default(20),
|
||||
cwndMultiplier: z.number().int().min(1).default(1),
|
||||
maxSendingWindow: z.number().int().min(0).default(2097152),
|
||||
});
|
||||
export type KcpStreamSettings = z.infer<typeof KcpStreamSettingsSchema>;
|
||||
47
frontend/src/schemas/protocols/stream/tcp.ts
Normal file
47
frontend/src/schemas/protocols/stream/tcp.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Xray's V2-style header map: { Host: ['example.com', ...], ... }. Each
|
||||
// header name maps to a string[] because HTTP allows repeated headers
|
||||
// (Accept, Cookie, etc.). The panel renders these as a flat name/value
|
||||
// table internally and flattens to this map on save via toV2Headers.
|
||||
export const V2HeaderMapSchema = z.record(z.string(), z.array(z.string()));
|
||||
export type V2HeaderMap = z.infer<typeof V2HeaderMapSchema>;
|
||||
|
||||
export const TcpRequestSchema = z.object({
|
||||
version: z.string().default('1.1'),
|
||||
method: z.string().default('GET'),
|
||||
path: z.array(z.string()).min(1).default(['/']),
|
||||
headers: V2HeaderMapSchema.default({}),
|
||||
});
|
||||
export type TcpRequest = z.infer<typeof TcpRequestSchema>;
|
||||
|
||||
export const TcpResponseSchema = z.object({
|
||||
version: z.string().default('1.1'),
|
||||
status: z.string().default('200'),
|
||||
reason: z.string().default('OK'),
|
||||
headers: V2HeaderMapSchema.default({}),
|
||||
});
|
||||
export type TcpResponse = z.infer<typeof TcpResponseSchema>;
|
||||
|
||||
// TCP stream `header` is the obfuscation header. type='none' (the wire
|
||||
// representation just omits `header` entirely) or type='http' (HTTP-1.1
|
||||
// camouflage with request/response sub-objects).
|
||||
export const TcpHeaderHttpSchema = z.object({
|
||||
type: z.literal('http'),
|
||||
request: TcpRequestSchema.optional(),
|
||||
response: TcpResponseSchema.optional(),
|
||||
});
|
||||
export const TcpHeaderNoneSchema = z.object({ type: z.literal('none') });
|
||||
export const TcpHeaderSchema = z.discriminatedUnion('type', [
|
||||
TcpHeaderNoneSchema,
|
||||
TcpHeaderHttpSchema,
|
||||
]);
|
||||
export type TcpHeader = z.infer<typeof TcpHeaderSchema>;
|
||||
|
||||
// Top-level TCP stream payload. `acceptProxyProtocol` only appears on the
|
||||
// wire when true (panel omits it when false), so we treat it as optional.
|
||||
export const TcpStreamSettingsSchema = z.object({
|
||||
acceptProxyProtocol: z.literal(true).optional(),
|
||||
header: TcpHeaderSchema.optional(),
|
||||
});
|
||||
export type TcpStreamSettings = z.infer<typeof TcpStreamSettingsSchema>;
|
||||
17
frontend/src/schemas/protocols/stream/ws.ts
Normal file
17
frontend/src/schemas/protocols/stream/ws.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// WebSocket stream uses the flat V1-style header map (string values only,
|
||||
// not arrays — the panel calls toV2Headers with arr=false). `path` and
|
||||
// `host` are the WS request line / Host header overrides. `heartbeatPeriod`
|
||||
// in seconds; 0 disables heartbeats.
|
||||
export const WsHeaderMapSchema = z.record(z.string(), z.string());
|
||||
export type WsHeaderMap = z.infer<typeof WsHeaderMapSchema>;
|
||||
|
||||
export const WsStreamSettingsSchema = z.object({
|
||||
acceptProxyProtocol: z.boolean().default(false),
|
||||
path: z.string().default('/'),
|
||||
host: z.string().default(''),
|
||||
headers: WsHeaderMapSchema.default({}),
|
||||
heartbeatPeriod: z.number().int().min(0).default(0),
|
||||
});
|
||||
export type WsStreamSettings = z.infer<typeof WsStreamSettingsSchema>;
|
||||
39
frontend/src/schemas/protocols/stream/xhttp.ts
Normal file
39
frontend/src/schemas/protocols/stream/xhttp.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WsHeaderMapSchema } from '@/schemas/protocols/stream/ws';
|
||||
|
||||
export const XHttpModeSchema = z.enum(['auto', 'packet-up', 'stream-up', 'stream-one']);
|
||||
export type XHttpMode = z.infer<typeof XHttpModeSchema>;
|
||||
|
||||
// xHTTP (SplitHTTPConfig) is xray-core's modern stream-multiplexed transport.
|
||||
// The field set is large because the schema mirrors what the server-side
|
||||
// listener reads — plus a few client-only fields (`uplinkHTTPMethod`,
|
||||
// `headers`) the panel embeds into share-link `extra` blobs even though the
|
||||
// server ignores them at runtime. Outbound has additional fields (uplinkChunk
|
||||
// sizes, noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) which
|
||||
// belong on the outbound class instead, not modeled here.
|
||||
export const XHttpStreamSettingsSchema = z.object({
|
||||
path: z.string().default('/'),
|
||||
host: z.string().default(''),
|
||||
mode: XHttpModeSchema.default('auto'),
|
||||
xPaddingBytes: z.string().default('100-1000'),
|
||||
xPaddingObfsMode: z.boolean().default(false),
|
||||
xPaddingKey: z.string().default(''),
|
||||
xPaddingHeader: z.string().default(''),
|
||||
xPaddingPlacement: z.string().default(''),
|
||||
xPaddingMethod: z.string().default(''),
|
||||
sessionPlacement: z.string().default(''),
|
||||
sessionKey: z.string().default(''),
|
||||
seqPlacement: z.string().default(''),
|
||||
seqKey: z.string().default(''),
|
||||
uplinkDataPlacement: z.string().default(''),
|
||||
uplinkDataKey: z.string().default(''),
|
||||
scMaxEachPostBytes: z.string().default('1000000'),
|
||||
noSSEHeader: z.boolean().default(false),
|
||||
scMaxBufferedPosts: z.number().int().min(0).default(30),
|
||||
scStreamUpServerSecs: z.string().default('20-80'),
|
||||
serverMaxHeaderBytes: z.number().int().min(0).default(0),
|
||||
uplinkHTTPMethod: z.string().default(''),
|
||||
headers: WsHeaderMapSchema.default({}),
|
||||
});
|
||||
export type XHttpStreamSettings = z.infer<typeof XHttpStreamSettingsSchema>;
|
||||
Reference in New Issue
Block a user