实现配置生成

This commit is contained in:
limil 2026-02-13 00:12:09 +08:00
parent a0275e28a8
commit b18e21b620
7 changed files with 141 additions and 54 deletions

View File

@ -4,7 +4,7 @@
- [x] 完成边的编辑窗口
- [x] 完成全局设置编辑窗口
- [x] 完成参数校验以及参数联动逻辑
- [ ] 实现配置生成逻辑,并验证有效
- [x] 实现配置生成逻辑,并验证有效
- [ ] 实现子网路由功能,并验证有效

View File

@ -47,7 +47,7 @@
}
.node-info {
font-size: 10px;
font-size: 8px;
border-bottom: 1px solid #eee;
margin-bottom: 4px;
}

View File

@ -1,10 +1,23 @@
import { ReactNode, useContext } from 'react';
import { Handle, Position, NodeProps, useReactFlow} from '@xyflow/react';
import { AppNode, AppEdge, SettingsContext, NodeData, Settings } from '../types/graph';
import { CIDR, IPNetwork } from '../utils/iputils';
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,
@ -13,12 +26,48 @@ class ConfigResult {
) {}
}
function generateConfig(
settings: Settings,
data: NodeData,
getEdges: () => AppEdge[],
getEdge: (id: string) => AppEdge | undefined,
getNode: (id: string) => AppNode | undefined) : 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 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, getEdges: GetEdges, getEdge: GetEdge, getNode: GetNode) : ConfigResult {
const config = generateInterfaceConfig(settings, data);
const getNearEdges = (node: AppNode) : AppEdge[] => {
return getEdges().filter(edge => edge.source === node.id || edge.target === node.id);
@ -30,13 +79,12 @@ function generateConfig(
};
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);
const result = IPUtils.parse(ip);
if(!result.cidr) {
return new ConfigResult(false, undefined, `无效的禁止访问IP: ${ip} (${result.error})`);
}
@ -44,7 +92,6 @@ function generateConfig(
}
}
// 2. 按边分组
const belongsToEdge : Record<string, string | undefined> = {[node.id]: node.id};
const queue : AppNode[] = [];
@ -81,50 +128,72 @@ function generateConfig(
}
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 groupNodeIds = groupedByEdge[edgeId]!;
const edge = getEdge(edgeId)!;
const nextNode = getNextNode(edge, node)!;
const nextNodeData = nextNode.data;
const targetCIDRs = groupNodeIds.flatMap(nodeId => {
const cidr = nodeIdToCIDR[nodeId];
if(!cidr || disallowCIDRs.some(disallow => disallow.contains(cidr))) {
return [];
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}`);
}
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()));
if(!nextNodeData.listenAddress) {
return new ConfigResult(false, undefined, `节点 ${nextNodeData.label} 未设置监听地址,无法生成配置`);
}
const result = IPNetwork.mergeCIDRs(allCIDRs, targetCIDRs);
const parse = IPUtils.parse(`${nextNodeData.listenAddress}/0`);
if(!parse.cidr) {
return new ConfigResult(false, undefined, `节点 ${nextNodeData.label} 的监听地址无效`);
}
console.table(result?.map(c => c.toString()) || []);
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(', ')}`);
}
}
return new ConfigResult(false);
// console.log(config.toString());
return new ConfigResult(true, config.toString());
}
export default function CustomNode({
@ -164,6 +233,11 @@ export default function CustomNode({
<span className="label">IPv6地址</span>
<span className="value">{data.ipv6Address || "未设置"}</span>
</div>
<div className="info-item">
<span className="label"></span>
<span className="value">{data.listenAddress || "未设置"}</span>
</div>
</div>
<div className="node-actions">

View File

@ -1,7 +1,7 @@
import { useState, ReactNode } from 'react';
import { NodeData, Settings, NodeDataUpdate } from '../types/graph';
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
import { IPNetwork} from '../utils/iputils'
import { IPUtils} from '../utils/iputils'
import './FormEditor.css';
import Folder from './Folder'
@ -25,14 +25,14 @@ function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
}
if(ipv4Address) {
const result = IPNetwork.parse(ipv4Address);
const result = IPUtils.parse(ipv4Address);
if(!result.cidr || result.cidr.version !== 'IPv4') {
errors.push("IPv4地址无效");
}
}
if(ipv6Address) {
const result = IPNetwork.parse(ipv6Address);
const result = IPUtils.parse(ipv6Address);
if(!result.cidr || result.cidr.version !== 'IPv6') {
errors.push("IPv6地址无效");
}
@ -71,6 +71,7 @@ export default function NodeEditor({
const [ipv4Address, setIpv4Address] = useState(node.ipv4Address);
const [ipv6Address, setIpv6Address] = useState(node.ipv6Address);
const [disallowIPs, setDisallowIPs] = useState(node.disallowIPs);
const [listenAddress, setListenAddress] = useState(node.listenAddress);
const [listenPort, setListenPort] = useState(node.listenPort);
const [mtu, setmtu] = useState(node.mtu);
const [dnsServers, setdnsServers] = useState(node.dnsServers)
@ -88,6 +89,7 @@ export default function NodeEditor({
postUp: postUp,
postDown: postDown,
mtu: mtu,
listenAddress: listenAddress,
listenPort: listenPort,
dnsServers: dnsServers,
notes: notes
@ -167,6 +169,16 @@ export default function NodeEditor({
/>
</div>
<div className="form-group">
<label></label>
<input
type="text"
value={listenAddress || ''}
onChange={e => setListenAddress(e.target.value)}
placeholder={`留空代表不监听地址`}
/>
</div>
<Folder title='高级'>
<div className="form-group">
<label></label>

View File

@ -1,7 +1,7 @@
import { useState, ReactNode } from 'react';
import { Settings } from '../types/graph';
import './FormEditor.css';
import {IPNetwork} from '../utils/iputils'
import {IPUtils} from '../utils/iputils'
interface SettingEditorProps {
settings: Settings;

View File

@ -18,6 +18,7 @@ export type NodeDataUpdate = {
postUp?: string;
postDown?: string;
mtu?: number;
listenAddress?: string;
listenPort?: number;
dnsServers?: string;
notes?: string;

View File

@ -39,7 +39,7 @@ export interface CIDRParseResult {
error: string;
}
export class IPNetwork {
export class IPUtils {
static mergeCIDRs(allCIDRs: CIDR[], targetCIDRs: CIDR[]): CIDR[] | undefined {
// 返回符合条件的CIDR集合
// 1. 它们能够覆盖所有targetCIDRs但是不能覆盖到任何在allCIDRs中不属于targetCIDRs的CIDR