实现路由生成和聚合

This commit is contained in:
limil 2026-02-12 19:35:42 +08:00
parent 0c58ae4d3d
commit a0275e28a8
5 changed files with 267 additions and 211 deletions

View File

@ -1,6 +1,7 @@
import { ReactNode, useContext } from 'react';
import { Handle, Position, NodeProps, useReactFlow} from '@xyflow/react';
import { AppNode, AppEdge, SettingsContext, NodeData } from '../types/graph';
import { AppNode, AppEdge, SettingsContext, NodeData, Settings } from '../types/graph';
import { CIDR, IPNetwork } from '../utils/iputils';
import './CustomNode.css';
import toast from 'react-hot-toast';
@ -13,34 +14,45 @@ class ConfigResult {
}
function generateConfig(
data : NodeData,
getEdges : () => AppEdge[],
getNode : (id: string) => AppNode | undefined) : ConfigResult {
settings: Settings,
data: NodeData,
getEdges: () => AppEdge[],
getEdge: (id: string) => AppEdge | undefined,
getNode: (id: string) => AppNode | undefined) : ConfigResult {
const getNearEdges = (node: AppNode) : AppEdge[] => {
return getEdges().filter(edge => edge.source === node.id || edge.target === node.id);
};
const getNextNode = (edge: AppEdge, node: AppNode) : AppNode | undefined => {
const getNextNode = (edge: AppEdge, node: AppNode) : AppNode => {
const nextNodeId = edge.source === node.id ? edge.target : edge.source;
return getNode(nextNodeId);
return getNode(nextNodeId)!;
};
const node = getNode(data.id);
if(!node) {
return new ConfigResult(false, undefined, "节点未找到");
const node = getNode(data.id)!;
// 1. 预处理disallowIPs
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 = IPNetwork.parse(ip);
if(!result.cidr) {
return new ConfigResult(false, undefined, `无效的禁止访问IP: ${ip} (${result.error})`);
}
disallowCIDRs.push(result.cidr);
}
}
// 2. 按边分组
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);
if(nextNode) {
belongsToEdge[nextNode.id] = edge.id;
queue.push(nextNode);
}
const nextNode = getNextNode(edge, node)!;
belongsToEdge[nextNode.id] = edge.id;
queue.push(nextNode);
});
while(queue.length > 0) {
@ -49,8 +61,8 @@ function generateConfig(
if(!fromEdgeId) continue;
getNearEdges(currentNode).forEach(edge => {
const nextNode = getNextNode(edge, currentNode);
if(nextNode && !belongsToEdge[nextNode.id]) {
const nextNode = getNextNode(edge, currentNode)!;
if(!belongsToEdge[nextNode.id]) {
belongsToEdge[nextNode.id] = fromEdgeId;
queue.push(nextNode);
}
@ -58,30 +70,72 @@ function generateConfig(
}
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);
}
// 3. 生成节点到cidr的映射ipv4
const nodeIdToCIDR : Record<string, CIDR> = {};
for(const nodeId of nodeIds) {
const node = getNode(nodeId)!;
const ipv4 = node.data.ipv4Address;
if(!ipv4) continue;
const result = IPNetwork.parse(ipv4);
const cidr = result.cidr;
if(!cidr) {
return new ConfigResult(false, undefined, `节点 ${node.data.label} 的IPv4地址无效: ${ipv4} (${result.error})`);
}
cidr.mask = 32;
nodeIdToCIDR[nodeId] = cidr;
}
// 4. 为每个分组生成allowIPs列表
const allCIDRs = nodeIds.flatMap(nodeId => {
const cidr = nodeIdToCIDR[nodeId];
return cidr ? [cidr] : [];
});
for(const edgeId in groupedByEdge) {
const groupNodeIds = groupedByEdge[edgeId];
if(!groupNodeIds) continue;
const targetCIDRs = groupNodeIds.flatMap(nodeId => {
const cidr = nodeIdToCIDR[nodeId];
if(!cidr || disallowCIDRs.some(disallow => disallow.contains(cidr))) {
return [];
}
return [cidr];
});
const nextNode = getNextNode(getEdge(edgeId)!, node)!;
console.log(`${node.data.label} -> ${nextNode.data.label}:`);
console.table(allCIDRs.map(c => c.toString()));
console.table(targetCIDRs.map(c => c.toString()));
const result = IPNetwork.mergeCIDRs(allCIDRs, targetCIDRs);
console.table(result?.map(c => c.toString()) || []);
}
return new ConfigResult(false);
}
export default function CustomNode({
data,
selected
}: NodeProps<AppNode>): ReactNode {
const { getNode, getEdges } = useReactFlow<AppNode, AppEdge>();
const settings = useContext(SettingsContext);
const { getNode, getEdge, getEdges } = useReactFlow<AppNode, AppEdge>();
const handleGenerate = (node : NodeData) => {
const result = generateConfig(node, getEdges, getNode);
const result = generateConfig(settings, node, getEdges, getEdge, getNode);
if(result.success && result.config) {
navigator.clipboard.writeText(result.config).then(() => {
toast.success("配置已复制到剪贴板");
@ -93,7 +147,6 @@ export default function CustomNode({
}
};
const settings = useContext(SettingsContext);
return (
<div className={`custom-node ${selected ? 'selected' : ''}`}>
@ -102,25 +155,15 @@ export default function CustomNode({
</div>
<div className='node-info'>
{settings.ipv4Subnet && (
<div className="info-item">
<span className="label">IPv4地址</span>
<span className="value">{data.ipv4Address || "未设置"}</span>
</div>
)}
<div className="info-item">
<span className="label">IPv4地址</span>
<span className="value">{data.ipv4Address || "未设置"}</span>
</div>
{settings.ipv6Subnet && (
<div className="info-item">
<span className="label">IPv6地址</span>
<span className="value">{data.ipv6Address || "未设置"}</span>
</div>
)}
{(!settings.ipv4Subnet && !settings.ipv6Subnet) && (
<div className="info-item">
<span className="label"></span>
</div>
)}
<div className="info-item">
<span className="label">IPv6地址</span>
<span className="value">{data.ipv6Address || "未设置"}</span>
</div>
</div>
<div className="node-actions">

View File

@ -15,7 +15,6 @@ interface NodeEditorProps {
function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
const errors: string[] = [];
const {ipv4Address, ipv6Address, mtu, listenPort} = updateData;
const {ipv4Subnet, ipv6Subnet} = settings;
if(!updateData.label) {
errors.push("Label不能是空");
@ -25,17 +24,17 @@ function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
errors.push("privateKey不能是空");
}
if(ipv4Subnet) {
const cidr = IPNetwork.parse(ipv4Subnet);
if(ipv4Address && !cidr.contains(IPNetwork.parse(`${ipv4Address}/32`))) {
errors.push("IPv4不在子网范围中");
if(ipv4Address) {
const result = IPNetwork.parse(ipv4Address);
if(!result.cidr || result.cidr.version !== 'IPv4') {
errors.push("IPv4地址无效");
}
}
if(ipv6Subnet) {
const cidr = IPNetwork.parse(ipv6Subnet);
if(ipv6Address && !cidr.contains(IPNetwork.parse(`${ipv6Address}/128`))) {
errors.push("IPv6不在子网范围中");
if(ipv6Address) {
const result = IPNetwork.parse(ipv6Address);
if(!result.cidr || result.cidr.version !== 'IPv6') {
errors.push("IPv6地址无效");
}
}
@ -148,29 +147,25 @@ export default function NodeEditor({
</div>
</div>
{settings.ipv4Subnet && (
<div className="form-group">
<label>IPv4地址</label>
<input
type="text"
value={ipv4Address || ''}
onChange={e => setIpv4Address(e.target.value)}
placeholder={`当前子网:${settings.ipv4Subnet}`}
/>
</div>
)}
<div className="form-group">
<label>IPv4地址</label>
<input
type="text"
value={ipv4Address || ''}
onChange={e => setIpv4Address(e.target.value)}
placeholder={`例如172.29.0.1/16留空代表不使用ipv4`}
/>
</div>
{settings.ipv6Subnet && (
<div className="form-group">
<label>IPv6地址</label>
<input
type="text"
value={ipv6Address || ''}
onChange={e => setIpv6Address(e.target.value)}
placeholder={`当前子网:${settings.ipv6Subnet}`}
/>
</div>
)}
<div className="form-group">
<label>IPv6地址</label>
<input
type="text"
value={ipv6Address || ''}
onChange={e => setIpv6Address(e.target.value)}
placeholder={`例如fd23:23:23::1/64留空代表不使用ipv6`}
/>
</div>
<Folder title='高级'>
<div className="form-group">

View File

@ -12,25 +12,7 @@ interface SettingEditorProps {
function Validate(updateData: Settings) : string[] {
const errors: string[] = [];
const {ipv4Subnet, ipv6Subnet, mtu, listenPort} = updateData;
if(ipv4Subnet) {
const result = IPNetwork.parse(ipv4Subnet)
if(!result.isValid) {
errors.push("IPv4子网" + (result.error || "ipv4子网不合法"))
} else if(result.version != 'IPv4') {
errors.push("IPv4子网" + "非IPv4 CIDR");
}
}
if(ipv6Subnet) {
const result = IPNetwork.parse(ipv6Subnet)
if(!result.isValid) {
errors.push("IPv6子网" + (result.error || "子网不合法"));
} else if(result.version != 'IPv6') {
errors.push("IPv6子网" + "非IPv6 CIDR");
}
}
const {mtu, listenPort} = updateData;
if(isNaN(listenPort)) {
errors.push("监听端口不是数字");
@ -56,15 +38,11 @@ export default function SettingsEditor({
const [listenPort, setListenPort] = useState<number>(settings.listenPort);
const [mtu, setmtu] = useState<number>(settings.mtu);
const [ipv4Subnet, setIpv4Subnet] = useState(settings.ipv4Subnet)
const [ipv6Subnet, setIpv6Subnet] = useState(settings.ipv6Subnet)
const handleSave = (): void => {
const updateData = {
listenPort: listenPort,
mtu: mtu,
ipv4Subnet: ipv4Subnet,
ipv6Subnet: ipv6Subnet
};
const validation = Validate(updateData);
@ -122,26 +100,6 @@ export default function SettingsEditor({
/>
</div>
<div className="form-group">
<label>ipv4子网</label>
<input
type="text"
value={ipv4Subnet || ''}
onChange={e => setIpv4Subnet(e.target.value)}
placeholder='例如172.29.0.0/16留空代表不使用ipv4'
/>
</div>
<div className="form-group">
<label>ipv6子网</label>
<input
type="text"
value={ipv6Subnet || ''}
onChange={e => setIpv6Subnet(e.target.value)}
placeholder='例如fd23:23:23::/64留空代表不使用ipv6'
/>
</div>
<div className="editor-actions">
<button className="btn-save" onClick={handleSave}></button>
<button className="btn-cancel" onClick={onClose}></button>

View File

@ -35,9 +35,6 @@ export type EdgeDataUpdate = {
export interface Settings {
listenPort: number;
mtu: number;
ipv4Subnet?: string;
ipv6Subnet?: string;
}
export const initialSettings : Settings = {

View File

@ -2,115 +2,178 @@ export type IPVersion = 'IPv4' | 'IPv6' | 'invalid';
export class CIDR {
constructor(
public isValid: boolean,
public version: IPVersion,
public binary: string,
public mask: number,
public error?: string
public version: IPVersion,
public binary: string,
public mask: number,
) {}
contains(cidr: CIDR) : boolean {
if(!cidr.isValid || !this.isValid) return false;
if(this.version !== cidr.version) return false;
if(this.mask > cidr.mask) return false;
contains(cidr: CIDR): boolean {
if (this.version !== cidr.version) return false;
if (this.mask > cidr.mask) return false;
return this.binary.slice(0, this.mask) === cidr.binary.slice(0, this.mask);
}
toString(): string {
if (this.version === 'invalid') return 'invalid';
if (this.version === 'IPv4') {
// Split 32 bits into 4 octets
const octets = [];
for (let i = 0; i < 32; i += 8) {
octets.push(parseInt(this.binary.slice(i, i + 8), 2));
}
return `${octets.join('.')}/${this.mask}`;
} else {
// Split 128 bits into 8 blocks of 16 bits
const blocks = [];
for (let i = 0; i < 128; i += 16) {
blocks.push(parseInt(this.binary.slice(i, i + 16), 2).toString(16));
}
return `${blocks.join(':')}/${this.mask}`;
}
}
}
export interface CIDRParseResult {
cidr?: CIDR;
error: string;
}
export class IPNetwork {
/**
* CIDR
*/
static parse(cidr: string): CIDR {
const parts = cidr.split('/');
if (parts.length !== 2) {
return this.invalid('格式错误,缺少掩码 (如 /24)');
static mergeCIDRs(allCIDRs: CIDR[], targetCIDRs: CIDR[]): CIDR[] | undefined {
// 返回符合条件的CIDR集合
// 1. 它们能够覆盖所有targetCIDRs但是不能覆盖到任何在allCIDRs中不属于targetCIDRs的CIDR
// 2. 如果有多个集合满足条件返回其中CIDR数量最少的一个如果有多个数量最少的集合返回掩码尽可能大的
// 3. 如果无法找到这样的集合例如存在属于allCIDRs但不属于targetCIDRs的CIDR它被targetCIDRs中的某个CIDR包含则返回undefined
// 1. 获取排除列表:属于 allCIDRs 但不属于 targetCIDRs 的
const excluded = allCIDRs.filter(a =>
!targetCIDRs.some(t => t.binary === a.binary && t.mask === a.mask)
);
// 2. 基础冲突检查
// 如果 target 包含 excluded或者 excluded 包含 target返回 undefined
for (const t of targetCIDRs) {
for (const e of excluded) {
if (t.contains(e) || e.contains(t)) return undefined;
}
}
// 3. 分版本处理
const v4 = this._runAggregation('IPv4', 32, targetCIDRs, excluded);
const v6 = this._runAggregation('IPv6', 128, targetCIDRs, excluded);
return [...v4, ...v6];
}
private static _runAggregation(version: IPVersion, maxBits: number, targets: CIDR[], excluded: CIDR[]): CIDR[] {
const versionTargets = targets.filter(c => c.version === version);
const versionExcluded = excluded.filter(c => c.version === version);
if (versionTargets.length === 0) return [];
// 初始根节点0.0.0.0/0 或 ::/0
const rootBinary = '0'.repeat(maxBits);
return this._solve(new CIDR(version, rootBinary, 0), versionTargets, versionExcluded, maxBits);
}
private static _solve(node: CIDR, targets: CIDR[], excluded: CIDR[], maxBits: number): CIDR[] {
// 如果当前节点不包含任何目标,直接返回空
const hasTarget = targets.some(t => node.contains(t) || t.contains(node));
if (!hasTarget) return [];
// 如果当前节点包含任何排除项,必须向下拆分
const hasExcluded = excluded.some(e => node.contains(e));
if (hasExcluded) {
return this._splitAndSolve(node, targets, excluded, maxBits);
}
// 走到这里说明 node 是“干净”的(不含任何 excluded
// 检查:如果这个 node 已经在 targets 里的某一个被完全包含,或者它本身就是 target
// 我们需要判断:是直接用这个大的 node还是用更小的子 node
// 获取子节点的递归结果
const subResults = this._splitAndSolve(node, targets, excluded, maxBits);
// 核心逻辑:
// 1. 如果子节点汇总后数量 > 1合并成当前 node 可以减少数量,选当前 node
// 2. 如果子节点汇总后数量 <= 1 且能覆盖所有目标,保持子节点(因为子节点掩码更大)
if (subResults.length > 1) {
return [node];
} else {
// 特殊情况:如果当前 node 本身就在 targets 中,且 subResults 为空(因为 targets 可能在更深层)
// 或者 subResults 长度就是 1我们返回 subResults 以保持 mask 尽可能大
return subResults.length === 0 ? (targets.some(t => t.contains(node)) ? [node] : []) : subResults;
}
}
private static _splitAndSolve(node: CIDR, targets: CIDR[], excluded: CIDR[], maxBits: number): CIDR[] {
if (node.mask >= maxBits) return [];
const nextMask = node.mask + 1;
// 左子节点:第 mask 位为 0
const leftBinary = node.binary.slice(0, node.mask) + '0' + node.binary.slice(nextMask);
// 右子节点:第 mask 位为 1
const rightBinary = node.binary.slice(0, node.mask) + '1' + node.binary.slice(nextMask);
const left = new CIDR(node.version, leftBinary, nextMask);
const right = new CIDR(node.version, rightBinary, nextMask);
return [
...this._solve(left, targets, excluded, maxBits),
...this._solve(right, targets, excluded, maxBits)
];
}
static parse(cidrStr: string): CIDRParseResult {
const parts = cidrStr.split('/');
if (parts.length !== 2) return { error: 'Invalid CIDR format' };
const [ip, maskStr] = parts;
const mask = parseInt(maskStr, 10);
const version = this.getVersion(ip);
// 基础校验
if (version === 'invalid') return this.invalid('非法的 IP 格式');
if (isNaN(mask)) return this.invalid('掩码必须是数字');
// 掩码范围校验
if (version === 'IPv4' && (mask < 0 || mask > 32)) return this.invalid('IPv4 掩码范围应为 0-32');
if (version === 'IPv6' && (mask < 0 || mask > 128)) return this.invalid('IPv6 掩码范围应为 0-128');
try {
const binary = version === 'IPv4' ? this.ipv4ToBinary(ip) : this.ipv6ToBinary(ip);
if (!binary) return this.invalid('IP 地址数值非法');
return new CIDR(true, version, binary, mask);
} catch (e) {
return this.invalid('解析过程中出错');
}
}
private static getVersion(ip: string): IPVersion {
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return 'IPv4';
if (ip.includes(':')) return 'IPv6'; // IPv6 逻辑较复杂,在转换函数中进一步精确校验
return 'invalid';
}
private static ipv4ToBinary(ip: string): string | null {
const octets = ip.split('.');
let binary = '';
for (const o of octets) {
const n = parseInt(o, 10);
if (n < 0 || n > 255 || isNaN(n)) return null;
binary += n.toString(2).padStart(8, '0');
}
return binary;
}
private static ipv6ToBinary(ip: string): string | null {
let fullIP = ip;
try {
if (ip.includes('::')) {
const parts = ip.split('::');
if (parts.length > 2) return null; // 只能有一个 ::
const left = parts[0].split(':').filter(x => x.length > 0);
const right = parts[1].split(':').filter(x => x.length > 0);
const missing = 8 - (left.length + right.length);
if (missing < 0) return null;
fullIP = [...left, ...Array(missing).fill('0'), ...right].join(':');
}
const groups = fullIP.split(':');
if (groups.length !== 8) return null;
if (ip.includes('.')) {
// IPv4 Logic
if (isNaN(mask) || mask < 0 || mask > 32) return { error: 'Invalid IPv4 mask' };
const octets = ip.split('.');
if (octets.length !== 4) return { error: 'Invalid IPv4 address' };
let binary = '';
for (const hex of groups) {
const n = parseInt(hex, 16);
if (isNaN(n) || n < 0 || n > 0xFFFF) return null;
binary += n.toString(2).padStart(16, '0');
for (const octet of octets) {
const val = parseInt(octet, 10);
if (isNaN(val) || val < 0 || val > 255) return { error: 'Invalid IPv4 octet' };
binary += val.toString(2).padStart(8, '0');
}
return binary;
} catch {
return null;
}
}
private static invalid(msg: string): CIDR {
return new CIDR(false, 'invalid', '', -1, msg);
}
/**
* CIDR
*/
static fromBinary(binary: string, mask: number, version: IPVersion): string {
if (version === 'IPv4' && binary.length === 32) {
const octets = [];
for (let i = 0; i < 32; i += 8) octets.push(parseInt(binary.slice(i, i + 8), 2));
return `${octets.join('.')}/${mask}`;
return { cidr: new CIDR('IPv4', binary, mask), error: '' };
}
if (version === 'IPv6' && binary.length === 128) {
const segments = [];
for (let i = 0; i < 128; i += 16) segments.push(parseInt(binary.slice(i, i + 16), 2).toString(16));
return `${segments.join(':')}/${mask}`;
if (ip.includes(':')) {
// IPv6 Logic
if (isNaN(mask) || mask < 0 || mask > 128) return { error: 'Invalid IPv6 mask' };
// Expand "::" shorthand
let fullIp = ip;
if (ip.includes('::')) {
const sides = ip.split('::');
const left = sides[0].split(':').filter(x => x !== '');
const right = sides[1].split(':').filter(x => x !== '');
const missingCount = 8 - (left.length + right.length);
const middle = new Array(missingCount).fill('0');
fullIp = [...left, ...middle, ...right].join(':');
}
const blocks = fullIp.split(':');
if (blocks.length !== 8) return { error: 'Invalid IPv6 address' };
let binary = '';
for (const block of blocks) {
const val = parseInt(block || '0', 16);
if (isNaN(val) || val < 0 || val > 0xFFFF) return { error: 'Invalid IPv6 block' };
binary += val.toString(2).padStart(16, '0');
}
return { cidr: new CIDR('IPv6', binary, mask), error: '' };
}
return 'invalid-input';
return { error: '未知 IP version' };
}
}