outbound-form-helpers.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
  2. import { canEnableReality, canEnableTls } from '@/lib/xray/protocol-capabilities';
  3. import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
  4. import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
  5. import { MUX_PROTOCOLS } from './outbound-form-constants';
  6. export function isMuxAllowed(protocol: string, flow: string, network: string): boolean {
  7. if (!MUX_PROTOCOLS.has(protocol)) return false;
  8. if (protocol === 'vless' && flow) return false;
  9. if (network === 'xhttp') return false;
  10. return true;
  11. }
  12. // Per-network bootstrap. Mirrors the legacy class constructors so the
  13. // initial state for each transport matches what xray-core expects.
  14. export function newStreamSlice(network: string): Record<string, unknown> {
  15. switch (network) {
  16. case 'tcp':
  17. return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
  18. case 'kcp':
  19. return {
  20. network: 'kcp',
  21. kcpSettings: {
  22. mtu: 1350,
  23. tti: 20,
  24. uplinkCapacity: 5,
  25. downlinkCapacity: 20,
  26. cwndMultiplier: 1,
  27. maxSendingWindow: 2097152,
  28. },
  29. };
  30. case 'ws':
  31. return {
  32. network: 'ws',
  33. wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 },
  34. };
  35. case 'grpc':
  36. return {
  37. network: 'grpc',
  38. grpcSettings: { serviceName: '', authority: '', multiMode: false },
  39. };
  40. case 'httpupgrade':
  41. return {
  42. network: 'httpupgrade',
  43. httpupgradeSettings: { path: '/', host: '', headers: {} },
  44. };
  45. case 'xhttp':
  46. return {
  47. network: 'xhttp',
  48. xhttpSettings: {
  49. path: '/',
  50. host: '',
  51. mode: '',
  52. headers: [],
  53. xPaddingBytes: '100-1000',
  54. },
  55. };
  56. case 'hysteria':
  57. return {
  58. network: 'hysteria',
  59. hysteriaSettings: {
  60. version: 2,
  61. auth: '',
  62. udpIdleTimeout: 60,
  63. },
  64. };
  65. case 'xdrive':
  66. return { network: 'xdrive', xdriveSettings: XDriveStreamSettingsSchema.parse({}) };
  67. case 'masque':
  68. return { network: 'masque', masqueSettings: {} };
  69. default:
  70. return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
  71. }
  72. }
  73. // Hysteria2 always rides its own QUIC transport with TLS — the panel never
  74. // offers another transport or 'none' security for it.
  75. export function hysteriaStreamSlice(): Record<string, unknown> {
  76. return {
  77. ...newStreamSlice('hysteria'),
  78. security: 'tls',
  79. tlsSettings: {
  80. serverName: '',
  81. alpn: ['h3'],
  82. fingerprint: '',
  83. echConfigList: '',
  84. verifyPeerCertByName: '',
  85. pinnedPeerCertSha256: '',
  86. },
  87. };
  88. }
  89. // MASQUE needs TLS (xray-core refuses it otherwise) and dials HTTP/3 unless the ALPN
  90. // offers h2 alone, so it starts on h3 like Hysteria.
  91. export function masqueStreamSlice(): Record<string, unknown> {
  92. const { tlsSettings } = hysteriaStreamSlice();
  93. return { ...newStreamSlice('masque'), security: 'tls', tlsSettings };
  94. }
  95. // Network change cascade: swap the per-network sub-key (tcpSettings,
  96. // wsSettings, etc.) so the DU branch matches. Carry over the security mode
  97. // and its settings (tlsSettings/realitySettings, including SNI serverName)
  98. // when the new network still supports it; otherwise fall back to 'none'.
  99. // Dropping tlsSettings here silently wiped the spoofed SNI on save (#4791).
  100. export function applyNetworkChange(
  101. protocol: string,
  102. prevStream: Record<string, unknown> | undefined,
  103. next: string,
  104. ): Record<string, unknown> {
  105. if (next === 'hysteria') return hysteriaStreamSlice();
  106. if (next === 'masque') return masqueStreamSlice();
  107. const stream = prevStream ?? {};
  108. const currentSecurity = (stream.security as string) ?? 'none';
  109. const stillTls = canEnableTls({
  110. protocol,
  111. streamSettings: { network: next, security: currentSecurity },
  112. });
  113. const stillReality = canEnableReality({
  114. protocol,
  115. streamSettings: { network: next, security: currentSecurity },
  116. });
  117. const newSecurity =
  118. currentSecurity === 'tls' && !stillTls
  119. ? 'none'
  120. : currentSecurity === 'reality' && !stillReality
  121. ? 'none'
  122. : currentSecurity;
  123. const newStream: Record<string, unknown> = { ...newStreamSlice(next), security: newSecurity };
  124. if (newSecurity === 'tls' && stream.tlsSettings) newStream.tlsSettings = stream.tlsSettings;
  125. else if (newSecurity === 'reality' && stream.realitySettings)
  126. newStream.realitySettings = stream.realitySettings;
  127. return newStream;
  128. }
  129. export function buildAddModeValues(): OutboundFormValues {
  130. return rawOutboundToFormValues({});
  131. }