diff --git a/go-backend/internal/store/repo/repository.go b/go-backend/internal/store/repo/repository.go index b2c4b80..7727721 100644 --- a/go-backend/internal/store/repo/repository.go +++ b/go-backend/internal/store/repo/repository.go @@ -865,6 +865,9 @@ func (r *Repository) ListTunnels() ([]map[string]interface{}, error) { if c.Strategy.Valid { nodeObj["strategy"] = c.Strategy.String } + if c.ConnectIP.Valid { + nodeObj["connectIp"] = c.ConnectIP.String + } switch chainTypeInt { case 1: diff --git a/vite-frontend/src/api/types.ts b/vite-frontend/src/api/types.ts index 94516c1..94f817c 100644 --- a/vite-frontend/src/api/types.ts +++ b/vite-frontend/src/api/types.ts @@ -217,6 +217,7 @@ export interface NodeMutationPayload { serverIp?: string; serverIpV4?: string; serverIpV6?: string; + extraIPs?: string; port?: string; tcpListenAddr?: string; udpListenAddr?: string; @@ -230,6 +231,7 @@ export interface TunnelChainNodePayload { nodeId: number; protocol?: string; strategy?: string; + connectIp?: string; chainType?: number; inx?: number; } diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index c2816ef..66e80a5 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -59,6 +59,8 @@ import { deleteForward, forceDeleteForward, userTunnel, + getTunnelList, + getNodeList, pauseForwardService, resumeForwardService, diagnoseForward, @@ -117,10 +119,20 @@ interface Forward { interface Tunnel { id: number; name: string; + inIp?: string; + inNodeId?: Array<{ nodeId: number }>; inNodePortSta?: number; inNodePortEnd?: number; } +interface Node { + id: number; + serverIp?: string; + serverIpV4?: string; + serverIpV6?: string; + extraIPs?: string; +} + interface ForwardForm { id?: number; userId?: number; @@ -463,6 +475,8 @@ export default function ForwardPage() { const [loading, setLoading] = useState(true); const [forwards, setForwards] = useState([]); const [tunnels, setTunnels] = useState([]); + const [allTunnels, setAllTunnels] = useState([]); + const [nodes, setNodes] = useState([]); const [speedLimits, setSpeedLimits] = useState([]); const isMobile = useMobileBreakpoint(); const [searchKeyword, setSearchKeyword] = useLocalStorageState( @@ -575,6 +589,67 @@ export default function ForwardPage() { const tokenRoleId = JwtUtil.getRoleIdFromToken(); const isAdmin = tokenRoleId === 0; + const parseNodeIPs = (node?: Node): string[] => { + if (!node) { + return []; + } + + const ips: string[] = []; + const add = (value?: string) => { + const trimmed = (value || "").trim(); + + if (trimmed) { + ips.push(trimmed); + } + }; + + add(node.serverIpV4); + add(node.serverIpV6); + add(node.serverIp); + + (node.extraIPs || "") + .split(",") + .map((v) => v.trim()) + .filter((v) => v) + .forEach((v) => ips.push(v)); + + return Array.from(new Set(ips)); + }; + + const tunnelInIpOptionMap = useMemo(() => { + const map = new Map(); + const nodeMap = new Map(nodes.map((n) => [n.id, n])); + + for (const tunnel of allTunnels) { + const collected: string[] = []; + const entryNodes = tunnel.inNodeId || []; + + for (const entry of entryNodes) { + collected.push(...parseNodeIPs(nodeMap.get(entry.nodeId))); + } + + if (collected.length === 0) { + (tunnel.inIp || "") + .split(",") + .map((v) => v.trim()) + .filter((v) => v) + .forEach((v) => collected.push(v)); + } + + map.set(tunnel.id, Array.from(new Set(collected))); + } + + return map; + }, [allTunnels, nodes]); + + const currentTunnelIpOptions = useMemo(() => { + if (!form.tunnelId) { + return []; + } + + return tunnelInIpOptionMap.get(form.tunnelId) || []; + }, [form.tunnelId, tunnelInIpOptionMap]); + useEffect(() => { return () => { diagnosisAbortRef.current?.abort(); @@ -1035,6 +1110,10 @@ export default function ForwardPage() { userTunnel(), getSpeedLimitList(), ]); + const [allTunnelsRes, nodesRes] = await Promise.allSettled([ + getTunnelList(), + getNodeList(), + ]); if (forwardsRes.code === 0) { const forwardsData = @@ -1068,6 +1147,14 @@ export default function ForwardPage() { } else { } + if (allTunnelsRes.status === "fulfilled" && allTunnelsRes.value.code === 0) { + setAllTunnels((allTunnelsRes.value.data || []) as Tunnel[]); + } + + if (nodesRes.status === "fulfilled" && nodesRes.value.code === 0) { + setNodes((nodesRes.value.data || []) as Node[]); + } + if (speedLimitsRes.code === 0) { setSpeedLimits(speedLimitsRes.data || []); } @@ -1240,7 +1327,14 @@ export default function ForwardPage() { // 处理隧道选择变化 const handleTunnelChange = (tunnelId: string) => { - setForm((prev) => ({ ...prev, tunnelId: parseInt(tunnelId) })); + const nextTunnelId = parseInt(tunnelId); + const options = tunnelInIpOptionMap.get(nextTunnelId) || []; + + setForm((prev) => ({ + ...prev, + tunnelId: nextTunnelId, + inIp: options.includes(prev.inIp) ? prev.inIp : "", + })); }; // 提交表单 @@ -1267,7 +1361,7 @@ export default function ForwardPage() { name: form.name, tunnelId: form.tunnelId, inPort: form.inPort, - inIp: form.inIp || null, + inIp: form.inIp || undefined, remoteAddr: processedRemoteAddr, strategy: addressCount > 1 ? form.strategy : "fifo", speedId: normalizeSpeedId(form.speedId), @@ -1279,7 +1373,7 @@ export default function ForwardPage() { name: form.name, tunnelId: form.tunnelId, inPort: form.inPort, - inIp: form.inIp || null, + inIp: form.inIp || undefined, remoteAddr: processedRemoteAddr, strategy: addressCount > 1 ? form.strategy : "fifo", speedId: normalizeSpeedId(form.speedId), @@ -4028,16 +4122,33 @@ export default function ForwardPage() { }} /> - - setForm((prev) => ({ ...prev, inIp: e.target.value })) + placeholder={ + form.tunnelId + ? currentTunnelIpOptions.length > 0 + ? "选择入口监听IP" + : "当前隧道入口节点暂无可选IP" + : "请先选择隧道" } - /> + selectedKeys={[form.inIp || "__default__"]} + variant="bordered" + onSelectionChange={(keys) => { + const selectedKey = Array.from(keys)[0] as string; + + setForm((prev) => ({ + ...prev, + inIp: selectedKey === "__default__" ? "" : selectedKey, + })); + }} + > + 默认入口IP + {currentTunnelIpOptions.map((ip) => ( + {ip} + ))} +