import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Collapse, Divider, Form, Input, message, Modal, Tag } from 'antd'; import { ApiOutlined, SyncOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { HttpUtil, SizeFormatter, ObjectUtil, Wireguard } from '@/utils'; import { FormField } from '@/components/form/rhf'; import './WarpModal.css'; interface WarpModalProps { open: boolean; templateSettings: { outbounds?: { tag?: string }[] } | null; onClose: () => void; onAddOutbound: (outbound: Record) => void; onResetOutbound: (payload: { index: number; outbound: Record }) => void; onRemoveOutbound: (tag: string) => void; } interface WarpData { access_token?: string; device_id?: string; license_key?: string; private_key?: string; client_id?: string; } interface WarpConfig { name?: string; model?: string; enabled?: boolean; config?: { client_id?: string; interface?: { addresses?: { v4?: string; v6?: string } }; peers?: { public_key?: string; endpoint?: { host?: string } }[]; }; account?: { account_type?: string; role?: string; premium_data?: number; quota?: number; usage?: number; }; } interface WarpFormValues { warpPlus: string; updateInterval: number; } const EMPTY: WarpFormValues = { warpPlus: '', updateInterval: 0 }; function addressesFor(addrs: { v4?: string; v6?: string }): string[] { const out: string[] = []; if (addrs.v4) out.push(`${addrs.v4}/32`); if (addrs.v6) out.push(`${addrs.v6}/128`); return out; } function reservedFor(clientId?: string): number[] { if (!clientId) return []; const decoded = atob(clientId); const out: number[] = []; for (let i = 0; i < decoded.length; i += 1) out.push(decoded.charCodeAt(i)); return out; } export function mergeWarpRotation( existing: Record | undefined, data: WarpData | null, config: WarpConfig | null, ): Record | null { const cfg = config?.config; const peer = cfg?.peers?.[0]; if (!cfg || !peer) return null; const base: Record = existing && typeof existing === 'object' ? { ...existing } : { tag: 'warp', protocol: 'wireguard' }; const prevSettings = base.settings && typeof base.settings === 'object' ? { ...(base.settings as Record) } : {}; const prevPeers = Array.isArray(prevSettings.peers) ? [...(prevSettings.peers as Record[])] : []; const prevFirstPeer = prevPeers[0] && typeof prevPeers[0] === 'object' ? { ...(prevPeers[0] as Record) } : {}; prevFirstPeer.publicKey = peer.public_key; prevFirstPeer.endpoint = peer.endpoint?.host; prevPeers[0] = prevFirstPeer; prevSettings.secretKey = data?.private_key; prevSettings.address = addressesFor(cfg.interface?.addresses || {}); prevSettings.reserved = reservedFor(cfg.client_id ?? data?.client_id); prevSettings.peers = prevPeers; base.settings = prevSettings; base.tag = 'warp'; base.protocol = 'wireguard'; return base; } export default function WarpModal({ open, templateSettings, onClose, onAddOutbound, onResetOutbound, onRemoveOutbound, }: WarpModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const [loading, setLoading] = useState(false); const [warpData, setWarpData] = useState(null); const [warpConfig, setWarpConfig] = useState(null); const [licenseError, setLicenseError] = useState(''); const [stagedOutbound, setStagedOutbound] = useState | null>(null); const methods = useForm({ defaultValues: EMPTY }); const warpPlusValue = useWatch({ control: methods.control, name: 'warpPlus' }) ?? ''; const warpOutboundIndex = useMemo(() => { const list = templateSettings?.outbounds; if (!list) return -1; return list.findIndex((o) => o?.tag === 'warp'); }, [templateSettings?.outbounds]); const collectConfig = useCallback( (data: WarpData | null, config: WarpConfig | null): Record | null => { const cfg = config?.config; if (!cfg?.peers?.length) return null; const peer = cfg.peers[0]; const outbound: Record = { tag: 'warp', protocol: 'wireguard', settings: { mtu: 1420, secretKey: data?.private_key, address: addressesFor(cfg.interface?.addresses || {}), reserved: reservedFor(cfg.client_id ?? data?.client_id), // Prefer IPv4 with IPv6 fallback: plain ForceIP may pick the AAAA // record for engage.cloudflareclient.com, and a host with // half-configured IPv6 then blackholes the handshake with no error // logged (#5205). domainStrategy: 'ForceIPv4v6', peers: [{ publicKey: peer.public_key, endpoint: peer.endpoint?.host }], // Userspace TUN: kernel TUN needs CAP_NET_ADMIN + fwmark routing and // fails silently on many VPS setups, and it is a different data path // than the panel's connectivity test (which always probes with // noKernelTun=true), so "test ok" and "traffic flows" can disagree. noKernelTun: true, }, }; setStagedOutbound(outbound); return outbound; }, [], ); const fetchData = useCallback(async () => { setLoading(true); try { const msg = await HttpUtil.post('/panel/api/xray/warp/data'); if (msg?.success) { const raw = msg.obj; setWarpData(raw && raw.length > 0 ? JSON.parse(raw) : null); } const settingMsg = await HttpUtil.post>('/panel/api/setting/all'); if (settingMsg?.success && settingMsg.obj) { methods.setValue('updateInterval', Number(settingMsg.obj.warpUpdateInterval) || 0); } } finally { setLoading(false); } }, [methods]); const [wasOpen, setWasOpen] = useState(false); if (open !== wasOpen) { setWasOpen(open); if (open) { setWarpConfig(null); setStagedOutbound(null); setLicenseError(''); } } useEffect(() => { if (!open) return; let cancelled = false; void (async () => { await fetchData(); if (cancelled) return; })(); return () => { cancelled = true; }; }, [open, fetchData]); async function register() { setLoading(true); try { const keys = Wireguard.generateKeypair(); const msg = await HttpUtil.post('/panel/api/xray/warp/reg', keys); if (msg?.success && msg.obj) { const resp = JSON.parse(msg.obj); setWarpData(resp.data); setWarpConfig(resp.config); collectConfig(resp.data, resp.config); } } finally { setLoading(false); } } async function getConfig() { setLoading(true); try { const msg = await HttpUtil.post('/panel/api/xray/warp/config'); if (msg?.success && msg.obj) { const parsed = JSON.parse(msg.obj); setWarpConfig(parsed); collectConfig(warpData, parsed); } } finally { setLoading(false); } } async function changeIp() { setLoading(true); try { const msg = await HttpUtil.post('/panel/api/xray/warp/changeIp'); if (msg?.success && msg.obj) { const parsed = JSON.parse(msg.obj); setWarpData(parsed.data); setWarpConfig(parsed.config); collectConfig(parsed.data, parsed.config); if (warpOutboundIndex >= 0) { const existing = templateSettings?.outbounds?.[warpOutboundIndex] as | Record | undefined; const merged = mergeWarpRotation(existing, parsed.data, parsed.config); if (merged) { onResetOutbound({ index: warpOutboundIndex, outbound: merged }); } } if (parsed.warning) { messageApi.warning(parsed.warning); } messageApi.success(t('pages.xray.warp.changeIpSuccess', 'WARP IP changed successfully!')); } } finally { setLoading(false); } } async function saveInterval() { setLoading(true); try { const msg = await HttpUtil.post('/panel/api/xray/warp/interval', { interval: methods.getValues('updateInterval'), }); if (msg?.success) { messageApi.success(t('pages.setting.toasts.saveSuccess', 'Settings saved successfully')); } } finally { setLoading(false); } } async function updateLicense() { const licenseValue = methods.getValues('warpPlus'); if (licenseValue.length < 26) return; setLoading(true); setLicenseError(''); try { const msg = await HttpUtil.post('/panel/api/xray/warp/license', { license: licenseValue, }); if (msg?.success && msg.obj) { setWarpData(JSON.parse(msg.obj)); setWarpConfig(null); methods.setValue('warpPlus', ''); } else { setLicenseError(msg?.msg || t('pages.xray.warp.licenseError')); } } finally { setLoading(false); } } async function delConfig() { setLoading(true); try { const msg = await HttpUtil.post('/panel/api/xray/warp/del'); if (msg?.success) { setWarpData(null); setWarpConfig(null); setStagedOutbound(null); onRemoveOutbound('warp'); onClose(); } } finally { setLoading(false); } } function addOutbound() { if (!stagedOutbound) { messageApi.warning(t('pages.xray.warp.fetchFirst')); return; } onAddOutbound(stagedOutbound); onClose(); } function resetOutbound() { if (!stagedOutbound) return; onResetOutbound({ index: warpOutboundIndex, outbound: stagedOutbound }); onClose(); } const hasWarp = !ObjectUtil.isEmpty(warpData); const hasConfig = !ObjectUtil.isEmpty(warpConfig); return ( <> {messageContextHolder} {!hasWarp ? ( ) : ( <>
{t('pages.xray.warp.accessToken')} {warpData?.access_token}
{t('pages.xray.warp.deviceId')} {warpData?.device_id}
{t('pages.xray.warp.licenseKey')} {warpData?.license_key}
{t('pages.xray.warp.privateKey')} {warpData?.private_key}
{t('pages.xray.warp.settings')} setLicenseError('')} >
{licenseError && ( )}
), }, { key: '2', label: t('pages.xray.warp.autoUpdateIp', 'Auto Update IP Address'), children: (
Number(v) }} >
), }, ]} /> {t('pages.xray.warp.accountInfo')}
{hasConfig && ( <> {warpConfig?.account && ( <> {warpConfig.account.usage != null && ( )} )}
{t('pages.xray.warp.deviceName')} {warpConfig?.name}
{t('pages.xray.warp.deviceModel')} {warpConfig?.model}
{t('pages.xray.warp.deviceEnabled')} {String(warpConfig?.enabled)}
{t('pages.xray.warp.accountType')} {warpConfig.account.account_type}
{t('pages.xray.warp.role')} {warpConfig.account.role}
{t('pages.xray.warp.warpPlusData')} {SizeFormatter.sizeFormat(warpConfig.account.premium_data)}
{t('pages.xray.warp.quota')} {SizeFormatter.sizeFormat(warpConfig.account.quota)}
{t('pages.xray.warp.usage')} {SizeFormatter.sizeFormat(warpConfig.account.usage)}
{t('pages.xray.outbound.outboundStatus')} {warpOutboundIndex >= 0 ? ( <> {t('enabled')} ) : ( <> {t('disabled')} )} )} )}
); }