feat(frontend): align finalmask + sockopt with xray docs, add golden fixtures

Schema fixes per https://xtls.github.io/config/transports/finalmask.html
and https://xtls.github.io/config/transports/sockopt.html:

finalmask:
- QuicCongestionSchema: remove non-doc 'cubic', keep reno/bbr/brutal/force-brutal
- Add BbrProfileSchema (conservative/standard/aggressive) and bbrProfile field
- brutalUp/brutalDown: number -> string per docs (units like '60 mbps')
- Tighten ranges: maxIdleTimeout 4-120, keepAlivePeriod 2-60, maxIncomingStreams min 8
- UdpMaskTypeSchema: add missing 'sudoku'
- udpHop.interval stays as preprocessed string-range per intentional B19 divergence

sockopt:
- tcpFastOpen: boolean -> union(boolean, number) per docs (number tunes queue size)
- mark: drop min(0) (can be any int)
- domainStrategy default: 'UseIP' -> 'AsIs' per docs
- tcpKeepAlive Interval/Idle defaults: 0/300 -> 45/45 per docs (outbound)
- Add AddressPortStrategySchema enum (7 values) + addressPortStrategy field
- Add HappyEyeballsSchema (tryDelayMs/prioritizeIPv6/interleave/maxConcurrentTry)
- Add CustomSockoptSchema (system/type/level/opt/value) + customSockopt array

Bug fixes:
- options.ts: Address_Port_Strategy values were lowercase ('srvportonly');
  xray-core requires camelCase ('SrvPortOnly'). Fixed all 6 entries.
- OutboundFormModal: domainStrategy Select was mistakenly populated from
  ADDRESS_PORT_STRATEGY_OPTIONS; now uses DOMAIN_STRATEGY_OPTION.
- OutboundFormModal: inline sockopt defaults (hardcoded {acceptProxyProtocol:
  false, domainStrategy: 'UseIP', ...}) replaced with
  SockoptStreamSettingsSchema.parse({}) so schema is the single source.

Form additions (both InboundFormModal + OutboundFormModal):
- Address+port strategy Select
- Happy Eyeballs Switch + sub-form (tryDelayMs/prioritizeIPv6/interleave/maxConcurrentTry)
- Custom sockopt Form.List (system/type/level/opt/value)
- FinalMaskForm: BBR Profile Select (visible when congestion='bbr'),
  Brutal Up/Down placeholders updated to string format

Golden fixtures (8 new + 4 xhttp extras):
- finalmask/{tcp-mask, udp-mask, quic-params, combined}.json — cover all TCP
  mask types, 7 UDP mask types including new sudoku, full QUIC params shape
- sockopt/{defaults, tcp-tuning, tproxy, full}.json — full sockopt knobs
- stream/xhttp-{basic, extra-padding, extra-placement, extra-tuning}.json —
  cover the extra-blob fields bundled into share-link extra=<json>

Tests now at 312 (up from 300); typecheck/lint clean.
This commit is contained in:
MHSanaei 2026-05-26 22:14:38 +02:00
parent 3fdd9765a7
commit 0442be5078
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
23 changed files with 966 additions and 54 deletions

View file

