feat(NetworkConfigManager): enhance IP generation logic for /30 networks; support paired local and remote IP generation
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s
This commit is contained in:
@@ -489,26 +489,58 @@ function NetworkConfigManager() {
|
|||||||
}, [config.tunnelInterfaces]);
|
}, [config.tunnelInterfaces]);
|
||||||
|
|
||||||
// === Генерация свободного IP адреса из приватных диапазонов ===
|
// === Генерация свободного IP адреса из приватных диапазонов ===
|
||||||
const generateFreePrivateIp = (excludeIp = null) => {
|
// Генерирует IP в сетях /30 (четные базовые адреса)
|
||||||
|
const generateFreePrivateIp = (excludeIp = null, forRemote = false, pairedLocalIp = null) => {
|
||||||
const usedIps = new Set(getAllUsedIpsSet);
|
const usedIps = new Set(getAllUsedIpsSet);
|
||||||
if (excludeIp) {
|
if (excludeIp) {
|
||||||
usedIps.delete(excludeIp.trim());
|
usedIps.delete(excludeIp.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если подбираем Remote IP и есть Local IP, генерируем парный IP в той же сети /30
|
||||||
|
if (forRemote && pairedLocalIp) {
|
||||||
|
const localIpParts = pairedLocalIp.trim().split('.');
|
||||||
|
if (localIpParts.length === 4) {
|
||||||
|
const baseOctet = parseInt(localIpParts[3]);
|
||||||
|
// Определяем базовый адрес сети /30 (округляем вниз до кратного 4)
|
||||||
|
const networkBase = Math.floor(baseOctet / 4) * 4;
|
||||||
|
const localOffset = baseOctet - networkBase;
|
||||||
|
|
||||||
|
// Если Local IP уже .1, то Remote должен быть .2
|
||||||
|
// Если Local IP .2, то Remote должен быть .1
|
||||||
|
let remoteOffset;
|
||||||
|
if (localOffset === 1) {
|
||||||
|
remoteOffset = 2;
|
||||||
|
} else if (localOffset === 2) {
|
||||||
|
remoteOffset = 1;
|
||||||
|
} else {
|
||||||
|
// Если Local IP не .1 или .2, используем стандартную логику
|
||||||
|
remoteOffset = localOffset === 0 ? 2 : (localOffset === 3 ? 1 : 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteOctet = networkBase + remoteOffset;
|
||||||
|
if (remoteOctet >= 0 && remoteOctet <= 255) {
|
||||||
|
const remoteIp = `${localIpParts[0]}.${localIpParts[1]}.${localIpParts[2]}.${remoteOctet}`;
|
||||||
|
if (!usedIps.has(remoteIp)) {
|
||||||
|
return remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Приватные диапазоны для туннелей (RFC 1918)
|
// Приватные диапазоны для туннелей (RFC 1918)
|
||||||
// Начинаем с 10.10.0.0/16 для удобства
|
// Генерируем пары IP в сетях /30 (базовый адрес кратен 4)
|
||||||
const ranges = [
|
const ranges = [
|
||||||
{ start: [10, 10, 0, 1], end: [10, 10, 255, 254] }, // 10.10.0.0/16
|
{ start: [10, 10, 0, 0], end: [10, 10, 255, 252] }, // 10.10.0.0/16
|
||||||
{ start: [10, 0, 0, 1], end: [10, 255, 255, 254] }, // 10.0.0.0/8 (весь диапазон)
|
{ start: [10, 0, 0, 0], end: [10, 255, 255, 252] }, // 10.0.0.0/8
|
||||||
{ start: [172, 16, 0, 1], end: [172, 31, 255, 254] }, // 172.16.0.0/12
|
{ start: [172, 16, 0, 0], end: [172, 31, 255, 252] }, // 172.16.0.0/12
|
||||||
{ start: [192, 168, 0, 1], end: [192, 168, 255, 254] }, // 192.168.0.0/16
|
{ start: [192, 168, 0, 0], end: [192, 168, 255, 252] }, // 192.168.0.0/16
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const range of ranges) {
|
for (const range of ranges) {
|
||||||
const [a, b, c, d] = range.start;
|
const [a, b, c, d] = range.start;
|
||||||
const [aEnd, bEnd, cEnd, dEnd] = range.end;
|
const [aEnd, bEnd, cEnd, dEnd] = range.end;
|
||||||
|
|
||||||
// Перебираем IP в диапазоне
|
// Перебираем IP в диапазоне с шагом 4 (сети /30)
|
||||||
for (let aVal = a; aVal <= aEnd; aVal++) {
|
for (let aVal = a; aVal <= aEnd; aVal++) {
|
||||||
const bStart = (aVal === a) ? b : 0;
|
const bStart = (aVal === a) ? b : 0;
|
||||||
const bEndVal = (aVal === aEnd) ? bEnd : 255;
|
const bEndVal = (aVal === aEnd) ? bEnd : 255;
|
||||||
@@ -516,12 +548,17 @@ function NetworkConfigManager() {
|
|||||||
const cStart = (aVal === a && bVal === b) ? c : 0;
|
const cStart = (aVal === a && bVal === b) ? c : 0;
|
||||||
const cEndVal = (aVal === aEnd && bVal === bEnd) ? cEnd : 255;
|
const cEndVal = (aVal === aEnd && bVal === bEnd) ? cEnd : 255;
|
||||||
for (let cVal = cStart; cVal <= cEndVal; cVal++) {
|
for (let cVal = cStart; cVal <= cEndVal; cVal++) {
|
||||||
const dStart = (aVal === a && bVal === b && cVal === c) ? d : 1;
|
const dStart = (aVal === a && bVal === b && cVal === c) ? d : 0;
|
||||||
const dEndVal = (aVal === aEnd && bVal === bEnd && cVal === cEnd) ? dEnd : 254;
|
const dEndVal = (aVal === aEnd && bVal === bEnd && cVal === cEnd) ? dEnd : 252;
|
||||||
for (let dVal = dStart; dVal <= dEndVal; dVal++) {
|
// Перебираем только базовые адреса сетей /30 (кратные 4)
|
||||||
const ip = `${aVal}.${bVal}.${cVal}.${dVal}`;
|
for (let dVal = dStart; dVal <= dEndVal; dVal += 4) {
|
||||||
if (!usedIps.has(ip)) {
|
// Local IP = базовый + 1, Remote IP = базовый + 2
|
||||||
return ip;
|
const localIp = `${aVal}.${bVal}.${cVal}.${dVal + 1}`;
|
||||||
|
const remoteIp = `${aVal}.${bVal}.${cVal}.${dVal + 2}`;
|
||||||
|
|
||||||
|
// Проверяем, свободны ли оба IP
|
||||||
|
if (!usedIps.has(localIp) && !usedIps.has(remoteIp)) {
|
||||||
|
return forRemote ? remoteIp : localIp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,13 +566,13 @@ function NetworkConfigManager() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если все IP заняты (маловероятно), возвращаем null
|
// Если все IP заняты, возвращаем null
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// === Подбор IP для интерфейса ===
|
// === Подбор IP для интерфейса ===
|
||||||
const handleSuggestLocalIp = () => {
|
const handleSuggestLocalIp = () => {
|
||||||
const freeIp = generateFreePrivateIp(editingInterface?.localIp);
|
const freeIp = generateFreePrivateIp(editingInterface?.localIp, false, null);
|
||||||
if (freeIp) {
|
if (freeIp) {
|
||||||
setEditingInterface({ ...editingInterface, localIp: freeIp });
|
setEditingInterface({ ...editingInterface, localIp: freeIp });
|
||||||
notify.success(`Подобран Local IP: ${freeIp}`);
|
notify.success(`Подобран Local IP: ${freeIp}`);
|
||||||
@@ -545,7 +582,12 @@ function NetworkConfigManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSuggestRemoteIp = () => {
|
const handleSuggestRemoteIp = () => {
|
||||||
const freeIp = generateFreePrivateIp(editingInterface?.remoteIp);
|
// Если есть Local IP, генерируем парный Remote IP в той же сети /30
|
||||||
|
const freeIp = generateFreePrivateIp(
|
||||||
|
editingInterface?.remoteIp,
|
||||||
|
true,
|
||||||
|
editingInterface?.localIp || null
|
||||||
|
);
|
||||||
if (freeIp) {
|
if (freeIp) {
|
||||||
setEditingInterface({ ...editingInterface, remoteIp: freeIp });
|
setEditingInterface({ ...editingInterface, remoteIp: freeIp });
|
||||||
notify.success(`Подобран Remote IP: ${freeIp}`);
|
notify.success(`Подобран Remote IP: ${freeIp}`);
|
||||||
@@ -2372,16 +2414,15 @@ function NetworkConfigManager() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4">
|
<div className="col-md-4">
|
||||||
<div className="d-flex align-items-end gap-2">
|
<label className="form-label">Local IP</label>
|
||||||
<div className="flex-grow-1">
|
<div className="input-group">
|
||||||
<FormField
|
<input
|
||||||
label="Local IP"
|
type="text"
|
||||||
name="localIp"
|
className="form-control"
|
||||||
value={editingInterface.localIp}
|
value={editingInterface.localIp || ''}
|
||||||
onChange={(val) => setEditingInterface({ ...editingInterface, localIp: val })}
|
onChange={(e) => setEditingInterface({ ...editingInterface, localIp: e.target.value })}
|
||||||
placeholder="10.10.0.1"
|
placeholder="10.10.0.1"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-primary"
|
className="btn btn-outline-primary"
|
||||||
@@ -2407,21 +2448,20 @@ function NetworkConfigManager() {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4">
|
<div className="col-md-4">
|
||||||
<div className="d-flex align-items-end gap-2">
|
<label className="form-label">Remote IP</label>
|
||||||
<div className="flex-grow-1">
|
<div className="input-group">
|
||||||
<FormField
|
<input
|
||||||
label="Remote IP"
|
type="text"
|
||||||
name="remoteIp"
|
className="form-control"
|
||||||
value={editingInterface.remoteIp}
|
value={editingInterface.remoteIp || ''}
|
||||||
onChange={(val) => setEditingInterface({ ...editingInterface, remoteIp: val })}
|
onChange={(e) => setEditingInterface({ ...editingInterface, remoteIp: e.target.value })}
|
||||||
placeholder="10.10.0.2"
|
placeholder="10.10.0.2"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-primary"
|
className="btn btn-outline-primary"
|
||||||
onClick={handleSuggestRemoteIp}
|
onClick={handleSuggestRemoteIp}
|
||||||
title="Подобрать свободный IP из приватного диапазона"
|
title="Подобрать свободный IP из приватного диапазона (в той же сети /30 что и Local IP)"
|
||||||
>
|
>
|
||||||
<IconWand size={16} />
|
<IconWand size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user