From aeda4b51795727514b2a28cef478ede6bdea963b Mon Sep 17 00:00:00 2001 From: limil Date: Sun, 8 Feb 2026 16:24:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 5 +- src/App.tsx | 2 +- src/components/NodeEditor.tsx | 18 ++--- src/components/SettingsEditor.tsx | 98 ++++++++++++++++++++++++++- src/types/graph.ts | 12 ---- src/utils/iputils.ts | 107 ++++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+), 29 deletions(-) create mode 100644 src/utils/iputils.ts diff --git a/TODO.md b/TODO.md index 533fc2e..5d4e075 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,9 @@ - [ ] 完成全局设置编辑窗口 - [ ] 完成参数校验以及参数联动逻辑 - [ ] 实现配置生成逻辑,并验证有效 + +- [ ] 实现子网路由功能,并验证有效 + - [ ] 实现配置保存和加载功能 - [ ] 实现加密功能(完全加密和只加密私钥) -- [ ] 完成!奖励自己 +- [ ] 完成! diff --git a/src/App.tsx b/src/App.tsx index 1946df6..c68e263 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -207,7 +207,7 @@ function FlowContent(): ReactNode { {editSettings && ( {}} + onUpdate={settingsUpdate => {setSettings(settingsUpdate)}} onClose={() => setEditSettings(false)} /> )} diff --git a/src/components/NodeEditor.tsx b/src/components/NodeEditor.tsx index 06b1284..eb09584 100644 --- a/src/components/NodeEditor.tsx +++ b/src/components/NodeEditor.tsx @@ -4,12 +4,6 @@ import { generateWireGuardPrivateKey } from '../utils/wireguardConfig' import './FormEditor.css'; import Folder from './Folder' - -interface Validation { - isValid: boolean, - errors: string[] -} - interface NodeEditorProps { node: NodeData; settings: Settings; @@ -39,11 +33,7 @@ export default function NodeEditor({ const [notes, setNotes] = useState(node.notes) const handleSave = (): void => { - // const validation = validateNodeConfig(formData); - // if (!validation.isValid) { - // setErrors(validation.errors); - // return; - // } + // todo: 校验 setErrors([]); onUpdate({ label: label, @@ -112,6 +102,7 @@ export default function NodeEditor({ type="text" value={ipv4Address || ''} onChange={e => setIpv4Address(e.target.value)} + placeholder={`当前子网:${settings.ipv4Subnet}`} /> )} @@ -123,6 +114,7 @@ export default function NodeEditor({ type="text" value={ipv6Address || ''} onChange={e => setIpv6Address(e.target.value)} + placeholder={`当前子网:${settings.ipv6Subnet}`} /> )} @@ -141,7 +133,7 @@ export default function NodeEditor({ mtu { diff --git a/src/components/SettingsEditor.tsx b/src/components/SettingsEditor.tsx index b641102..7d570a8 100644 --- a/src/components/SettingsEditor.tsx +++ b/src/components/SettingsEditor.tsx @@ -1,10 +1,11 @@ import { useState, ReactNode } from 'react'; import { Settings } from '../types/graph'; import './FormEditor.css'; +import {IPNetwork} from '../utils/iputils' interface SettingEditorProps { settings: Settings; - onUpdate?: (data: Settings) => void; + onUpdate: (data: Settings) => void; onClose: () => void; } @@ -13,9 +14,39 @@ export default function SettingsEditor({ onUpdate, onClose }: SettingEditorProps): ReactNode { + const [errors, setErrors] = useState([]); + + const [listenPort, setListenPort] = useState(settings.listenPort); + const [mtu, setmtu] = useState(settings.mtu); + const [ipv4Subnet, setIpv4Subnet] = useState(settings.ipv4Subnet) + const [ipv6Subnet, setIpv6Subnet] = useState(settings.ipv6Subnet) const handleSave = (): void => { - // onUpdate(); + const errorInfo : string[] = []; + if(ipv4Subnet) { + const result = IPNetwork.parse(ipv4Subnet) + if(!result.isValid) { + errorInfo.push(result.error ?? "ipv4子网不合法") + } + } + if(ipv6Subnet) { + const result = IPNetwork.parse(ipv6Subnet) + if(!result.isValid) { + errorInfo.push(result.error ?? "ipv6子网不合法") + } + } + if(errorInfo.length > 0) { + setErrors(errorInfo); + return ; + } + + setErrors([]); + onUpdate({ + listenPort: listenPort, + mtu: mtu, + ipv4Subnet: ipv4Subnet, + ipv6Subnet: ipv6Subnet + }); onClose(); }; @@ -27,6 +58,69 @@ export default function SettingsEditor({ + {errors.length > 0 && ( +
+
setErrors([])} // 点击清空错误数组 + title="关闭提示" + >×
+ + {errors.map((error, idx) => ( +

• {error}

+ ))} +
+ )} + +
+ + { + const value = e.target.valueAsNumber; + setListenPort(isNaN(value) ? 38894 : value); + }} + /> +
+ +
+ + { + const value = e.target.valueAsNumber; + setmtu(isNaN(value) ? 1420 : value); + }} + /> +
+ +
+ + setIpv4Subnet(e.target.value)} + placeholder='例如:172.29.0.0/16,留空代表不使用ipv4' + /> +
+ +
+ + setIpv6Subnet(e.target.value)} + placeholder='例如:fd23:23:23::/64,留空代表不使用ipv6' + /> +
+
diff --git a/src/types/graph.ts b/src/types/graph.ts index 9a24d18..d21e068 100644 --- a/src/types/graph.ts +++ b/src/types/graph.ts @@ -31,22 +31,10 @@ export type EdgeDataUpdate = { persistentKeepalive?: number; } -export class SubNetRouter { - private _nodes : Record = {}; - - constructor( - public subnet: string, - public readonly kind: 'ipv4' | 'ipv6' - ) {} -} - export interface Settings { listenPort: number; mtu: number; ipv4Subnet?: string; - ipv4SubNetRouter?: SubNetRouter; - ipv6Subnet?: string; - ipv6SubnetRouter?: SubNetRouter; } \ No newline at end of file diff --git a/src/utils/iputils.ts b/src/utils/iputils.ts new file mode 100644 index 0000000..f20c747 --- /dev/null +++ b/src/utils/iputils.ts @@ -0,0 +1,107 @@ +export type IPVersion = 'IPv4' | 'IPv6' | 'invalid'; + +export interface IPResult { + isValid: boolean; + version: IPVersion; + binary: string; + mask: number; + error?: string; +} + +export class IPNetwork { + /** + * 解析 CIDR,返回结果对象(不抛出异常) + */ + static parse(cidr: string): IPResult { + const parts = cidr.split('/'); + if (parts.length !== 2) { + return this.invalid('格式错误,缺少掩码 (如 /24)'); + } + + const [ip, maskStr] = parts; + const mask = parseInt(maskStr, 10); + const version = this.getVersion(ip); + + // 基础校验 + if (version === 'invalid') return this.invalid('非法的 IP 格式'); + if (isNaN(mask)) return this.invalid('掩码必须是数字'); + + // 掩码范围校验 + if (version === 'IPv4' && (mask < 0 || mask > 32)) return this.invalid('IPv4 掩码范围应为 0-32'); + if (version === 'IPv6' && (mask < 0 || mask > 128)) return this.invalid('IPv6 掩码范围应为 0-128'); + + try { + const binary = version === 'IPv4' ? this.ipv4ToBinary(ip) : this.ipv6ToBinary(ip); + if (!binary) return this.invalid('IP 地址数值非法'); + + return { isValid: true, version, binary, mask }; + } catch (e) { + return this.invalid('解析过程中出错'); + } + } + + private static getVersion(ip: string): IPVersion { + if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return 'IPv4'; + if (ip.includes(':')) return 'IPv6'; // IPv6 逻辑较复杂,在转换函数中进一步精确校验 + return 'invalid'; + } + + private static ipv4ToBinary(ip: string): string | null { + const octets = ip.split('.'); + let binary = ''; + for (const o of octets) { + const n = parseInt(o, 10); + if (n < 0 || n > 255 || isNaN(n)) return null; + binary += n.toString(2).padStart(8, '0'); + } + return binary; + } + + private static ipv6ToBinary(ip: string): string | null { + let fullIP = ip; + try { + if (ip.includes('::')) { + const parts = ip.split('::'); + if (parts.length > 2) return null; // 只能有一个 :: + const left = parts[0].split(':').filter(x => x.length > 0); + const right = parts[1].split(':').filter(x => x.length > 0); + const missing = 8 - (left.length + right.length); + if (missing < 0) return null; + fullIP = [...left, ...Array(missing).fill('0'), ...right].join(':'); + } + const groups = fullIP.split(':'); + if (groups.length !== 8) return null; + + let binary = ''; + for (const hex of groups) { + const n = parseInt(hex, 16); + if (isNaN(n) || n < 0 || n > 0xFFFF) return null; + binary += n.toString(2).padStart(16, '0'); + } + return binary; + } catch { + return null; + } + } + + private static invalid(msg: string): IPResult { + return { isValid: false, version: 'invalid', binary: '', mask: -1, error: msg }; + } + + /** + * 二进制转回 CIDR 字符串 + */ + static fromBinary(binary: string, mask: number, version: IPVersion): string { + if (version === 'IPv4' && binary.length === 32) { + const octets = []; + for (let i = 0; i < 32; i += 8) octets.push(parseInt(binary.slice(i, i + 8), 2)); + return `${octets.join('.')}/${mask}`; + } + if (version === 'IPv6' && binary.length === 128) { + const segments = []; + for (let i = 0; i < 128; i += 16) segments.push(parseInt(binary.slice(i, i + 16), 2).toString(16)); + return `${segments.join(':')}/${mask}`; + } + return 'invalid-input'; + } +} \ No newline at end of file