diff --git a/TODO.md b/TODO.md index a5515e4..f657286 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,7 @@ - [x] 完成边的编辑窗口 - [x] 完成全局设置编辑窗口 - [x] 完成参数校验以及参数联动逻辑 -- [ ] 实现配置生成逻辑,并验证有效 +- [x] 实现配置生成逻辑,并验证有效 - [ ] 实现子网路由功能,并验证有效 diff --git a/src/components/CustomNode.css b/src/components/CustomNode.css index a04f184..7d7e674 100644 --- a/src/components/CustomNode.css +++ b/src/components/CustomNode.css @@ -47,7 +47,7 @@ } .node-info { - font-size: 10px; + font-size: 8px; border-bottom: 1px solid #eee; margin-bottom: 4px; } diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index 5e72e89..404d411 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -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 { + 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 { + 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 = {[node.id]: node.id}; const queue : AppNode[] = []; @@ -81,50 +128,72 @@ function generateConfig( } groupedByEdge[edgeId].push(nodeId); } - - // 3. 生成节点到cidr的映射(ipv4) - const nodeIdToCIDR : Record = {}; - 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[] = []; + + 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({ IPv6地址: {data.ipv6Address || "未设置"} + +
+ 监听地址: + {data.listenAddress || "未设置"} +
diff --git a/src/components/NodeEditor.tsx b/src/components/NodeEditor.tsx index 61909bd..5ce6237 100644 --- a/src/components/NodeEditor.tsx +++ b/src/components/NodeEditor.tsx @@ -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({ />
+
+ + setListenAddress(e.target.value)} + placeholder={`留空代表不监听地址`} + /> +
+
diff --git a/src/components/SettingsEditor.tsx b/src/components/SettingsEditor.tsx index 009ccb4..790de78 100644 --- a/src/components/SettingsEditor.tsx +++ b/src/components/SettingsEditor.tsx @@ -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; diff --git a/src/types/graph.ts b/src/types/graph.ts index 394225a..6ada0f5 100644 --- a/src/types/graph.ts +++ b/src/types/graph.ts @@ -18,6 +18,7 @@ export type NodeDataUpdate = { postUp?: string; postDown?: string; mtu?: number; + listenAddress?: string; listenPort?: number; dnsServers?: string; notes?: string; diff --git a/src/utils/iputils.ts b/src/utils/iputils.ts index f643851..370ee7b 100644 --- a/src/utils/iputils.ts +++ b/src/utils/iputils.ts @@ -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