inbound-from-db.test.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { describe, expect, it } from 'vitest';
  2. import { inboundFromDb } from '@/lib/xray/inbound-from-db';
  3. import {
  4. genAllLinks,
  5. genInboundLinks,
  6. genWireguardConfigs,
  7. genWireguardLinks,
  8. getInboundClients,
  9. } from '@/lib/xray/inbound-link';
  10. import {
  11. canEnableTlsFlow,
  12. isSS2022,
  13. isSSMultiUser,
  14. } from '@/lib/xray/protocol-capabilities';
  15. const FALLBACK_HOST = 'panel.example.test';
  16. const BASE_DB_FIELDS = {
  17. port: 12345,
  18. listen: '',
  19. tag: '',
  20. remark: 'unit',
  21. enable: true,
  22. expiryTime: 0,
  23. up: 0,
  24. down: 0,
  25. total: 0,
  26. sniffing: '',
  27. };
  28. describe('inboundFromDb', () => {
  29. it('coerces JSON-string settings into a parsed object', () => {
  30. const raw = {
  31. ...BASE_DB_FIELDS,
  32. protocol: 'vless',
  33. settings: JSON.stringify({
  34. clients: [{ id: 'abc', email: 'a@test', flow: '' }],
  35. decryption: 'none',
  36. }),
  37. streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }),
  38. };
  39. const inbound = inboundFromDb(raw);
  40. expect(inbound.protocol).toBe('vless');
  41. expect(inbound.port).toBe(12345);
  42. expect((inbound.settings as { decryption?: string }).decryption).toBe('none');
  43. expect((inbound.streamSettings as { network?: string })?.network).toBe('tcp');
  44. });
  45. it('fills schema defaults onto partial object settings', () => {
  46. const settings = { clients: [], decryption: 'none' };
  47. const raw = {
  48. ...BASE_DB_FIELDS,
  49. protocol: 'vless',
  50. settings,
  51. streamSettings: { network: 'ws', security: 'tls' },
  52. };
  53. const inbound = inboundFromDb(raw);
  54. // encryption/fallbacks defaulted by schema, original settings ref not preserved
  55. expect(inbound.settings).not.toBe(settings);
  56. expect((inbound.settings as { encryption?: string }).encryption).toBe('none');
  57. expect((inbound.streamSettings as { security?: string })?.security).toBe('tls');
  58. });
  59. it('returns schema-default settings for missing/empty fields without throwing', () => {
  60. const raw = {
  61. ...BASE_DB_FIELDS,
  62. protocol: 'http',
  63. settings: '',
  64. streamSettings: '',
  65. sniffing: '',
  66. };
  67. const inbound = inboundFromDb(raw);
  68. // http settings has its own schema defaults (accounts: [], allowTransparent: false)
  69. expect(inbound.settings).toEqual(expect.objectContaining({ accounts: [] }));
  70. expect(inbound.streamSettings).toEqual({});
  71. expect(inbound.sniffing).toEqual({});
  72. });
  73. it('feeds genInboundLinks for vless without throwing', () => {
  74. const raw = {
  75. ...BASE_DB_FIELDS,
  76. protocol: 'vless',
  77. settings: {
  78. clients: [{ id: '8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02', email: 'alice@test', flow: '' }],
  79. decryption: 'none',
  80. },
  81. streamSettings: { network: 'tcp', security: 'none' },
  82. };
  83. const inbound = inboundFromDb(raw);
  84. const links = genInboundLinks({
  85. inbound,
  86. remark: 'unit',
  87. fallbackHostname: FALLBACK_HOST,
  88. });
  89. expect(links).toContain('vless://');
  90. expect(links).toContain('encryption=none');
  91. });
  92. it('feeds genWireguardConfigs + genWireguardLinks for wireguard peers', () => {
  93. const raw = {
  94. ...BASE_DB_FIELDS,
  95. protocol: 'wireguard',
  96. settings: {
  97. mtu: 1420,
  98. secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
  99. peers: [
  100. {
  101. privateKey: 'iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=',
  102. publicKey: 'DGSYIcEKAUkA7HhzGSjxLZuV67BR3LeyU0BMLJzNVHQ=',
  103. allowedIPs: ['10.0.0.2/32'],
  104. keepAlive: 25,
  105. },
  106. ],
  107. noKernelTun: false,
  108. },
  109. streamSettings: '',
  110. };
  111. const inbound = inboundFromDb(raw);
  112. const configs = genWireguardConfigs({ inbound, remark: 'wg', fallbackHostname: FALLBACK_HOST });
  113. expect(configs).toContain('[Interface]');
  114. expect(configs).toContain('[Peer]');
  115. const links = genWireguardLinks({ inbound, remark: 'wg', fallbackHostname: FALLBACK_HOST });
  116. expect(links).toMatch(/^wireguard:\/\//);
  117. });
  118. it('feeds genAllLinks per client', () => {
  119. const raw = {
  120. ...BASE_DB_FIELDS,
  121. protocol: 'trojan',
  122. settings: {
  123. clients: [
  124. { password: 'pw1', email: 'one@test' },
  125. { password: 'pw2', email: 'two@test' },
  126. ],
  127. },
  128. streamSettings: { network: 'tcp', security: 'tls', tlsSettings: { serverName: 'example.test' } },
  129. };
  130. const inbound = inboundFromDb(raw);
  131. const entries = genAllLinks({
  132. inbound,
  133. remark: 'trojan',
  134. client: { password: 'pw1', email: 'one@test' },
  135. fallbackHostname: FALLBACK_HOST,
  136. });
  137. expect(entries.length).toBeGreaterThan(0);
  138. expect(entries[0].link).toContain('trojan://');
  139. });
  140. });
  141. describe('protocol-capability helpers with raw coerced shapes', () => {
  142. it('isSSMultiUser returns true for legacy SS methods', () => {
  143. expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(true);
  144. expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } })).toBe(true);
  145. });
  146. it('isSSMultiUser returns false for single-user blake3-chacha20 method', () => {
  147. expect(isSSMultiUser({
  148. protocol: 'shadowsocks',
  149. settings: { method: '2022-blake3-chacha20-poly1305' },
  150. })).toBe(false);
  151. });
  152. it('isSS2022 detects 2022-blake3 family', () => {
  153. expect(isSS2022({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } })).toBe(true);
  154. expect(isSS2022({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(false);
  155. });
  156. it('canEnableTlsFlow gates on vless + tcp + tls/reality', () => {
  157. expect(canEnableTlsFlow({
  158. protocol: 'vless',
  159. streamSettings: { network: 'tcp', security: 'tls' },
  160. })).toBe(true);
  161. expect(canEnableTlsFlow({
  162. protocol: 'vless',
  163. streamSettings: { network: 'ws', security: 'tls' },
  164. })).toBe(false);
  165. expect(canEnableTlsFlow({
  166. protocol: 'vmess',
  167. streamSettings: { network: 'tcp', security: 'tls' },
  168. })).toBe(false);
  169. });
  170. it('canEnableTlsFlow allows vless + xhttp when vlessenc encryption is set', () => {
  171. const enc = 'mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y';
  172. const dec = 'mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E';
  173. // XHTTP + a real (generated) encryption value → Vision flow allowed.
  174. expect(canEnableTlsFlow({
  175. protocol: 'vless',
  176. settings: { encryption: enc },
  177. streamSettings: { network: 'xhttp', security: 'none' },
  178. })).toBe(true);
  179. // decryption alone (server-side value) is enough on XHTTP.
  180. expect(canEnableTlsFlow({
  181. protocol: 'vless',
  182. settings: { decryption: dec, encryption: 'none' },
  183. streamSettings: { network: 'xhttp', security: 'none' },
  184. })).toBe(true);
  185. // No encryption → stays gated off.
  186. expect(canEnableTlsFlow({
  187. protocol: 'vless',
  188. settings: { encryption: 'none' },
  189. streamSettings: { network: 'xhttp', security: 'none' },
  190. })).toBe(false);
  191. // vlessenc is XHTTP-only: TCP without tls/reality is not Vision-capable.
  192. expect(canEnableTlsFlow({
  193. protocol: 'vless',
  194. settings: { decryption: dec, encryption: enc },
  195. streamSettings: { network: 'tcp', security: 'none' },
  196. })).toBe(false);
  197. });
  198. });
  199. describe('getInboundClients with schema-shaped inbound', () => {
  200. it('returns clients array for vless/vmess/trojan/hysteria', () => {
  201. const inbound = inboundFromDb({
  202. ...BASE_DB_FIELDS,
  203. protocol: 'vless',
  204. settings: { clients: [{ id: 'x', email: 'e@test' }], decryption: 'none' },
  205. streamSettings: { network: 'tcp', security: 'none' },
  206. });
  207. expect(getInboundClients(inbound)).toHaveLength(1);
  208. });
  209. it('returns null for SS single-user', () => {
  210. const inbound = inboundFromDb({
  211. ...BASE_DB_FIELDS,
  212. protocol: 'shadowsocks',
  213. settings: { method: '2022-blake3-chacha20-poly1305', password: 'pw', clients: [] },
  214. streamSettings: { network: 'tcp', security: 'none' },
  215. });
  216. expect(getInboundClients(inbound)).toBeNull();
  217. });
  218. it('returns null for non-client protocols (http/mixed/tun/tunnel)', () => {
  219. for (const protocol of ['http', 'mixed', 'tun', 'tunnel']) {
  220. const inbound = inboundFromDb({
  221. ...BASE_DB_FIELDS,
  222. protocol,
  223. settings: {},
  224. streamSettings: '',
  225. });
  226. expect(getInboundClients(inbound)).toBeNull();
  227. }
  228. });
  229. });