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

This commit is contained in:
2026-01-22 12:05:07 +07:00
parent bfae6def11
commit d3bf307d6c
3 changed files with 92 additions and 61 deletions
+1 -8
View File
@@ -2,7 +2,6 @@
"gateways": [
{
"id": "gw-cloudflare-swe",
"name": "SWE-IHOR",
"ip": "94.142.140.1",
"description": "Cloudflare Sweden - основной выход",
"serverId": "SWE-HIPHOST",
@@ -11,7 +10,6 @@
},
{
"id": "gw-bunny-swe",
"name": "SWE-BUNNY",
"ip": "94.142.140.2",
"description": "Bunny CDN Sweden",
"serverId": "SWE-HIPHOST",
@@ -20,7 +18,6 @@
},
{
"id": "gw-telegram-nl",
"name": "TG-PROXY-1",
"ip": "149.154.167.50",
"description": "Telegram DC2",
"serverId": "NL-AMSTERDAM",
@@ -29,7 +26,6 @@
},
{
"id": "gw-hetzner-de",
"name": "DE-HETZNER",
"ip": "195.100.30.21",
"description": "Hetzner Frankfurt",
"serverId": "DE-FRANKFURT",
@@ -38,7 +34,6 @@
},
{
"id": "gw-fastly-us",
"name": "US-FASTLY",
"ip": "151.101.1.1",
"description": "Fastly US East",
"serverId": "US-NEWYORK",
@@ -47,9 +42,8 @@
},
{
"id": "gw-recursive-swe",
"name": "SWE-RECURSIVE",
"ip": "10.0.0.1",
"description": "Рекурсивный gateway через SWE-IHOR",
"description": "Рекурсивный gateway через 94.142.140.1",
"serverId": "SWE-HIPHOST",
"country": "SWE",
"type": "recursive",
@@ -72,7 +66,6 @@
"type": "WireGuard",
"localIp": "10.20.0.1",
"remoteIp": "10.20.0.2",
"port": "51820",
"serverId": "SWE-HIPHOST"
},
{
+3 -2
View File
@@ -789,9 +789,10 @@ function FilterManager() {
});
networkGateways.forEach(gw => {
if (!gw.ip) return; // Пропускаем gateway без IP
gws.push({
name: gw.name,
ip: gw.ip || '',
name: gw.ip, // Используем IP как имя для совместимости со старой логикой
ip: gw.ip,
comment: gw.description || '',
serverDns: serverDns,
country: country,
+88 -51
View File
@@ -61,7 +61,6 @@ const GATEWAY_TYPES = [
// Пустые объекты
const getEmptyGateway = () => ({
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
name: '',
ip: '',
description: '',
serverId: '',
@@ -76,7 +75,6 @@ const getEmptyInterface = () => ({
type: 'GRE',
localIp: '',
remoteIp: '',
port: '',
serverId: '',
});
@@ -232,7 +230,6 @@ function NetworkConfigManager() {
result = result.filter(g => {
const server = getServerInfo(g.serverId);
return (
g.name?.toLowerCase().includes(term) ||
g.ip?.toLowerCase().includes(term) ||
g.country?.toLowerCase().includes(term) ||
server?.country?.toLowerCase().includes(term) ||
@@ -501,37 +498,48 @@ function NetworkConfigManager() {
code += `\n# === Рекурсивные маршруты для сервера: ${serverName} ===\n`;
gateways.forEach(gw => {
const parentGateway = getParentGateway(gw.parentGatewayId);
const parent = getParentGateway(gw.parentGatewayId);
if (!parentGateway) {
code += `\n# Gateway "${gw.name}" - родительский gateway не найден, пропущен\n`;
if (!parent) {
code += `\n# Gateway "${gw.ip || 'без IP'}" - родительский gateway/интерфейс не найден, пропущен\n`;
return;
}
if (!gw.ip) {
code += `\n# Gateway "${gw.name}" - IP адрес не указан, пропущен\n`;
code += `\n# Gateway без IP адреса - пропущен\n`;
return;
}
if (!parentGateway.ip) {
code += `\n# Gateway "${gw.name}" - родительский gateway "${parentGateway.name}" не имеет IP, пропущен\n`;
// Определяем IP родителя в зависимости от типа
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;
}
validRoutesCount++;
// Создаём маршрут для рекурсивного gateway
// Маршрут к IP рекурсивного gateway направляется через родительский gateway
code += `\n# Рекурсивный gateway: ${gw.name} (IP: ${gw.ip})\n`;
code += `# Родительский gateway: ${parentGateway.name} (IP: ${parentGateway.ip})\n`;
const parentLabel = parent.type === 'interface'
? `интерфейс ${parent.name || parent.type} (${parent.remoteIp})`
: `gateway ${parent.ip}`;
code += `\n# Рекурсивный gateway: ${gw.ip}\n`;
code += `# Родительский: ${parentLabel}\n`;
// Создаём маршрут к 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
if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) {
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 ===
const getParentGateway = (gatewayId) => {
if (!gatewayId) return null;
return config.gateways?.find(g => g.id === gatewayId);
// === Получение родительского gateway или интерфейса ===
const getParentGateway = (parentId) => {
if (!parentId) return null;
// Сначала ищем в 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 ===
@@ -678,7 +695,7 @@ function NetworkConfigManager() {
</span>
<div className="flex-fill">
<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' }}>
{gatewayType.label}
</span>
@@ -730,7 +747,12 @@ function NetworkConfigManager() {
{parentGateway && (
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
<span className="text-muted"></span>
<span>Родитель: {parentGateway.name}</span>
<span>
Родитель: {parentGateway.type === 'interface'
? `Интерфейс ${parentGateway.name || parentGateway.type} (${parentGateway.remoteIp})`
: `Gateway ${parentGateway.ip}`
}
</span>
</div>
)}
@@ -756,12 +778,11 @@ function NetworkConfigManager() {
</span>
<div className="flex-fill">
<div className="fw-medium">{iface.name || '—'}</div>
<div className="text-muted small">
<span className={`badge bg-${getInterfaceTypeColor(iface.type)}-lt text-${getInterfaceTypeColor(iface.type)}`}>
{iface.type}
</span>
{iface.port && <span className="ms-1">:{iface.port}</span>}
</div>
<div className="text-muted small">
<span className={`badge bg-${getInterfaceTypeColor(iface.type)}-lt text-${getInterfaceTypeColor(iface.type)}`}>
{iface.type}
</span>
</div>
</div>
<div className="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`}>
<IconCircleFilled size={8} className={`text-${gatewayType.color}`} />
</span>
<strong>{gateway.name || '—'}</strong>
<code className="fw-medium">{gateway.ip || '—'}</code>
</div>
</td>
<td>
@@ -1112,7 +1133,10 @@ function NetworkConfigManager() {
</span>
{parentGateway && (
<div className="text-muted small mt-1">
{parentGateway.name}
{parentGateway.type === 'interface'
? `Интерфейс ${parentGateway.name || parentGateway.type} (${parentGateway.remoteIp})`
: `Gateway ${parentGateway.ip}`
}
</div>
)}
</td>
@@ -1234,7 +1258,6 @@ function NetworkConfigManager() {
<th>Тип</th>
<th>Local IP</th>
<th>Remote IP</th>
<th>Порт</th>
<th>Сервер</th>
<th className="text-end" style={{ width: 100 }}>Действия</th>
</tr>
@@ -1265,7 +1288,6 @@ function NetworkConfigManager() {
</code>
</td>
<td><code>{iface.remoteIp || '—'}</code></td>
<td>{iface.port || '—'}</td>
<td>
{server ? (
<div className="d-flex align-items-center">
@@ -1420,7 +1442,13 @@ function NetworkConfigManager() {
onChange={(val) => setEditingGateway({ ...editingGateway, ip: val })}
placeholder="94.142.140.1"
required
disabled={gatewayModalMode === 'edit'}
/>
{gatewayModalMode === 'edit' && (
<div className="form-text text-muted">
IP адрес нельзя изменить после создания
</div>
)}
</div>
<div className="col-md-6">
<FormField
@@ -1454,23 +1482,41 @@ function NetworkConfigManager() {
</div>
{editingGateway.type === 'recursive' && (
<div className="col-12">
<label className="form-label">Родительский gateway</label>
<label className="form-label">Родительский gateway или интерфейс</label>
<select
className="form-select"
value={editingGateway.parentGatewayId || ''}
onChange={(e) => setEditingGateway({ ...editingGateway, parentGatewayId: e.target.value })}
>
<option value="">Выберите родительский gateway</option>
{config.gateways
.filter(g => g.id !== editingGateway.id && g.type === 'direct')
.map(g => (
<option key={g.id} value={g.id}>
{g.ip || g.name} {g.description ? `(${g.description})` : ''}
</option>
))}
<option value="">Выберите родительский gateway или интерфейс</option>
{/* Прямые gateway */}
<optgroup label="Прямые gateway">
{config.gateways
.filter(g => g.id !== editingGateway.id && g.type === 'direct' && g.ip)
.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>
<div className="form-text">
Рекурсивный gateway ссылается на другой gateway (обычно прямой)
Рекурсивный gateway может ссылаться на прямой gateway или на интерфейс (по его remote IP)
</div>
</div>
)}
@@ -1546,7 +1592,7 @@ function NetworkConfigManager() {
placeholder="10.10.0.1"
/>
</div>
<div className="col-md-4">
<div className="col-md-6">
<FormField
label="Remote IP"
name="remoteIp"
@@ -1555,15 +1601,6 @@ function NetworkConfigManager() {
placeholder="10.10.0.2"
/>
</div>
<div className="col-md-4">
<FormField
label="Порт"
name="port"
value={editingInterface.port}
onChange={(val) => setEditingInterface({ ...editingInterface, port: val })}
placeholder="51820"
/>
</div>
</div>
)}
</FormModal>
@@ -1621,7 +1658,7 @@ function NetworkConfigManager() {
message={
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>
</>
) : null