inbound-from-db.test.ts 8.8 KB

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