amneziawgConfig.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { formatInboundLabel } from '@/lib/inbounds/label';
  2. import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
  3. import { effectiveMtu } from '@/lib/xray/amneziawg-obfuscation';
  4. import type { ClientRecord, InboundOption } from '@/hooks/useClients';
  5. // AmneziaWG clients are wire-identical to WireGuard clients (same
  6. // privateKey/publicKey/allowedIPs/preSharedKey/keepAlive fields on
  7. // model.Client — see wireguardConfig.ts's isWireguardClient), so this duck
  8. // type can't tell the two protocols apart on its own; findAmneziaWGInbounds's
  9. // protocol==='amneziawg' filter below is what actually disambiguates.
  10. export function isAmneziaWGClient(client: ClientRecord | null | undefined): boolean {
  11. if (!client) return false;
  12. return !!(
  13. client.privateKey ||
  14. client.publicKey ||
  15. client.allowedIPs ||
  16. client.preSharedKey ||
  17. client.keepAlive
  18. );
  19. }
  20. export function findAmneziaWGInbounds(
  21. client: ClientRecord | null | undefined,
  22. inboundsById: Record<number, InboundOption>,
  23. ): InboundOption[] {
  24. return (client?.inboundIds || [])
  25. .map((id) => inboundsById?.[id])
  26. .filter((ib): ib is InboundOption => ib?.protocol === 'amneziawg');
  27. }
  28. // h4Line renders one H magic-header line, matching the Go backend's
  29. // hOrDefault fallback (blank -> the classic 1/2/3/4 WireGuard message type).
  30. function hLine(key: string, value: string | undefined, fallback: string): string {
  31. return `${key} = ${value && value.trim() !== '' ? value : fallback}`;
  32. }
  33. // addressOverride carries this inbound's own AllowedIPs (ClientHydrateSchema's
  34. // tunnelAllowedIPs). ClientRecord.allowedIPs is a single shared column, so for
  35. // an identity attached to both WireGuard and AmneziaWG it holds the WireGuard
  36. // address — writing that into the AmneziaWG .conf yields an unroutable peer.
  37. export function buildAmneziaWGClientConfig(
  38. client: ClientRecord,
  39. inbound: InboundOption | undefined,
  40. host = window.location.hostname,
  41. publicHost = '',
  42. addressOverride = '',
  43. ): string {
  44. const server = inbound?.awgServer;
  45. const endpointHost = resolveShareHost(
  46. inbound ?? {},
  47. inbound?.nodeAddress ?? '',
  48. preferPublicHost(host, publicHost),
  49. );
  50. const address = addressOverride || client.allowedIPs || '10.8.1.2/32';
  51. const endpoint = `${endpointHost}:${inbound?.port || ''}`;
  52. const inboundName = inbound ? formatInboundLabel(inbound.tag, inbound.remark) : '';
  53. const remark = [inboundName, client.email, client.comment].filter(Boolean).join(' - ');
  54. // These land unescaped in [Interface]; a newline here would inject a
  55. // config line (e.g. a rogue PostUp) into the downloaded .conf.
  56. const privateKey = client.privateKey || client.password || '';
  57. for (const v of [privateKey, server?.primaryDns ?? '', server?.secondaryDns ?? '', remark]) {
  58. if (/[\r\n]/.test(v)) return '';
  59. }
  60. const dnsParts = [server?.primaryDns, server?.secondaryDns].filter((v) => !!v && v.trim() !== '');
  61. const lines = ['[Interface]', `PrivateKey = ${privateKey}`, `Address = ${address}`];
  62. if (dnsParts.length > 0) lines.push(`DNS = ${dnsParts.join(', ')}`);
  63. lines.push(`MTU = ${effectiveMtu(server?.mtu, server?.s4)}`);
  64. // AmneziaWG obfuscation parameters — must match the server's values.
  65. lines.push(`Jc = ${server?.jc ?? 5}`);
  66. lines.push(`Jmin = ${server?.jmin ?? 10}`);
  67. lines.push(`Jmax = ${server?.jmax ?? 50}`);
  68. lines.push(`S1 = ${server?.s1 ?? 30}`);
  69. lines.push(`S2 = ${server?.s2 ?? 45}`);
  70. if (server?.s3) lines.push(`S3 = ${server.s3}`);
  71. if (server?.s4) lines.push(`S4 = ${server.s4}`);
  72. lines.push(hLine('H1', server?.h1, '1'));
  73. lines.push(hLine('H2', server?.h2, '2'));
  74. lines.push(hLine('H3', server?.h3, '3'));
  75. lines.push(hLine('H4', server?.h4, '4'));
  76. if (server?.i1) lines.push(`I1 = ${server.i1}`);
  77. if (server?.i2) lines.push(`I2 = ${server.i2}`);
  78. if (server?.i3) lines.push(`I3 = ${server.i3}`);
  79. if (server?.i4) lines.push(`I4 = ${server.i4}`);
  80. if (server?.i5) lines.push(`I5 = ${server.i5}`);
  81. const optional31: Array<[string, string | undefined]> = [
  82. ['HeaderProtectionKey', server?.headerProtectionKey],
  83. ['ContentPaddingAddition', server?.contentPaddingAddition],
  84. ['RekeyAfterTime', server?.rekeyAfterTime],
  85. ['RekeyTimeout', server?.rekeyTimeout],
  86. ['RejectAfterTime', server?.rejectAfterTime],
  87. ['KeepaliveTimeout', server?.keepaliveTimeout],
  88. ['MaxHandshakeAttempts', server?.maxHandshakeAttempts],
  89. ];
  90. for (const [key, value] of optional31) {
  91. if (value && value.trim() !== '') lines.push(`${key} = ${value}`);
  92. }
  93. if (server?.randomTrailers) lines.push('RandomTrailers = on');
  94. if (server?.disableCookies) lines.push('DisableCookies = on');
  95. lines.push('');
  96. if (remark) lines.push(`# ${remark}`);
  97. lines.push('[Peer]', `PublicKey = ${server?.publicKey || ''}`);
  98. if (client.preSharedKey) lines.push(`PresharedKey = ${client.preSharedKey}`);
  99. lines.push('AllowedIPs = 0.0.0.0/0, ::/0', `Endpoint = ${endpoint}`);
  100. if (client.keepAlive && client.keepAlive > 0)
  101. lines.push(`PersistentKeepalive = ${client.keepAlive}`);
  102. return lines.join('\n');
  103. }