stream-wire-normalize.test.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /// <reference types="vite/client" />
  2. import { describe, expect, it } from 'vitest';
  3. import { formValuesToWirePayload } from '@/lib/xray/inbound-form-adapter';
  4. import { formValuesToWirePayload as outboundToWire } from '@/lib/xray/outbound-form-adapter';
  5. import {
  6. normalizeSockoptForWire,
  7. normalizeStreamSettingsForWire,
  8. normalizeXhttpForWire,
  9. validateRealityTarget,
  10. } from '@/lib/xray/stream-wire-normalize';
  11. import type { InboundFormValues } from '@/schemas/forms/inbound-form';
  12. describe('validateRealityTarget', () => {
  13. it('accepts host:port and bare port', () => {
  14. expect(validateRealityTarget('play.google.com:443')).toBeUndefined();
  15. expect(validateRealityTarget('443')).toBeUndefined();
  16. });
  17. it('rejects host without port', () => {
  18. expect(validateRealityTarget('play.google.com')).toBe('pages.inbounds.form.realityTargetNeedsPort');
  19. expect(validateRealityTarget('')).toBe('pages.inbounds.form.realityTargetRequired');
  20. });
  21. });
  22. describe('normalizeXhttpForWire stream-one', () => {
  23. it('drops packet-up and stream-up-only fields on inbound', () => {
  24. const out = normalizeXhttpForWire({
  25. path: '/app',
  26. host: 'play.google.com',
  27. mode: 'stream-one',
  28. xPaddingBytes: '100-1000',
  29. scMaxEachPostBytes: '1000000',
  30. scMinPostsIntervalMs: '30',
  31. scMaxBufferedPosts: 30,
  32. scStreamUpServerSecs: '20-80',
  33. enableXmux: false,
  34. headers: {},
  35. }, 'inbound');
  36. expect(out).toMatchObject({
  37. path: '/app',
  38. host: 'play.google.com',
  39. mode: 'stream-one',
  40. xPaddingBytes: '100-1000',
  41. });
  42. expect(out).not.toHaveProperty('scMaxEachPostBytes');
  43. expect(out).not.toHaveProperty('scMinPostsIntervalMs');
  44. expect(out).not.toHaveProperty('scMaxBufferedPosts');
  45. expect(out).not.toHaveProperty('scStreamUpServerSecs');
  46. expect(out).not.toHaveProperty('enableXmux');
  47. expect(out).not.toHaveProperty('headers');
  48. });
  49. it('keeps xmux on outbound stream-one', () => {
  50. const out = normalizeXhttpForWire({
  51. path: '/app',
  52. mode: 'stream-one',
  53. xPaddingBytes: '100-1000',
  54. xmux: { maxConcurrency: '16-32' },
  55. scMaxEachPostBytes: '1000000',
  56. }, 'outbound');
  57. expect(out.xmux).toEqual({ maxConcurrency: '16-32' });
  58. expect(out).not.toHaveProperty('scMaxEachPostBytes');
  59. });
  60. });
  61. describe('normalizeSockoptForWire', () => {
  62. it('omits doc-example defaults that throttle throughput', () => {
  63. const out = normalizeSockoptForWire({
  64. tcpWindowClamp: 0,
  65. tcpMaxSeg: 0,
  66. tcpUserTimeout: 0,
  67. tcpFastOpen: true,
  68. tcpcongestion: 'bbr',
  69. domainStrategy: 'AsIs',
  70. tproxy: 'off',
  71. mark: 0,
  72. });
  73. expect(out).toEqual({
  74. tcpFastOpen: true,
  75. tcpcongestion: 'bbr',
  76. });
  77. });
  78. it('preserves happyEyeballs on freedom-style outbound', () => {
  79. const out = normalizeSockoptForWire({
  80. domainStrategy: 'UseIP',
  81. happyEyeballs: {
  82. tryDelayMs: 150,
  83. prioritizeIPv6: true,
  84. interleave: 1,
  85. maxConcurrentTry: 4,
  86. },
  87. });
  88. expect(out?.happyEyeballs).toMatchObject({
  89. tryDelayMs: 150,
  90. prioritizeIPv6: true,
  91. });
  92. expect(out?.domainStrategy).toBe('UseIP');
  93. });
  94. });
  95. describe('normalizeStreamSettingsForWire reality', () => {
  96. it('preserves the nested client settings on inbound (share links read publicKey from there)', () => {
  97. const out = normalizeStreamSettingsForWire({
  98. network: 'xhttp',
  99. security: 'reality',
  100. realitySettings: {
  101. target: 'play.google.com:443',
  102. privateKey: 'priv',
  103. serverNames: ['play.google.com'],
  104. shortIds: ['abcd'],
  105. settings: {
  106. publicKey: 'pub',
  107. fingerprint: 'chrome',
  108. spiderX: '/',
  109. },
  110. },
  111. }, { side: 'inbound' });
  112. const reality = out.realitySettings as Record<string, unknown>;
  113. expect(reality.target).toBe('play.google.com:443');
  114. expect(reality.privateKey).toBe('priv');
  115. const settings = reality.settings as Record<string, unknown>;
  116. expect(settings.publicKey).toBe('pub');
  117. expect(settings.spiderX).toBe('/');
  118. });
  119. it('passes client realitySettings through unchanged on outbound', () => {
  120. const out = normalizeStreamSettingsForWire({
  121. network: 'xhttp',
  122. security: 'reality',
  123. realitySettings: {
  124. publicKey: 'pub',
  125. fingerprint: 'chrome',
  126. serverName: 'play.google.com',
  127. shortId: 'abcd',
  128. spiderX: '/x',
  129. },
  130. }, { side: 'outbound' });
  131. const reality = out.realitySettings as Record<string, unknown>;
  132. expect(reality.publicKey).toBe('pub');
  133. expect(reality.serverName).toBe('play.google.com');
  134. expect(reality.spiderX).toBe('/x');
  135. });
  136. });
  137. describe('inbound formValuesToWirePayload integration', () => {
  138. it('emits lean stream-one xhttp + sockopt on save', () => {
  139. const values = {
  140. remark: 't',
  141. enable: true,
  142. port: 443,
  143. listen: '0.0.0.0',
  144. tag: 'in-443',
  145. expiryTime: 0,
  146. sniffing: { enabled: false },
  147. up: 0,
  148. down: 0,
  149. total: 0,
  150. trafficReset: 'never',
  151. lastTrafficResetTime: 0,
  152. nodeId: null,
  153. protocol: 'vless',
  154. settings: { clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }], decryption: 'none' },
  155. streamSettings: {
  156. network: 'xhttp',
  157. security: 'reality',
  158. realitySettings: {
  159. target: 'play.google.com:443',
  160. privateKey: 'priv',
  161. serverNames: ['play.google.com'],
  162. shortIds: ['44003d86dc1e'],
  163. settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
  164. },
  165. xhttpSettings: {
  166. path: '/app',
  167. host: 'play.google.com',
  168. mode: 'stream-one',
  169. xPaddingBytes: '100-1000',
  170. scMaxEachPostBytes: '1000000',
  171. scMinPostsIntervalMs: '30',
  172. enableXmux: false,
  173. },
  174. sockopt: {
  175. tcpWindowClamp: 0,
  176. tcpMaxSeg: 0,
  177. tcpUserTimeout: 0,
  178. tcpFastOpen: true,
  179. tcpcongestion: 'bbr',
  180. },
  181. },
  182. } as InboundFormValues;
  183. const payload = formValuesToWirePayload(values);
  184. const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
  185. const xhttp = stream.xhttpSettings as Record<string, unknown>;
  186. const sockopt = stream.sockopt as Record<string, unknown>;
  187. const reality = stream.realitySettings as Record<string, unknown>;
  188. expect(xhttp).not.toHaveProperty('scMaxEachPostBytes');
  189. expect(sockopt).not.toHaveProperty('tcpWindowClamp');
  190. expect(sockopt.tcpFastOpen).toBe(true);
  191. const realitySettings = reality.settings as Record<string, unknown>;
  192. expect(realitySettings.publicKey).toBe('pub');
  193. });
  194. });
  195. describe('freedom outbound sockopt wire payload', () => {
  196. it('preserves happyEyeballs on direct freedom outbound', () => {
  197. const wire = outboundToWire({
  198. protocol: 'freedom',
  199. tag: 'direct',
  200. settings: { domainStrategy: 'UseIP' },
  201. streamSettings: {
  202. sockopt: {
  203. domainStrategy: 'UseIP',
  204. happyEyeballs: {
  205. tryDelayMs: 150,
  206. prioritizeIPv6: true,
  207. interleave: 1,
  208. maxConcurrentTry: 4,
  209. },
  210. },
  211. },
  212. } as Parameters<typeof outboundToWire>[0]);
  213. expect(wire.streamSettings).toMatchObject({
  214. sockopt: {
  215. domainStrategy: 'UseIP',
  216. happyEyeballs: {
  217. tryDelayMs: 150,
  218. prioritizeIPv6: true,
  219. },
  220. },
  221. });
  222. });
  223. });