3x-ui/frontend/src/schemas/protocols/stream/tcp.ts
MHSanaei 9721dae2b6
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).
2026-05-25 23:13:29 +02:00

47 lines
1.9 KiB
TypeScript

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