diff --git a/.gitignore b/.gitignore index f9658f7..c5131ef 100644 --- a/.gitignore +++ b/.gitignore @@ -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/**/.* diff --git a/article/electron-desktop-guide.md b/article/electron-desktop-guide.md index e2b26dc..b77d339 100644 --- a/article/electron-desktop-guide.md +++ b/article/electron-desktop-guide.md @@ -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 { + return await (window as any).api.showNativeModal(config); + } + + async openFileDialog(config: FileDialogConfig): Promise { + return await (window as any).api.openFileDialog(config); + } + + async saveFileDialog(config: SaveDialogConfig): Promise { + return await (window as any).api.saveFileDialog(config); + } + + async loadFileContent(filePath: string): Promise { + return await (window as any).api.loadFileContent(filePath); + } + + async saveFileContent(filePath: string, content: string): Promise { + 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 { + 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 скрипты для безопасной передачи данных. diff --git a/back/cmd/knock_routes.go b/back/cmd/knock_routes.go index 83b4562..f0c3a16 100644 --- a/back/cmd/knock_routes.go +++ b/back/cmd/knock_routes.go @@ -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"}) diff --git a/back/cmd/root.go b/back/cmd/root.go index 59b9202..e73a84e 100644 --- a/back/cmd/root.go +++ b/back/cmd/root.go @@ -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) diff --git a/back/cmd/serve.go b/back/cmd/serve.go index 454bf64..deea27b 100644 --- a/back/cmd/serve.go +++ b/back/cmd/serve.go @@ -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) } diff --git a/desktop/VPN_BYPASS_DEBUG.md b/desktop/VPN_BYPASS_DEBUG.md deleted file mode 100644 index b5eb576..0000000 --- a/desktop/VPN_BYPASS_DEBUG.md +++ /dev/null @@ -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 или системной маршрутизации, а не в коде приложения.