feat(NetworkConfigManager, FilterManager): update gateway configuration to use IP as name; enhance filtering logic and UI to accommodate changes in gateway structure
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m44s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m44s
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
"gateways": [
|
"gateways": [
|
||||||
{
|
{
|
||||||
"id": "gw-cloudflare-swe",
|
"id": "gw-cloudflare-swe",
|
||||||
"name": "SWE-IHOR",
|
|
||||||
"ip": "94.142.140.1",
|
"ip": "94.142.140.1",
|
||||||
"description": "Cloudflare Sweden - основной выход",
|
"description": "Cloudflare Sweden - основной выход",
|
||||||
"serverId": "SWE-HIPHOST",
|
"serverId": "SWE-HIPHOST",
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-bunny-swe",
|
"id": "gw-bunny-swe",
|
||||||
"name": "SWE-BUNNY",
|
|
||||||
"ip": "94.142.140.2",
|
"ip": "94.142.140.2",
|
||||||
"description": "Bunny CDN Sweden",
|
"description": "Bunny CDN Sweden",
|
||||||
"serverId": "SWE-HIPHOST",
|
"serverId": "SWE-HIPHOST",
|
||||||
@@ -20,7 +18,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-telegram-nl",
|
"id": "gw-telegram-nl",
|
||||||
"name": "TG-PROXY-1",
|
|
||||||
"ip": "149.154.167.50",
|
"ip": "149.154.167.50",
|
||||||
"description": "Telegram DC2",
|
"description": "Telegram DC2",
|
||||||
"serverId": "NL-AMSTERDAM",
|
"serverId": "NL-AMSTERDAM",
|
||||||
@@ -29,7 +26,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-hetzner-de",
|
"id": "gw-hetzner-de",
|
||||||
"name": "DE-HETZNER",
|
|
||||||
"ip": "195.100.30.21",
|
"ip": "195.100.30.21",
|
||||||
"description": "Hetzner Frankfurt",
|
"description": "Hetzner Frankfurt",
|
||||||
"serverId": "DE-FRANKFURT",
|
"serverId": "DE-FRANKFURT",
|
||||||
@@ -38,7 +34,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-fastly-us",
|
"id": "gw-fastly-us",
|
||||||
"name": "US-FASTLY",
|
|
||||||
"ip": "151.101.1.1",
|
"ip": "151.101.1.1",
|
||||||
"description": "Fastly US East",
|
"description": "Fastly US East",
|
||||||
"serverId": "US-NEWYORK",
|
"serverId": "US-NEWYORK",
|
||||||
@@ -47,9 +42,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-recursive-swe",
|
"id": "gw-recursive-swe",
|
||||||
"name": "SWE-RECURSIVE",
|
|
||||||
"ip": "10.0.0.1",
|
"ip": "10.0.0.1",
|
||||||
"description": "Рекурсивный gateway через SWE-IHOR",
|
"description": "Рекурсивный gateway через 94.142.140.1",
|
||||||
"serverId": "SWE-HIPHOST",
|
"serverId": "SWE-HIPHOST",
|
||||||
"country": "SWE",
|
"country": "SWE",
|
||||||
"type": "recursive",
|
"type": "recursive",
|
||||||
@@ -72,7 +66,6 @@
|
|||||||
"type": "WireGuard",
|
"type": "WireGuard",
|
||||||
"localIp": "10.20.0.1",
|
"localIp": "10.20.0.1",
|
||||||
"remoteIp": "10.20.0.2",
|
"remoteIp": "10.20.0.2",
|
||||||
"port": "51820",
|
|
||||||
"serverId": "SWE-HIPHOST"
|
"serverId": "SWE-HIPHOST"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -789,9 +789,10 @@ function FilterManager() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
networkGateways.forEach(gw => {
|
networkGateways.forEach(gw => {
|
||||||
|
if (!gw.ip) return; // Пропускаем gateway без IP
|
||||||
gws.push({
|
gws.push({
|
||||||
name: gw.name,
|
name: gw.ip, // Используем IP как имя для совместимости со старой логикой
|
||||||
ip: gw.ip || '',
|
ip: gw.ip,
|
||||||
comment: gw.description || '',
|
comment: gw.description || '',
|
||||||
serverDns: serverDns,
|
serverDns: serverDns,
|
||||||
country: country,
|
country: country,
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ const GATEWAY_TYPES = [
|
|||||||
// Пустые объекты
|
// Пустые объекты
|
||||||
const getEmptyGateway = () => ({
|
const getEmptyGateway = () => ({
|
||||||
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
|
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
|
||||||
name: '',
|
|
||||||
ip: '',
|
ip: '',
|
||||||
description: '',
|
description: '',
|
||||||
serverId: '',
|
serverId: '',
|
||||||
@@ -76,7 +75,6 @@ const getEmptyInterface = () => ({
|
|||||||
type: 'GRE',
|
type: 'GRE',
|
||||||
localIp: '',
|
localIp: '',
|
||||||
remoteIp: '',
|
remoteIp: '',
|
||||||
port: '',
|
|
||||||
serverId: '',
|
serverId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +230,6 @@ function NetworkConfigManager() {
|
|||||||
result = result.filter(g => {
|
result = result.filter(g => {
|
||||||
const server = getServerInfo(g.serverId);
|
const server = getServerInfo(g.serverId);
|
||||||
return (
|
return (
|
||||||
g.name?.toLowerCase().includes(term) ||
|
|
||||||
g.ip?.toLowerCase().includes(term) ||
|
g.ip?.toLowerCase().includes(term) ||
|
||||||
g.country?.toLowerCase().includes(term) ||
|
g.country?.toLowerCase().includes(term) ||
|
||||||
server?.country?.toLowerCase().includes(term) ||
|
server?.country?.toLowerCase().includes(term) ||
|
||||||
@@ -501,37 +498,48 @@ function NetworkConfigManager() {
|
|||||||
code += `\n# === Рекурсивные маршруты для сервера: ${serverName} ===\n`;
|
code += `\n# === Рекурсивные маршруты для сервера: ${serverName} ===\n`;
|
||||||
|
|
||||||
gateways.forEach(gw => {
|
gateways.forEach(gw => {
|
||||||
const parentGateway = getParentGateway(gw.parentGatewayId);
|
const parent = getParentGateway(gw.parentGatewayId);
|
||||||
|
|
||||||
if (!parentGateway) {
|
if (!parent) {
|
||||||
code += `\n# Gateway "${gw.name}" - родительский gateway не найден, пропущен\n`;
|
code += `\n# Gateway "${gw.ip || 'без IP'}" - родительский gateway/интерфейс не найден, пропущен\n`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gw.ip) {
|
if (!gw.ip) {
|
||||||
code += `\n# Gateway "${gw.name}" - IP адрес не указан, пропущен\n`;
|
code += `\n# Gateway без IP адреса - пропущен\n`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentGateway.ip) {
|
// Определяем IP родителя в зависимости от типа
|
||||||
code += `\n# Gateway "${gw.name}" - родительский gateway "${parentGateway.name}" не имеет IP, пропущен\n`;
|
const parentIp = parent.type === 'interface' ? parent.remoteIp : parent.ip;
|
||||||
|
|
||||||
|
if (!parentIp) {
|
||||||
|
const parentType = parent.type === 'interface' ? 'интерфейс' : 'gateway';
|
||||||
|
code += `\n# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
validRoutesCount++;
|
validRoutesCount++;
|
||||||
|
|
||||||
// Создаём маршрут для рекурсивного gateway
|
// Создаём маршрут для рекурсивного gateway
|
||||||
// Маршрут к IP рекурсивного gateway направляется через родительский gateway
|
const parentLabel = parent.type === 'interface'
|
||||||
code += `\n# Рекурсивный gateway: ${gw.name} (IP: ${gw.ip})\n`;
|
? `интерфейс ${parent.name || parent.type} (${parent.remoteIp})`
|
||||||
code += `# Родительский gateway: ${parentGateway.name} (IP: ${parentGateway.ip})\n`;
|
: `gateway ${parent.ip}`;
|
||||||
|
|
||||||
|
code += `\n# Рекурсивный gateway: ${gw.ip}\n`;
|
||||||
|
code += `# Родительский: ${parentLabel}\n`;
|
||||||
|
|
||||||
// Создаём маршрут к IP рекурсивного gateway через родительский
|
// Создаём маршрут к IP рекурсивного gateway через родительский
|
||||||
code += `/ip route add dst-address=${gw.ip}/32 gateway=${parentGateway.ip} comment="Recursive: ${gw.name} -> ${parentGateway.name}"\n`;
|
const comment = gw.description
|
||||||
|
? `${gw.description} -> ${parentIp}`
|
||||||
|
: `Recursive: ${gw.ip} -> ${parentIp}`;
|
||||||
|
code += `/ip route add dst-address=${gw.ip}/32 gateway=${parentIp} comment="${comment}"\n`;
|
||||||
|
|
||||||
// Если у рекурсивного gateway есть описание с указанием на default route
|
// Если у рекурсивного gateway есть описание с указанием на default route
|
||||||
if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) {
|
if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) {
|
||||||
code += `# Дополнительный default route через рекурсивный gateway\n`;
|
code += `# Дополнительный default route через рекурсивный gateway\n`;
|
||||||
code += `/ip route add dst-address=0.0.0.0/0 gateway=${parentGateway.ip} comment="Recursive default: ${gw.name} -> ${parentGateway.name}"\n`;
|
const defaultComment = gw.description ? `${gw.description} (default)` : `Recursive default: ${gw.ip}`;
|
||||||
|
code += `/ip route add dst-address=0.0.0.0/0 gateway=${parentIp} comment="${defaultComment}"\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -650,10 +658,19 @@ function NetworkConfigManager() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// === Получение родительского gateway ===
|
// === Получение родительского gateway или интерфейса ===
|
||||||
const getParentGateway = (gatewayId) => {
|
const getParentGateway = (parentId) => {
|
||||||
if (!gatewayId) return null;
|
if (!parentId) return null;
|
||||||
return config.gateways?.find(g => g.id === gatewayId);
|
|
||||||
|
// Сначала ищем в gateway
|
||||||
|
const gateway = config.gateways?.find(g => g.id === parentId);
|
||||||
|
if (gateway) return { type: 'gateway', ...gateway };
|
||||||
|
|
||||||
|
// Если не найден, ищем в интерфейсах
|
||||||
|
const iface = config.tunnelInterfaces?.find(i => i.id === parentId);
|
||||||
|
if (iface) return { type: 'interface', ...iface };
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// === Рендер карточки Gateway ===
|
// === Рендер карточки Gateway ===
|
||||||
@@ -678,7 +695,7 @@ function NetworkConfigManager() {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex-fill">
|
<div className="flex-fill">
|
||||||
<div className="d-flex align-items-center gap-1">
|
<div className="d-flex align-items-center gap-1">
|
||||||
<span className="fw-medium">{gateway.name || '—'}</span>
|
<code className="fw-medium">{gateway.ip || '—'}</code>
|
||||||
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`} style={{ fontSize: '0.65rem' }}>
|
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`} style={{ fontSize: '0.65rem' }}>
|
||||||
{gatewayType.label}
|
{gatewayType.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -730,7 +747,12 @@ function NetworkConfigManager() {
|
|||||||
{parentGateway && (
|
{parentGateway && (
|
||||||
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
|
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
|
||||||
<span className="text-muted">→</span>
|
<span className="text-muted">→</span>
|
||||||
<span>Родитель: {parentGateway.name}</span>
|
<span>
|
||||||
|
Родитель: {parentGateway.type === 'interface'
|
||||||
|
? `Интерфейс ${parentGateway.name || parentGateway.type} (${parentGateway.remoteIp})`
|
||||||
|
: `Gateway ${parentGateway.ip}`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -756,12 +778,11 @@ function NetworkConfigManager() {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex-fill">
|
<div className="flex-fill">
|
||||||
<div className="fw-medium">{iface.name || '—'}</div>
|
<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>
|
||||||
{iface.port && <span className="ms-1">:{iface.port}</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">
|
||||||
@@ -1103,7 +1124,7 @@ function NetworkConfigManager() {
|
|||||||
<span className={`avatar avatar-xs bg-${gatewayType.color}-lt me-2`}>
|
<span className={`avatar avatar-xs bg-${gatewayType.color}-lt me-2`}>
|
||||||
<IconCircleFilled size={8} className={`text-${gatewayType.color}`} />
|
<IconCircleFilled size={8} className={`text-${gatewayType.color}`} />
|
||||||
</span>
|
</span>
|
||||||
<strong>{gateway.name || '—'}</strong>
|
<code className="fw-medium">{gateway.ip || '—'}</code>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -1112,7 +1133,10 @@ function NetworkConfigManager() {
|
|||||||
</span>
|
</span>
|
||||||
{parentGateway && (
|
{parentGateway && (
|
||||||
<div className="text-muted small mt-1">
|
<div className="text-muted small mt-1">
|
||||||
→ {parentGateway.name}
|
→ {parentGateway.type === 'interface'
|
||||||
|
? `Интерфейс ${parentGateway.name || parentGateway.type} (${parentGateway.remoteIp})`
|
||||||
|
: `Gateway ${parentGateway.ip}`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@@ -1234,7 +1258,6 @@ 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>Сервер</th>
|
||||||
<th className="text-end" style={{ width: 100 }}>Действия</th>
|
<th className="text-end" style={{ width: 100 }}>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1265,7 +1288,6 @@ function NetworkConfigManager() {
|
|||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
<td><code>{iface.remoteIp || '—'}</code></td>
|
<td><code>{iface.remoteIp || '—'}</code></td>
|
||||||
<td>{iface.port || '—'}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{server ? (
|
{server ? (
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
@@ -1420,7 +1442,13 @@ function NetworkConfigManager() {
|
|||||||
onChange={(val) => setEditingGateway({ ...editingGateway, ip: val })}
|
onChange={(val) => setEditingGateway({ ...editingGateway, ip: val })}
|
||||||
placeholder="94.142.140.1"
|
placeholder="94.142.140.1"
|
||||||
required
|
required
|
||||||
|
disabled={gatewayModalMode === 'edit'}
|
||||||
/>
|
/>
|
||||||
|
{gatewayModalMode === 'edit' && (
|
||||||
|
<div className="form-text text-muted">
|
||||||
|
IP адрес нельзя изменить после создания
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -1454,23 +1482,41 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
{editingGateway.type === 'recursive' && (
|
{editingGateway.type === 'recursive' && (
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<label className="form-label">Родительский gateway</label>
|
<label className="form-label">Родительский gateway или интерфейс</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={editingGateway.parentGatewayId || ''}
|
value={editingGateway.parentGatewayId || ''}
|
||||||
onChange={(e) => setEditingGateway({ ...editingGateway, parentGatewayId: e.target.value })}
|
onChange={(e) => setEditingGateway({ ...editingGateway, parentGatewayId: e.target.value })}
|
||||||
>
|
>
|
||||||
<option value="">Выберите родительский gateway</option>
|
<option value="">Выберите родительский gateway или интерфейс</option>
|
||||||
{config.gateways
|
|
||||||
.filter(g => g.id !== editingGateway.id && g.type === 'direct')
|
{/* Прямые gateway */}
|
||||||
.map(g => (
|
<optgroup label="Прямые gateway">
|
||||||
<option key={g.id} value={g.id}>
|
{config.gateways
|
||||||
{g.ip || g.name} {g.description ? `(${g.description})` : ''}
|
.filter(g => g.id !== editingGateway.id && g.type === 'direct' && g.ip)
|
||||||
</option>
|
.map(g => (
|
||||||
))}
|
<option key={g.id} value={g.id}>
|
||||||
|
Gateway: {g.ip} {g.description ? `(${g.description})` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
{/* Интерфейсы */}
|
||||||
|
<optgroup label="Интерфейсы (по remote IP)">
|
||||||
|
{config.tunnelInterfaces
|
||||||
|
.filter(i => i.remoteIp)
|
||||||
|
.map(i => {
|
||||||
|
const server = getServerInfo(i.serverId);
|
||||||
|
return (
|
||||||
|
<option key={i.id} value={i.id}>
|
||||||
|
Интерфейс: {i.remoteIp} ({i.name || i.type}) {server ? `- ${server.dns || server.ip}` : ''}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
<div className="form-text">
|
<div className="form-text">
|
||||||
Рекурсивный gateway ссылается на другой gateway (обычно прямой)
|
Рекурсивный gateway может ссылаться на прямой gateway или на интерфейс (по его remote IP)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1546,7 +1592,7 @@ function NetworkConfigManager() {
|
|||||||
placeholder="10.10.0.1"
|
placeholder="10.10.0.1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4">
|
<div className="col-md-6">
|
||||||
<FormField
|
<FormField
|
||||||
label="Remote IP"
|
label="Remote IP"
|
||||||
name="remoteIp"
|
name="remoteIp"
|
||||||
@@ -1555,15 +1601,6 @@ function NetworkConfigManager() {
|
|||||||
placeholder="10.10.0.2"
|
placeholder="10.10.0.2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4">
|
|
||||||
<FormField
|
|
||||||
label="Порт"
|
|
||||||
name="port"
|
|
||||||
value={editingInterface.port}
|
|
||||||
onChange={(val) => setEditingInterface({ ...editingInterface, port: val })}
|
|
||||||
placeholder="51820"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormModal>
|
</FormModal>
|
||||||
@@ -1621,7 +1658,7 @@ function NetworkConfigManager() {
|
|||||||
message={
|
message={
|
||||||
itemToDelete ? (
|
itemToDelete ? (
|
||||||
<>
|
<>
|
||||||
<p>Вы уверены, что хотите удалить <strong>{itemToDelete.name || itemToDelete.cidr || 'этот элемент'}</strong>?</p>
|
<p>Вы уверены, что хотите удалить <strong>{itemToDelete.ip || itemToDelete.name || itemToDelete.cidr || 'этот элемент'}</strong>?</p>
|
||||||
<p className="text-muted mb-0">Это действие нельзя отменить.</p>
|
<p className="text-muted mb-0">Это действие нельзя отменить.</p>
|
||||||
</>
|
</>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
Reference in New Issue
Block a user