实现配置生成
This commit is contained in:
parent
a0275e28a8
commit
b18e21b620
2
TODO.md
2
TODO.md
@ -4,7 +4,7 @@
|
||||
- [x] 完成边的编辑窗口
|
||||
- [x] 完成全局设置编辑窗口
|
||||
- [x] 完成参数校验以及参数联动逻辑
|
||||
- [ ] 实现配置生成逻辑,并验证有效
|
||||
- [x] 实现配置生成逻辑,并验证有效
|
||||
|
||||
- [ ] 实现子网路由功能,并验证有效
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
}
|
||||
|
||||
.node-info {
|
||||
font-size: 10px;
|
||||
font-size: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -18,6 +18,7 @@ export type NodeDataUpdate = {
|
||||
postUp?: string;
|
||||
postDown?: string;
|
||||
mtu?: number;
|
||||
listenAddress?: string;
|
||||
listenPort?: number;
|
||||
dnsServers?: string;
|
||||
notes?: string;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user