实现配置生成
This commit is contained in:
parent
a0275e28a8
commit
b18e21b620
2
TODO.md
2
TODO.md
@ -4,7 +4,7 @@
|
|||||||
- [x] 完成边的编辑窗口
|
- [x] 完成边的编辑窗口
|
||||||
- [x] 完成全局设置编辑窗口
|
- [x] 完成全局设置编辑窗口
|
||||||
- [x] 完成参数校验以及参数联动逻辑
|
- [x] 完成参数校验以及参数联动逻辑
|
||||||
- [ ] 实现配置生成逻辑,并验证有效
|
- [x] 实现配置生成逻辑,并验证有效
|
||||||
|
|
||||||
- [ ] 实现子网路由功能,并验证有效
|
- [ ] 实现子网路由功能,并验证有效
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node-info {
|
.node-info {
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,23 @@
|
|||||||
import { ReactNode, useContext } from 'react';
|
import { ReactNode, useContext } from 'react';
|
||||||
import { Handle, Position, NodeProps, useReactFlow} from '@xyflow/react';
|
import { Handle, Position, NodeProps, useReactFlow} from '@xyflow/react';
|
||||||
import { AppNode, AppEdge, SettingsContext, NodeData, Settings } from '../types/graph';
|
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 './CustomNode.css';
|
||||||
import toast from 'react-hot-toast';
|
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 {
|
class ConfigResult {
|
||||||
constructor(
|
constructor(
|
||||||
public success: boolean,
|
public success: boolean,
|
||||||
@ -13,12 +26,48 @@ class ConfigResult {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateConfig(
|
type GetEdge = (id: string) => AppEdge | undefined;
|
||||||
settings: Settings,
|
type GetNode = (id: string) => AppNode | undefined;
|
||||||
data: NodeData,
|
type GetEdges = () => AppEdge[];
|
||||||
getEdges: () => AppEdge[],
|
type GetAddress = (nodeId: string) => string | undefined;
|
||||||
getEdge: (id: string) => AppEdge | undefined,
|
|
||||||
getNode: (id: string) => AppNode | undefined) : ConfigResult {
|
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[] => {
|
const getNearEdges = (node: AppNode) : AppEdge[] => {
|
||||||
return getEdges().filter(edge => edge.source === node.id || edge.target === node.id);
|
return getEdges().filter(edge => edge.source === node.id || edge.target === node.id);
|
||||||
@ -30,13 +79,12 @@ function generateConfig(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const node = getNode(data.id)!;
|
const node = getNode(data.id)!;
|
||||||
|
|
||||||
// 1. 预处理disallowIPs
|
|
||||||
const disallowCIDRs : CIDR[] = [];
|
const disallowCIDRs : CIDR[] = [];
|
||||||
if(data.disallowIPs) {
|
if(data.disallowIPs) {
|
||||||
const disallowList = data.disallowIPs.split(',').map(ip => ip.trim()).filter(ip => ip.length > 0);
|
const disallowList = data.disallowIPs.split(',').map(ip => ip.trim()).filter(ip => ip.length > 0);
|
||||||
for(const ip of disallowList) {
|
for(const ip of disallowList) {
|
||||||
const result = IPNetwork.parse(ip);
|
const result = IPUtils.parse(ip);
|
||||||
if(!result.cidr) {
|
if(!result.cidr) {
|
||||||
return new ConfigResult(false, undefined, `无效的禁止访问IP: ${ip} (${result.error})`);
|
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 belongsToEdge : Record<string, string | undefined> = {[node.id]: node.id};
|
||||||
|
|
||||||
const queue : AppNode[] = [];
|
const queue : AppNode[] = [];
|
||||||
@ -81,50 +128,72 @@ function generateConfig(
|
|||||||
}
|
}
|
||||||
groupedByEdge[edgeId].push(nodeId);
|
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) {
|
for(const edgeId in groupedByEdge) {
|
||||||
const groupNodeIds = groupedByEdge[edgeId];
|
const groupNodeIds = groupedByEdge[edgeId]!;
|
||||||
if(!groupNodeIds) continue;
|
const edge = getEdge(edgeId)!;
|
||||||
|
const nextNode = getNextNode(edge, node)!;
|
||||||
|
const nextNodeData = nextNode.data;
|
||||||
|
|
||||||
const targetCIDRs = groupNodeIds.flatMap(nodeId => {
|
const publicKey = tryDerivePublicKey(nextNodeData.privateKey);
|
||||||
const cidr = nodeIdToCIDR[nodeId];
|
if(!publicKey) return new ConfigResult(false, undefined, "无法从私钥派生公钥");
|
||||||
if(!cidr || disallowCIDRs.some(disallow => disallow.contains(cidr))) {
|
|
||||||
return [];
|
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)!;
|
if(!nextNodeData.listenAddress) {
|
||||||
console.log(`${node.data.label} -> ${nextNode.data.label}:`);
|
return new ConfigResult(false, undefined, `节点 ${nextNodeData.label} 未设置监听地址,无法生成配置`);
|
||||||
console.table(allCIDRs.map(c => c.toString()));
|
}
|
||||||
console.table(targetCIDRs.map(c => c.toString()));
|
|
||||||
|
|
||||||
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({
|
export default function CustomNode({
|
||||||
@ -164,6 +233,11 @@ export default function CustomNode({
|
|||||||
<span className="label">IPv6地址:</span>
|
<span className="label">IPv6地址:</span>
|
||||||
<span className="value">{data.ipv6Address || "未设置"}</span>
|
<span className="value">{data.ipv6Address || "未设置"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="info-item">
|
||||||
|
<span className="label">监听地址:</span>
|
||||||
|
<span className="value">{data.listenAddress || "未设置"}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="node-actions">
|
<div className="node-actions">
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, ReactNode } from 'react';
|
import { useState, ReactNode } from 'react';
|
||||||
import { NodeData, Settings, NodeDataUpdate } from '../types/graph';
|
import { NodeData, Settings, NodeDataUpdate } from '../types/graph';
|
||||||
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
|
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
|
||||||
import { IPNetwork} from '../utils/iputils'
|
import { IPUtils} from '../utils/iputils'
|
||||||
import './FormEditor.css';
|
import './FormEditor.css';
|
||||||
import Folder from './Folder'
|
import Folder from './Folder'
|
||||||
|
|
||||||
@ -25,14 +25,14 @@ function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(ipv4Address) {
|
if(ipv4Address) {
|
||||||
const result = IPNetwork.parse(ipv4Address);
|
const result = IPUtils.parse(ipv4Address);
|
||||||
if(!result.cidr || result.cidr.version !== 'IPv4') {
|
if(!result.cidr || result.cidr.version !== 'IPv4') {
|
||||||
errors.push("IPv4地址无效");
|
errors.push("IPv4地址无效");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ipv6Address) {
|
if(ipv6Address) {
|
||||||
const result = IPNetwork.parse(ipv6Address);
|
const result = IPUtils.parse(ipv6Address);
|
||||||
if(!result.cidr || result.cidr.version !== 'IPv6') {
|
if(!result.cidr || result.cidr.version !== 'IPv6') {
|
||||||
errors.push("IPv6地址无效");
|
errors.push("IPv6地址无效");
|
||||||
}
|
}
|
||||||
@ -71,6 +71,7 @@ export default function NodeEditor({
|
|||||||
const [ipv4Address, setIpv4Address] = useState(node.ipv4Address);
|
const [ipv4Address, setIpv4Address] = useState(node.ipv4Address);
|
||||||
const [ipv6Address, setIpv6Address] = useState(node.ipv6Address);
|
const [ipv6Address, setIpv6Address] = useState(node.ipv6Address);
|
||||||
const [disallowIPs, setDisallowIPs] = useState(node.disallowIPs);
|
const [disallowIPs, setDisallowIPs] = useState(node.disallowIPs);
|
||||||
|
const [listenAddress, setListenAddress] = useState(node.listenAddress);
|
||||||
const [listenPort, setListenPort] = useState(node.listenPort);
|
const [listenPort, setListenPort] = useState(node.listenPort);
|
||||||
const [mtu, setmtu] = useState(node.mtu);
|
const [mtu, setmtu] = useState(node.mtu);
|
||||||
const [dnsServers, setdnsServers] = useState(node.dnsServers)
|
const [dnsServers, setdnsServers] = useState(node.dnsServers)
|
||||||
@ -88,6 +89,7 @@ export default function NodeEditor({
|
|||||||
postUp: postUp,
|
postUp: postUp,
|
||||||
postDown: postDown,
|
postDown: postDown,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
|
listenAddress: listenAddress,
|
||||||
listenPort: listenPort,
|
listenPort: listenPort,
|
||||||
dnsServers: dnsServers,
|
dnsServers: dnsServers,
|
||||||
notes: notes
|
notes: notes
|
||||||
@ -167,6 +169,16 @@ export default function NodeEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>监听地址</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={listenAddress || ''}
|
||||||
|
onChange={e => setListenAddress(e.target.value)}
|
||||||
|
placeholder={`留空代表不监听地址`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Folder title='高级'>
|
<Folder title='高级'>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>子网黑名单</label>
|
<label>子网黑名单</label>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, ReactNode } from 'react';
|
import { useState, ReactNode } from 'react';
|
||||||
import { Settings } from '../types/graph';
|
import { Settings } from '../types/graph';
|
||||||
import './FormEditor.css';
|
import './FormEditor.css';
|
||||||
import {IPNetwork} from '../utils/iputils'
|
import {IPUtils} from '../utils/iputils'
|
||||||
|
|
||||||
interface SettingEditorProps {
|
interface SettingEditorProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export type NodeDataUpdate = {
|
|||||||
postUp?: string;
|
postUp?: string;
|
||||||
postDown?: string;
|
postDown?: string;
|
||||||
mtu?: number;
|
mtu?: number;
|
||||||
|
listenAddress?: string;
|
||||||
listenPort?: number;
|
listenPort?: number;
|
||||||
dnsServers?: string;
|
dnsServers?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export interface CIDRParseResult {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IPNetwork {
|
export class IPUtils {
|
||||||
static mergeCIDRs(allCIDRs: CIDR[], targetCIDRs: CIDR[]): CIDR[] | undefined {
|
static mergeCIDRs(allCIDRs: CIDR[], targetCIDRs: CIDR[]): CIDR[] | undefined {
|
||||||
// 返回符合条件的CIDR集合:
|
// 返回符合条件的CIDR集合:
|
||||||
// 1. 它们能够覆盖所有targetCIDRs,但是不能覆盖到任何在allCIDRs中不属于targetCIDRs的CIDR
|
// 1. 它们能够覆盖所有targetCIDRs,但是不能覆盖到任何在allCIDRs中不属于targetCIDRs的CIDR
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user