diff --git a/package-lock.json b/package-lock.json index 8681a5e..a05e094 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1637,6 +1638,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1719,6 +1721,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1858,6 +1861,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2003,7 +2007,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-color": { "version": "3.1.0", @@ -2062,6 +2067,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -2230,6 +2236,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3048,6 +3055,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3120,6 +3128,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3129,6 +3138,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3514,6 +3524,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3635,6 +3646,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index 597c32a..6885f91 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -1,23 +1,14 @@ import { ReactNode, useContext } from 'react'; import { Handle, Position, NodeProps, useReactFlow} from '@xyflow/react'; -import { AppNode, AppEdge, SettingsContext, NodeData, Settings } from '../types/graph'; +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 StringBuilder { - private lines: string[] = []; - - appendLine(value: string = "") { - this.lines.push(value); - } - - toString() { - return this.lines.join('\n'); - } -} - class ConfigResult { constructor( public success: boolean, @@ -26,32 +17,27 @@ class ConfigResult { ) {} } -type GetEdge = (id: string) => AppEdge | undefined; -type GetNode = (id: string) => AppNode | undefined; -type GetEdges = () => AppEdge[]; -type GetAddress = (nodeId: string) => string | undefined; +// function mapAddressToCIDR(nodeIds : string[], getAddress: GetAddress) : Record { +// const nodeIdToCIDR : Record = {}; +// 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 mapAddressToCIDR(nodeIds : string[], getAddress: GetAddress) : Record { - const nodeIdToCIDR : Record = {}; - 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 { +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]`); @@ -66,20 +52,9 @@ function generateInterfaceConfig(settings: Settings,data: NodeData) : StringBuil return config; } -function generateConfig(settings: Settings, data: NodeData, getEdges: GetEdges, getEdge: GetEdge, getNode: GetNode) : ConfigResult { +function generateConfig(settings: Settings, data: NodeData, graph: AppGraph) : ConfigResult { const config = generateInterfaceConfig(settings, data); - const getNearEdges = (node: AppNode) : AppEdge[] => { - return getEdges().filter(edge => edge.source === node.id || edge.target === node.id); - }; - - const getNextNode = (edge: AppEdge, node: AppNode) : AppNode => { - const nextNodeId = edge.source === node.id ? edge.target : edge.source; - return getNode(nextNodeId)!; - }; - - const node = getNode(data.id)!; - const disallowCIDRs : CIDR[] = []; if(data.disallowIPs) { const disallowList = data.disallowIPs.split(',').map(ip => ip.trim()).filter(ip => ip.length > 0); diff --git a/src/components/SettingsEditor.tsx b/src/components/SettingsEditor.tsx index e323e42..248d54c 100644 --- a/src/components/SettingsEditor.tsx +++ b/src/components/SettingsEditor.tsx @@ -1,5 +1,6 @@ import { useState, ReactNode } from 'react'; -import { Settings, SubnetInfo, AppEdge, AppNode } from '../types/graph'; +import { AppEdge, AppNode } from '../types/graph'; +import {Settings, SubnetInfo} from '../types/settings' import { useReactFlow} from '@xyflow/react'; import './FormEditor.css'; import './SettingsEditor.css'; diff --git a/src/types/graph.ts b/src/types/graph.ts index 606c639..19d2080 100644 --- a/src/types/graph.ts +++ b/src/types/graph.ts @@ -1,6 +1,4 @@ import { Node, Edge } from '@xyflow/react'; -import { createContext } from 'react'; -import { CIDR } from '../utils/iputils'; export type AppNode = Node; @@ -34,22 +32,58 @@ export type EdgeDataUpdate = { persistentKeepalive?: number; } -export interface SubnetInfo { - subnet: CIDR; - nodes: Array<{nodeId: string, cidr: CIDR | undefined}>; -} +type GetEdges = () => AppEdge[]; +type GetNodes = () => AppNode[]; -export interface Settings { - listenPort: number; - mtu: number; - subnets: SubnetInfo[]; -} +export class AppGraph { + private readonly _getNextNodeIds = new Map(); -export const initialSettings : Settings = { - listenPort: 38894, - mtu: 1420, - subnets: [] -}; + constructor( + public readonly getEdges: GetEdges, + public readonly getNodes: GetNodes + ) { + const getNextNodeIds = this._getNextNodeIds; + const nodeIds = getNodes().map(node => node.id); -export const SettingsContext = createContext(initialSettings); \ No newline at end of file + for(const edge of getEdges()) { + if(!nodeIds.includes(edge.source) || !nodeIds.includes(edge.target)) { + continue; + } + const sourceChildren = getNextNodeIds.get(edge.source) ?? []; + const targetChildren = getNextNodeIds.get(edge.target) ?? []; + getNextNodeIds.set(edge.source, [...sourceChildren, edge.target]); + getNextNodeIds.set(edge.target, [...targetChildren, edge.source]); + } + } + + + private static checkConnected(graph: AppGraph): boolean { + const nodes = graph.getNodes(); + if(nodes.length === 0) return true; + + const visited = new Set(); + const queue = []; + const first = nodes[0]; + queue.push(first.id); + visited.add(first.id); + + const getNextNodeIds = graph._getNextNodeIds; + while(queue.length > 0) { + const curr = queue.shift()!; + const next = getNextNodeIds.get(curr); + if(!next) continue; + for(const nextId of next) { + if(visited.has(nextId)) continue; + visited.add(nextId); + queue.push(nextId); + } + } + + return visited.size === nodes.length; + } + + getConnectedSubgraph(nodeIds: string[]): AppGraph | undefined { + const node + } +} \ No newline at end of file diff --git a/src/types/settings.ts b/src/types/settings.ts new file mode 100644 index 0000000..19709e6 --- /dev/null +++ b/src/types/settings.ts @@ -0,0 +1,22 @@ +import { createContext } from 'react'; +import { CIDR } from '../utils/iputils'; + +export interface SubnetInfo { + subnet: CIDR; + nodes: Array<{nodeId: string, cidr: CIDR | undefined}>; +} + +export interface Settings { + listenPort: number; + mtu: number; + + subnets: SubnetInfo[]; +} + +export const initialSettings : Settings = { + listenPort: 38894, + mtu: 1420, + subnets: [] +}; + +export const SettingsContext = createContext(initialSettings); diff --git a/src/utils/StringBuilder.ts b/src/utils/StringBuilder.ts new file mode 100644 index 0000000..1e14eae --- /dev/null +++ b/src/utils/StringBuilder.ts @@ -0,0 +1,11 @@ +export default class StringBuilder { + private lines: string[] = []; + + appendLine(value: string = "") { + this.lines.push(value); + } + + toString() { + return this.lines.join('\n'); + } +} \ No newline at end of file