412 lines
14 KiB
Markdown
412 lines
14 KiB
Markdown
# Локальное простукивание портов (Local Port Knocking)
|
||
|
||
## Обзор
|
||
|
||
Функционал локального простукивания позволяет выполнять knock операции напрямую через Node.js API без использования внешнего HTTP API сервера. Это обеспечивает независимость от внешних сервисов и возможность работы в автономном режиме.
|
||
|
||
## Условия активации
|
||
|
||
Локальное простукивание активируется автоматически когда:
|
||
|
||
1. **API URL пуст** - поле `apiBase` не заполнено или содержит пустую строку
|
||
2. **API URL = "internal"** - значение `apiBase` установлено в `"internal"`
|
||
3. **API URL не задан** - значение `apiBase` равно `null` или `undefined`
|
||
|
||
## Архитектура
|
||
|
||
### Файлы реализации
|
||
|
||
#### 1. `src/main/main.js` - Основная логика
|
||
|
||
**Строки 210-367**: Реализация локального простукивания
|
||
|
||
**Ключевые функции:**
|
||
|
||
- `parseTarget(targetStr)` - парсинг строки цели в объект
|
||
- `parseDelay(delayStr)` - конвертация задержки в миллисекунды
|
||
- `knockTcp(host, port, timeout)` - TCP простукивание
|
||
- `knockUdp(host, port, timeout)` - UDP простукивание
|
||
- `performLocalKnock(targets, delay, verbose)` - основная функция простукивания
|
||
- `ipcMain.handle('knock:local', ...)` - IPC обработчик
|
||
|
||
**Поддерживаемые протоколы:**
|
||
|
||
- **TCP** - создает соединение и немедленно закрывает
|
||
- **UDP** - отправляет пакет данных (fire-and-forget)
|
||
|
||
**Формат целей:**
|
||
|
||
``` text
|
||
protocol:host:port[:gateway]
|
||
```
|
||
|
||
Примеры:
|
||
|
||
- `tcp:127.0.0.1:22`
|
||
- `udp:192.168.1.1:53`
|
||
- `tcp:example.com:80:gateway.com`
|
||
|
||
**Поддержка Gateway:**
|
||
|
||
Gateway можно указать двумя способами:
|
||
|
||
1. **В строке цели**: `tcp:host:port:gateway_ip`
|
||
2. **Глобально через поле Gateway**: используется для всех целей, если не указан в самой цели
|
||
|
||
**Приоритет gateway:**
|
||
|
||
- Gateway из строки цели имеет приоритет над глобальным
|
||
- Если gateway не указан, используется системный маршрут по умолчанию
|
||
|
||
**Обход VPN/туннелей:**
|
||
|
||
Gateway использует `localAddress` для принудительного направления трафика через указанный локальный IP-адрес. Это позволяет:
|
||
- Обходить VPN соединения (WireGuard, OpenVPN и др.)
|
||
- Использовать конкретный сетевой интерфейс
|
||
- Направлять трафик через локальный шлюз
|
||
|
||
**Пример обхода WireGuard:**
|
||
```json
|
||
{
|
||
"gateway": "192.168.89.1"
|
||
}
|
||
```
|
||
Трафик будет направлен через интерфейс с IP `192.168.89.1`, минуя WireGuard туннель.
|
||
|
||
## Хелпер для gateway (Rust приоритетно, Go как fallback)
|
||
|
||
Когда задан `gateway` (IP или имя интерфейса), десктоп-приложение запускает встроенный бинарь из `desktop/bin/`:
|
||
|
||
- `knock-local-rust` — приоритетный Rust-хелпер (если присутствует)
|
||
- `knock-local` — Go-хелпер как запасной вариант
|
||
|
||
Оба на Linux используют `SO_BINDTODEVICE` для привязки к интерфейсу и надежного обхода VPN/туннелей (WireGuard и пр.).
|
||
|
||
Сборка при разработке:
|
||
|
||
- `npm run rust:build` — соберёт Rust-хелпер
|
||
- `npm run go:build` — соберёт Go-хелпер
|
||
|
||
В прод-сборках оба бинаря автоматически включаются в образ приложения.
|
||
|
||
Важно для TCP: привязка интерфейса устанавливается до `connect()`. Это гарантирует, что исходящее соединение пойдёт через нужный интерфейс, а не в туннель.
|
||
|
||
**Формат задержки:**
|
||
|
||
- `1s` - 1 секунда
|
||
- `500ms` - 500 миллисекунд (не поддерживается, используйте `0.5s`)
|
||
- `2m` - 2 минуты
|
||
- `1h` - 1 час
|
||
|
||
#### 2. `src/preload/preload.js` - IPC мост
|
||
|
||
**Строка 13**: Добавлен метод `localKnock`
|
||
|
||
```javascript
|
||
localKnock: async (payload) => ipcRenderer.invoke('knock:local', payload)
|
||
```
|
||
|
||
#### 3. `src/renderer/renderer.js` - UI логика
|
||
|
||
**Строки 317-376**: Логика выбора между локальным и API простукиванием
|
||
|
||
**Ключевые изменения:**
|
||
|
||
- Проверка условия `useLocalKnock = !apiBase || apiBase.trim() === '' || apiBase === 'internal'`
|
||
- Извлечение targets из всех режимов (inline, form, yaml)
|
||
- Вызов `window.api.localKnock()` вместо HTTP запросов
|
||
|
||
## Режимы работы
|
||
|
||
### 1. Inline режим
|
||
|
||
```javascript
|
||
// Извлекает targets из поля #targets
|
||
targets = qsi("#targets").value.split(';').filter(t => t.trim());
|
||
```
|
||
|
||
### 2. Form режим
|
||
|
||
```javascript
|
||
// Сериализует формы в строку targets
|
||
targets = [serializeFormTargetsToInline()];
|
||
```
|
||
|
||
### 3. YAML режим
|
||
|
||
```javascript
|
||
// Парсит YAML и извлекает targets
|
||
const config = yaml.load(yamlContent);
|
||
targets = config.targets.map(t => {
|
||
const protocol = t.protocol || 'tcp';
|
||
const host = t.host || '127.0.0.1';
|
||
const ports = t.ports || [t.port] || [22];
|
||
return ports.map(port => `${protocol}:${host}:${port}`);
|
||
}).flat();
|
||
```
|
||
|
||
## API локального простукивания
|
||
|
||
### Входные параметры
|
||
|
||
```javascript
|
||
{
|
||
targets: string[], // Массив целей в формате "protocol:host:port[:gateway]"
|
||
delay: string, // Задержка между целями (например "1s")
|
||
verbose: boolean, // Подробный вывод в консоль
|
||
gateway: string // Глобальный gateway для всех целей (опционально)
|
||
}
|
||
```
|
||
|
||
### Выходные данные
|
||
|
||
```javascript
|
||
{
|
||
success: boolean, // Успешность операции
|
||
results: [ // Детальные результаты по каждой цели
|
||
{
|
||
target: string, // Исходная строка цели
|
||
success: boolean, // Успешность простукивания
|
||
message: string // Сообщение о результате
|
||
}
|
||
],
|
||
summary: { // Общая статистика
|
||
total: number, // Общее количество целей
|
||
successful: number, // Количество успешных
|
||
failed: number // Количество неудачных
|
||
}
|
||
}
|
||
```
|
||
|
||
## Примеры использования
|
||
|
||
### Настройка для локального режима
|
||
|
||
#### Вариант 1: Пустой API URL
|
||
|
||
```json
|
||
{
|
||
"apiBase": ""
|
||
}
|
||
```
|
||
|
||
#### Вариант 2: Специальное значение
|
||
|
||
```json
|
||
{
|
||
"apiBase": "internal"
|
||
}
|
||
```
|
||
|
||
### Пример конфигурации
|
||
|
||
```json
|
||
{
|
||
"apiBase": "internal",
|
||
"gateway": "192.168.1.1",
|
||
"inlineTargets": "tcp:127.0.0.1:22;tcp:192.168.1.100:80",
|
||
"delay": "2s"
|
||
}
|
||
```
|
||
|
||
### Пример YAML конфигурации
|
||
|
||
```yaml
|
||
targets:
|
||
- protocol: tcp
|
||
host: 127.0.0.1
|
||
ports: [22, 80]
|
||
- protocol: udp
|
||
host: 192.168.1.1
|
||
ports: [53]
|
||
delay: 1s
|
||
```
|
||
|
||
## Логирование и отладка
|
||
|
||
### Консольный вывод
|
||
|
||
При `verbose: true` в консоли main процесса появляются сообщения:
|
||
|
||
``` text
|
||
Knocking TCP 127.0.0.1:22
|
||
Knocking UDP 192.168.1.1:53 via 192.168.1.1
|
||
Knocking TCP example.com:80 via 10.0.0.1
|
||
```
|
||
|
||
### Результаты в DevTools
|
||
|
||
Детальные результаты логируются в консоль renderer процесса:
|
||
|
||
```javascript
|
||
console.log('Local knock results:', result.results);
|
||
```
|
||
|
||
### Статус в UI
|
||
|
||
В интерфейсе отображается краткий статус:
|
||
|
||
``` text
|
||
"Локальное простукивание завершено: 2/3 успешно"
|
||
```
|
||
|
||
## Ограничения
|
||
|
||
### Поддерживаемые протоколы
|
||
|
||
- ✅ **TCP** - полная поддержка
|
||
- ✅ **UDP** - отправка пакетов
|
||
- ❌ **ICMP** - не поддерживается
|
||
- ❌ **Другие протоколы** - не поддерживаются
|
||
|
||
### Таймауты
|
||
|
||
- **TCP**: 5 секунд по умолчанию
|
||
- **UDP**: 5 секунд по умолчанию
|
||
- Настраивается в коде функций `knockTcp` и `knockUdp`
|
||
|
||
### Сетевая безопасность
|
||
|
||
- Локальное простукивание использует системные сокеты
|
||
- Подчиняется правилам файрвола операционной системы
|
||
- Не требует дополнительных разрешений в Electron
|
||
|
||
## Совместимость
|
||
|
||
### Операционные системы
|
||
|
||
- ✅ **Windows** - полная поддержка
|
||
- ✅ **macOS** - полная поддержка
|
||
- ✅ **Linux** - полная поддержка
|
||
|
||
### Electron версии
|
||
|
||
- ✅ **v28+** - протестировано
|
||
- ⚠️ **v27 и ниже** - может потребовать адаптации
|
||
|
||
## Переключение между режимами
|
||
|
||
### API → Локальный
|
||
|
||
1. Открыть настройки (Ctrl/Cmd+,)
|
||
2. Установить `apiBase` в `"internal"`
|
||
3. Сохранить настройки
|
||
4. Перезапустить приложение
|
||
|
||
### Локальный → API
|
||
|
||
1. Открыть настройки
|
||
2. Установить корректный `apiBase` URL
|
||
3. Сохранить настройки
|
||
4. Перезапустить приложение
|
||
|
||
## Устранение неполадок
|
||
|
||
### Проблема: "No targets provided"
|
||
|
||
**Причина**: Не удалось извлечь цели из конфигурации
|
||
**Решение**: Проверить корректность заполнения полей targets
|
||
|
||
### Проблема: "Unsupported protocol"
|
||
|
||
**Причина**: Использован неподдерживаемый протокол
|
||
**Решение**: Использовать только `tcp` или `udp`
|
||
|
||
### Проблема: "Connection timeout"
|
||
|
||
**Причина**: Цель недоступна или заблокирована файрволом
|
||
**Решение**: Проверить доступность цели и настройки файрвола
|
||
|
||
### Проблема: "Invalid target format"
|
||
|
||
**Причина**: Неверный формат строки цели
|
||
**Решение**: Использовать формат `protocol:host:port`
|
||
|
||
### Проблема: "Uncaught Exception"
|
||
|
||
**Причина**: Необработанные ошибки в асинхронных операциях
|
||
**Решение**: ✅ **ИСПРАВЛЕНО** - Добавлены глобальные обработчики ошибок и защита от двойного resolve
|
||
|
||
**Исправления в версии 1.1:**
|
||
|
||
- Добавлен флаг `resolved` в TCP/UDP функциях для предотвращения двойного вызова resolve
|
||
- Глобальные обработчики `uncaughtException` и `unhandledRejection` в main процессе
|
||
- Глобальные обработчики ошибок в renderer процессе
|
||
- Улучшенная валидация входных данных в IPC обработчике
|
||
- Try-catch блоки вокруг всех критических операций
|
||
|
||
## Безопасность
|
||
|
||
### Ограничения доступа
|
||
|
||
- Локальное простукивание выполняется с правами пользователя приложения
|
||
- Не требует root/administrator прав
|
||
- Подчиняется системным ограничениям сетевого доступа
|
||
|
||
### Логирование
|
||
|
||
- Результаты простукивания логируются в консоль
|
||
- Не сохраняются в файлы по умолчанию
|
||
- Можно отключить через параметр `verbose: false`
|
||
|
||
## Разработка и расширение
|
||
|
||
### Добавление новых протоколов
|
||
|
||
1. Создать функцию `knockProtocol()` в `src/main/main.js`
|
||
2. Добавить обработку в `performLocalKnock()`
|
||
3. Обновить документацию
|
||
|
||
### Настройка таймаутов
|
||
|
||
```javascript
|
||
// В src/main/main.js
|
||
function knockTcp(host, port, timeout = 10000) { // 10 секунд
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### Добавление дополнительных опций
|
||
|
||
```javascript
|
||
// Расширить payload в IPC
|
||
{
|
||
targets: string[],
|
||
delay: string,
|
||
verbose: boolean,
|
||
timeout: number, // новый параметр
|
||
retries: number // новый параметр
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Пример обхода WireGuard
|
||
|
||
### Проблема
|
||
WireGuard активен, весь трафик идет через туннель, но нужно простучать порт через локальный шлюз.
|
||
|
||
### Решение
|
||
```json
|
||
{
|
||
"apiBase": "internal",
|
||
"gateway": "192.168.89.1",
|
||
"inlineTargets": "tcp:external-server.com:22",
|
||
"delay": "1s"
|
||
}
|
||
```
|
||
|
||
### Логи
|
||
```
|
||
Using localAddress 192.168.89.1 to bypass VPN/tunnel
|
||
Knocking TCP external-server.com:22 via 192.168.89.1
|
||
TCP connection to external-server.com:22 via 192.168.89.1 successful
|
||
```
|
||
|
||
---
|
||
|
||
**Версия документации**: 1.2
|
||
**Дата создания**: 2024
|
||
**Дата обновления**: 2024 (поддержка обхода VPN)
|
||
**Совместимость**: Electron Desktop App v1.0+
|