2026-02-18 07:48:05 +08:00

236 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ReactNode, useContext } from 'react';
import { Handle, Position, NodeProps, useReactFlow} from '@xyflow/react';
import { AppNode, AppEdge, AppGraph, NodeData } from '../types/graph';
import {Settings, SettingsContext} from '../types/settings'
import StringBuilder from '../utils/StringBuilder';
import { CIDR, IPUtils } from '../utils/iputils';
import { tryDerivePublicKey } from '../utils/wireguardConfig'
import './CustomNode.css';
import toast from 'react-hot-toast';
class ConfigResult {
constructor(
public success: boolean,
public config?: string,
public error?: string
) {}
}
// function mapAddressToCIDR(nodeIds : string[], getAddress: GetAddress) : Record<string, CIDR> {
// const nodeIdToCIDR : Record<string, CIDR> = {};
// for(const nodeId of nodeIds) {
// const address = getAddress(nodeId);
// if(!address) continue;
// const result = IPUtils.parse(address);
// const cidr = result.cidr;
// if(!cidr) {
// throw new Error("节点地址无效");
// }
// if(cidr.version === 'IPv4') {
// cidr.mask = 32;
// } else if(cidr.version === 'IPv6') {
// cidr.mask = 128;
// }
// nodeIdToCIDR[nodeId] = cidr;
// }
// return nodeIdToCIDR;
// }
function generateInterfaceConfig(settings: Settings, data: NodeData) : StringBuilder {
const address = [data.ipv4Address, data.ipv6Address].flatMap(p => p ? [p] : []).join(', ');
const config = new StringBuilder();
config.appendLine(`[Interface]`);
config.appendLine(`# ${data.label}`);
config.appendLine(`PrivateKey = ${data.privateKey}`);
config.appendLine(`ListenPort = ${data.listenPort || settings.listenPort}`);
config.appendLine(`MTU = ${data.mtu || settings.mtu}`);
if(address) config.appendLine(`Address = ${address}`);
if(data.postUp) config.appendLine(`PostUp = ${data.postUp}`);
if(data.postDown) config.appendLine(`PostDown = ${data.postDown}`);
if(data.dnsServers) config.appendLine(`DNS = ${data.dnsServers}`);
return config;
}
function generateConfig(settings: Settings, data: NodeData, graph: AppGraph) : ConfigResult {
const config = generateInterfaceConfig(settings, data);
const disallowCIDRs : CIDR[] = [];
if(data.disallowIPs) {
const disallowList = data.disallowIPs.split(',').map(ip => ip.trim()).filter(ip => ip.length > 0);
for(const ip of disallowList) {
const result = IPUtils.parse(ip);
if(!result.cidr) {
return new ConfigResult(false, undefined, `无效的禁止访问IP: ${ip} (${result.error})`);
}
disallowCIDRs.push(result.cidr);
}
}
const belongsToEdge : Record<string, string | undefined> = {[node.id]: node.id};
const queue : AppNode[] = [];
const nearEdges = getNearEdges(node);
nearEdges.forEach(edge => {
const nextNode = getNextNode(edge, node)!;
belongsToEdge[nextNode.id] = edge.id;
queue.push(nextNode);
});
while(queue.length > 0) {
const currentNode = queue.shift()!;
const fromEdgeId = belongsToEdge[currentNode.id];
if(!fromEdgeId) continue;
getNearEdges(currentNode).forEach(edge => {
const nextNode = getNextNode(edge, currentNode)!;
if(!belongsToEdge[nextNode.id]) {
belongsToEdge[nextNode.id] = fromEdgeId;
queue.push(nextNode);
}
});
}
const groupedByEdge: Record<string, string[] | undefined> = {};
const nodeIds : string[] = [];
for (const nodeId in belongsToEdge) {
const edgeId = belongsToEdge[nodeId];
if(edgeId === nodeId) continue; // 跳过起始节点
nodeIds.push(nodeId);
if(!edgeId) continue;
if (!groupedByEdge[edgeId]) {
groupedByEdge[edgeId] = [];
}
groupedByEdge[edgeId].push(nodeId);
}
for(const edgeId in groupedByEdge) {
const groupNodeIds = groupedByEdge[edgeId]!;
const edge = getEdge(edgeId)!;
const nextNode = getNextNode(edge, node)!;
const nextNodeData = nextNode.data;
const publicKey = tryDerivePublicKey(nextNodeData.privateKey);
if(!publicKey) return new ConfigResult(false, undefined, "无法从私钥派生公钥");
config.appendLine("");
config.appendLine("[Peer]");
config.appendLine(`# ${nextNodeData.label}`);
config.appendLine(`PublicKey = ${ publicKey}`);
if(edge.data?.isTwoWayEdge || edge.source === node.id) {
if(edge.data?.persistentKeepalive) {
config.appendLine(`PersistentKeepalive = ${edge.data.persistentKeepalive}`);
}
if(!nextNodeData.listenAddress) {
return new ConfigResult(false, undefined, `节点 ${nextNodeData.label} 未设置监听地址,无法生成配置`);
}
const parse = IPUtils.parse(`${nextNodeData.listenAddress}/0`);
if(!parse.cidr) {
return new ConfigResult(false, undefined, `节点 ${nextNodeData.label} 的监听地址无效`);
}
const listenAddress = parse.cidr.version === 'IPv4' ? nextNodeData.listenAddress : `[${nextNodeData.listenAddress}]`;
const listenPort = nextNodeData.listenPort || settings.listenPort;
config.appendLine(`EndPoint = ${listenAddress}:${listenPort}`);
}
const subnets : Record<string, CIDR>[] = [];
try {
subnets.push(mapAddressToCIDR(nodeIds, nodeId => node.data.ipv4Address && getNode(nodeId)?.data.ipv4Address));
subnets.push(mapAddressToCIDR(nodeIds, nodeId => node.data.ipv6Address && getNode(nodeId)?.data.ipv6Address));
} catch(e) {
if(e instanceof Error) {
return new ConfigResult(false, undefined, e.message);
}
}
const allowIPs : string[] = [];
for(const subnetMap of subnets) {
const allCIDRs = nodeIds.flatMap(id => subnetMap[id] ? [subnetMap[id]] : []);
const targetCIDRs = groupNodeIds.flatMap(id => {
const cidr = subnetMap[id];
if(!cidr || disallowCIDRs.some(disallow => disallow.contains(cidr))) return [];
return [cidr];
});
const mergeResult = IPUtils.mergeCIDRs(allCIDRs, targetCIDRs);
if(!mergeResult) {
return new ConfigResult(false, undefined, `无法生成路由配置`);
}
mergeResult.forEach(cidr => {allowIPs.push(cidr.toString())});
}
if(allowIPs.length > 0) {
config.appendLine(`AllowedIPs = ${allowIPs.join(', ')}`);
}
}
// console.log(config.toString());
return new ConfigResult(true, config.toString());
}
export default function CustomNode({
data,
selected
}: NodeProps<AppNode>): ReactNode {
const settings = useContext(SettingsContext);
const { getNode, getEdge, getEdges } = useReactFlow<AppNode, AppEdge>();
const handleGenerate = (node : NodeData) => {
const result = generateConfig(settings, node, getEdges, getEdge, getNode);
if(result.success && result.config) {
navigator.clipboard.writeText(result.config).then(() => {
toast.success("配置已复制到剪贴板");
}).catch(() => {
toast.error("复制失败,请手动复制");
});
} else {
toast.error("配置生成失败:" + (result.error || "未知错误"));
}
};
const empty = !(data.ipv4Address || data.ipv6Address);
return (
<div className={`custom-node ${selected ? 'selected' : ''}`}>
<div className="node-header">
<label className="node-label">{data.label}</label>
</div>
<div className='node-info'>
{empty && <div className="empty-tip"></div>}
{data.ipv4Address && <div className="info-item">
<span className="label">IPv4地址</span>
<span className="value">{data.ipv4Address || "未设置"}</span>
</div>}
{data.ipv6Address && <div className="info-item">
<span className="label">IPv6地址</span>
<span className="value">{data.ipv6Address || "未设置"}</span>
</div>}
{data.listenAddress && <div className="info-item">
<span className="label"></span>
<span className="value">{data.listenAddress || "未设置"}</span>
</div>}
</div>
<div className="node-actions">
<button className="gen-btn" onClick={e => {
e.stopPropagation();
handleGenerate(data);
}} onDoubleClick={e => e.stopPropagation()}></button>
</div>
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => (
(["target", "source"] as const).map((type) => (
<Handle type={type} position={position} id={position} key={`${type}-${position}`} className="node-handle"/>
))
))}
</div>
);
}