reality-config-generator.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. 'use client';
  2. import { useCallback, useEffect, useState } from 'react';
  3. import { RefreshCw } from 'lucide-react';
  4. import {
  5. generateX25519KeyPair,
  6. isX25519Available,
  7. randomShortId,
  8. randomUuid,
  9. realityClientLink,
  10. realityServerInbound,
  11. type RealityConfig,
  12. type X25519KeyPair,
  13. } from '@/lib/xray/reality';
  14. import { ToolFrame } from './tool-frame';
  15. import { TextField, SelectField } from './shared/fields';
  16. import { OutputBlock } from './shared/output-block';
  17. import { CopyButton } from './shared/copy-button';
  18. const FINGERPRINTS = ['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'] as const;
  19. export function RealityConfigGenerator() {
  20. const [address, setAddress] = useState('your-server.com');
  21. const [port, setPort] = useState('443');
  22. const [dest, setDest] = useState('www.microsoft.com:443');
  23. const [sni, setSni] = useState('www.microsoft.com');
  24. const [fingerprint, setFingerprint] = useState<string>('chrome');
  25. const [uuid, setUuid] = useState('');
  26. const [shortId, setShortId] = useState('');
  27. const [keys, setKeys] = useState<X25519KeyPair | null>(null);
  28. const [unavailable, setUnavailable] = useState(false);
  29. const regenerate = useCallback(async () => {
  30. setUuid(randomUuid());
  31. setShortId(randomShortId(4));
  32. if (!isX25519Available()) {
  33. setUnavailable(true);
  34. return;
  35. }
  36. try {
  37. setKeys(await generateX25519KeyPair());
  38. setUnavailable(false);
  39. } catch {
  40. setUnavailable(true);
  41. }
  42. }, []);
  43. // Generate keys/identifiers on the client after hydration. This is a genuine
  44. // client-only side effect (WebCrypto + randomness), not derived render state.
  45. useEffect(() => {
  46. // eslint-disable-next-line react-hooks/set-state-in-effect
  47. void regenerate();
  48. }, [regenerate]);
  49. const config: RealityConfig | null =
  50. keys && uuid
  51. ? {
  52. address,
  53. port: Number(port) || 443,
  54. uuid,
  55. dest,
  56. serverNames: [sni],
  57. shortIds: [shortId],
  58. privateKey: keys.privateKey,
  59. publicKey: keys.publicKey,
  60. fingerprint,
  61. spiderX: '/',
  62. flow: 'xtls-rprx-vision',
  63. }
  64. : null;
  65. const serverJson = config ? JSON.stringify(realityServerInbound(config), null, 2) : '';
  66. const clientLink = config ? realityClientLink(config) : '';
  67. return (
  68. <ToolFrame
  69. title="REALITY config generator"
  70. description="Generate a VLESS + REALITY inbound and client link. Keys are created in your browser — nothing is sent anywhere."
  71. onReset={() => void regenerate()}
  72. >
  73. <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
  74. <TextField
  75. label="Server address"
  76. value={address}
  77. onChange={setAddress}
  78. hint="Your domain or IP"
  79. />
  80. <TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
  81. <TextField
  82. label="Dest (camouflage target)"
  83. value={dest}
  84. onChange={setDest}
  85. hint="A real TLS 1.3 site, e.g. www.microsoft.com:443"
  86. />
  87. <TextField label="SNI / Server name" value={sni} onChange={setSni} />
  88. <SelectField
  89. label="Fingerprint"
  90. value={fingerprint}
  91. onChange={setFingerprint}
  92. options={FINGERPRINTS}
  93. />
  94. </div>
  95. {unavailable ? (
  96. <div className="mt-4 rounded-xl border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
  97. Your browser can&apos;t generate X25519 keys here. Generate them on the server instead:
  98. <div className="mt-2">
  99. <OutputBlock label="run on the server" value="xray x25519" />
  100. </div>
  101. </div>
  102. ) : (
  103. <>
  104. <div className="mt-4 flex flex-wrap items-center gap-2">
  105. <span className="text-sm font-medium">Generated keys &amp; identifiers</span>
  106. <button
  107. type="button"
  108. onClick={() => void regenerate()}
  109. className="inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
  110. >
  111. <RefreshCw className="size-3.5" aria-hidden />
  112. Regenerate
  113. </button>
  114. </div>
  115. <div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
  116. <KeyRow label="Public key" value={keys?.publicKey ?? ''} />
  117. <KeyRow label="Private key" value={keys?.privateKey ?? ''} />
  118. <KeyRow label="UUID" value={uuid} />
  119. <KeyRow label="Short ID" value={shortId} />
  120. </div>
  121. <div className="mt-4 grid grid-cols-1 gap-4">
  122. <OutputBlock label="Server inbound (Xray JSON)" value={serverJson} />
  123. <OutputBlock label="Client share link" value={clientLink} qr />
  124. </div>
  125. </>
  126. )}
  127. </ToolFrame>
  128. );
  129. }
  130. function KeyRow({ label, value }: { label: string; value: string }) {
  131. return (
  132. <div className="flex items-center gap-2 rounded-lg border bg-fd-background px-3 py-2">
  133. <span className="shrink-0 text-xs font-medium text-fd-muted-foreground">{label}</span>
  134. <code dir="ltr" className="flex-1 truncate text-start text-xs">
  135. {value}
  136. </code>
  137. <CopyButton value={value} label="" className="px-1.5" />
  138. </div>
  139. );
  140. }