更新节点
This commit is contained in:
parent
49d20d3dbd
commit
aeda4b5179
5
TODO.md
5
TODO.md
@ -5,6 +5,9 @@
|
|||||||
- [ ] 完成全局设置编辑窗口
|
- [ ] 完成全局设置编辑窗口
|
||||||
- [ ] 完成参数校验以及参数联动逻辑
|
- [ ] 完成参数校验以及参数联动逻辑
|
||||||
- [ ] 实现配置生成逻辑,并验证有效
|
- [ ] 实现配置生成逻辑,并验证有效
|
||||||
|
|
||||||
|
- [ ] 实现子网路由功能,并验证有效
|
||||||
|
|
||||||
- [ ] 实现配置保存和加载功能
|
- [ ] 实现配置保存和加载功能
|
||||||
- [ ] 实现加密功能(完全加密和只加密私钥)
|
- [ ] 实现加密功能(完全加密和只加密私钥)
|
||||||
- [ ] 完成!奖励自己
|
- [ ] 完成!
|
||||||
|
|||||||
@ -207,7 +207,7 @@ function FlowContent(): ReactNode {
|
|||||||
{editSettings && (
|
{editSettings && (
|
||||||
<SettingsEditor
|
<SettingsEditor
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onUpdate={settingsUpdate => {}}
|
onUpdate={settingsUpdate => {setSettings(settingsUpdate)}}
|
||||||
onClose={() => setEditSettings(false)}
|
onClose={() => setEditSettings(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -4,12 +4,6 @@ import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
|
|||||||
import './FormEditor.css';
|
import './FormEditor.css';
|
||||||
import Folder from './Folder'
|
import Folder from './Folder'
|
||||||
|
|
||||||
|
|
||||||
interface Validation {
|
|
||||||
isValid: boolean,
|
|
||||||
errors: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NodeEditorProps {
|
interface NodeEditorProps {
|
||||||
node: NodeData;
|
node: NodeData;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@ -39,11 +33,7 @@ export default function NodeEditor({
|
|||||||
const [notes, setNotes] = useState(node.notes)
|
const [notes, setNotes] = useState(node.notes)
|
||||||
|
|
||||||
const handleSave = (): void => {
|
const handleSave = (): void => {
|
||||||
// const validation = validateNodeConfig(formData);
|
// todo: 校验
|
||||||
// if (!validation.isValid) {
|
|
||||||
// setErrors(validation.errors);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
setErrors([]);
|
setErrors([]);
|
||||||
onUpdate({
|
onUpdate({
|
||||||
label: label,
|
label: label,
|
||||||
@ -112,6 +102,7 @@ export default function NodeEditor({
|
|||||||
type="text"
|
type="text"
|
||||||
value={ipv4Address || ''}
|
value={ipv4Address || ''}
|
||||||
onChange={e => setIpv4Address(e.target.value)}
|
onChange={e => setIpv4Address(e.target.value)}
|
||||||
|
placeholder={`当前子网:${settings.ipv4Subnet}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -123,6 +114,7 @@ export default function NodeEditor({
|
|||||||
type="text"
|
type="text"
|
||||||
value={ipv6Address || ''}
|
value={ipv6Address || ''}
|
||||||
onChange={e => setIpv6Address(e.target.value)}
|
onChange={e => setIpv6Address(e.target.value)}
|
||||||
|
placeholder={`当前子网:${settings.ipv6Subnet}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -141,7 +133,7 @@ export default function NodeEditor({
|
|||||||
<label>侦听端口</label>
|
<label>侦听端口</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1024"
|
min="30000"
|
||||||
max="49151"
|
max="49151"
|
||||||
step="1"
|
step="1"
|
||||||
value={listenPort || ''}
|
value={listenPort || ''}
|
||||||
@ -157,7 +149,7 @@ export default function NodeEditor({
|
|||||||
<label>mtu</label>
|
<label>mtu</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1200"
|
||||||
step="1"
|
step="1"
|
||||||
value={mtu || ''}
|
value={mtu || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
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'
|
||||||
|
|
||||||
interface SettingEditorProps {
|
interface SettingEditorProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
onUpdate?: (data: Settings) => void;
|
onUpdate: (data: Settings) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,9 +14,39 @@ export default function SettingsEditor({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onClose
|
onClose
|
||||||
}: SettingEditorProps): ReactNode {
|
}: 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 => {
|
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();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,6 +58,69 @@ export default function SettingsEditor({
|
|||||||
<button className="close-btn" onClick={onClose}>×</button>
|
<button className="close-btn" onClick={onClose}>×</button>
|
||||||
</div>
|
</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">
|
<div className="editor-actions">
|
||||||
<button className="btn-save" onClick={handleSave}>保存</button>
|
<button className="btn-save" onClick={handleSave}>保存</button>
|
||||||
<button className="btn-cancel" onClick={onClose}>取消</button>
|
<button className="btn-cancel" onClick={onClose}>取消</button>
|
||||||
|
|||||||
@ -31,22 +31,10 @@ export type EdgeDataUpdate = {
|
|||||||
persistentKeepalive?: number;
|
persistentKeepalive?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SubNetRouter {
|
|
||||||
private _nodes : Record<string, string | undefined> = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public subnet: string,
|
|
||||||
public readonly kind: 'ipv4' | 'ipv6'
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
listenPort: number;
|
listenPort: number;
|
||||||
mtu: number;
|
mtu: number;
|
||||||
|
|
||||||
ipv4Subnet?: string;
|
ipv4Subnet?: string;
|
||||||
ipv4SubNetRouter?: SubNetRouter;
|
|
||||||
|
|
||||||
ipv6Subnet?: string;
|
ipv6Subnet?: string;
|
||||||
ipv6SubnetRouter?: SubNetRouter;
|
|
||||||
}
|
}
|
||||||
107
src/utils/iputils.ts
Normal file
107
src/utils/iputils.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user