feat(NetworkConfigManager): implement IP suggestion feature for local and remote IPs; add functionality to generate free private IPs from defined ranges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s

This commit is contained in:
2026-01-22 17:40:14 +07:00
parent 51999d485c
commit 5c2edac25a
+119 -14
View File
@@ -19,6 +19,7 @@ import {
IconCopy,
IconSearch,
IconRefresh,
IconWand,
IconFilter,
IconX,
IconCircleFilled,
@@ -473,6 +474,86 @@ function NetworkConfigManager() {
return conflicts;
};
// === Получение множества всех используемых IP адресов ===
const getAllUsedIpsSet = useMemo(() => {
const usedIps = new Set();
(config.tunnelInterfaces || []).forEach(iface => {
if (iface.localIp && iface.localIp.trim()) {
usedIps.add(iface.localIp.trim());
}
if (iface.remoteIp && iface.remoteIp.trim()) {
usedIps.add(iface.remoteIp.trim());
}
});
return usedIps;
}, [config.tunnelInterfaces]);
// === Генерация свободного IP адреса из приватных диапазонов ===
const generateFreePrivateIp = (excludeIp = null) => {
const usedIps = new Set(getAllUsedIpsSet);
if (excludeIp) {
usedIps.delete(excludeIp.trim());
}
// Приватные диапазоны для туннелей (RFC 1918)
// Начинаем с 10.10.0.0/16 для удобства
const ranges = [
{ start: [10, 10, 0, 1], end: [10, 10, 255, 254] }, // 10.10.0.0/16
{ start: [10, 0, 0, 1], end: [10, 255, 255, 254] }, // 10.0.0.0/8 (весь диапазон)
{ start: [172, 16, 0, 1], end: [172, 31, 255, 254] }, // 172.16.0.0/12
{ start: [192, 168, 0, 1], end: [192, 168, 255, 254] }, // 192.168.0.0/16
];
for (const range of ranges) {
const [a, b, c, d] = range.start;
const [aEnd, bEnd, cEnd, dEnd] = range.end;
// Перебираем IP в диапазоне
for (let aVal = a; aVal <= aEnd; aVal++) {
const bStart = (aVal === a) ? b : 0;
const bEndVal = (aVal === aEnd) ? bEnd : 255;
for (let bVal = bStart; bVal <= bEndVal; bVal++) {
const cStart = (aVal === a && bVal === b) ? c : 0;
const cEndVal = (aVal === aEnd && bVal === bEnd) ? cEnd : 255;
for (let cVal = cStart; cVal <= cEndVal; cVal++) {
const dStart = (aVal === a && bVal === b && cVal === c) ? d : 1;
const dEndVal = (aVal === aEnd && bVal === bEnd && cVal === cEnd) ? dEnd : 254;
for (let dVal = dStart; dVal <= dEndVal; dVal++) {
const ip = `${aVal}.${bVal}.${cVal}.${dVal}`;
if (!usedIps.has(ip)) {
return ip;
}
}
}
}
}
}
// Если все IP заняты (маловероятно), возвращаем null
return null;
};
// === Подбор IP для интерфейса ===
const handleSuggestLocalIp = () => {
const freeIp = generateFreePrivateIp(editingInterface?.localIp);
if (freeIp) {
setEditingInterface({ ...editingInterface, localIp: freeIp });
notify.success(`Подобран Local IP: ${freeIp}`);
} else {
notify.error('Не удалось найти свободный IP адрес');
}
};
const handleSuggestRemoteIp = () => {
const freeIp = generateFreePrivateIp(editingInterface?.remoteIp);
if (freeIp) {
setEditingInterface({ ...editingInterface, remoteIp: freeIp });
notify.success(`Подобран Remote IP: ${freeIp}`);
} else {
notify.error('Не удалось найти свободный IP адрес');
}
};
// === Получение реестра всех используемых IP адресов ===
const ipRegistry = useMemo(() => {
const registry = [];
@@ -2285,13 +2366,25 @@ function NetworkConfigManager() {
/>
</div>
<div className="col-md-4">
<FormField
label="Local IP"
name="localIp"
value={editingInterface.localIp}
onChange={(val) => setEditingInterface({ ...editingInterface, localIp: val })}
placeholder="10.10.0.1"
/>
<div className="d-flex align-items-end gap-2">
<div className="flex-grow-1">
<FormField
label="Local IP"
name="localIp"
value={editingInterface.localIp}
onChange={(val) => setEditingInterface({ ...editingInterface, localIp: val })}
placeholder="10.10.0.1"
/>
</div>
<button
type="button"
className="btn btn-outline-primary"
onClick={handleSuggestLocalIp}
title="Подобрать свободный IP из приватного диапазона"
>
<IconWand size={16} />
</button>
</div>
{editingInterface.localIp && (() => {
const conflicts = checkInterfaceIpConflict(
{ localIp: editingInterface.localIp },
@@ -2308,13 +2401,25 @@ function NetworkConfigManager() {
})()}
</div>
<div className="col-md-4">
<FormField
label="Remote IP"
name="remoteIp"
value={editingInterface.remoteIp}
onChange={(val) => setEditingInterface({ ...editingInterface, remoteIp: val })}
placeholder="10.10.0.2"
/>
<div className="d-flex align-items-end gap-2">
<div className="flex-grow-1">
<FormField
label="Remote IP"
name="remoteIp"
value={editingInterface.remoteIp}
onChange={(val) => setEditingInterface({ ...editingInterface, remoteIp: val })}
placeholder="10.10.0.2"
/>
</div>
<button
type="button"
className="btn btn-outline-primary"
onClick={handleSuggestRemoteIp}
title="Подобрать свободный IP из приватного диапазона"
>
<IconWand size={16} />
</button>
</div>
{editingInterface.remoteIp && (() => {
const conflicts = checkInterfaceIpConflict(
{ remoteIp: editingInterface.remoteIp },