fix desktop

This commit is contained in:
2025-10-03 16:51:29 +06:00
parent cce64b19e8
commit e28d0ec77e
6 changed files with 249 additions and 212 deletions

13
.gitignore vendored
View File

@@ -1,8 +1,21 @@
.old
**/node_modules
**/dist
**/bin
**/target
**/target/**/*
**/target/**/.*
**/dist/**/*
**/dist/**/.*
**/bin/**/*
**/bin/**/.*
ui/node_modules
desktop/node_modules
desktop-angular/node_modules
desktop-angular/bin
ui/dist
desktop/dist
desktop-angular/dist
desktop/bin
desktop/bin/**/*
desktop/bin/**/.*

View File

@@ -352,11 +352,11 @@ Renderer остался «чистым» фронтом: получает `windo
## Глава 4. Секретный Go-движок внутри
Написан маленький Go-хелпер, который делает РОВНО то, что нужно:
Написан маленький Go-хелпер, который делает кнокинг портов в соответствии с конфигурацией:
- читает из stdin JSON: `targets`, `delay`, `gateway`,
- превращает в конфиг,
- вызывает `internal.PortKnocker()` (в нём `SO_BINDTODEVICE`, `LocalAddr`, TCP/UDP),
- превращает их в конфиг,
- вызывает `internal.PortKnocker()` (в нём `SO_BINDTODEVICE`),
- печатает в stdout один короткий JSON: «успех/ошибка».
```go
@@ -512,7 +512,7 @@ clearTimeout(timeout);
## Глава 6. UI только для UI
В `renderer` ничего такого что непосредственно работает с ядром системы.
В `renderer` ничего такого что непосредственно работает с ядром системы.
Собираем `targets`, `delay`, забираем `gateway` с формы — и даём команду:
```js
@@ -552,18 +552,18 @@ Renderer ничего не знает про сокеты, туннели и Go.
При разработке запускаем `npm run dev` — он сначала соберёт Go, потом стартанёт Electron. В проде `electron-builder` положит бинарь в `resources/bin`. Пользователь вообще не в курсе, что внутри слишком умный Go сидит и пинает пакеты в нужные цели.
Можно конечно и не Го-хелпер запилить - например на Rust или на С++
Можно конечно и не Го-хелпер запилить - например на Rust (смотри последнюю версию проекта в репозитории) или на С++
---
## Глава 8. Почему мы пошли этим путём (немного философии)
## Глава 8. Почему именно так
- Node — шикарен для UI и связки слоёв, но ему не хватает «низкоуровневого директа» в сокетах.
- Go умеет то, что нам нужно: `SO_BINDTODEVICE`, привязка к интерфейсу, «я сказал — поедешь тут».
- Electron — как контейнер: упаковали веб, настроили мосты, подложили Go, включили DevTools красота.
- Node — хорош для UI и связки слоёв, но ему не хватает «низкоуровневого директа» в сокетах.
- Go умеет то, что нужно: `SO_BINDTODEVICE`, привязка к интерфейсу.
- Electron — как контейнер: упаковали веб, настроили мосты, подложили Go, включили DevTools = красота.
Минус: надо собирать маленький бинарь.
Плюс: он работает невидимо и делает «как надо».
Минус: надо собирать маленький бинарничек, который немного увеличит размер пакета (так как он и так ого-го-то несущественно).
Плюс: он работает невидимо и делает что требуется.
---
@@ -573,13 +573,209 @@ Renderer ничего не знает про сокеты, туннели и Go.
- Следите за stdout хелпера — там должен быть только JSON (мы жёстко это контролируем).
- Путь к бинарю в деве и проде разный — мы это учли (ищем сначала `bin/`, потом `resources/bin`).
Мем-чекилст:
- [x] «Всё уходит в WireGuard» → ставим `gateway` → Go рулит.
- [x] «JSON не парсится» → убрали verbose → теперь парсится.
- [x] «Где бинарь?» → смотрим `resources/bin`.
---
## Эпилог
Мы сделали десктопное приложение, которое вообще-то «веб», но изнутри умеет кое что еще.
---
## Эпилог: помогло?
## А как же фреймворки?
Мы сделали десктопное приложение, которое вообще-то «веб», но изнутри умеет очень взрослые вещи. Пользователь нажимает кнопку — а Go в это время спорит с системой маршрутизации, выигрывает и бьёт туда, куда надо.
На Vanilla js все просто и понятно писать но очень многсловно и трудно сделать все структурировано и красиво "по фен-шую".
Давайте коротенько рассмотрим, а как же нам портировать имеющийся ui ангуляр проект, который героически был вшит в go-knocker в десктопном проекте electron.
Итак, у нас есть готовый или не очень Angular проект который уже работает как веб-приложение. Теперь нужно интегрировать его в Electron так, чтобы получить все возможности поюзать его как графическое приложение.
### Архитектурное решение
Мы создали отдельную папку `desktop-angular/`, которая содержит:
1. **Electron обертку** - основной процесс и настройки
2. **Копию Angular проекта** в `src/frontend/` - для удобства разработки
3. **IPC сервисы** - для взаимодействия с нативным кодом
4. **Кастомные модальные окна** - нативные диалоги Electron
### Структура проекта
``` text
desktop-angular/
├── src/
│ ├── main/ # Electron main process
│ │ ├── main.js # Основной процесс
│ │ ├── modal.html # Кастомные модальные окна
│ │ ├── open-dialog.html # Диалог открытия файлов
│ │ └── save-dialog.html # Диалог сохранения файлов
│ ├── preload/ # Безопасный мост
│ │ └── preload.js # API для рендерера
│ └── frontend/ # Angular приложение
│ ├── src/app/
│ │ ├── ipc.service.ts # Сервис для IPC
│ │ ├── modal.service.ts # Сервис модальных окон
│ │ └── root.component.ts # Главный компонент
│ └── package.json # Зависимости Angular
├── package.json # Конфигурация Electron
└── bin/ # Скомпилированный Go бэкенд
```
### Ключевые особенности реализации
#### 1. Двойной режим работы
В `main.js` реализована логика, которая определяет режим работы:
```javascript
const isDev = process.env.NODE_ENV !== "production" && !app.isPackaged;
if (isDev) {
// В режиме разработки загружаем с ng serve
win.loadURL('http://localhost:4200');
win.webContents.openDevTools();
} else {
// В продакшене загружаем собранные файлы
const indexPath = app.isPackaged
? path.join(process.resourcesPath, 'ui-dist', 'index.html')
: path.resolve(__dirname, '../frontend/dist/project-front/browser/index.html');
win.loadFile(indexPath);
}
```
#### 2. IPC сервис для Angular
Создан специальный сервис `IpcService`, который предоставляет удобный API для взаимодействия с Electron:
```typescript
@Injectable({
providedIn: 'root'
})
export class IpcService {
async showNativeModal(config: ModalConfig): Promise<string> {
return await (window as any).api.showNativeModal(config);
}
async openFileDialog(config: FileDialogConfig): Promise<string[]> {
return await (window as any).api.openFileDialog(config);
}
async saveFileDialog(config: SaveDialogConfig): Promise<string> {
return await (window as any).api.saveFileDialog(config);
}
async loadFileContent(filePath: string): Promise<string> {
return await (window as any).api.loadFileContent(filePath);
}
async saveFileContent(filePath: string, content: string): Promise<void> {
return await (window as any).api.saveFileContent(filePath, content);
}
}
```
#### 3. Кастомные нативные диалоги
Вместо стандартных системных диалогов созданы кастомные HTML/CSS/JS диалоги, которые:
- Работают даже если Angular UI "зависла"
- Имеют единый стиль с приложением
- Поддерживают превью файлов
- Имеют расширенную функциональность
#### 4. Интеграция с Go бэкендом
Angular приложение работает с тем же Go бэкендом, что и веб-версия:
```typescript
export class KnockService {
private apiBase = 'http://localhost:8080/api/v1';
async knock(config: KnockConfig): Promise<KnockResult> {
const response = await fetch(`${this.apiBase}/knock`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
return await response.json();
}
}
```
### Скрипты сборки
В `package.json` настроены удобные команды:
```json
{
"scripts": {
"dev": "concurrently -k -n UI,ELECTRON -c green,cyan \"cd src/frontend && npm start\" \"wait-on http://localhost:4200 && cross-env NODE_ENV=development electron .\"",
"build:ui": "cd src/frontend && npm run build",
"go:build": "bash -lc 'mkdir -p bin && cd ../back && go build -o ../desktop-angular/bin/full-go-knocker .'",
"dist": "npm run build:ui && npm run go:build && cross-env NODE_ENV=production electron-builder"
}
}
```
### Конфигурация упаковки
Electron Builder настроен для включения всех необходимых файлов:
```json
{
"build": {
"files": [
"src/main/**/*",
"src/preload/**/*",
"package.json",
"bin/**/*"
],
"extraResources": [
{
"from": "src/frontend/dist/project-front/browser",
"to": "ui-dist"
},
{
"from": "bin",
"to": "bin"
}
]
}
}
```
### Преимущества такого подхода
1. **Переиспользование кода** - Angular приложение остается тем же самым
2. **Нативные возможности** - доступ к файловой системе, системным диалогам
3. **Единая кодовая база** - один Angular проект для веба и десктопа
4. **Удобная разработка** - hot reload в режиме разработки
5. **Простая сборка** - автоматическая упаковка всех компонентов
### Результат
Получилось полноценное десктопное приложение, которое:
- Выглядит и работает как нативное
- Использует весь функционал Angular
- Интегрировано с Go бэкендом
- Имеет кастомные нативные диалоги
- Упаковывается в единый исполняемый файл
Это решение демонстрирует, как можно элегантно интегрировать современный веб-фреймворк в десктопное приложение, сохраняя все преимущества обеих платформ.
---
## Заключение
Мы рассмотрели два подхода к созданию десктопных приложений:
1. **Vanilla JavaScript** - простой, быстрый, но многословный
2. **Angular интеграция** - структурированный, переиспользуемый, но более сложный
Оба подхода имеют свои преимущества и недостатки. Выбор зависит от ваших потребностей:
- Для быстрого прототипа или простого UI - Vanilla JS
- Для сложного приложения с переиспользованием веб-кода - Angular
Главное - правильно спроектировать архитектуру взаимодействия между Electron и вашим UI, используя IPC и preload скрипты для безопасной передачи данных.

View File

@@ -24,10 +24,13 @@ func setupKnockRoutes(api *gin.RouterGroup) {
ConfigYaml string `json:"config_yaml"`
}
if err := c.BindJSON(&req); err != nil {
// fmt.Printf("bad json: %v\n", err)
c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)})
return
}
// fmt.Printf("req: %+v\n", req)
knocker := internal.NewPortKnocker()
// Определяем режим: inline или YAML
@@ -39,6 +42,8 @@ func setupKnockRoutes(api *gin.RouterGroup) {
return
}
fmt.Printf("config: %+v\n", config)
// Применяем дополнительные параметры из запроса
if req.Gateway != "" {
for i := range config.Targets {
@@ -47,7 +52,8 @@ func setupKnockRoutes(api *gin.RouterGroup) {
}
if err := knocker.ExecuteWithConfig(config, req.Verbose, req.WaitConnection); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
fmt.Printf("error: %v\n", err)
c.JSON(400, gin.H{"status": "error","error": err.Error()})
return
}
c.JSON(200, gin.H{"status": "ok"})

View File

@@ -1,6 +1,6 @@
package cmd
import (
import (
"fmt"
"strconv"
"strings"
@@ -100,13 +100,17 @@ func parseInlineTargets(targetsStr, delayStr string) (*internal.Config, error) {
// Разбираем формат [proto]:[host]:[port]
parts := strings.Split(targetStr, ":")
if len(parts) != 3 {
return nil, fmt.Errorf("неверный формат цели '%s', ожидается [proto]:[host]:[port]", targetStr)
if len(parts) != 3 && len(parts) != 4 {
return nil, fmt.Errorf("неверный формат цели '%s', ожидается [proto]:[host]:[port] или [proto]:[host]:[port]:[gateway]", targetStr)
}
protocol := strings.TrimSpace(parts[0])
host := strings.TrimSpace(parts[1])
portStr := strings.TrimSpace(parts[2])
gateway := ""
if len(parts) == 4 {
gateway = strings.TrimSpace(parts[3])
}
// Проверяем протокол
if protocol != "tcp" && protocol != "udp" {
@@ -130,7 +134,7 @@ func parseInlineTargets(targetsStr, delayStr string) (*internal.Config, error) {
Protocol: protocol,
Delay: internal.Duration(delay),
WaitConnection: false,
Gateway: "",
Gateway: gateway,
}
config.Targets = append(config.Targets, target)

View File

@@ -43,11 +43,16 @@ func runServe(cmd *cobra.Command, args []string) error {
port = "8888"
}
host := os.Getenv("GO_KNOCKER_SERVE_HOST")
if strings.TrimSpace(port) == "" {
host = ""
}
r := gin.Default()
// CORS: разрешаем для локальной разработки
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:4200", "http://127.0.0.1:8888", "http://localhost:8888"},
AllowOrigins: []string{"http://localhost:4200", "http://127.0.0.1:8888", "http://localhost:" + port},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
@@ -65,6 +70,6 @@ func runServe(cmd *cobra.Command, args []string) error {
setupCryptoRoutes(api, passHash)
setupStaticRoutes(r, embeddedFS)
fmt.Printf("Serving on :%s\n", port)
return r.Run(":" + port)
fmt.Printf("Serving on %s:%s\n", host, port)
return r.Run(host + ":" + port)
}

View File

@@ -1,187 +0,0 @@
# Диагностика обхода VPN для Gateway
## Проблема
Gateway не работает - пакеты все еще идут через WireGuard туннель вместо локального шлюза.
## Диагностика
### 1. Проверьте сетевые интерфейсы
Откройте DevTools (F12) и выполните:
```javascript
window.testNetworkInterfaces()
```
Это покажет все сетевые интерфейсы и их IP-адреса. Убедитесь, что `192.168.89.18` действительно существует.
### 2. Проверьте тестовое подключение
В DevTools выполните:
```javascript
window.testConnection()
```
Это протестирует подключение к `192.168.89.1:2655` с использованием gateway `192.168.89.18`.
### 3. Проверьте логи в консоли main процесса
При выполнении простукивания должны появиться логи:
```
Binding socket to localAddress 192.168.89.18 to bypass VPN/tunnel
TCP connected from 192.168.89.18:XXXXX to 192.168.89.1:2655
```
### 4. Проверьте системные маршруты
Выполните в терминале:
```bash
# Показать все маршруты
ip route show
# Показать маршрут к конкретной цели
ip route get 192.168.89.1
# Показать интерфейсы
ip addr show
```
## Возможные проблемы и решения
### Проблема 1: IP-адрес не существует
**Симптом**: Ошибка "EADDRNOTAVAIL"
**Решение**: Убедитесь, что `192.168.89.18` действительно привязан к интерфейсу
### Проблема 2: Нет маршрута к цели
**Симптом**: Ошибка "ENETUNREACH" или таймаут
**Решение**: Проверьте, что есть маршрут к `192.168.89.1` через интерфейс с IP `192.168.89.18`
### Проблема 3: WireGuard перехватывает трафик
**Симптом**: Трафик все еще идет через туннель
**Решение**:
1. Проверьте таблицу маршрутизации WireGuard
2. Убедитесь, что `192.168.89.0/24` не входит в AllowedIPs WireGuard
3. Проверьте приоритет маршрутов
### Проблема 4: Неправильное использование bind() для TCP
**Симптом**: Ошибка "socket.bind is not a function"
**Решение**: TCP сокеты НЕ поддерживают `bind()`. Используйте `localAddress` в `connect()`:
```javascript
// Неправильно (для TCP):
socket.bind(0, gateway);
socket.connect(port, host);
// Правильно (для TCP):
socket.connect({
port: port,
host: host,
localAddress: gateway
});
```
## Альтернативные подходы
### 1. Использование SO_BINDTODEVICE (Linux)
Если доступно, можно привязать сокет к конкретному интерфейсу:
```javascript
// Требует root права
socket.bind(0, '192.168.89.18');
socket._handle.setsockopt(socket._handle.constructor.SOL_SOCKET, 25, 'eth0'); // SO_BINDTODEVICE
```
### 2. Использование netstat для проверки
```bash
# Мониторинг активных соединений
netstat -an | grep 192.168.89.1
# Мониторинг с tcpdump
sudo tcpdump -i any -n host 192.168.89.1
```
### 3. Проверка через ss
```bash
# Показать активные соединения
ss -tuln | grep 192.168.89.1
# Показать соединения с конкретным локальным IP
ss -tuln src 192.168.89.18
```
## Тестирование
### Шаг 1: Проверьте интерфейсы
```javascript
window.testNetworkInterfaces()
```
### Шаг 2: Проверьте тестовое подключение
```javascript
window.testConnection()
```
### Шаг 3: Выполните реальное простукивание
```json
{
"apiBase": "internal",
"gateway": "192.168.89.18",
"inlineTargets": "tcp:192.168.89.1:2655",
"delay": "2s"
}
```
### Шаг 4: Проверьте логи
В консоли main процесса должны быть:
```
Using localAddress 192.168.89.18 to bypass VPN/tunnel
Knocking TCP 192.168.89.1:2655 via 192.168.89.18
TCP connected from 192.168.89.18:XXXXX to 192.168.89.1:2655
```
## Ожидаемые результаты
### Успешный обход VPN:
- Локальный IP в логах: `192.168.89.18`
- Подключение успешно
- На шлюзе `192.168.89.1` видны пакеты от `192.168.89.18`
### Неуспешный обход VPN:
- Локальный IP в логах: IP туннеля WireGuard
- Подключение может быть успешным, но через туннель
- На шлюзе `192.168.89.1` НЕ видны пакеты от `192.168.89.18`
## Дополнительная диагностика
### Проверка WireGuard конфигурации
```bash
# Показать статус WireGuard
sudo wg show
# Показать маршруты WireGuard
ip route show table 51820 # или другой номер таблицы
```
### Проверка таблицы маршрутизации
```bash
# Показать все таблицы маршрутизации
ip rule show
# Показать маршруты в конкретной таблице
ip route show table main
ip route show table local
```
---
**Важно**: Если диагностика показывает, что `bind()` работает, но трафик все еще идет через VPN, проблема может быть в настройках WireGuard или системной маршрутизации, а не в коде приложения.