Files
3x-ui/frontend/src/schemas/xray.ts
T
MHSanaei b6c859f5b9 feat(xray): edit Lua DNS and routing scripts
xray-core 26.10.10 can hand DNS and routing to Lua: dns.script and
routing.script name a .lua file (absolute, or found next to the Xray
binary) that defines HandleDNSQuery / HandleRoute, and a DNS server's id
is how a DNS script picks it. The keys round-tripped through the JSON
template but had no field.

- DNS: a Lua Script field, and an ID field in the server modal;
- Basics: a Routing Script field beside the routing strategy;
- Routing: a warning above the rules while a script is set, since the
  core then routes only through the script and skips every rule, the
  panel's api rule included.

The descriptions say the script must still send the api inbound to the
api outbound, and that Xray will not start if the file is missing. An
empty field drops the key, because the core rejects an empty path.
2026-10-11 15:37:20 +02:00

155 lines
4.8 KiB
TypeScript

import { z } from 'zod';
import { DnsObjectSchema } from './dns';
import {
BalancerObjectSchema,
BalancerStrategySettingsSchema,
BalancerStrategyTypeSchema,
RuleObjectSchema,
} from './routing';
export const XraySettingsValueSchema = z
.object({
inbounds: z.array(z.unknown()).optional(),
outbounds: z
.array(
z
.object({
tag: z.string().optional(),
protocol: z.string().optional(),
settings: z.unknown().optional(),
streamSettings: z.unknown().optional(),
})
.loose(),
)
.optional(),
routing: z
.object({
rules: z.array(RuleObjectSchema).optional(),
balancers: z.array(BalancerObjectSchema).optional(),
domainStrategy: z.string().optional(),
script: z.string().optional(),
})
.loose()
.optional(),
dns: DnsObjectSchema.optional(),
log: z.record(z.string(), z.unknown()).optional(),
policy: z
.object({
system: z.record(z.string(), z.boolean()).optional(),
levels: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
})
.loose()
.optional(),
observatory: z.unknown().optional(),
burstObservatory: z.unknown().optional(),
fakedns: z.unknown().optional(),
})
.loose();
export const XrayConfigPayloadSchema = z
.object({
xraySetting: XraySettingsValueSchema,
inboundTags: z.array(z.string()).optional(),
clientReverseTags: z.array(z.string()).optional(),
outboundTestUrl: z.string().optional(),
// Subscription outbounds are injected at runtime (not persisted in xraySetting).
// They are provided here so the UI can display them and use their tags in
// balancers / routing rules.
subscriptionOutbounds: z.array(z.unknown()).optional(),
subscriptionOutboundTags: z.array(z.string()).optional(),
geodataSources: z.array(z.object({ url: z.string(), file: z.string() })).optional(),
})
.loose();
export const OutboundTrafficRowSchema = z.object({
tag: z.string(),
up: z.number(),
down: z.number(),
});
export const OutboundTrafficListSchema = z.array(OutboundTrafficRowSchema);
export const OutboundTestResultSchema = z
.object({
tag: z.string().optional(),
success: z.boolean(),
delay: z.number().optional(),
error: z.string().optional(),
mode: z.string().optional(),
// HTTP-mode extras: status answered by the test URL plus the httptrace
// timing breakdown (dial to local inbound / target TLS via the outbound /
// time to first byte).
httpStatus: z.number().optional(),
connectMs: z.number().optional(),
tlsMs: z.number().optional(),
ttfbMs: z.number().optional(),
endpoints: z
.array(
z
.object({
address: z.string(),
delay: z.number().optional(),
success: z.boolean(),
error: z.string().optional(),
})
.loose(),
)
.optional(),
egress: z
.object({
ipv4: z.string().optional(),
ipv6: z.string().optional(),
country: z.string().optional(),
warp: z.string().optional(),
})
.loose()
.optional(),
})
.loose();
// Batch results from /xray/testOutbounds, aligned with the request order.
export const OutboundTestResultListSchema = z.array(OutboundTestResultSchema);
export const RuleFormSchema = z.object({
enabled: z.boolean(),
comment: z.string(),
domain: z.string(),
ip: z.string(),
port: z.string(),
sourcePort: z.string(),
vlessRoute: z.string(),
network: z.string(),
sourceIP: z.string(),
user: z.string(),
inboundTag: z.array(z.string()),
protocol: z.array(z.string()),
attrs: z.array(z.tuple([z.string(), z.string()])),
outboundTag: z.string(),
balancerTag: z.string(),
});
export const BalancerFormSchema = z.object({
tag: z
.string()
.trim()
.min(1, 'pages.xray.balancerTagRequired')
.refine((val) => !val.startsWith('_bl_'), { error: 'pages.xray.balancer.reservedPrefix' }),
strategy: BalancerStrategyTypeSchema.default('random'),
selector: z.array(z.string()).min(1, 'pages.xray.balancerSelectorRequired'),
fallbackTag: z.string().default(''),
settings: BalancerStrategySettingsSchema.optional(),
});
export const OutboundTagSchema = z
.string()
.trim()
.min(1, 'pages.xray.outboundTagRequired')
.refine((val) => !val.startsWith('_bl_'), { error: 'pages.xray.balancer.reservedPrefix' });
export type BalancerFormValues = z.infer<typeof BalancerFormSchema>;
export type RuleFormValues = z.infer<typeof RuleFormSchema>;
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
export type XrayConfigPayload = z.infer<typeof XrayConfigPayloadSchema>;
export type OutboundTrafficRow = z.infer<typeof OutboundTrafficRowSchema>;
export type OutboundTestResult = z.infer<typeof OutboundTestResultSchema>;