重构子网生成逻辑

This commit is contained in:
limil 2026-02-18 07:48:05 +08:00
parent 9933d56250
commit 260dcbb652
6 changed files with 124 additions and 69 deletions

14
package-lock.json generated
View File

@ -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"
}

View File

@ -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<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 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 {
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);

View File

@ -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';

View File

@ -1,6 +1,4 @@
import { Node, Edge } from '@xyflow/react';
import { createContext } from 'react';
import { CIDR } from '../utils/iputils';
export type AppNode = Node<NodeData>;
@ -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<string, string[]>();
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<Settings>(initialSettings);
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<string>();
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
}
}

22
src/types/settings.ts Normal file
View File

@ -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<Settings>(initialSettings);

View File

@ -0,0 +1,11 @@
export default class StringBuilder {
private lines: string[] = [];
appendLine(value: string = "") {
this.lines.push(value);
}
toString() {
return this.lines.join('\n');
}
}