Prechádzať zdrojové kódy

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.
MHSanaei 22 hodín pred
rodič
commit
0442be5078
23 zmenil súbory, kde vykonal 966 pridanie a 54 odobranie
  1. 15 4
      frontend/src/components/FinalMaskForm.tsx
  2. 115 1
      frontend/src/pages/inbounds/InboundFormModal.tsx
  3. 118 22
      frontend/src/pages/xray/OutboundFormModal.tsx
  4. 6 6
      frontend/src/schemas/primitives/options.ts
  5. 11 6
      frontend/src/schemas/protocols/stream/finalmask.ts
  6. 36 15
      frontend/src/schemas/protocols/stream/sockopt.ts
  7. 174 0
      frontend/src/test/__snapshots__/finalmask.test.ts.snap
  8. 100 0
      frontend/src/test/__snapshots__/sockopt.test.ts.snap
  9. 147 0
      frontend/src/test/__snapshots__/stream.test.ts.snap
  10. 26 0
      frontend/src/test/finalmask.test.ts
  11. 15 0
      frontend/src/test/golden/fixtures/finalmask/combined.json
  12. 16 0
      frontend/src/test/golden/fixtures/finalmask/quic-params.json
  13. 30 0
      frontend/src/test/golden/fixtures/finalmask/tcp-mask.json
  14. 29 0
      frontend/src/test/golden/fixtures/finalmask/udp-mask.json
  15. 1 0
      frontend/src/test/golden/fixtures/sockopt/defaults.json
  16. 19 0
      frontend/src/test/golden/fixtures/sockopt/full.json
  17. 10 0
      frontend/src/test/golden/fixtures/sockopt/tcp-tuning.json
  18. 7 0
      frontend/src/test/golden/fixtures/sockopt/tproxy.json
  19. 8 0
      frontend/src/test/golden/fixtures/stream/xhttp-basic.json
  20. 14 0
      frontend/src/test/golden/fixtures/stream/xhttp-extra-padding.json
  21. 14 0
      frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json
  22. 29 0
      frontend/src/test/golden/fixtures/stream/xhttp-extra-tuning.json
  23. 26 0
      frontend/src/test/sockopt.test.ts

+ 15 - 4
frontend/src/components/FinalMaskForm.tsx

@@ -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>
         </>
       )}

+ 115 - 1
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -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>
                 </>
               )}
             </>

+ 118 - 22
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -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>
                                 </>
                               )}
                             </>

+ 6 - 6
frontend/src/schemas/primitives/options.ts

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

+ 11 - 6
frontend/src/schemas/protocols/stream/finalmask.ts

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

+ 36 - 15
frontend/src/schemas/protocols/stream/sockopt.ts

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

+ 174 - 0
frontend/src/test/__snapshots__/finalmask.test.ts.snap

@@ -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",
+    },
+  ],
+}
+`;

+ 100 - 0
frontend/src/test/__snapshots__/sockopt.test.ts.snap

@@ -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": [],
+}
+`;

+ 147 - 0
frontend/src/test/__snapshots__/stream.test.ts.snap

@@ -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,
+    },
+  },
+}
+`;

+ 26 - 0
frontend/src/test/finalmask.test.ts

@@ -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();
+    });
+  }
+});

+ 15 - 0
frontend/src/test/golden/fixtures/finalmask/combined.json

@@ -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" }
+  }
+}

+ 16 - 0
frontend/src/test/golden/fixtures/finalmask/quic-params.json

@@ -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
+  }
+}

+ 30 - 0
frontend/src/test/golden/fixtures/finalmask/tcp-mask.json

@@ -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": []
+      }
+    }
+  ]
+}

+ 29 - 0
frontend/src/test/golden/fixtures/finalmask/udp-mask.json

@@ -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 }
+    }
+  ]
+}

+ 1 - 0
frontend/src/test/golden/fixtures/sockopt/defaults.json

@@ -0,0 +1 @@
+{}

+ 19 - 0
frontend/src/test/golden/fixtures/sockopt/full.json

@@ -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"]
+}

+ 10 - 0
frontend/src/test/golden/fixtures/sockopt/tcp-tuning.json

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

+ 7 - 0
frontend/src/test/golden/fixtures/sockopt/tproxy.json

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

+ 8 - 0
frontend/src/test/golden/fixtures/stream/xhttp-basic.json

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

+ 14 - 0
frontend/src/test/golden/fixtures/stream/xhttp-extra-padding.json

@@ -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"
+  }
+}

+ 14 - 0
frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json

@@ -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"
+  }
+}

+ 29 - 0
frontend/src/test/golden/fixtures/stream/xhttp-extra-tuning.json

@@ -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
+    }
+  }
+}

+ 26 - 0
frontend/src/test/sockopt.test.ts

@@ -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();
+    });
+  }
+});