From a0275e28a828f5f3f7fe3e314206de3e662667e6 Mon Sep 17 00:00:00 2001 From: limil Date: Thu, 12 Feb 2026 19:35:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=B7=AF=E7=94=B1=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=92=8C=E8=81=9A=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CustomNode.tsx | 123 ++++++++++----- src/components/NodeEditor.tsx | 57 ++++--- src/components/SettingsEditor.tsx | 44 +----- src/types/graph.ts | 3 - src/utils/iputils.ts | 251 +++++++++++++++++++----------- 5 files changed, 267 insertions(+), 211 deletions(-) diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index dd5812b..5e72e89 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -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 = {[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 = {}; + 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 = {}; + 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): ReactNode { - const { getNode, getEdges } = useReactFlow(); + const settings = useContext(SettingsContext); + const { getNode, getEdge, getEdges } = useReactFlow(); 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 (
@@ -102,25 +155,15 @@ export default function CustomNode({
- {settings.ipv4Subnet && ( -
- IPv4地址: - {data.ipv4Address || "未设置"} -
- )} +
+ IPv4地址: + {data.ipv4Address || "未设置"} +
- {settings.ipv6Subnet && ( -
- IPv6地址: - {data.ipv6Address || "未设置"} -
- )} - - {(!settings.ipv4Subnet && !settings.ipv6Subnet) && ( -
- 未设置任何子网 -
- )} +
+ IPv6地址: + {data.ipv6Address || "未设置"} +
diff --git a/src/components/NodeEditor.tsx b/src/components/NodeEditor.tsx index eb1317a..61909bd 100644 --- a/src/components/NodeEditor.tsx +++ b/src/components/NodeEditor.tsx @@ -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({
- {settings.ipv4Subnet && ( -
- - setIpv4Address(e.target.value)} - placeholder={`当前子网:${settings.ipv4Subnet}`} - /> -
- )} +
+ + setIpv4Address(e.target.value)} + placeholder={`例如:172.29.0.1/16,留空代表不使用ipv4`} + /> +
- {settings.ipv6Subnet && ( -
- - setIpv6Address(e.target.value)} - placeholder={`当前子网:${settings.ipv6Subnet}`} - /> -
- )} +
+ + setIpv6Address(e.target.value)} + placeholder={`例如:fd23:23:23::1/64,留空代表不使用ipv6`} + /> +
diff --git a/src/components/SettingsEditor.tsx b/src/components/SettingsEditor.tsx index 15fdced..009ccb4 100644 --- a/src/components/SettingsEditor.tsx +++ b/src/components/SettingsEditor.tsx @@ -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(settings.listenPort); const [mtu, setmtu] = useState(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({ />
-
- - setIpv4Subnet(e.target.value)} - placeholder='例如:172.29.0.0/16,留空代表不使用ipv4' - /> -
- -
- - setIpv6Subnet(e.target.value)} - placeholder='例如:fd23:23:23::/64,留空代表不使用ipv6' - /> -
-
diff --git a/src/types/graph.ts b/src/types/graph.ts index 97fe114..394225a 100644 --- a/src/types/graph.ts +++ b/src/types/graph.ts @@ -35,9 +35,6 @@ export type EdgeDataUpdate = { export interface Settings { listenPort: number; mtu: number; - - ipv4Subnet?: string; - ipv6Subnet?: string; } export const initialSettings : Settings = { diff --git a/src/utils/iputils.ts b/src/utils/iputils.ts index d1d451f..f643851 100644 --- a/src/utils/iputils.ts +++ b/src/utils/iputils.ts @@ -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' }; } } \ No newline at end of file