outbounds.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { describe, it, expect } from 'vitest';
  2. import {
  3. buildOutbound,
  4. buildOutboundJson,
  5. buildStreamSettings,
  6. type OutboundInput,
  7. type StreamInput,
  8. } from './outbounds';
  9. describe('buildOutbound — freedom & blackhole', () => {
  10. it('freedom has empty settings by default and no streamSettings', () => {
  11. const ob = buildOutbound({ kind: 'freedom', tag: 'direct' });
  12. expect(ob.protocol).toBe('freedom');
  13. expect(ob.tag).toBe('direct');
  14. expect(ob.settings).toEqual({});
  15. expect('streamSettings' in ob).toBe(false);
  16. });
  17. it('freedom carries domainStrategy when set', () => {
  18. const ob = buildOutbound({ kind: 'freedom', tag: 'direct', domainStrategy: 'UseIP' });
  19. expect(ob.settings).toEqual({ domainStrategy: 'UseIP' });
  20. });
  21. it('blackhole has empty settings', () => {
  22. const ob = buildOutbound({ kind: 'blackhole', tag: 'block' });
  23. expect(ob.protocol).toBe('blackhole');
  24. expect(ob.settings).toEqual({});
  25. });
  26. });
  27. describe('buildOutbound — proxy protocols', () => {
  28. const base = { address: 'example.com', port: 443 };
  29. it('vless uses the FLAT settings form (address/port/id/flow/encryption), not vnext', () => {
  30. const ob = buildOutbound({
  31. kind: 'vless',
  32. tag: 'v',
  33. server: { ...base, id: 'uuid-1', flow: 'xtls-rprx-vision' },
  34. });
  35. expect(ob.protocol).toBe('vless');
  36. const s = ob.settings as Record<string, unknown>;
  37. expect(s.address).toBe('example.com');
  38. expect(s.port).toBe(443);
  39. expect(s.id).toBe('uuid-1');
  40. expect(s.flow).toBe('xtls-rprx-vision');
  41. expect(s.encryption).toBe('none');
  42. expect('vnext' in s).toBe(false);
  43. });
  44. it('vmess uses the vnext settings form', () => {
  45. const ob = buildOutbound({ kind: 'vmess', tag: 'm', server: { ...base, id: 'uuid-2' } });
  46. const vnext = (ob.settings as { vnext: Array<Record<string, unknown>> }).vnext;
  47. expect(vnext[0].address).toBe('example.com');
  48. expect(vnext[0].port).toBe(443);
  49. const users = vnext[0].users as Array<Record<string, unknown>>;
  50. expect(users[0].id).toBe('uuid-2');
  51. expect(users[0].security).toBe('auto');
  52. });
  53. it('trojan uses servers[] with a password', () => {
  54. const ob = buildOutbound({ kind: 'trojan', tag: 't', server: { ...base, password: 'pw' } });
  55. const servers = (ob.settings as { servers: Array<Record<string, unknown>> }).servers;
  56. expect(servers[0].address).toBe('example.com');
  57. expect(servers[0].password).toBe('pw');
  58. });
  59. it('shadowsocks servers[] include a method', () => {
  60. const ob = buildOutbound({
  61. kind: 'shadowsocks',
  62. tag: 's',
  63. server: { ...base, password: 'pw', method: 'aes-256-gcm' },
  64. });
  65. const servers = (ob.settings as { servers: Array<Record<string, unknown>> }).servers;
  66. expect(servers[0].method).toBe('aes-256-gcm');
  67. expect(servers[0].password).toBe('pw');
  68. });
  69. it('coerces a string port to a number', () => {
  70. const ob = buildOutbound({
  71. kind: 'vless',
  72. tag: 'v',
  73. server: { address: 'a', port: '8443' as unknown as number, id: 'x' },
  74. });
  75. expect((ob.settings as Record<string, unknown>).port).toBe(8443);
  76. });
  77. });
  78. describe('buildStreamSettings', () => {
  79. const baseStream: StreamInput = { network: 'ws', security: 'none' };
  80. it('ws emits wsSettings.path and omits an empty host', () => {
  81. const st = buildStreamSettings({ ...baseStream, path: '/ray' });
  82. expect((st.wsSettings as Record<string, unknown>).path).toBe('/ray');
  83. expect('host' in (st.wsSettings as Record<string, unknown>)).toBe(false);
  84. expect('tlsSettings' in st).toBe(false);
  85. });
  86. it('reality emits realitySettings and no tlsSettings', () => {
  87. const st = buildStreamSettings({
  88. network: 'tcp',
  89. security: 'reality',
  90. publicKey: 'PBK',
  91. shortId: 'ab',
  92. sni: 'www.microsoft.com',
  93. });
  94. const r = st.realitySettings as Record<string, unknown>;
  95. expect(r.publicKey).toBe('PBK');
  96. expect(r.shortId).toBe('ab');
  97. expect(r.serverName).toBe('www.microsoft.com');
  98. expect(r.fingerprint).toBe('chrome');
  99. expect('tlsSettings' in st).toBe(false);
  100. });
  101. it('tls emits tlsSettings.serverName from sni', () => {
  102. const st = buildStreamSettings({ network: 'ws', security: 'tls', sni: 'a.com', path: '/' });
  103. expect((st.tlsSettings as Record<string, unknown>).serverName).toBe('a.com');
  104. });
  105. it('security none emits no security sub-object', () => {
  106. const st = buildStreamSettings({ network: 'grpc', security: 'none', serviceName: 'svc' });
  107. expect('tlsSettings' in st).toBe(false);
  108. expect('realitySettings' in st).toBe(false);
  109. expect((st.grpcSettings as Record<string, unknown>).serviceName).toBe('svc');
  110. });
  111. it('kcp emits the version-correct defaults (no header/seed)', () => {
  112. const st = buildStreamSettings({ network: 'kcp', security: 'none' });
  113. const k = st.kcpSettings as Record<string, unknown>;
  114. expect(k.tti).toBe(20);
  115. expect(k.cwndMultiplier).toBe(1);
  116. expect(k.maxSendingWindow).toBe(2097152);
  117. expect('header' in k).toBe(false);
  118. expect('seed' in k).toBe(false);
  119. });
  120. });
  121. describe('buildOutbound — stream attachment', () => {
  122. it('attaches streamSettings for vless when a stream is provided', () => {
  123. const ob = buildOutbound({
  124. kind: 'vless',
  125. tag: 'v',
  126. server: { address: 'a', port: 443, id: 'x' },
  127. stream: { network: 'ws', security: 'tls', path: '/p', sni: 's' },
  128. });
  129. expect((ob.streamSettings as Record<string, unknown>).network).toBe('ws');
  130. });
  131. it('does not attach streamSettings to freedom even if a stream is provided', () => {
  132. const ob = buildOutbound({
  133. kind: 'freedom',
  134. tag: 'd',
  135. stream: { network: 'ws', security: 'none' },
  136. });
  137. expect('streamSettings' in ob).toBe(false);
  138. });
  139. });
  140. describe('buildOutbound — wireguard & warp', () => {
  141. it('wireguard peers carry publicKey, endpoint, and default allowedIPs', () => {
  142. const ob = buildOutbound({
  143. kind: 'wireguard',
  144. tag: 'wg',
  145. wireguard: { secretKey: 'sk', address: ['10.0.0.2/32'], publicKey: 'pk', endpoint: 'host:51820' },
  146. });
  147. const s = ob.settings as Record<string, unknown>;
  148. expect(s.secretKey).toBe('sk');
  149. expect(s.mtu).toBe(1420);
  150. const peers = s.peers as Array<Record<string, unknown>>;
  151. expect(peers[0].publicKey).toBe('pk');
  152. expect(peers[0].endpoint).toBe('host:51820');
  153. expect(peers[0].allowedIPs).toEqual(['0.0.0.0/0', '::/0']);
  154. });
  155. it('warp uses the wireguard protocol, forces tag=warp, and the Cloudflare endpoint', () => {
  156. const ob = buildOutbound({
  157. kind: 'warp',
  158. tag: 'ignored',
  159. wireguard: { secretKey: 'sk', address: [], publicKey: 'pk', endpoint: '' },
  160. });
  161. expect(ob.protocol).toBe('wireguard');
  162. expect(ob.tag).toBe('warp');
  163. const peers = (ob.settings as Record<string, unknown>).peers as Array<Record<string, unknown>>;
  164. expect(peers[0].endpoint).toBe('engage.cloudflareclient.com:2408');
  165. });
  166. });
  167. describe('buildOutboundJson', () => {
  168. it('round-trips and is 2-space indented', () => {
  169. const input: OutboundInput = {
  170. kind: 'vless',
  171. tag: 'v',
  172. server: { address: 'a', port: 443, id: 'x' },
  173. };
  174. const json = buildOutboundJson(input);
  175. expect(json).toContain('\n "');
  176. expect(JSON.parse(json)).toEqual(buildOutbound(input));
  177. });
  178. });