mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-28 07:59:35 +00:00
feat(frontend): protocol-leaf Zod schemas with discriminated unions
Stand up schemas/primitives (Port, Flow, Protocol, Sniffing) and per-protocol
leaf schemas for all 10 inbound and 13 outbound xray protocols. The leaves
omit any inner `protocol` literal — the discriminator lives at the parent
level so consumers narrow on `.protocol` without redundant projection. Wire
shape is preserved per protocol: vmess outbound stays in `vnext[]`, trojan
and shadowsocks outbound in `servers[]`, vless outbound flat, http/socks
outbound in `servers[].users[]`.
Cross-protocol atoms (port, flow, sniffing dest, protocol enum) live in
primitives. Protocol-specific enums (vmess security, ss method/network,
hysteria version, freedom domain strategy, dns rule action) stay with their
leaves. Tagged-wrapper `z.discriminatedUnion('protocol', [...])` composes
both InboundSettingsSchema and OutboundSettingsSchema; existing class-based
models in src/models/ are untouched and will be retired in Step 3 once the
golden-file safety net is in place.
This commit is contained in:
2
frontend/src/schemas/index.ts
Normal file
2
frontend/src/schemas/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './primitives';
|
||||
export * from './protocols';
|
||||
8
frontend/src/schemas/primitives/flow.ts
Normal file
8
frontend/src/schemas/primitives/flow.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FlowSchema = z.enum([
|
||||
'',
|
||||
'xtls-rprx-vision',
|
||||
'xtls-rprx-vision-udp443',
|
||||
]);
|
||||
export type Flow = z.infer<typeof FlowSchema>;
|
||||
4
frontend/src/schemas/primitives/index.ts
Normal file
4
frontend/src/schemas/primitives/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './port';
|
||||
export * from './protocol';
|
||||
export * from './sniffing';
|
||||
export * from './flow';
|
||||
4
frontend/src/schemas/primitives/port.ts
Normal file
4
frontend/src/schemas/primitives/port.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PortSchema = z.number().int().min(1).max(65535);
|
||||
export type Port = z.infer<typeof PortSchema>;
|
||||
15
frontend/src/schemas/primitives/protocol.ts
Normal file
15
frontend/src/schemas/primitives/protocol.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ProtocolSchema = z.enum([
|
||||
'vmess',
|
||||
'vless',
|
||||
'trojan',
|
||||
'shadowsocks',
|
||||
'wireguard',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'http',
|
||||
'mixed',
|
||||
'tunnel',
|
||||
]);
|
||||
export type Protocol = z.infer<typeof ProtocolSchema>;
|
||||
16
frontend/src/schemas/primitives/sniffing.ts
Normal file
16
frontend/src/schemas/primitives/sniffing.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SniffingDestSchema = z.enum(['http', 'tls', 'quic', 'fakedns']);
|
||||
export type SniffingDest = z.infer<typeof SniffingDestSchema>;
|
||||
|
||||
export const SniffingSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
destOverride: z
|
||||
.array(SniffingDestSchema)
|
||||
.default(['http', 'tls', 'quic', 'fakedns']),
|
||||
metadataOnly: z.boolean().default(false),
|
||||
routeOnly: z.boolean().default(false),
|
||||
ipsExcluded: z.array(z.string()).default([]),
|
||||
domainsExcluded: z.array(z.string()).default([]),
|
||||
});
|
||||
export type Sniffing = z.infer<typeof SniffingSchema>;
|
||||
17
frontend/src/schemas/protocols/inbound/http.ts
Normal file
17
frontend/src/schemas/protocols/inbound/http.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// HTTP proxy inbound — a classic forward proxy. Accounts are user/pass pairs;
|
||||
// `allowTransparent` exposes Xray's option to forward requests with the
|
||||
// original Host header. No client tracking (no email/limits) at the Xray
|
||||
// settings level — the panel doesn't model HTTP users as billable clients.
|
||||
export const HttpAccountSchema = z.object({
|
||||
user: z.string().min(1),
|
||||
pass: z.string().min(1),
|
||||
});
|
||||
export type HttpAccount = z.infer<typeof HttpAccountSchema>;
|
||||
|
||||
export const HttpInboundSettingsSchema = z.object({
|
||||
accounts: z.array(HttpAccountSchema).default([]),
|
||||
allowTransparent: z.boolean().default(false),
|
||||
});
|
||||
export type HttpInboundSettings = z.infer<typeof HttpInboundSettingsSchema>;
|
||||
26
frontend/src/schemas/protocols/inbound/hysteria.ts
Normal file
26
frontend/src/schemas/protocols/inbound/hysteria.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Hysteria v1 inbound (legacy — upstream xray-core kept v1 support but the
|
||||
// panel defaults to v2). Each client supplies an `auth` token instead of a
|
||||
// UUID/password.
|
||||
export const HysteriaClientSchema = z.object({
|
||||
auth: z.string().min(1),
|
||||
email: z.string().min(1),
|
||||
limitIp: z.number().int().min(0).default(0),
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.number().int().default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
created_at: z.number().int().optional(),
|
||||
updated_at: z.number().int().optional(),
|
||||
});
|
||||
export type HysteriaClient = z.infer<typeof HysteriaClientSchema>;
|
||||
|
||||
export const HysteriaInboundSettingsSchema = z.object({
|
||||
version: z.number().int().min(1).default(2),
|
||||
clients: z.array(HysteriaClientSchema).default([]),
|
||||
});
|
||||
export type HysteriaInboundSettings = z.infer<typeof HysteriaInboundSettingsSchema>;
|
||||
13
frontend/src/schemas/protocols/inbound/hysteria2.ts
Normal file
13
frontend/src/schemas/protocols/inbound/hysteria2.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { HysteriaClientSchema } from '@/schemas/protocols/inbound/hysteria';
|
||||
|
||||
// hysteria2 is wire-distinct from hysteria (different parent protocol literal,
|
||||
// different Go validate tag) but the panel's settings payload is structurally
|
||||
// identical — same client shape, same auth-based clients. We pin `version` to
|
||||
// the literal 2 here so a hysteria2 inbound can never silently downgrade.
|
||||
export const Hysteria2InboundSettingsSchema = z.object({
|
||||
version: z.literal(2).default(2),
|
||||
clients: z.array(HysteriaClientSchema).default([]),
|
||||
});
|
||||
export type Hysteria2InboundSettings = z.infer<typeof Hysteria2InboundSettingsSchema>;
|
||||
42
frontend/src/schemas/protocols/inbound/index.ts
Normal file
42
frontend/src/schemas/protocols/inbound/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { HttpInboundSettingsSchema } from './http';
|
||||
import { Hysteria2InboundSettingsSchema } from './hysteria2';
|
||||
import { HysteriaInboundSettingsSchema } from './hysteria';
|
||||
import { MixedInboundSettingsSchema } from './mixed';
|
||||
import { ShadowsocksInboundSettingsSchema } from './shadowsocks';
|
||||
import { TrojanInboundSettingsSchema } from './trojan';
|
||||
import { TunnelInboundSettingsSchema } from './tunnel';
|
||||
import { VlessInboundSettingsSchema } from './vless';
|
||||
import { VmessInboundSettingsSchema } from './vmess';
|
||||
import { WireguardInboundSettingsSchema } from './wireguard';
|
||||
|
||||
export * from './http';
|
||||
export * from './hysteria';
|
||||
export * from './hysteria2';
|
||||
export * from './mixed';
|
||||
export * from './shadowsocks';
|
||||
export * from './trojan';
|
||||
export * from './tunnel';
|
||||
export * from './vless';
|
||||
export * from './vmess';
|
||||
export * from './wireguard';
|
||||
|
||||
// Tagged-wrapper discriminated union. The discriminator (`protocol`) lives on
|
||||
// the wrapper, not inside `settings`, mirroring the wire format Xray emits:
|
||||
// { protocol: 'vless', settings: { clients: [...], ... }, ... }
|
||||
// Consumers narrow on `.protocol` and TypeScript narrows `.settings` to the
|
||||
// matching leaf type.
|
||||
export const InboundSettingsSchema = z.discriminatedUnion('protocol', [
|
||||
z.object({ protocol: z.literal('vmess'), settings: VmessInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vless'), settings: VlessInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('trojan'), settings: TrojanInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('shadowsocks'), settings: ShadowsocksInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria2'), settings: Hysteria2InboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('http'), settings: HttpInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('mixed'), settings: MixedInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('tunnel'), settings: TunnelInboundSettingsSchema }),
|
||||
]);
|
||||
export type InboundSettings = z.infer<typeof InboundSettingsSchema>;
|
||||
21
frontend/src/schemas/protocols/inbound/mixed.ts
Normal file
21
frontend/src/schemas/protocols/inbound/mixed.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const MixedAuthSchema = z.enum(['password', 'noauth']);
|
||||
export type MixedAuth = z.infer<typeof MixedAuthSchema>;
|
||||
|
||||
// SOCKS/HTTP combined inbound. When auth==='noauth' the `accounts` field is
|
||||
// omitted from the wire payload (the panel writes `undefined`), so we accept
|
||||
// either an array or absence here.
|
||||
export const MixedAccountSchema = z.object({
|
||||
user: z.string().min(1),
|
||||
pass: z.string().min(1),
|
||||
});
|
||||
export type MixedAccount = z.infer<typeof MixedAccountSchema>;
|
||||
|
||||
export const MixedInboundSettingsSchema = z.object({
|
||||
auth: MixedAuthSchema.default('password'),
|
||||
accounts: z.array(MixedAccountSchema).optional(),
|
||||
udp: z.boolean().default(false),
|
||||
ip: z.string().default('127.0.0.1'),
|
||||
});
|
||||
export type MixedInboundSettings = z.infer<typeof MixedInboundSettingsSchema>;
|
||||
45
frontend/src/schemas/protocols/inbound/shadowsocks.ts
Normal file
45
frontend/src/schemas/protocols/inbound/shadowsocks.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SSMethodSchema = z.enum([
|
||||
'aes-256-gcm',
|
||||
'chacha20-poly1305',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
'2022-blake3-chacha20-poly1305',
|
||||
]);
|
||||
export type SSMethod = z.infer<typeof SSMethodSchema>;
|
||||
|
||||
export const SSNetworkSchema = z.enum(['tcp', 'udp', 'tcp,udp']);
|
||||
export type SSNetwork = z.infer<typeof SSNetworkSchema>;
|
||||
|
||||
// On a single-user shadowsocks inbound the client carries no method/password
|
||||
// of its own — the inbound-level method+password are authoritative. On a
|
||||
// 2022-blake3 multi-user setup each client provides its own password (and
|
||||
// optionally a per-client method).
|
||||
export const ShadowsocksClientSchema = z.object({
|
||||
method: z.string().default(''),
|
||||
password: z.string().default(''),
|
||||
email: z.string().min(1),
|
||||
limitIp: z.number().int().min(0).default(0),
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.number().int().default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
created_at: z.number().int().optional(),
|
||||
updated_at: z.number().int().optional(),
|
||||
});
|
||||
export type ShadowsocksClient = z.infer<typeof ShadowsocksClientSchema>;
|
||||
|
||||
export const ShadowsocksInboundSettingsSchema = z.object({
|
||||
method: SSMethodSchema.default('2022-blake3-aes-256-gcm'),
|
||||
password: z.string().default(''),
|
||||
network: SSNetworkSchema.default('tcp'),
|
||||
clients: z.array(ShadowsocksClientSchema).default([]),
|
||||
ivCheck: z.boolean().default(false),
|
||||
});
|
||||
export type ShadowsocksInboundSettings = z.infer<typeof ShadowsocksInboundSettingsSchema>;
|
||||
32
frontend/src/schemas/protocols/inbound/trojan.ts
Normal file
32
frontend/src/schemas/protocols/inbound/trojan.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const TrojanFallbackSchema = z.object({
|
||||
name: z.string().default(''),
|
||||
alpn: z.string().default(''),
|
||||
path: z.string().default(''),
|
||||
dest: z.union([z.string(), z.number()]).default(''),
|
||||
xver: z.number().int().min(0).default(0),
|
||||
});
|
||||
export type TrojanFallback = z.infer<typeof TrojanFallbackSchema>;
|
||||
|
||||
export const TrojanClientSchema = z.object({
|
||||
password: z.string().min(1),
|
||||
email: z.string().min(1),
|
||||
limitIp: z.number().int().min(0).default(0),
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.number().int().default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
created_at: z.number().int().optional(),
|
||||
updated_at: z.number().int().optional(),
|
||||
});
|
||||
export type TrojanClient = z.infer<typeof TrojanClientSchema>;
|
||||
|
||||
export const TrojanInboundSettingsSchema = z.object({
|
||||
clients: z.array(TrojanClientSchema).default([]),
|
||||
fallbacks: z.array(TrojanFallbackSchema).default([]),
|
||||
});
|
||||
export type TrojanInboundSettings = z.infer<typeof TrojanInboundSettingsSchema>;
|
||||
19
frontend/src/schemas/protocols/inbound/tunnel.ts
Normal file
19
frontend/src/schemas/protocols/inbound/tunnel.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
export const TunnelNetworkSchema = z.enum(['tcp', 'udp', 'tcp,udp']);
|
||||
export type TunnelNetwork = z.infer<typeof TunnelNetworkSchema>;
|
||||
|
||||
// Tunnel inbound (Xray's `dokodemo-door`-style transparent forwarder).
|
||||
// `portMap` is persisted as Record<string, string> on the wire — the panel
|
||||
// flattens an internal array-of-{name,value} into that map via toV2Headers
|
||||
// with arr=false.
|
||||
export const TunnelInboundSettingsSchema = z.object({
|
||||
rewriteAddress: z.string().optional(),
|
||||
rewritePort: PortSchema.optional(),
|
||||
portMap: z.record(z.string(), z.string()).default({}),
|
||||
allowedNetwork: TunnelNetworkSchema.default('tcp,udp'),
|
||||
followRedirect: z.boolean().default(false),
|
||||
});
|
||||
export type TunnelInboundSettings = z.infer<typeof TunnelInboundSettingsSchema>;
|
||||
50
frontend/src/schemas/protocols/inbound/vless.ts
Normal file
50
frontend/src/schemas/protocols/inbound/vless.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FlowSchema, SniffingSchema } from '@/schemas/primitives';
|
||||
|
||||
export const VlessFallbackSchema = z.object({
|
||||
name: z.string().default(''),
|
||||
alpn: z.string().default(''),
|
||||
path: z.string().default(''),
|
||||
dest: z.union([z.string(), z.number()]).default(''),
|
||||
xver: z.number().int().min(0).default(0),
|
||||
});
|
||||
export type VlessFallback = z.infer<typeof VlessFallbackSchema>;
|
||||
|
||||
export const VlessClientSchema = z.object({
|
||||
id: z.uuid(),
|
||||
email: z.string().min(1),
|
||||
flow: FlowSchema.default(''),
|
||||
limitIp: z.number().int().min(0).default(0),
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.number().int().default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
// VLESS simple reverse-proxy: which reverse tag this client routes to,
|
||||
// plus an optional sniffing override for that path. Distinct from the
|
||||
// inbound-level `fallbacks` feature.
|
||||
reverse: z
|
||||
.object({
|
||||
tag: z.string(),
|
||||
sniffing: SniffingSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
created_at: z.number().int().optional(),
|
||||
updated_at: z.number().int().optional(),
|
||||
});
|
||||
export type VlessClient = z.infer<typeof VlessClientSchema>;
|
||||
|
||||
export const VlessInboundSettingsSchema = z.object({
|
||||
clients: z.array(VlessClientSchema).default([]),
|
||||
decryption: z.literal('none').default('none'),
|
||||
encryption: z.literal('none').default('none'),
|
||||
fallbacks: z.array(VlessFallbackSchema).default([]),
|
||||
// TODO: narrow to flow === 'xtls-rprx-vision' once a per-flow discriminator
|
||||
// exists. 4-positive-int padding seed for xtls-rprx-vision; backend uses
|
||||
// safe defaults when omitted.
|
||||
testseed: z.array(z.number().int().positive()).length(4).optional(),
|
||||
});
|
||||
export type VlessInboundSettings = z.infer<typeof VlessInboundSettingsSchema>;
|
||||
32
frontend/src/schemas/protocols/inbound/vmess.ts
Normal file
32
frontend/src/schemas/protocols/inbound/vmess.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const VmessSecuritySchema = z.enum([
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'auto',
|
||||
'none',
|
||||
'zero',
|
||||
]);
|
||||
export type VmessSecurity = z.infer<typeof VmessSecuritySchema>;
|
||||
|
||||
export const VmessClientSchema = z.object({
|
||||
id: z.uuid(),
|
||||
security: VmessSecuritySchema.default('auto'),
|
||||
email: z.string().min(1),
|
||||
limitIp: z.number().int().min(0).default(0),
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.number().int().default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
created_at: z.number().int().optional(),
|
||||
updated_at: z.number().int().optional(),
|
||||
});
|
||||
export type VmessClient = z.infer<typeof VmessClientSchema>;
|
||||
|
||||
export const VmessInboundSettingsSchema = z.object({
|
||||
clients: z.array(VmessClientSchema).default([]),
|
||||
});
|
||||
export type VmessInboundSettings = z.infer<typeof VmessInboundSettingsSchema>;
|
||||
23
frontend/src/schemas/protocols/inbound/wireguard.ts
Normal file
23
frontend/src/schemas/protocols/inbound/wireguard.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Wireguard inbound is peer-based (no clients). Each peer is a client device
|
||||
// the server accepts; secretKey is the server-side private key and pubKey is
|
||||
// derived from it at runtime (not persisted on the wire). Inbound peers
|
||||
// optionally store the client's privateKey so the panel can render configs
|
||||
// for the user — outbound peers never have a privateKey.
|
||||
export const WireguardInboundPeerSchema = z.object({
|
||||
privateKey: z.string().optional(),
|
||||
publicKey: z.string().min(1),
|
||||
preSharedKey: z.string().optional(),
|
||||
allowedIPs: z.array(z.string()).default([]),
|
||||
keepAlive: z.number().int().min(0).optional(),
|
||||
});
|
||||
export type WireguardInboundPeer = z.infer<typeof WireguardInboundPeerSchema>;
|
||||
|
||||
export const WireguardInboundSettingsSchema = z.object({
|
||||
mtu: z.number().int().min(1).optional(),
|
||||
secretKey: z.string().min(1),
|
||||
peers: z.array(WireguardInboundPeerSchema).default([]),
|
||||
noKernelTun: z.boolean().default(false),
|
||||
});
|
||||
export type WireguardInboundSettings = z.infer<typeof WireguardInboundSettingsSchema>;
|
||||
7
frontend/src/schemas/protocols/index.ts
Normal file
7
frontend/src/schemas/protocols/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * as Inbound from './inbound';
|
||||
export * as Outbound from './outbound';
|
||||
|
||||
export { InboundSettingsSchema } from './inbound';
|
||||
export type { InboundSettings } from './inbound';
|
||||
export { OutboundSettingsSchema } from './outbound';
|
||||
export type { OutboundSettings } from './outbound';
|
||||
13
frontend/src/schemas/protocols/outbound/blackhole.ts
Normal file
13
frontend/src/schemas/protocols/outbound/blackhole.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const BlackholeResponseTypeSchema = z.enum(['none', 'http']);
|
||||
export type BlackholeResponseType = z.infer<typeof BlackholeResponseTypeSchema>;
|
||||
|
||||
// Blackhole drops traffic. `response.type` is the only knob — when set, Xray
|
||||
// returns the canned 403 HTTP response before closing; when omitted it
|
||||
// silently drops. The panel stores it as { response: { type } } or omits the
|
||||
// whole `response` key when type is empty.
|
||||
export const BlackholeOutboundSettingsSchema = z.object({
|
||||
response: z.object({ type: BlackholeResponseTypeSchema }).optional(),
|
||||
});
|
||||
export type BlackholeOutboundSettings = z.infer<typeof BlackholeOutboundSettingsSchema>;
|
||||
27
frontend/src/schemas/protocols/outbound/dns.ts
Normal file
27
frontend/src/schemas/protocols/outbound/dns.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
export const DNSRuleActionSchema = z.enum(['direct', 'reject', 'rejectIPv4', 'rejectIPv6']);
|
||||
|
||||
// On the wire `qtype` is either a number (DNS type code) or a string like
|
||||
// "A"/"AAAA"/"TXT"; the panel normalizes numeric strings to numbers in
|
||||
// toJson. `domain` is a string[] (split from a comma-joined input).
|
||||
export const DNSRuleSchema = z.object({
|
||||
action: DNSRuleActionSchema.default('direct'),
|
||||
qtype: z.union([z.string(), z.number().int()]).optional(),
|
||||
domain: z.array(z.string()).optional(),
|
||||
});
|
||||
export type DNSRule = z.infer<typeof DNSRuleSchema>;
|
||||
|
||||
// DNS outbound rewrites DNS queries onto a different transport. All five
|
||||
// fields are emitted conditionally — empty/zero values are omitted from the
|
||||
// wire payload entirely (handled at the caller, not here).
|
||||
export const DNSOutboundSettingsSchema = z.object({
|
||||
rewriteNetwork: z.string().optional(),
|
||||
rewriteAddress: z.string().optional(),
|
||||
rewritePort: PortSchema.optional(),
|
||||
userLevel: z.number().int().min(0).optional(),
|
||||
rules: z.array(DNSRuleSchema).optional(),
|
||||
});
|
||||
export type DNSOutboundSettings = z.infer<typeof DNSOutboundSettingsSchema>;
|
||||
59
frontend/src/schemas/protocols/outbound/freedom.ts
Normal file
59
frontend/src/schemas/protocols/outbound/freedom.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const OutboundDomainStrategySchema = z.enum([
|
||||
'AsIs',
|
||||
'UseIP',
|
||||
'UseIPv4',
|
||||
'UseIPv6',
|
||||
'UseIPv6v4',
|
||||
'UseIPv4v6',
|
||||
'ForceIP',
|
||||
'ForceIPv6v4',
|
||||
'ForceIPv6',
|
||||
'ForceIPv4v6',
|
||||
'ForceIPv4',
|
||||
]);
|
||||
export type OutboundDomainStrategy = z.infer<typeof OutboundDomainStrategySchema>;
|
||||
|
||||
// Fragment knobs are TCP-level splitting controls; all four fields are
|
||||
// dash-range strings (e.g. '1-3', '10-20').
|
||||
export const FreedomFragmentSchema = z.object({
|
||||
packets: z.string().default('1-3'),
|
||||
length: z.string().default(''),
|
||||
interval: z.string().default(''),
|
||||
maxSplit: z.string().default(''),
|
||||
});
|
||||
export type FreedomFragment = z.infer<typeof FreedomFragmentSchema>;
|
||||
|
||||
export const FreedomNoiseTypeSchema = z.enum(['rand', 'str', 'base64', 'hex']);
|
||||
export const FreedomNoiseApplyToSchema = z.enum(['ip', 'host', 'all']);
|
||||
|
||||
export const FreedomNoiseSchema = z.object({
|
||||
type: FreedomNoiseTypeSchema.default('rand'),
|
||||
packet: z.string().default('10-20'),
|
||||
delay: z.string().default('10-16'),
|
||||
applyTo: FreedomNoiseApplyToSchema.default('ip'),
|
||||
});
|
||||
export type FreedomNoise = z.infer<typeof FreedomNoiseSchema>;
|
||||
|
||||
export const FreedomFinalRuleActionSchema = z.enum(['allow', 'block']);
|
||||
|
||||
// Final rules express the legacy ipsBlocked behavior plus generalized
|
||||
// allow/block per network+port+ip combinations.
|
||||
export const FreedomFinalRuleSchema = z.object({
|
||||
action: FreedomFinalRuleActionSchema.default('block'),
|
||||
network: z.string().optional(),
|
||||
port: z.string().optional(),
|
||||
ip: z.array(z.string()).default([]),
|
||||
blockDelay: z.string().optional(),
|
||||
});
|
||||
export type FreedomFinalRule = z.infer<typeof FreedomFinalRuleSchema>;
|
||||
|
||||
export const FreedomOutboundSettingsSchema = z.object({
|
||||
domainStrategy: OutboundDomainStrategySchema.optional(),
|
||||
redirect: z.string().optional(),
|
||||
fragment: FreedomFragmentSchema.optional(),
|
||||
noises: z.array(FreedomNoiseSchema).optional(),
|
||||
finalRules: z.array(FreedomFinalRuleSchema).optional(),
|
||||
});
|
||||
export type FreedomOutboundSettings = z.infer<typeof FreedomOutboundSettingsSchema>;
|
||||
25
frontend/src/schemas/protocols/outbound/http.ts
Normal file
25
frontend/src/schemas/protocols/outbound/http.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
// HTTP outbound persists in Xray's `servers[].users[]` shape. The panel only
|
||||
// supports a single server with at most one user (the constructor reads
|
||||
// servers[0] / users[0]). We model the wire shape rather than the panel's
|
||||
// flattened class fields so saves round-trip exactly.
|
||||
export const HttpOutboundUserSchema = z.object({
|
||||
user: z.string().min(1),
|
||||
pass: z.string().min(1),
|
||||
});
|
||||
export type HttpOutboundUser = z.infer<typeof HttpOutboundUserSchema>;
|
||||
|
||||
export const HttpOutboundServerSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
port: PortSchema,
|
||||
users: z.array(HttpOutboundUserSchema).default([]),
|
||||
});
|
||||
export type HttpOutboundServer = z.infer<typeof HttpOutboundServerSchema>;
|
||||
|
||||
export const HttpOutboundSettingsSchema = z.object({
|
||||
servers: z.array(HttpOutboundServerSchema).min(1),
|
||||
});
|
||||
export type HttpOutboundSettings = z.infer<typeof HttpOutboundSettingsSchema>;
|
||||
12
frontend/src/schemas/protocols/outbound/hysteria.ts
Normal file
12
frontend/src/schemas/protocols/outbound/hysteria.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
// Hysteria outbound is a thin connect-target descriptor — the actual auth and
|
||||
// transport knobs live on the stream/transport layer, not in settings.
|
||||
export const HysteriaOutboundSettingsSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
port: PortSchema,
|
||||
version: z.number().int().min(1).default(2),
|
||||
});
|
||||
export type HysteriaOutboundSettings = z.infer<typeof HysteriaOutboundSettingsSchema>;
|
||||
12
frontend/src/schemas/protocols/outbound/hysteria2.ts
Normal file
12
frontend/src/schemas/protocols/outbound/hysteria2.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
// Outbound counterpart to hysteria2 — same {address, port} connect descriptor
|
||||
// as hysteria, but version locked to 2.
|
||||
export const Hysteria2OutboundSettingsSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
port: PortSchema,
|
||||
version: z.literal(2).default(2),
|
||||
});
|
||||
export type Hysteria2OutboundSettings = z.infer<typeof Hysteria2OutboundSettingsSchema>;
|
||||
50
frontend/src/schemas/protocols/outbound/index.ts
Normal file
50
frontend/src/schemas/protocols/outbound/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BlackholeOutboundSettingsSchema } from './blackhole';
|
||||
import { DNSOutboundSettingsSchema } from './dns';
|
||||
import { FreedomOutboundSettingsSchema } from './freedom';
|
||||
import { HttpOutboundSettingsSchema } from './http';
|
||||
import { Hysteria2OutboundSettingsSchema } from './hysteria2';
|
||||
import { HysteriaOutboundSettingsSchema } from './hysteria';
|
||||
import { LoopbackOutboundSettingsSchema } from './loopback';
|
||||
import { ShadowsocksOutboundSettingsSchema } from './shadowsocks';
|
||||
import { SocksOutboundSettingsSchema } from './socks';
|
||||
import { TrojanOutboundSettingsSchema } from './trojan';
|
||||
import { VlessOutboundSettingsSchema } from './vless';
|
||||
import { VmessOutboundSettingsSchema } from './vmess';
|
||||
import { WireguardOutboundSettingsSchema } from './wireguard';
|
||||
|
||||
export * from './blackhole';
|
||||
export * from './dns';
|
||||
export * from './freedom';
|
||||
export * from './http';
|
||||
export * from './hysteria';
|
||||
export * from './hysteria2';
|
||||
export * from './loopback';
|
||||
export * from './shadowsocks';
|
||||
export * from './socks';
|
||||
export * from './trojan';
|
||||
export * from './vless';
|
||||
export * from './vmess';
|
||||
export * from './wireguard';
|
||||
|
||||
// Outbound discriminated union spans 13 protocols (mixed/tunnel are
|
||||
// inbound-only; freedom/blackhole/dns/loopback are outbound-only). The wire
|
||||
// shape is `{ protocol, settings }` — same wrapper pattern as the inbound
|
||||
// union, even though some leaf schemas (freedom, blackhole) are sparse.
|
||||
export const OutboundSettingsSchema = z.discriminatedUnion('protocol', [
|
||||
z.object({ protocol: z.literal('vmess'), settings: VmessOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vless'), settings: VlessOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('trojan'), settings: TrojanOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('shadowsocks'), settings: ShadowsocksOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria2'), settings: Hysteria2OutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('http'), settings: HttpOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('socks'), settings: SocksOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('freedom'), settings: FreedomOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('blackhole'), settings: BlackholeOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('dns'), settings: DNSOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('loopback'), settings: LoopbackOutboundSettingsSchema }),
|
||||
]);
|
||||
export type OutboundSettings = z.infer<typeof OutboundSettingsSchema>;
|
||||
8
frontend/src/schemas/protocols/outbound/loopback.ts
Normal file
8
frontend/src/schemas/protocols/outbound/loopback.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Loopback outbound reinjects traffic back into a named inbound for chained
|
||||
// routing. The single `inboundTag` field references an inbound tag by name.
|
||||
export const LoopbackOutboundSettingsSchema = z.object({
|
||||
inboundTag: z.string().optional(),
|
||||
});
|
||||
export type LoopbackOutboundSettings = z.infer<typeof LoopbackOutboundSettingsSchema>;
|
||||
21
frontend/src/schemas/protocols/outbound/shadowsocks.ts
Normal file
21
frontend/src/schemas/protocols/outbound/shadowsocks.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
|
||||
|
||||
// Shadowsocks outbound persists as { servers: [{ ... }] }, with UDP-over-TCP
|
||||
// knobs (uot, UoTVersion) attached per-server when the user enabled them.
|
||||
export const ShadowsocksOutboundServerSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
port: PortSchema,
|
||||
password: z.string().min(1),
|
||||
method: SSMethodSchema,
|
||||
uot: z.boolean().optional(),
|
||||
UoTVersion: z.number().int().min(1).max(2).optional(),
|
||||
});
|
||||
export type ShadowsocksOutboundServer = z.infer<typeof ShadowsocksOutboundServerSchema>;
|
||||
|
||||
export const ShadowsocksOutboundSettingsSchema = z.object({
|
||||
servers: z.array(ShadowsocksOutboundServerSchema).min(1),
|
||||
});
|
||||
export type ShadowsocksOutboundSettings = z.infer<typeof ShadowsocksOutboundSettingsSchema>;
|
||||
24
frontend/src/schemas/protocols/outbound/socks.ts
Normal file
24
frontend/src/schemas/protocols/outbound/socks.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
// SOCKS outbound persists in Xray's `servers[].users[]` shape — wire-identical
|
||||
// to HTTP outbound but with `socks` as the parent protocol literal. The panel
|
||||
// only supports a single server with at most one user.
|
||||
export const SocksOutboundUserSchema = z.object({
|
||||
user: z.string().min(1),
|
||||
pass: z.string().min(1),
|
||||
});
|
||||
export type SocksOutboundUser = z.infer<typeof SocksOutboundUserSchema>;
|
||||
|
||||
export const SocksOutboundServerSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
port: PortSchema,
|
||||
users: z.array(SocksOutboundUserSchema).default([]),
|
||||
});
|
||||
export type SocksOutboundServer = z.infer<typeof SocksOutboundServerSchema>;
|
||||
|
||||
export const SocksOutboundSettingsSchema = z.object({
|
||||
servers: z.array(SocksOutboundServerSchema).min(1),
|
||||
});
|
||||
export type SocksOutboundSettings = z.infer<typeof SocksOutboundSettingsSchema>;
|
||||
18
frontend/src/schemas/protocols/outbound/trojan.ts
Normal file
18
frontend/src/schemas/protocols/outbound/trojan.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
// Trojan outbound persists as { servers: [{ address, port, password }] }
|
||||
// — distinct from VLESS outbound which stores the connect target flat at
|
||||
// the settings root. The wrapping mirrors what Xray expects.
|
||||
export const TrojanOutboundServerSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
port: PortSchema,
|
||||
password: z.string().min(1),
|
||||
});
|
||||
export type TrojanOutboundServer = z.infer<typeof TrojanOutboundServerSchema>;
|
||||
|
||||
export const TrojanOutboundSettingsSchema = z.object({
|
||||
servers: z.array(TrojanOutboundServerSchema).min(1),
|
||||
});
|
||||
export type TrojanOutboundSettings = z.infer<typeof TrojanOutboundSettingsSchema>;
|
||||
22
frontend/src/schemas/protocols/outbound/vless.ts
Normal file
22
frontend/src/schemas/protocols/outbound/vless.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FlowSchema, SniffingSchema } from '@/schemas/primitives';
|
||||
|
||||
export const VlessOutboundSettingsSchema = z.object({
|
||||
address: z.string(),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
id: z.uuid(),
|
||||
flow: FlowSchema.default(''),
|
||||
encryption: z.literal('none').default('none'),
|
||||
reverse: z
|
||||
.object({
|
||||
tag: z.string(),
|
||||
sniffing: SniffingSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
testpre: z.number().int().min(0).optional(),
|
||||
// TODO: narrow to flow === 'xtls-rprx-vision' once a per-flow discriminator
|
||||
// exists.
|
||||
testseed: z.array(z.number().int().positive()).length(4).optional(),
|
||||
});
|
||||
export type VlessOutboundSettings = z.infer<typeof VlessOutboundSettingsSchema>;
|
||||
25
frontend/src/schemas/protocols/outbound/vmess.ts
Normal file
25
frontend/src/schemas/protocols/outbound/vmess.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
import { VmessSecuritySchema } from '@/schemas/protocols/inbound/vmess';
|
||||
|
||||
// Vmess outbound persists in the standard Xray `vnext` shape:
|
||||
// { vnext: [{ address, port, users: [{ id, security }] }] }
|
||||
// — distinct from VLESS outbound which the panel stores flat.
|
||||
export const VmessOutboundUserSchema = z.object({
|
||||
id: z.uuid(),
|
||||
security: VmessSecuritySchema.default('auto'),
|
||||
});
|
||||
export type VmessOutboundUser = z.infer<typeof VmessOutboundUserSchema>;
|
||||
|
||||
export const VmessOutboundServerSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
port: PortSchema,
|
||||
users: z.array(VmessOutboundUserSchema).min(1),
|
||||
});
|
||||
export type VmessOutboundServer = z.infer<typeof VmessOutboundServerSchema>;
|
||||
|
||||
export const VmessOutboundSettingsSchema = z.object({
|
||||
vnext: z.array(VmessOutboundServerSchema).min(1),
|
||||
});
|
||||
export type VmessOutboundSettings = z.infer<typeof VmessOutboundSettingsSchema>;
|
||||
36
frontend/src/schemas/protocols/outbound/wireguard.ts
Normal file
36
frontend/src/schemas/protocols/outbound/wireguard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const WireguardDomainStrategySchema = z.enum([
|
||||
'ForceIP',
|
||||
'ForceIPv4',
|
||||
'ForceIPv4v6',
|
||||
'ForceIPv6',
|
||||
'ForceIPv6v4',
|
||||
]);
|
||||
export type WireguardDomainStrategy = z.infer<typeof WireguardDomainStrategySchema>;
|
||||
|
||||
// Outbound peer is the remote server we connect to: no privateKey, but an
|
||||
// `endpoint` (host:port) the inbound side does not need.
|
||||
export const WireguardOutboundPeerSchema = z.object({
|
||||
publicKey: z.string().min(1),
|
||||
preSharedKey: z.string().optional(),
|
||||
allowedIPs: z.array(z.string()).default(['0.0.0.0/0', '::/0']),
|
||||
endpoint: z.string().min(1),
|
||||
keepAlive: z.number().int().min(0).optional(),
|
||||
});
|
||||
export type WireguardOutboundPeer = z.infer<typeof WireguardOutboundPeerSchema>;
|
||||
|
||||
// Wire format: address is a string[] (Xray expects an array even though the
|
||||
// panel UI stores it comma-joined); reserved is number[] (panel splits the
|
||||
// comma string and Number()-coerces each entry).
|
||||
export const WireguardOutboundSettingsSchema = z.object({
|
||||
mtu: z.number().int().min(1).optional(),
|
||||
secretKey: z.string().min(1),
|
||||
address: z.array(z.string()).default([]),
|
||||
workers: z.number().int().min(1).optional(),
|
||||
domainStrategy: WireguardDomainStrategySchema.optional(),
|
||||
reserved: z.array(z.number().int()).optional(),
|
||||
peers: z.array(WireguardOutboundPeerSchema).min(1),
|
||||
noKernelTun: z.boolean().default(false),
|
||||
});
|
||||
export type WireguardOutboundSettings = z.infer<typeof WireguardOutboundSettingsSchema>;
|
||||
Reference in New Issue
Block a user