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": [ "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"
}, },
{ {
+3 -2
View File
@@ -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,
+88 -51
View File
@@ -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