更新节点

This commit is contained in:
limil 2026-02-08 16:24:55 +08:00
parent 49d20d3dbd
commit aeda4b5179
6 changed files with 213 additions and 29 deletions

View File

@ -5,6 +5,9 @@
- [ ] 完成全局设置编辑窗口
- [ ] 完成参数校验以及参数联动逻辑
- [ ] 实现配置生成逻辑,并验证有效
- [ ] 实现子网路由功能,并验证有效
- [ ] 实现配置保存和加载功能
- [ ] 实现加密功能(完全加密和只加密私钥)
- [ ] 完成!奖励自己
- [ ] 完成!

View File

@ -207,7 +207,7 @@ function FlowContent(): ReactNode {
{editSettings && (
<SettingsEditor
settings={settings}
onUpdate={settingsUpdate => {}}
onUpdate={settingsUpdate => {setSettings(settingsUpdate)}}
onClose={() => setEditSettings(false)}
/>
)}

View File

@ -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}`}
/>
</div>
)}
@ -123,6 +114,7 @@ export default function NodeEditor({
type="text"
value={ipv6Address || ''}
onChange={e => setIpv6Address(e.target.value)}
placeholder={`当前子网:${settings.ipv6Subnet}`}
/>
</div>
)}
@ -141,7 +133,7 @@ export default function NodeEditor({
<label></label>
<input
type="number"
min="1024"
min="30000"
max="49151"
step="1"
value={listenPort || ''}
@ -157,7 +149,7 @@ export default function NodeEditor({
<label>mtu</label>
<input
type="number"
min="1"
min="1200"
step="1"
value={mtu || ''}
onChange={e => {

View File

@ -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<string[]>([]);
const [listenPort, setListenPort] = useState<number>(settings.listenPort);
const [mtu, setmtu] = useState<number>(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({
<button className="close-btn" onClick={onClose}>×</button>
</div>
{errors.length > 0 && (
<div className="error-box">
<div
className="error-close-btn"
onClick={() => setErrors([])} // 点击清空错误数组
title="关闭提示"
>×</div>
{errors.map((error, idx) => (
<p key={idx} className="error-message"> {error}</p>
))}
</div>
)}
<div className="form-group">
<label></label>
<input
type="number"
min="30000"
max="49151"
step="1"
value={listenPort}
onChange={e => {
const value = e.target.valueAsNumber;
setListenPort(isNaN(value) ? 38894 : value);
}}
/>
</div>
<div className="form-group">
<label>mtu</label>
<input
type="number"
min="1200"
step="1"
value={mtu || ''}
onChange={e => {
const value = e.target.valueAsNumber;
setmtu(isNaN(value) ? 1420 : value);
}}
/>
</div>
<div className="form-group">
<label>ipv4子网</label>
<input
type="text"
value={ipv4Subnet || ''}
onChange={e => setIpv4Subnet(e.target.value)}
placeholder='例如172.29.0.0/16留空代表不使用ipv4'
/>
</div>
<div className="form-group">
<label>ipv6子网</label>
<input
type="text"
value={ipv6Subnet || ''}
onChange={e => setIpv6Subnet(e.target.value)}
placeholder='例如fd23:23:23::/64留空代表不使用ipv6'
/>
</div>
<div className="editor-actions">
<button className="btn-save" onClick={handleSave}></button>
<button className="btn-cancel" onClick={onClose}></button>

View File

@ -31,22 +31,10 @@ export type EdgeDataUpdate = {
persistentKeepalive?: number;
}
export class SubNetRouter {
private _nodes : Record<string, string | undefined> = {};
constructor(
public subnet: string,
public readonly kind: 'ipv4' | 'ipv6'
) {}
}
export interface Settings {
listenPort: number;
mtu: number;
ipv4Subnet?: string;
ipv4SubNetRouter?: SubNetRouter;
ipv6Subnet?: string;
ipv6SubnetRouter?: SubNetRouter;
}

107
src/utils/iputils.ts Normal file
View File

@ -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';
}
}