@ -83,8 +83,6 @@ function defaultQuicParams(): Record<string, unknown> {
return {
congestion: 'bbr',
debug: false,
brutalUp: 0,
brutalDown: 0,
maxIdleTimeout: 30,
keepAlivePeriod: 10,
disablePathMTUDiscovery: false,
@ -680,6 +678,19 @@ function QuicParamsForm({ base, form }: { base: (string | number)[]; form: FormI
]}
/>
</Form.Item>
{congestion === 'bbr' && (
<Form.Item label="BBR Profile" name={[...base, 'bbrProfile']}>
<Select
allowClear
placeholder="standard"
options={[
{ value: 'conservative', label: 'Conservative' },
{ value: 'standard', label: 'Standard' },
{ value: 'aggressive', label: 'Aggressive' },
]}
/>
</Form.Item>
)}
<Form.Item label="Debug" name={[...base, 'debug']} valuePropName="checked">
<Switch />
</Form.Item>
@ -687,10 +698,10 @@ function QuicParamsForm({ base, form }: { base: (string | number)[]; form: FormI
{(congestion === 'brutal' || congestion === 'force-brutal') && (
<>
<Form.Item label="Brutal Up" name={[...base, 'brutalUp']}>
<Input placeholder="65537" />
<Input placeholder="e.g. 60 mbps" />
</Form.Item>
<Form.Item label="Brutal Down" name={[...base, 'brutalDown']}>
<Input placeholder="65537" />
<Input placeholder="e.g. 100 mbps" />
</Form.Item>
</>
)}

View file

@ -56,6 +56,7 @@ import {
import { antdRule } from '@/utils/zodForm';
import {
ALPN_OPTION,
Address_Port_Strategy,
DOMAIN_STRATEGY_OPTION,
Protocols,
SNIFFING_OPTION,
@ -65,7 +66,10 @@ import {
USAGE_OPTION,
UTLS_FINGERPRINT,
} from '@/schemas/primitives';
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
import {
HappyEyeballsSchema,
SockoptStreamSettingsSchema,
} from '@/schemas/protocols/stream/sockopt';
import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
@ -2260,6 +2264,116 @@ export default function InboundFormModal({
<Select.Option value="X-Client-IP">X-Client-IP</Select.Option>
</Select>
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'addressPortStrategy']}
label="Address+port strategy"
>
<Select style={{ width: '50%' }}>
{Object.values(Address_Port_Strategy).map((v) => (
<Select.Option key={v} value={v}>{v}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{({ getFieldValue, setFieldValue }) => {
const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
const hasHe = he != null;
return (
<>
<Form.Item label="Happy Eyeballs">
<Switch
checked={hasHe}
onChange={(v) => {
setFieldValue(
['streamSettings', 'sockopt', 'happyEyeballs'],
v ? HappyEyeballsSchema.parse({}) : undefined,
);
}}
/>
</Form.Item>
{hasHe && (
<>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
label="Try delay (ms)"
>
<InputNumber min={0} placeholder="0 disabled — 250 recommended" />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
label="Prioritize IPv6"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
label="Interleave"
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
label="Max concurrent try"
>
<InputNumber min={0} />
</Form.Item>
</>
)}
</>
);
}}
</Form.Item>
<Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
{(fields, { add, remove }) => (
<>
<Form.Item label="Custom sockopt">
<Button
type="dashed"
size="small"
onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
>
+ Add custom option
</Button>
</Form.Item>
{fields.map((field) => (
<Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
<Form.Item name={[field.name, 'system']} noStyle>
<Select
placeholder="all"
allowClear
style={{ width: 100 }}
options={[
{ value: 'linux', label: 'linux' },
{ value: 'windows', label: 'windows' },
{ value: 'darwin', label: 'darwin' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'type']} noStyle>
<Select
style={{ width: 80 }}
options={[
{ value: 'int', label: 'int' },
{ value: 'str', label: 'str' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'level']} noStyle>
<Input placeholder="level (6=TCP)" style={{ width: 100 }} />
</Form.Item>
<Form.Item name={[field.name, 'opt']} noStyle>
<Input placeholder="opt" style={{ width: 120 }} />
</Form.Item>
<Form.Item name={[field.name, 'value']} noStyle>
<Input placeholder="value" style={{ flex: 1 }} />
</Form.Item>
<Button danger onClick={() => remove(field.name)}></Button>
</Space.Compact>
))}
</>
)}
</Form.List>
</>
)}
</>

View file

@ -37,6 +37,7 @@ import {
ALPN_OPTION,
Address_Port_Strategy,
DNSRuleActions,
DOMAIN_STRATEGY_OPTION,
MODE_OPTION,
OutboundDomainStrategies,
OutboundProtocols as Protocols,
@ -47,6 +48,10 @@ import {
UTLS_FINGERPRINT,
WireguardDomainStrategy,
} from '@/schemas/primitives';
import {
HappyEyeballsSchema,
SockoptStreamSettingsSchema,
} from '@/schemas/protocols/stream/sockopt';
import {
canEnableReality,
canEnableStream,
@ -1897,27 +1902,7 @@ export default function OutboundFormModal({
onChange={(checked) => {
form.setFieldValue(
['streamSettings', 'sockopt'],
checked
? {
acceptProxyProtocol: false,
tcpFastOpen: false,
mark: 0,
tproxy: 'off',
tcpMptcp: false,
penetrate: false,
domainStrategy: 'UseIP',
tcpMaxSeg: 1440,
dialerProxy: '',
tcpKeepAliveInterval: 0,
tcpKeepAliveIdle: 300,
tcpUserTimeout: 10000,
tcpcongestion: 'bbr',
V6Only: false,
tcpWindowClamp: 600,
interfaceName: '',
trustedXForwardedFor: [],
}
: undefined,
checked ? SockoptStreamSettingsSchema.parse({}) : undefined,
);
}}
/>
@ -1935,9 +1920,18 @@ export default function OutboundFormModal({
name={['streamSettings', 'sockopt', 'domainStrategy']}
>
<Select
options={ADDRESS_PORT_STRATEGY_OPTIONS}
options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({
value: v,
label: v,
}))}
/>
</Form.Item>
<Form.Item
label="Address+port strategy"
name={['streamSettings', 'sockopt', 'addressPortStrategy']}
>
<Select options={ADDRESS_PORT_STRATEGY_OPTIONS} />
</Form.Item>
<Form.Item
label="Keep alive interval"
name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
@ -2048,6 +2042,108 @@ export default function OutboundFormModal({
placeholder="trusted-proxy.example,10.0.0.0/8"
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const he = form.getFieldValue([
'streamSettings', 'sockopt', 'happyEyeballs',
]);
const hasHe = he != null;
return (
<>
<Form.Item label="Happy Eyeballs">
<Switch
checked={hasHe}
onChange={(v) => {
form.setFieldValue(
['streamSettings', 'sockopt', 'happyEyeballs'],
v ? HappyEyeballsSchema.parse({}) : undefined,
);
}}
/>
</Form.Item>
{hasHe && (
<>
<Form.Item
label="Try delay (ms)"
name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
>
<InputNumber min={0} style={{ width: '100%' }} placeholder="0 (disabled) — 250 recommended" />
</Form.Item>
<Form.Item
label="Prioritize IPv6"
name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Interleave"
name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label="Max concurrent try"
name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</>
)}
</>
);
}}
</Form.Item>
<Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
{(fields, { add, remove }) => (
<>
<Form.Item label="Custom sockopt">
<Button
type="dashed"
size="small"
onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
>
+ Add custom option
</Button>
</Form.Item>
{fields.map((field) => (
<Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
<Form.Item name={[field.name, 'system']} noStyle>
<Select
placeholder="all"
allowClear
style={{ width: 100 }}
options={[
{ value: 'linux', label: 'linux' },
{ value: 'windows', label: 'windows' },
{ value: 'darwin', label: 'darwin' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'type']} noStyle>
<Select
style={{ width: 80 }}
options={[
{ value: 'int', label: 'int' },
{ value: 'str', label: 'str' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'level']} noStyle>
<Input placeholder="level (6=TCP)" style={{ width: 100 }} />
</Form.Item>
<Form.Item name={[field.name, 'opt']} noStyle>
<Input placeholder="opt (decimal)" style={{ width: 120 }} />
</Form.Item>
<Form.Item name={[field.name, 'value']} noStyle>
<Input placeholder="value" style={{ flex: 1 }} />
</Form.Item>
<Button danger onClick={() => remove(field.name)}></Button>
</Space.Compact>
))}
</>
)}
</Form.List>
</>
)}
</>

View file

@ -51,12 +51,12 @@ export const WireguardDomainStrategy = Object.freeze([
export const Address_Port_Strategy = Object.freeze({
NONE: 'none',
SrvPortOnly: 'srvportonly',
SrvAddressOnly: 'srvaddressonly',
SrvPortAndAddress: 'srvportandaddress',
TxtPortOnly: 'txtportonly',
TxtAddressOnly: 'txtaddressonly',
TxtPortAndAddress: 'txtportandaddress',
SRV_PORT_ONLY: 'SrvPortOnly',
SRV_ADDRESS_ONLY: 'SrvAddressOnly',
SRV_PORT_AND_ADDRESS: 'SrvPortAndAddress',
TXT_PORT_ONLY: 'TxtPortOnly',
TXT_ADDRESS_ONLY: 'TxtAddressOnly',
TXT_PORT_AND_ADDRESS: 'TxtPortAndAddress',
});
export const DNSRuleActions = Object.freeze(['direct', 'drop', 'reject', 'hijack'] as const);

View file

@ -33,6 +33,7 @@ export const UdpMaskTypeSchema = z.enum([
'xdns',
'xicmp',
'noise',
'sudoku',
]);
export type UdpMaskType = z.infer<typeof UdpMaskTypeSchema>;
@ -42,9 +43,12 @@ export const UdpMaskSchema = z.object({
});
export type UdpMask = z.infer<typeof UdpMaskSchema>;
export const QuicCongestionSchema = z.enum(['bbr', 'cubic', 'reno', 'brutal', 'force-brutal']);
export const QuicCongestionSchema = z.enum(['reno', 'bbr', 'brutal', 'force-brutal']);
export type QuicCongestion = z.infer<typeof QuicCongestionSchema>;
export const BbrProfileSchema = z.enum(['conservative', 'standard', 'aggressive']);
export type BbrProfile = z.infer<typeof BbrProfileSchema>;
// udpHop randomizes the QUIC port between a range every `interval` seconds
// to dodge port-based blocking. Both fields are dash-range strings on the
// wire (e.g. '20000-50000', '5-10'). preprocess coerces legacy DB rows
@ -62,18 +66,19 @@ export type QuicUdpHop = z.infer<typeof QuicUdpHopSchema>;
export const QuicParamsSchema = z.object({
congestion: QuicCongestionSchema.default('bbr'),
bbrProfile: BbrProfileSchema.optional(),
debug: z.boolean().optional(),
brutalUp: z.number().int().min(0).optional(),
brutalDown: z.number().int().min(0).optional(),
brutalUp: z.string().optional(),
brutalDown: z.string().optional(),
udpHop: QuicUdpHopSchema.optional(),
initStreamReceiveWindow: z.number().int().min(0).optional(),
maxStreamReceiveWindow: z.number().int().min(0).optional(),
initConnectionReceiveWindow: z.number().int().min(0).optional(),
maxConnectionReceiveWindow: z.number().int().min(0).optional(),
maxIdleTimeout: z.number().int().min(0).optional(),
keepAlivePeriod: z.number().int().min(0).optional(),
maxIdleTimeout: z.number().int().min(4).max(120).optional(),
keepAlivePeriod: z.number().int().min(2).max(60).optional(),
disablePathMTUDiscovery: z.boolean().optional(),
maxIncomingStreams: z.number().int().min(0).optional(),
maxIncomingStreams: z.number().int().min(8).optional(),
});
export type QuicParams = z.infer<typeof QuicParamsSchema>;

View file

@ -21,33 +21,54 @@ export type TcpCongestion = z.infer<typeof TcpCongestionSchema>;
export const TproxyModeSchema = z.enum(['off', 'redirect', 'tproxy']);
export type TproxyMode = z.infer<typeof TproxyModeSchema>;
// Sockopt knobs are an orthogonal layer on streamSettings — they tune
// the underlying socket (TCP keepalive, TFO, mark, tproxy, dialer proxy,
// IPv6-only, MPTCP). The wire field is `interface` (single word) but the
// panel class names it `interfaceName` internally to avoid the JS
// reserved keyword. We use `interfaceName` here too and document the
// renames; serializers writing back to wire must rename.
//
// trustedXForwardedFor is omitted from the wire payload when empty
// (legacy toJson() filters it); our default([]) lets parsing succeed but
// the shadow canonicalize step treats [] and absence as equivalent.
export const AddressPortStrategySchema = z.enum([
'none',
'SrvPortOnly',
'SrvAddressOnly',
'SrvPortAndAddress',
'TxtPortOnly',
'TxtAddressOnly',
'TxtPortAndAddress',
]);
export type AddressPortStrategy = z.infer<typeof AddressPortStrategySchema>;
export const HappyEyeballsSchema = z.object({
tryDelayMs: z.number().int().min(0).default(0),
prioritizeIPv6: z.boolean().default(false),
interleave: z.number().int().min(1).default(1),
maxConcurrentTry: z.number().int().min(0).default(4),
});
export type HappyEyeballs = z.infer<typeof HappyEyeballsSchema>;
export const CustomSockoptSchema = z.object({
system: z.enum(['linux', 'windows', 'darwin']).optional(),
type: z.enum(['int', 'str']),
level: z.string().default('6'),
opt: z.string(),
value: z.union([z.string(), z.number()]),
});
export type CustomSockopt = z.infer<typeof CustomSockoptSchema>;
export const SockoptStreamSettingsSchema = z.object({
acceptProxyProtocol: z.boolean().default(false),
tcpFastOpen: z.boolean().default(false),
mark: z.number().int().min(0).default(0),
tcpFastOpen: z.union([z.boolean(), z.number().int()]).default(false),
mark: z.number().int().default(0),
tproxy: TproxyModeSchema.default('off'),
tcpMptcp: z.boolean().default(false),
penetrate: z.boolean().default(false),
domainStrategy: SockoptDomainStrategySchema.default('UseIP'),
domainStrategy: SockoptDomainStrategySchema.default('AsIs'),
tcpMaxSeg: z.number().int().min(0).default(1440),
dialerProxy: z.string().default(''),
tcpKeepAliveInterval: z.number().int().min(0).default(0),
tcpKeepAliveIdle: z.number().int().min(0).default(300),
tcpKeepAliveInterval: z.number().int().min(0).default(45),
tcpKeepAliveIdle: z.number().int().min(0).default(45),
tcpUserTimeout: z.number().int().min(0).default(10000),
tcpcongestion: TcpCongestionSchema.default('bbr'),
V6Only: z.boolean().default(false),
tcpWindowClamp: z.number().int().min(0).default(600),
interfaceName: z.string().default(''),
trustedXForwardedFor: z.array(z.string()).default([]),
addressPortStrategy: AddressPortStrategySchema.default('none'),
happyEyeballs: HappyEyeballsSchema.optional(),
customSockopt: z.array(CustomSockoptSchema).default([]),
});
export type SockoptStreamSettings = z.infer<typeof SockoptStreamSettingsSchema>;

View file

@ -0,0 +1,174 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`FinalMaskStreamSettingsSchema fixtures > parses combined byte-stably 1`] = `
{
"quicParams": {
"brutalDown": "200 mbps",
"brutalUp": "100 mbps",
"congestion": "brutal",
"udpHop": {
"interval": "5-10",
"ports": "10000-20000",
},
},
"tcp": [
{
"settings": {
"packets": "1-3",
},
"type": "fragment",
},
],
"udp": [
{
"settings": {
"password": "swordfish",
},
"type": "salamander",
},
{
"type": "header-wireguard",
},
],
}
`;
exports[`FinalMaskStreamSettingsSchema fixtures > parses quic-params byte-stably 1`] = `
{
"quicParams": {
"bbrProfile": "standard",
"congestion": "bbr",
"debug": false,
"disablePathMTUDiscovery": false,
"initConnectionReceiveWindow": 20971520,
"initStreamReceiveWindow": 8388608,
"keepAlivePeriod": 10,
"maxConnectionReceiveWindow": 20971520,
"maxIdleTimeout": 30,
"maxIncomingStreams": 1024,
"maxStreamReceiveWindow": 8388608,
"udpHop": {
"interval": "5-10",
"ports": "20000-50000",
},
},
"tcp": [],
"udp": [],
}
`;
exports[`FinalMaskStreamSettingsSchema fixtures > parses tcp-mask byte-stably 1`] = `
{
"tcp": [
{
"settings": {
"delay": "5-10",
"length": "10-20",
"maxSplit": "0",
"packets": "1-3",
},
"type": "fragment",
},
{
"type": "sudoku",
},
{
"settings": {
"clients": [
[
{
"delay": 0,
"packet": [
"GET / HTTP/1.1",
],
"type": "str",
},
],
],
"errors": [],
"servers": [
[
{
"delay": 0,
"packet": [
"HTTP/1.1 200 OK",
],
"type": "str",
},
],
],
},
"type": "header-custom",
},
],
"udp": [],
}
`;
exports[`FinalMaskStreamSettingsSchema fixtures > parses udp-mask byte-stably 1`] = `
{
"tcp": [],
"udp": [
{
"settings": {
"password": "swordfish",
},
"type": "salamander",
},
{
"settings": {
"password": "abcdef0123456789",
},
"type": "mkcp-aes128gcm",
},
{
"settings": {
"domain": "cloudflare.com",
},
"type": "header-dns",
},
{
"type": "header-wireguard",
},
{
"settings": {
"noise": [
{
"delay": "10-16",
"rand": "10-20",
"type": "rand",
},
{
"delay": "5",
"packet": [
"ping",
],
"type": "str",
},
],
"reset": "60",
},
"type": "noise",
},
{
"settings": {
"domains": [
"example.com:txt",
"example.org:a",
],
"resolvers": [
"example.com:txt+udp://1.1.1.1:53",
],
},
"type": "xdns",
},
{
"settings": {
"id": 0,
"listenIp": "0.0.0.0",
},
"type": "xicmp",
},
],
}
`;

View file

@ -0,0 +1,100 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SockoptStreamSettingsSchema fixtures > parses defaults byte-stably 1`] = `
{
"V6Only": false,
"acceptProxyProtocol": false,
"addressPortStrategy": "none",
"customSockopt": [],
"dialerProxy": "",
"domainStrategy": "AsIs",
"interfaceName": "",
"mark": 0,
"penetrate": false,
"tcpFastOpen": false,
"tcpKeepAliveIdle": 45,
"tcpKeepAliveInterval": 45,
"tcpMaxSeg": 1440,
"tcpMptcp": false,
"tcpUserTimeout": 10000,
"tcpWindowClamp": 600,
"tcpcongestion": "bbr",
"tproxy": "off",
"trustedXForwardedFor": [],
}
`;
exports[`SockoptStreamSettingsSchema fixtures > parses full byte-stably 1`] = `
{
"V6Only": false,
"acceptProxyProtocol": true,
"addressPortStrategy": "none",
"customSockopt": [],
"dialerProxy": "out-proxy-tag",
"domainStrategy": "UseIP",
"interfaceName": "eth0",
"mark": 100,
"penetrate": false,
"tcpFastOpen": true,
"tcpKeepAliveIdle": 300,
"tcpKeepAliveInterval": 15,
"tcpMaxSeg": 1440,
"tcpMptcp": true,
"tcpUserTimeout": 10000,
"tcpWindowClamp": 600,
"tcpcongestion": "cubic",
"tproxy": "redirect",
"trustedXForwardedFor": [
"10.0.0.0/8",
"192.168.0.0/16",
],
}
`;
exports[`SockoptStreamSettingsSchema fixtures > parses tcp-tuning byte-stably 1`] = `
{
"V6Only": false,
"acceptProxyProtocol": false,
"addressPortStrategy": "none",
"customSockopt": [],
"dialerProxy": "",
"domainStrategy": "AsIs",
"interfaceName": "",
"mark": 0,
"penetrate": false,
"tcpFastOpen": true,
"tcpKeepAliveIdle": 120,
"tcpKeepAliveInterval": 30,
"tcpMaxSeg": 1440,
"tcpMptcp": true,
"tcpUserTimeout": 5000,
"tcpWindowClamp": 600,
"tcpcongestion": "bbr",
"tproxy": "off",
"trustedXForwardedFor": [],
}
`;
exports[`SockoptStreamSettingsSchema fixtures > parses tproxy byte-stably 1`] = `
{
"V6Only": false,
"acceptProxyProtocol": false,
"addressPortStrategy": "none",
"customSockopt": [],
"dialerProxy": "",
"domainStrategy": "ForceIPv4",
"interfaceName": "",
"mark": 255,
"penetrate": true,
"tcpFastOpen": false,
"tcpKeepAliveIdle": 45,
"tcpKeepAliveInterval": 45,
"tcpMaxSeg": 1440,
"tcpMptcp": false,
"tcpUserTimeout": 10000,
"tcpWindowClamp": 600,
"tcpcongestion": "bbr",
"tproxy": "tproxy",
"trustedXForwardedFor": [],
}
`;

View file

@ -32,3 +32,150 @@ exports[`NetworkSettingsSchema fixtures > parses ws-default byte-stably 1`] = `
},
}
`;
exports[`NetworkSettingsSchema fixtures > parses xhttp-basic byte-stably 1`] = `
{
"network": "xhttp",
"xhttpSettings": {
"enableXmux": false,
"headers": {},
"host": "edge.example.test",
"mode": "auto",
"noGRPCHeader": false,
"noSSEHeader": false,
"path": "/sp",
"scMaxBufferedPosts": 30,
"scMaxEachPostBytes": "1000000",
"scMinPostsIntervalMs": "30",
"scStreamUpServerSecs": "20-80",
"seqKey": "",
"seqPlacement": "",
"serverMaxHeaderBytes": 0,
"sessionKey": "",
"sessionPlacement": "",
"uplinkChunkSize": 0,
"uplinkDataKey": "",
"uplinkDataPlacement": "",
"uplinkHTTPMethod": "",
"xPaddingBytes": "100-1000",
"xPaddingHeader": "",
"xPaddingKey": "",
"xPaddingMethod": "",
"xPaddingObfsMode": false,
"xPaddingPlacement": "",
},
}
`;
exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-padding byte-stably 1`] = `
{
"network": "xhttp",
"xhttpSettings": {
"enableXmux": false,
"headers": {},
"host": "edge.example.test",
"mode": "stream-up",
"noGRPCHeader": false,
"noSSEHeader": false,
"path": "/sp",
"scMaxBufferedPosts": 30,
"scMaxEachPostBytes": "1000000",
"scMinPostsIntervalMs": "30",
"scStreamUpServerSecs": "20-80",
"seqKey": "",
"seqPlacement": "",
"serverMaxHeaderBytes": 0,
"sessionKey": "",
"sessionPlacement": "",
"uplinkChunkSize": 0,
"uplinkDataKey": "",
"uplinkDataPlacement": "",
"uplinkHTTPMethod": "",
"xPaddingBytes": "500-1500",
"xPaddingHeader": "X-Pad",
"xPaddingKey": "secret-key",
"xPaddingMethod": "random",
"xPaddingObfsMode": true,
"xPaddingPlacement": "header",
},
}
`;
exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-placement byte-stably 1`] = `
{
"network": "xhttp",
"xhttpSettings": {
"enableXmux": false,
"headers": {},
"host": "edge.example.test",
"mode": "auto",
"noGRPCHeader": false,
"noSSEHeader": false,
"path": "/sp",
"scMaxBufferedPosts": 30,
"scMaxEachPostBytes": "1000000",
"scMinPostsIntervalMs": "30",
"scStreamUpServerSecs": "20-80",
"seqKey": "X-Seq",
"seqPlacement": "cookie",
"serverMaxHeaderBytes": 0,
"sessionKey": "X-Session",
"sessionPlacement": "header",
"uplinkChunkSize": 0,
"uplinkDataKey": "u",
"uplinkDataPlacement": "query",
"uplinkHTTPMethod": "",
"xPaddingBytes": "100-1000",
"xPaddingHeader": "",
"xPaddingKey": "",
"xPaddingMethod": "",
"xPaddingObfsMode": false,
"xPaddingPlacement": "",
},
}
`;
exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-tuning byte-stably 1`] = `
{
"network": "xhttp",
"xhttpSettings": {
"enableXmux": false,
"headers": {
"X-Forwarded-For": "10.0.0.1",
"X-Real-IP": "1.2.3.4",
},
"host": "edge.example.test",
"mode": "packet-up",
"noGRPCHeader": true,
"noSSEHeader": true,
"path": "/sp",
"scMaxBufferedPosts": 50,
"scMaxEachPostBytes": "2000000",
"scMinPostsIntervalMs": "60",
"scStreamUpServerSecs": "30-90",
"seqKey": "",
"seqPlacement": "",
"serverMaxHeaderBytes": 16384,
"sessionKey": "",
"sessionPlacement": "",
"uplinkChunkSize": 8192,
"uplinkDataKey": "",
"uplinkDataPlacement": "",
"uplinkHTTPMethod": "PUT",
"xPaddingBytes": "100-1000",
"xPaddingHeader": "",
"xPaddingKey": "",
"xPaddingMethod": "",
"xPaddingObfsMode": false,
"xPaddingPlacement": "",
"xmux": {
"cMaxReuseTimes": 0,
"hKeepAlivePeriod": 30,
"hMaxRequestTimes": "600-900",
"hMaxReusableSecs": "1800-3000",
"maxConcurrency": "16-32",
"maxConnections": 4,
},
},
}
`;

View file

@ -0,0 +1,26 @@
/// <reference types="vite/client" />
import { describe, expect, it } from 'vitest';
import { FinalMaskStreamSettingsSchema } from '@/schemas/protocols/stream';
const fixtures = import.meta.glob<unknown>(
'./golden/fixtures/finalmask/*.json',
{ eager: true, import: 'default' },
);
function fixtureName(path: string): string {
const file = path.split('/').pop() ?? path;
return file.replace(/\.json$/, '');
}
describe('FinalMaskStreamSettingsSchema fixtures', () => {
const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b));
expect(entries.length, 'expected at least one fixture under golden/fixtures/finalmask').toBeGreaterThan(0);
for (const [path, raw] of entries) {
it(`parses ${fixtureName(path)} byte-stably`, () => {
const parsed = FinalMaskStreamSettingsSchema.parse(raw);
expect(parsed).toMatchSnapshot();
});
}
});

View file

@ -0,0 +1,15 @@
{
"tcp": [
{ "type": "fragment", "settings": { "packets": "1-3" } }
],
"udp": [
{ "type": "salamander", "settings": { "password": "swordfish" } },
{ "type": "header-wireguard" }
],
"quicParams": {
"congestion": "brutal",
"brutalUp": "100 mbps",
"brutalDown": "200 mbps",
"udpHop": { "ports": "10000-20000", "interval": "5-10" }
}
}

View file

@ -0,0 +1,16 @@
{
"quicParams": {
"congestion": "bbr",
"bbrProfile": "standard",
"debug": false,
"udpHop": { "ports": "20000-50000", "interval": "5-10" },
"initStreamReceiveWindow": 8388608,
"maxStreamReceiveWindow": 8388608,
"initConnectionReceiveWindow": 20971520,
"maxConnectionReceiveWindow": 20971520,
"maxIdleTimeout": 30,
"keepAlivePeriod": 10,
"disablePathMTUDiscovery": false,
"maxIncomingStreams": 1024
}
}

View file

@ -0,0 +1,30 @@
{
"tcp": [
{
"type": "fragment",
"settings": {
"packets": "1-3",
"length": "10-20",
"delay": "5-10",
"maxSplit": "0"
}
},
{ "type": "sudoku" },
{
"type": "header-custom",
"settings": {
"clients": [
[
{ "type": "str", "packet": ["GET / HTTP/1.1"], "delay": 0 }
]
],
"servers": [
[
{ "type": "str", "packet": ["HTTP/1.1 200 OK"], "delay": 0 }
]
],
"errors": []
}
}
]
}

View file

@ -0,0 +1,29 @@
{
"udp": [
{ "type": "salamander", "settings": { "password": "swordfish" } },
{ "type": "mkcp-aes128gcm", "settings": { "password": "abcdef0123456789" } },
{ "type": "header-dns", "settings": { "domain": "cloudflare.com" } },
{ "type": "header-wireguard" },
{
"type": "noise",
"settings": {
"reset": "60",
"noise": [
{ "type": "rand", "rand": "10-20", "delay": "10-16" },
{ "type": "str", "packet": ["ping"], "delay": "5" }
]
}
},
{
"type": "xdns",
"settings": {
"domains": ["example.com:txt", "example.org:a"],
"resolvers": ["example.com:txt+udp://1.1.1.1:53"]
}
},
{
"type": "xicmp",
"settings": { "listenIp": "0.0.0.0", "id": 0 }
}
]
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,19 @@
{
"acceptProxyProtocol": true,
"tcpFastOpen": true,
"mark": 100,
"tproxy": "redirect",
"tcpMptcp": true,
"penetrate": false,
"domainStrategy": "UseIP",
"tcpMaxSeg": 1440,
"dialerProxy": "out-proxy-tag",
"tcpKeepAliveInterval": 15,
"tcpKeepAliveIdle": 300,
"tcpUserTimeout": 10000,
"tcpcongestion": "cubic",
"V6Only": false,
"tcpWindowClamp": 600,
"interfaceName": "eth0",
"trustedXForwardedFor": ["10.0.0.0/8", "192.168.0.0/16"]
}

View file

@ -0,0 +1,10 @@
{
"tcpFastOpen": true,
"tcpcongestion": "bbr",
"tcpKeepAliveInterval": 30,
"tcpKeepAliveIdle": 120,
"tcpUserTimeout": 5000,
"tcpMaxSeg": 1440,
"tcpWindowClamp": 600,
"tcpMptcp": true
}

View file

@ -0,0 +1,7 @@
{
"tproxy": "tproxy",
"mark": 255,
"domainStrategy": "ForceIPv4",
"V6Only": false,
"penetrate": true
}

View file

@ -0,0 +1,8 @@
{
"network": "xhttp",
"xhttpSettings": {
"path": "/sp",
"host": "edge.example.test",
"mode": "auto"
}
}

View file

@ -0,0 +1,14 @@
{
"network": "xhttp",
"xhttpSettings": {
"path": "/sp",
"host": "edge.example.test",
"mode": "stream-up",
"xPaddingBytes": "500-1500",
"xPaddingObfsMode": true,
"xPaddingKey": "secret-key",
"xPaddingHeader": "X-Pad",
"xPaddingPlacement": "header",
"xPaddingMethod": "random"
}
}

View file

@ -0,0 +1,14 @@
{
"network": "xhttp",
"xhttpSettings": {
"path": "/sp",
"host": "edge.example.test",
"mode": "auto",
"sessionPlacement": "header",
"sessionKey": "X-Session",
"seqPlacement": "cookie",
"seqKey": "X-Seq",
"uplinkDataPlacement": "query",
"uplinkDataKey": "u"
}
}

View file

@ -0,0 +1,29 @@
{
"network": "xhttp",
"xhttpSettings": {
"path": "/sp",
"host": "edge.example.test",
"mode": "packet-up",
"uplinkHTTPMethod": "PUT",
"scMaxEachPostBytes": "2000000",
"scMaxBufferedPosts": 50,
"scStreamUpServerSecs": "30-90",
"scMinPostsIntervalMs": "60",
"noSSEHeader": true,
"serverMaxHeaderBytes": 16384,
"uplinkChunkSize": 8192,
"noGRPCHeader": true,
"headers": {
"X-Real-IP": "1.2.3.4",
"X-Forwarded-For": "10.0.0.1"
},
"xmux": {
"maxConcurrency": "16-32",
"maxConnections": 4,
"cMaxReuseTimes": 0,
"hMaxRequestTimes": "600-900",
"hMaxReusableSecs": "1800-3000",
"hKeepAlivePeriod": 30
}
}
}

View file

@ -0,0 +1,26 @@
/// <reference types="vite/client" />
import { describe, expect, it } from 'vitest';
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream';
const fixtures = import.meta.glob<unknown>(
'./golden/fixtures/sockopt/*.json',
{ eager: true, import: 'default' },
);
function fixtureName(path: string): string {
const file = path.split('/').pop() ?? path;
return file.replace(/\.json$/, '');
}
describe('SockoptStreamSettingsSchema fixtures', () => {
const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b));
expect(entries.length, 'expected at least one fixture under golden/fixtures/sockopt').toBeGreaterThan(0);
for (const [path, raw] of entries) {
it(`parses ${fixtureName(path)} byte-stably`, () => {
const parsed = SockoptStreamSettingsSchema.parse(raw);
expect(parsed).toMatchSnapshot();
});
}
});