feat(NetworkConfigManager): add support for dual server configurations in network interfaces; enhance MikroTik code generation and UI to display both servers
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
This commit is contained in:
@@ -57,8 +57,8 @@
|
|||||||
"type": "GRE",
|
"type": "GRE",
|
||||||
"localIp": "10.10.0.1",
|
"localIp": "10.10.0.1",
|
||||||
"remoteIp": "10.10.0.2",
|
"remoteIp": "10.10.0.2",
|
||||||
"port": "",
|
"serverId": "SWE-HIPHOST",
|
||||||
"serverId": "SWE-HIPHOST"
|
"serverId2": "DE-FRANKFURT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "if-wg-swe-nl",
|
"id": "if-wg-swe-nl",
|
||||||
@@ -66,7 +66,8 @@
|
|||||||
"type": "WireGuard",
|
"type": "WireGuard",
|
||||||
"localIp": "10.20.0.1",
|
"localIp": "10.20.0.1",
|
||||||
"remoteIp": "10.20.0.2",
|
"remoteIp": "10.20.0.2",
|
||||||
"serverId": "SWE-HIPHOST"
|
"serverId": "SWE-HIPHOST",
|
||||||
|
"serverId2": "NL-AMSTERDAM"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "if-gre-de-us",
|
"id": "if-gre-de-us",
|
||||||
@@ -74,8 +75,8 @@
|
|||||||
"type": "GRE",
|
"type": "GRE",
|
||||||
"localIp": "10.10.1.1",
|
"localIp": "10.10.1.1",
|
||||||
"remoteIp": "10.10.1.2",
|
"remoteIp": "10.10.1.2",
|
||||||
"port": "",
|
"serverId": "DE-FRANKFURT",
|
||||||
"serverId": "DE-FRANKFURT"
|
"serverId2": "US-NEWYORK"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "if-ipsec-nl-jp",
|
"id": "if-ipsec-nl-jp",
|
||||||
@@ -83,7 +84,6 @@
|
|||||||
"type": "IPSec",
|
"type": "IPSec",
|
||||||
"localIp": "10.30.0.1",
|
"localIp": "10.30.0.1",
|
||||||
"remoteIp": "10.30.0.2",
|
"remoteIp": "10.30.0.2",
|
||||||
"port": "",
|
|
||||||
"serverId": "NL-AMSTERDAM"
|
"serverId": "NL-AMSTERDAM"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const getEmptyInterface = () => ({
|
|||||||
localIp: '',
|
localIp: '',
|
||||||
remoteIp: '',
|
remoteIp: '',
|
||||||
serverId: '',
|
serverId: '',
|
||||||
|
serverId2: '', // Второй сервер (опциональный)
|
||||||
});
|
});
|
||||||
|
|
||||||
const getEmptyIpPool = () => ({
|
const getEmptyIpPool = () => ({
|
||||||
@@ -298,9 +299,17 @@ function NetworkConfigManager() {
|
|||||||
const interfacesByServer = useMemo(() => {
|
const interfacesByServer = useMemo(() => {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
filteredInterfaces.forEach(iface => {
|
filteredInterfaces.forEach(iface => {
|
||||||
const key = iface.serverId || '__unassigned__';
|
// Группируем по первому серверу
|
||||||
if (!grouped[key]) grouped[key] = [];
|
const key1 = iface.serverId || '__unassigned__';
|
||||||
grouped[key].push(iface);
|
if (!grouped[key1]) grouped[key1] = [];
|
||||||
|
grouped[key1].push(iface);
|
||||||
|
|
||||||
|
// Если есть второй сервер, также добавляем в его группу
|
||||||
|
if (iface.serverId2) {
|
||||||
|
const key2 = iface.serverId2;
|
||||||
|
if (!grouped[key2]) grouped[key2] = [];
|
||||||
|
grouped[key2].push(iface);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return grouped;
|
return grouped;
|
||||||
}, [filteredInterfaces]);
|
}, [filteredInterfaces]);
|
||||||
@@ -492,6 +501,70 @@ function NetworkConfigManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === Генерация кода MikroTik для настройки IP адресов интерфейсов ===
|
||||||
|
const generateMikrotikInterfaceAddresses = () => {
|
||||||
|
const interfacesWithBothServers = (config.tunnelInterfaces || []).filter(i =>
|
||||||
|
i.serverId && i.serverId2 && i.localIp && i.remoteIp
|
||||||
|
);
|
||||||
|
|
||||||
|
if (interfacesWithBothServers.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = '\n# === Настройка IP адресов интерфейсов на обоих серверах ===\n';
|
||||||
|
code += '# Эти команды нужно выполнить на соответствующих серверах\n\n';
|
||||||
|
|
||||||
|
// Группируем по серверам
|
||||||
|
const interfacesByServer = {};
|
||||||
|
interfacesWithBothServers.forEach(iface => {
|
||||||
|
const server1 = getServerInfo(iface.serverId);
|
||||||
|
const server2 = getServerInfo(iface.serverId2);
|
||||||
|
|
||||||
|
if (!server1 || !server2) return;
|
||||||
|
|
||||||
|
const server1Name = server1.dns || server1.ip || iface.serverId;
|
||||||
|
const server2Name = server2.dns || server2.ip || iface.serverId2;
|
||||||
|
const interfaceName = iface.name || `${iface.type}-tunnel`;
|
||||||
|
|
||||||
|
// Для первого сервера - используем localIp
|
||||||
|
if (!interfacesByServer[server1Name]) {
|
||||||
|
interfacesByServer[server1Name] = [];
|
||||||
|
}
|
||||||
|
interfacesByServer[server1Name].push({
|
||||||
|
interfaceName,
|
||||||
|
ip: iface.localIp,
|
||||||
|
type: iface.type,
|
||||||
|
remoteIp: iface.remoteIp,
|
||||||
|
server2Name
|
||||||
|
});
|
||||||
|
|
||||||
|
// Для второго сервера - используем remoteIp
|
||||||
|
if (!interfacesByServer[server2Name]) {
|
||||||
|
interfacesByServer[server2Name] = [];
|
||||||
|
}
|
||||||
|
interfacesByServer[server2Name].push({
|
||||||
|
interfaceName,
|
||||||
|
ip: iface.remoteIp,
|
||||||
|
type: iface.type,
|
||||||
|
remoteIp: iface.localIp, // Для второго сервера remoteIp - это localIp первого
|
||||||
|
server2Name: server1Name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(interfacesByServer).forEach(([serverName, interfaces]) => {
|
||||||
|
code += `\n# --- Команды для сервера: ${serverName} ---\n`;
|
||||||
|
interfaces.forEach(iface => {
|
||||||
|
code += `# Интерфейс: ${iface.interfaceName} (${iface.type}) - связь с ${iface.server2Name}\n`;
|
||||||
|
code += `/ip address add address=${iface.ip}/32 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
code += '\n# Проверка настроенных адресов:\n';
|
||||||
|
code += '# /ip address print\n';
|
||||||
|
|
||||||
|
return code;
|
||||||
|
};
|
||||||
|
|
||||||
// === Генерация кода MikroTik для рекурсивных маршрутов ===
|
// === Генерация кода MikroTik для рекурсивных маршрутов ===
|
||||||
const generateMikrotikRecursiveRoutes = () => {
|
const generateMikrotikRecursiveRoutes = () => {
|
||||||
const recursiveGateways = (config.gateways || []).filter(gw => gw.type === 'recursive');
|
const recursiveGateways = (config.gateways || []).filter(gw => gw.type === 'recursive');
|
||||||
@@ -575,6 +648,12 @@ function NetworkConfigManager() {
|
|||||||
code += '# /ip route print where comment~"Recursive"\n';
|
code += '# /ip route print where comment~"Recursive"\n';
|
||||||
code += `\n# Всего создано маршрутов: ${validRoutesCount}\n`;
|
code += `\n# Всего создано маршрутов: ${validRoutesCount}\n`;
|
||||||
|
|
||||||
|
// Добавляем код для настройки IP адресов интерфейсов
|
||||||
|
const interfaceAddressesCode = generateMikrotikInterfaceAddresses();
|
||||||
|
if (interfaceAddressesCode) {
|
||||||
|
code += interfaceAddressesCode;
|
||||||
|
}
|
||||||
|
|
||||||
return code;
|
return code;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -813,73 +892,96 @@ function NetworkConfigManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// === Рендер карточки Interface ===
|
// === Рендер карточки Interface ===
|
||||||
const renderInterfaceCard = (iface) => (
|
const renderInterfaceCard = (iface) => {
|
||||||
<div key={iface.id} className="col-sm-6 col-lg-4">
|
const server = getServerInfo(iface.serverId);
|
||||||
<div className="card card-sm">
|
const server2 = getServerInfo(iface.serverId2);
|
||||||
<div className="card-body">
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
return (
|
||||||
<span className={`avatar avatar-sm bg-${getInterfaceTypeColor(iface.type)}-lt me-2`}>
|
<div key={iface.id} className="col-sm-6 col-lg-4">
|
||||||
<IconRouter size={16} className={`text-${getInterfaceTypeColor(iface.type)}`} />
|
<div className="card card-sm">
|
||||||
</span>
|
<div className="card-body">
|
||||||
<div className="flex-fill">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<div className="fw-medium">{iface.name || '—'}</div>
|
<span className={`avatar avatar-sm bg-${getInterfaceTypeColor(iface.type)}-lt me-2`}>
|
||||||
|
<IconRouter size={16} className={`text-${getInterfaceTypeColor(iface.type)}`} />
|
||||||
|
</span>
|
||||||
|
<div className="flex-fill">
|
||||||
|
<div className="fw-medium">{iface.name || '—'}</div>
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
<span className={`badge bg-${getInterfaceTypeColor(iface.type)}-lt text-${getInterfaceTypeColor(iface.type)}`}>
|
<span className={`badge bg-${getInterfaceTypeColor(iface.type)}-lt text-${getInterfaceTypeColor(iface.type)}`}>
|
||||||
{iface.type}
|
{iface.type}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dropdown">
|
<div className="dropdown">
|
||||||
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
|
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
|
||||||
<IconEdit size={16} />
|
<IconEdit size={16} />
|
||||||
</button>
|
|
||||||
<div className="dropdown-menu dropdown-menu-end">
|
|
||||||
<button className="dropdown-item" onClick={() => handleEditInterface(iface)}>
|
|
||||||
<IconEdit size={16} className="me-2" />Редактировать
|
|
||||||
</button>
|
|
||||||
<button className="dropdown-item text-danger" onClick={() => handleDeleteInterface(iface)}>
|
|
||||||
<IconTrash size={16} className="me-2" />Удалить
|
|
||||||
</button>
|
</button>
|
||||||
|
<div className="dropdown-menu dropdown-menu-end">
|
||||||
|
<button className="dropdown-item" onClick={() => handleEditInterface(iface)}>
|
||||||
|
<IconEdit size={16} className="me-2" />Редактировать
|
||||||
|
</button>
|
||||||
|
<button className="dropdown-item text-danger" onClick={() => handleDeleteInterface(iface)}>
|
||||||
|
<IconTrash size={16} className="me-2" />Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row g-2 small">
|
<div className="row g-2 small mb-2">
|
||||||
<div className="col-6">
|
<div className="col-6">
|
||||||
<div className="text-muted">Local IP</div>
|
<div className="text-muted">Local IP</div>
|
||||||
<div className="d-flex align-items-center gap-1">
|
<div className="d-flex align-items-center gap-1">
|
||||||
<code className="text-truncate">{iface.localIp || '—'}</code>
|
<code className="text-truncate">{iface.localIp || '—'}</code>
|
||||||
{iface.localIp && (
|
{iface.localIp && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-ghost-secondary btn-icon p-0"
|
className="btn btn-ghost-secondary btn-icon p-0"
|
||||||
onClick={() => copyToClipboard(iface.localIp)}
|
onClick={() => copyToClipboard(iface.localIp)}
|
||||||
style={{ width: 20, height: 20 }}
|
style={{ width: 20, height: 20 }}
|
||||||
>
|
>
|
||||||
<IconCopy size={12} />
|
<IconCopy size={12} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-6">
|
||||||
|
<div className="text-muted">Remote IP</div>
|
||||||
|
<div className="d-flex align-items-center gap-1">
|
||||||
|
<code className="text-truncate">{iface.remoteIp || '—'}</code>
|
||||||
|
{iface.remoteIp && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost-secondary btn-icon p-0"
|
||||||
|
onClick={() => copyToClipboard(iface.remoteIp)}
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
>
|
||||||
|
<IconCopy size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6">
|
|
||||||
<div className="text-muted">Remote IP</div>
|
{/* Серверы */}
|
||||||
<div className="d-flex align-items-center gap-1">
|
<div className="small">
|
||||||
<code className="text-truncate">{iface.remoteIp || '—'}</code>
|
{server && (
|
||||||
{iface.remoteIp && (
|
<div className="d-flex align-items-center gap-1 mb-1">
|
||||||
<button
|
<IconServer size={12} className="text-muted" />
|
||||||
className="btn btn-ghost-secondary btn-icon p-0"
|
<span className="text-muted">Сервер 1:</span>
|
||||||
onClick={() => copyToClipboard(iface.remoteIp)}
|
<span>{server.dns || server.ip}</span>
|
||||||
style={{ width: 20, height: 20 }}
|
</div>
|
||||||
>
|
)}
|
||||||
<IconCopy size={12} />
|
{server2 && (
|
||||||
</button>
|
<div className="d-flex align-items-center gap-1">
|
||||||
)}
|
<IconServer size={12} className="text-muted" />
|
||||||
</div>
|
<span className="text-muted">Сервер 2:</span>
|
||||||
|
<span>{server2.dns || server2.ip}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="network-config-manager">
|
<div className="network-config-manager">
|
||||||
@@ -1307,13 +1409,14 @@ function NetworkConfigManager() {
|
|||||||
<th>Тип</th>
|
<th>Тип</th>
|
||||||
<th>Local IP</th>
|
<th>Local IP</th>
|
||||||
<th>Remote IP</th>
|
<th>Remote IP</th>
|
||||||
<th>Сервер</th>
|
<th>Серверы</th>
|
||||||
<th className="text-end" style={{ width: 100 }}>Действия</th>
|
<th className="text-end" style={{ width: 100 }}>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredInterfaces.map(iface => {
|
{filteredInterfaces.map(iface => {
|
||||||
const server = getServerInfo(iface.serverId);
|
const server = getServerInfo(iface.serverId);
|
||||||
|
const server2 = getServerInfo(iface.serverId2);
|
||||||
return (
|
return (
|
||||||
<tr key={iface.id}>
|
<tr key={iface.id}>
|
||||||
<td><strong>{iface.name || '—'}</strong></td>
|
<td><strong>{iface.name || '—'}</strong></td>
|
||||||
@@ -1336,18 +1439,39 @@ function NetworkConfigManager() {
|
|||||||
)}
|
)}
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
<td><code>{iface.remoteIp || '—'}</code></td>
|
|
||||||
<td>
|
<td>
|
||||||
{server ? (
|
<code className="d-flex align-items-center gap-1">
|
||||||
<div className="d-flex align-items-center">
|
{iface.remoteIp || '—'}
|
||||||
<IconServer size={14} className="text-muted me-1" />
|
{iface.remoteIp && (
|
||||||
<span>{server.dns || server.ip}</span>
|
<button
|
||||||
</div>
|
className="btn btn-ghost-secondary btn-icon p-0"
|
||||||
) : iface.serverId ? (
|
onClick={() => copyToClipboard(iface.remoteIp)}
|
||||||
<span className="text-muted">{iface.serverId}</span>
|
style={{ width: 20, height: 20 }}
|
||||||
) : (
|
>
|
||||||
<span className="text-muted">—</span>
|
<IconCopy size={12} />
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
{server ? (
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<IconServer size={14} className="text-muted me-1" />
|
||||||
|
<span>{server.dns || server.ip}</span>
|
||||||
|
</div>
|
||||||
|
) : iface.serverId ? (
|
||||||
|
<span className="text-muted">{iface.serverId}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">—</span>
|
||||||
|
)}
|
||||||
|
{server2 && (
|
||||||
|
<div className="d-flex align-items-center mt-1">
|
||||||
|
<IconServer size={14} className="text-muted me-1" />
|
||||||
|
<span className="text-muted small">{server2.dns || server2.ip}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
<div className="btn-list gap-1 mb-0 justify-content-end">
|
<div className="btn-list gap-1 mb-0 justify-content-end">
|
||||||
@@ -1617,7 +1741,7 @@ function NetworkConfigManager() {
|
|||||||
placeholder="10.10.0.1"
|
placeholder="10.10.0.1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-4">
|
||||||
<FormField
|
<FormField
|
||||||
label="Remote IP"
|
label="Remote IP"
|
||||||
name="remoteIp"
|
name="remoteIp"
|
||||||
@@ -1626,6 +1750,52 @@ function NetworkConfigManager() {
|
|||||||
placeholder="10.10.0.2"
|
placeholder="10.10.0.2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label">Второй сервер (опционально)</label>
|
||||||
|
<ServerAutocompleteInput
|
||||||
|
value={editingInterface.serverId2 || ''}
|
||||||
|
onChange={(val) => setEditingInterface({ ...editingInterface, serverId2: val })}
|
||||||
|
servers={servers.filter(s => s.id !== editingInterface.serverId && s.ip !== editingInterface.serverId)}
|
||||||
|
placeholder="Выберите второй сервер (для понимания связи между серверами)..."
|
||||||
|
/>
|
||||||
|
<div className="form-text">
|
||||||
|
Укажите второй сервер, если интерфейс связывает два сервера
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{editingInterface.serverId && (() => {
|
||||||
|
const server = getServerInfo(editingInterface.serverId);
|
||||||
|
return server ? (
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="alert alert-info mb-0">
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<IconServer size={16} className="me-2" />
|
||||||
|
<div>
|
||||||
|
<strong>Сервер 1:</strong> {server.dns || server.ip}
|
||||||
|
{server.provider && <span className="ms-2">• {server.provider}</span>}
|
||||||
|
{server.country && <span className="ms-2">• {countryToFlag(server.country)} {server.country}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
{editingInterface.serverId2 && (() => {
|
||||||
|
const server2 = getServerInfo(editingInterface.serverId2);
|
||||||
|
return server2 ? (
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="alert alert-info mb-0">
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<IconServer size={16} className="me-2" />
|
||||||
|
<div>
|
||||||
|
<strong>Сервер 2:</strong> {server2.dns || server2.ip}
|
||||||
|
{server2.provider && <span className="ms-2">• {server2.provider}</span>}
|
||||||
|
{server2.country && <span className="ms-2">• {countryToFlag(server2.country)} {server2.country}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|||||||
Reference in New Issue
Block a user