amneziawg-conf-parity.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { describe, it, expect } from 'vitest';
  2. import { genAmneziaWGConfig } from '@/lib/xray/inbound-link';
  3. import { buildAmneziaWGClientConfig } from '@/pages/clients/amneziawgConfig';
  4. import type { AmneziawgInboundSettings } from '@/schemas/protocols/inbound/amneziawg';
  5. import type { ClientRecord, InboundOption } from '@/hooks/useClients';
  6. // wg-quick(8)'s own peer order. The panel emits an AmneziaWG .conf from three
  7. // independent places (this file's two, plus amneziaWGConfigText in Go), and a
  8. // user comparing a subscription link against a downloaded .conf sees any drift
  9. // between them immediately.
  10. const PEER_FIELD_ORDER = [
  11. 'PublicKey',
  12. 'PresharedKey',
  13. 'AllowedIPs',
  14. 'Endpoint',
  15. 'PersistentKeepalive',
  16. ];
  17. function peerFields(conf: string): string[] {
  18. const peerBlock = conf.slice(conf.indexOf('[Peer]'));
  19. return peerBlock
  20. .split('\n')
  21. .map((line) => line.split('=')[0].trim())
  22. .filter((key) => PEER_FIELD_ORDER.includes(key));
  23. }
  24. describe('AmneziaWG .conf emitters agree on the peer block', () => {
  25. const settings = {
  26. server: {
  27. publicKey: 'serverPubKey==',
  28. primaryDns: '8.8.8.8',
  29. secondaryDns: '',
  30. mtu: 1420,
  31. jc: 4,
  32. jmin: 40,
  33. jmax: 100,
  34. s1: 30,
  35. s2: 90,
  36. s3: 0,
  37. s4: 0,
  38. h1: '',
  39. h2: '',
  40. h3: '',
  41. h4: '',
  42. },
  43. clients: [
  44. {
  45. email: 'peer-1',
  46. privateKey: 'clientPrivKey==',
  47. allowedIPs: ['10.8.1.2/32'],
  48. preSharedKey: 'psk==',
  49. keepAlive: 25,
  50. },
  51. ],
  52. } as unknown as AmneziawgInboundSettings;
  53. const linkConf = genAmneziaWGConfig({
  54. settings,
  55. address: 'awg.example.test',
  56. port: 51820,
  57. remark: 'awg-peer-1',
  58. peerIndex: 0,
  59. });
  60. const client = {
  61. email: 'peer-1',
  62. privateKey: 'clientPrivKey==',
  63. allowedIPs: '10.8.1.2/32',
  64. preSharedKey: 'psk==',
  65. keepAlive: 25,
  66. } as unknown as ClientRecord;
  67. const inbound = {
  68. id: 1,
  69. tag: 'awg-1',
  70. remark: 'awg',
  71. port: 51820,
  72. protocol: 'amneziawg',
  73. awgServer: settings.server,
  74. } as unknown as InboundOption;
  75. const clientsPageConf = buildAmneziaWGClientConfig(client, inbound, 'awg.example.test');
  76. it('the share-link emitter uses the wg-quick peer order', () => {
  77. expect(peerFields(linkConf)).toEqual(PEER_FIELD_ORDER);
  78. });
  79. it('the clients-page emitter uses the same order', () => {
  80. expect(peerFields(clientsPageConf)).toEqual(PEER_FIELD_ORDER);
  81. });
  82. it('neither emitter leaves a trailing newline, so both end on their last set field', () => {
  83. expect(linkConf.endsWith('\n')).toBe(false);
  84. expect(clientsPageConf.endsWith('\n')).toBe(false);
  85. });
  86. it('an unset preSharedKey drops the line in both, without disturbing the rest', () => {
  87. const noPsk = {
  88. ...settings,
  89. clients: [{ ...settings.clients[0], preSharedKey: '' }],
  90. } as AmneziawgInboundSettings;
  91. const withoutPsk = genAmneziaWGConfig({
  92. settings: noPsk,
  93. address: 'awg.example.test',
  94. port: 51820,
  95. remark: 'awg-peer-1',
  96. peerIndex: 0,
  97. });
  98. const clientWithoutPsk = { ...client, preSharedKey: '' } as unknown as ClientRecord;
  99. const want = PEER_FIELD_ORDER.filter((f) => f !== 'PresharedKey');
  100. expect(peerFields(withoutPsk)).toEqual(want);
  101. expect(
  102. peerFields(buildAmneziaWGClientConfig(clientWithoutPsk, inbound, 'awg.example.test')),
  103. ).toEqual(want);
  104. });
  105. });
  106. // s4 junk is prepended to every transport packet and never clamped to the MTU,
  107. // so both emitters must write the same S4-aware value the server interface uses.
  108. describe('AmneziaWG .conf emitters agree on MTU', () => {
  109. function build(mtu: number | undefined, s4: number) {
  110. const settings = {
  111. server: {
  112. publicKey: 'serverPubKey==',
  113. primaryDns: '8.8.8.8',
  114. secondaryDns: '',
  115. mtu,
  116. jc: 4,
  117. jmin: 40,
  118. jmax: 100,
  119. s1: 30,
  120. s2: 90,
  121. s3: 0,
  122. s4,
  123. h1: '',
  124. h2: '',
  125. h3: '',
  126. h4: '',
  127. },
  128. clients: [{ email: 'peer-1', privateKey: 'clientPrivKey==', allowedIPs: ['10.8.1.2/32'] }],
  129. } as unknown as AmneziawgInboundSettings;
  130. const link = genAmneziaWGConfig({
  131. settings,
  132. address: 'awg.example.test',
  133. port: 51820,
  134. remark: 'awg-peer-1',
  135. peerIndex: 0,
  136. });
  137. const download = buildAmneziaWGClientConfig(
  138. {
  139. email: 'peer-1',
  140. privateKey: 'clientPrivKey==',
  141. allowedIPs: '10.8.1.2/32',
  142. } as unknown as ClientRecord,
  143. {
  144. id: 1,
  145. tag: 'awg-1',
  146. remark: 'awg',
  147. protocol: 'amneziawg',
  148. port: 51820,
  149. awgServer: settings.server,
  150. } as unknown as InboundOption,
  151. 'awg.example.test',
  152. );
  153. return { link, download };
  154. }
  155. function mtuLine(conf: string): string | undefined {
  156. return conf.split('\n').find((l) => l.startsWith('MTU = '));
  157. }
  158. it('always emits an MTU, even when the inbound has none set', () => {
  159. const { link, download } = build(undefined, 27);
  160. // 1420 - 27: without this the client stays on its own 1420 default and
  161. // fragments every full-size packet it sends.
  162. expect(mtuLine(link)).toBe('MTU = 1393');
  163. expect(mtuLine(download)).toBe('MTU = 1393');
  164. });
  165. it('keeps an explicit MTU untouched', () => {
  166. const { link, download } = build(1380, 27);
  167. expect(mtuLine(link)).toBe('MTU = 1380');
  168. expect(mtuLine(download)).toBe('MTU = 1380');
  169. });
  170. it('falls back to the plain default when there is no s4', () => {
  171. const { link, download } = build(undefined, 0);
  172. expect(mtuLine(link)).toBe('MTU = 1420');
  173. expect(mtuLine(download)).toBe('MTU = 1420');
  174. });
  175. });