236 lines
8.6 KiB
TypeScript
236 lines
8.6 KiB
TypeScript
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>
|
||
);
|
||
}
|