Initial commit
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# Binaries
|
||||
vpn-client
|
||||
vpn-client.exe
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Config and logs
|
||||
.vpn_client/
|
||||
logs/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
413
EXAMPLES.md
Normal file
413
EXAMPLES.md
Normal file
@@ -0,0 +1,413 @@
|
||||
# Примеры использования
|
||||
|
||||
## Примеры VLESS URL
|
||||
|
||||
### VLESS с Reality
|
||||
|
||||
```
|
||||
vless://uuid@server.com:443?security=reality&type=tcp&flow=xtls-rprx-vision&sni=example.com&fp=chrome&pbk=publickey&sid=shortid#ServerName
|
||||
```
|
||||
|
||||
### VLESS с TLS
|
||||
|
||||
```
|
||||
vless://uuid@server.com:443?security=tls&type=tcp&sni=example.com&fp=chrome#ServerName
|
||||
```
|
||||
|
||||
### VLESS с WebSocket
|
||||
|
||||
```
|
||||
vless://uuid@server.com:443?security=tls&type=ws&path=/path&host=example.com&sni=example.com#ServerName
|
||||
```
|
||||
|
||||
### VLESS с gRPC
|
||||
|
||||
```
|
||||
vless://uuid@server.com:443?security=tls&type=grpc&serviceName=ServiceName&mode=multi&sni=example.com#ServerName
|
||||
```
|
||||
|
||||
### VLESS с HTTP/2
|
||||
|
||||
```
|
||||
vless://uuid@server.com:443?security=tls&type=http&path=/path&host=example.com#ServerName
|
||||
```
|
||||
|
||||
## Примеры подписок
|
||||
|
||||
### Добавление подписки
|
||||
|
||||
```
|
||||
Имя подписки: MyVPN
|
||||
URL подписки: https://example.com/sub/xxxxx
|
||||
```
|
||||
|
||||
### Популярные VPN провайдеры
|
||||
|
||||
- AliusVPN
|
||||
- V2rayN
|
||||
- Shadowrocket
|
||||
- Clash
|
||||
|
||||
## Примеры конфигурационных файлов
|
||||
|
||||
### configs.json
|
||||
|
||||
```json
|
||||
{
|
||||
"wireguard": [],
|
||||
"vless": [
|
||||
{
|
||||
"name": "My Server",
|
||||
"url": "vless://uuid@server.com:443?security=reality&type=tcp&flow=xtls-rprx-vision&sni=example.com&fp=chrome&pbk=publickey&sid=shortid#MyServer",
|
||||
"protocol": "VLESS"
|
||||
},
|
||||
{
|
||||
"name": "[MyVPN] Server 1",
|
||||
"url": "vless://...",
|
||||
"protocol": "VLESS",
|
||||
"subscription": "MyVPN"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### subscriptions.json
|
||||
|
||||
```json
|
||||
{
|
||||
"subscriptions": [
|
||||
{
|
||||
"name": "MyVPN",
|
||||
"url": "https://example.com/sub/xxxxx"
|
||||
},
|
||||
{
|
||||
"name": "AnotherVPN",
|
||||
"url": "https://another.com/subscription"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### state.json (подключено)
|
||||
|
||||
```json
|
||||
{
|
||||
"connected": true,
|
||||
"config_name": "My Server",
|
||||
"config_type": "vless",
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"interface": "xray",
|
||||
"process_pid": 12345,
|
||||
"log_file": "/path/to/logs/vless_traffic_20240101_120000.log"
|
||||
}
|
||||
```
|
||||
|
||||
### state.json (отключено)
|
||||
|
||||
```json
|
||||
{
|
||||
"connected": false,
|
||||
"config_name": "",
|
||||
"config_type": "",
|
||||
"start_time": "",
|
||||
"interface": "",
|
||||
"process_pid": 0,
|
||||
"log_file": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Примеры использования CLI
|
||||
|
||||
### Базовое использование
|
||||
|
||||
```bash
|
||||
# Запуск клиента
|
||||
./vpn-client
|
||||
|
||||
# Главное меню
|
||||
# 1. VLESS
|
||||
# 2. Управление подписками
|
||||
# 3. Показать статус подключения
|
||||
# 4. Отключиться от VPN
|
||||
# 0. Выход
|
||||
```
|
||||
|
||||
### Работа с VLESS
|
||||
|
||||
```bash
|
||||
# В главном меню выберите 1 (VLESS)
|
||||
|
||||
# Меню VLESS
|
||||
# 1. Список конфигураций - показать все конфиги
|
||||
# 2. Добавить конфиг - добавить новый VLESS конфиг
|
||||
# 3. Удалить конфиг - удалить существующий конфиг
|
||||
# 4. Подключиться - подключиться к серверу
|
||||
# 5. Тестировать конфиг (пинг) - проверить доступность сервера
|
||||
# 0. Назад
|
||||
```
|
||||
|
||||
### Работа с подписками
|
||||
|
||||
```bash
|
||||
# В главном меню выберите 2 (Управление подписками)
|
||||
|
||||
# Меню подписок
|
||||
# 1. Список подписок - показать все подписки
|
||||
# 2. Добавить подписку - добавить новую подписку
|
||||
# 3. Удалить подписку - удалить существующую подписку
|
||||
# 4. Обновить конфиги из подписки - загрузить новые конфиги
|
||||
# 5. Показать конфиги из подписки - показать конфиги конкретной подписки
|
||||
# 6. Тестировать конфиги из подписки (пинг) - проверить все серверы
|
||||
# 0. Назад
|
||||
```
|
||||
|
||||
## Примеры сценариев
|
||||
|
||||
### Сценарий 1: Первое использование
|
||||
|
||||
```bash
|
||||
# 1. Запустите клиент
|
||||
./vpn-client
|
||||
|
||||
# 2. Добавьте подписку
|
||||
# Выберите: 2 (Управление подписками)
|
||||
# Выберите: 2 (Добавить подписку)
|
||||
# Введите имя: MyVPN
|
||||
# Введите URL: https://example.com/sub/xxxxx
|
||||
|
||||
# 3. Обновите конфиги
|
||||
# Выберите: 4 (Обновить конфиги из подписки)
|
||||
# Выберите номер подписки: 1
|
||||
|
||||
# 4. Протестируйте серверы
|
||||
# Выберите: 6 (Тестировать конфиги из подписки)
|
||||
# Выберите номер подписки: 1
|
||||
# Дождитесь результатов
|
||||
|
||||
# 5. Подключитесь к лучшему серверу
|
||||
# Выберите: 0 (Назад)
|
||||
# Выберите: 1 (VLESS)
|
||||
# Выберите: 4 (Подключиться)
|
||||
# Выберите номер конфига с наименьшим пингом
|
||||
|
||||
# 6. Проверьте статус
|
||||
# Выберите: 0 (Назад)
|
||||
# Выберите: 3 (Показать статус подключения)
|
||||
```
|
||||
|
||||
### Сценарий 2: Быстрое подключение
|
||||
|
||||
```bash
|
||||
# Если у вас уже есть VLESS URL
|
||||
|
||||
# 1. Запустите клиент
|
||||
./vpn-client
|
||||
|
||||
# 2. Добавьте конфиг
|
||||
# Выберите: 1 (VLESS)
|
||||
# Выберите: 2 (Добавить конфиг)
|
||||
# Введите имя: Quick Server
|
||||
# Вставьте VLESS URL
|
||||
|
||||
# 3. Подключитесь
|
||||
# Выберите: 4 (Подключиться)
|
||||
# Выберите: 1 (только что добавленный конфиг)
|
||||
|
||||
# 4. Готово!
|
||||
```
|
||||
|
||||
### Сценарий 3: Поиск лучшего сервера
|
||||
|
||||
```bash
|
||||
# 1. Запустите клиент
|
||||
./vpn-client
|
||||
|
||||
# 2. Обновите подписку
|
||||
# Выберите: 2 (Управление подписками)
|
||||
# Выберите: 4 (Обновить конфиги из подписки)
|
||||
# Выберите номер подписки
|
||||
|
||||
# 3. Протестируйте все серверы
|
||||
# Выберите: 6 (Тестировать конфиги из подписки)
|
||||
# Выберите номер подписки
|
||||
# Дождитесь завершения (может занять несколько минут)
|
||||
|
||||
# 4. Посмотрите результаты
|
||||
# Программа покажет топ-5 серверов с наименьшим пингом
|
||||
|
||||
# 5. Подключитесь к лучшему
|
||||
# Выберите: 0 (Назад)
|
||||
# Выберите: 1 (VLESS)
|
||||
# Выберите: 4 (Подключиться)
|
||||
# Найдите сервер из топ-5 и подключитесь
|
||||
```
|
||||
|
||||
### Сценарий 4: Переключение между серверами
|
||||
|
||||
```bash
|
||||
# 1. Отключитесь от текущего сервера
|
||||
# В главном меню выберите: 4 (Отключиться от VPN)
|
||||
|
||||
# 2. Подключитесь к другому серверу
|
||||
# Выберите: 1 (VLESS)
|
||||
# Выберите: 4 (Подключиться)
|
||||
# Выберите другой конфиг
|
||||
|
||||
# Или используйте быстрый способ:
|
||||
# Просто подключитесь к новому серверу
|
||||
# Клиент автоматически отключит предыдущее соединение
|
||||
```
|
||||
|
||||
## Примеры настройки браузера
|
||||
|
||||
### Firefox
|
||||
|
||||
```
|
||||
1. Откройте: about:preferences#general
|
||||
2. Прокрутите до "Параметры сети"
|
||||
3. Нажмите "Настроить..."
|
||||
4. Выберите "Ручная настройка прокси"
|
||||
5. SOCKS Host: 127.0.0.1
|
||||
6. Port: 10808
|
||||
7. Выберите "SOCKS v5"
|
||||
8. Отметьте "Использовать прокси DNS при использовании SOCKS v5"
|
||||
9. Нажмите "OK"
|
||||
```
|
||||
|
||||
### Chrome (с расширением)
|
||||
|
||||
```
|
||||
1. Установите расширение "Proxy SwitchyOmega"
|
||||
2. Создайте новый профиль
|
||||
3. Protocol: SOCKS5
|
||||
4. Server: 127.0.0.1
|
||||
5. Port: 10808
|
||||
6. Сохраните и активируйте профиль
|
||||
```
|
||||
|
||||
### Системный прокси (Windows)
|
||||
|
||||
```
|
||||
1. Настройки → Сеть и Интернет → Прокси
|
||||
2. Ручная настройка прокси
|
||||
3. Использовать прокси-сервер: Вкл
|
||||
4. Адрес: 127.0.0.1
|
||||
5. Порт: 10808
|
||||
6. Сохранить
|
||||
```
|
||||
|
||||
### Системный прокси (macOS)
|
||||
|
||||
```
|
||||
1. Системные настройки → Сеть
|
||||
2. Выберите активное подключение
|
||||
3. Дополнительно → Прокси
|
||||
4. Отметьте "SOCKS прокси"
|
||||
5. Сервер: 127.0.0.1
|
||||
6. Порт: 10808
|
||||
7. OK → Применить
|
||||
```
|
||||
|
||||
### Системный прокси (Linux)
|
||||
|
||||
```bash
|
||||
# Временно (для текущей сессии)
|
||||
export ALL_PROXY=socks5://127.0.0.1:10808
|
||||
|
||||
# Постоянно (добавьте в ~/.bashrc или ~/.zshrc)
|
||||
echo 'export ALL_PROXY=socks5://127.0.0.1:10808' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
## Примеры проверки подключения
|
||||
|
||||
### Проверка IP адреса
|
||||
|
||||
```bash
|
||||
# Через curl
|
||||
curl -x socks5://127.0.0.1:10808 https://api.ipify.org
|
||||
|
||||
# Через браузер
|
||||
# Откройте: https://whatismyipaddress.com/
|
||||
```
|
||||
|
||||
### Проверка работы прокси
|
||||
|
||||
```bash
|
||||
# Проверка доступности прокси
|
||||
curl -x socks5://127.0.0.1:10808 https://www.google.com
|
||||
|
||||
# Если работает, вы увидите HTML код страницы
|
||||
```
|
||||
|
||||
## Примеры логов
|
||||
|
||||
### Успешное подключение
|
||||
|
||||
```
|
||||
[2024-01-01 12:00:00] Создание конфига для сервера: server.com:443
|
||||
[2024-01-01 12:00:00] UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
[2024-01-01 12:00:00] Транспорт: tcp
|
||||
[2024-01-01 12:00:00] Безопасность: reality
|
||||
[2024-01-01 12:00:00] Успешно подключено к 'My Server' (PID: 12345, Лог: /path/to/log)
|
||||
```
|
||||
|
||||
### Ошибка подключения
|
||||
|
||||
```
|
||||
[2024-01-01 12:00:00] Начало подключения к 'My Server' через Xray
|
||||
[2024-01-01 12:00:01] Ошибка подключения: процесс xray завершился с ошибкой
|
||||
[2024-01-01 12:00:01] Проверьте конфигурацию и доступность сервера
|
||||
```
|
||||
|
||||
### Тестирование сервера
|
||||
|
||||
```
|
||||
Тестирование 'My Server'...
|
||||
Проверка доступности сервера...
|
||||
✓ Сервер доступен
|
||||
Адрес: server.com:443
|
||||
Пинг: 45.23 мс
|
||||
Качество: Отлично
|
||||
```
|
||||
|
||||
## Полезные команды
|
||||
|
||||
### Проверка процесса Xray
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
ps aux | grep xray
|
||||
|
||||
# Windows
|
||||
tasklist | findstr xray
|
||||
```
|
||||
|
||||
### Просмотр логов в реальном времени
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
tail -f logs/vless_traffic_*.log
|
||||
|
||||
# Windows (PowerShell)
|
||||
Get-Content logs\vless_traffic_*.log -Wait -Tail 50
|
||||
```
|
||||
|
||||
### Остановка зависшего процесса
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
killall xray
|
||||
|
||||
# Windows
|
||||
taskkill /F /IM xray.exe
|
||||
```
|
||||
|
||||
## Советы и трюки
|
||||
|
||||
1. **Используйте тестирование перед подключением** - это сэкономит время
|
||||
2. **Регулярно обновляйте подписки** - серверы могут меняться
|
||||
3. **Сохраняйте лучшие серверы** - добавьте их как отдельные конфиги
|
||||
4. **Проверяйте логи при проблемах** - там обычно есть ответы
|
||||
5. **Используйте серверы с пингом < 100ms** - для комфортной работы
|
||||
6. **Не забывайте отключаться** - освобождайте ресурсы
|
||||
119
Makefile
Normal file
119
Makefile
Normal file
@@ -0,0 +1,119 @@
|
||||
.PHONY: build clean run test install help
|
||||
|
||||
# Переменные
|
||||
BINARY_NAME=vpn-client
|
||||
BINARY_UNIX=$(BINARY_NAME)
|
||||
BINARY_WINDOWS=$(BINARY_NAME).exe
|
||||
GO=go
|
||||
GOFLAGS=-ldflags="-s -w"
|
||||
|
||||
# Цвета для вывода
|
||||
GREEN=\033[0;32m
|
||||
NC=\033[0m # No Color
|
||||
|
||||
help: ## Показать справку
|
||||
@echo "Доступные команды:"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
build: ## Собрать для текущей платформы
|
||||
@echo "$(GREEN)Сборка VPN Client...$(NC)"
|
||||
$(GO) mod download
|
||||
$(GO) build $(GOFLAGS) -o $(BINARY_NAME) main.go
|
||||
@echo "$(GREEN)✓ Сборка завершена: $(BINARY_NAME)$(NC)"
|
||||
|
||||
build-all: ## Собрать для всех платформ
|
||||
@echo "$(GREEN)Сборка для всех платформ...$(NC)"
|
||||
@$(MAKE) build-linux
|
||||
@$(MAKE) build-windows
|
||||
@$(MAKE) build-darwin
|
||||
@echo "$(GREEN)✓ Сборка для всех платформ завершена$(NC)"
|
||||
|
||||
build-linux: ## Собрать для Linux
|
||||
@echo "$(GREEN)Сборка для Linux...$(NC)"
|
||||
GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -o $(BINARY_UNIX)-linux-amd64 main.go
|
||||
@echo "$(GREEN)✓ Linux: $(BINARY_UNIX)-linux-amd64$(NC)"
|
||||
|
||||
build-windows: ## Собрать для Windows
|
||||
@echo "$(GREEN)Сборка для Windows...$(NC)"
|
||||
GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -o $(BINARY_WINDOWS) main.go
|
||||
@echo "$(GREEN)✓ Windows: $(BINARY_WINDOWS)$(NC)"
|
||||
|
||||
build-darwin: ## Собрать для macOS
|
||||
@echo "$(GREEN)Сборка для macOS...$(NC)"
|
||||
GOOS=darwin GOARCH=amd64 $(GO) build $(GOFLAGS) -o $(BINARY_UNIX)-darwin-amd64 main.go
|
||||
GOOS=darwin GOARCH=arm64 $(GO) build $(GOFLAGS) -o $(BINARY_UNIX)-darwin-arm64 main.go
|
||||
@echo "$(GREEN)✓ macOS Intel: $(BINARY_UNIX)-darwin-amd64$(NC)"
|
||||
@echo "$(GREEN)✓ macOS ARM: $(BINARY_UNIX)-darwin-arm64$(NC)"
|
||||
|
||||
run: build ## Собрать и запустить
|
||||
@echo "$(GREEN)Запуск VPN Client...$(NC)"
|
||||
./$(BINARY_NAME)
|
||||
|
||||
test: ## Запустить тесты
|
||||
@echo "$(GREEN)Запуск тестов...$(NC)"
|
||||
$(GO) test -v ./...
|
||||
|
||||
clean: ## Очистить собранные файлы
|
||||
@echo "$(GREEN)Очистка...$(NC)"
|
||||
$(GO) clean
|
||||
rm -f $(BINARY_NAME)
|
||||
rm -f $(BINARY_WINDOWS)
|
||||
rm -f $(BINARY_UNIX)-*
|
||||
@echo "$(GREEN)✓ Очистка завершена$(NC)"
|
||||
|
||||
install: build ## Установить в систему
|
||||
@echo "$(GREEN)Установка VPN Client...$(NC)"
|
||||
@if [ "$(shell uname)" = "Linux" ] || [ "$(shell uname)" = "Darwin" ]; then \
|
||||
sudo cp $(BINARY_NAME) /usr/local/bin/; \
|
||||
sudo chmod +x /usr/local/bin/$(BINARY_NAME); \
|
||||
echo "$(GREEN)✓ Установлено в /usr/local/bin/$(BINARY_NAME)$(NC)"; \
|
||||
else \
|
||||
echo "Установка поддерживается только на Linux и macOS"; \
|
||||
fi
|
||||
|
||||
uninstall: ## Удалить из системы
|
||||
@echo "$(GREEN)Удаление VPN Client...$(NC)"
|
||||
@if [ "$(shell uname)" = "Linux" ] || [ "$(shell uname)" = "Darwin" ]; then \
|
||||
sudo rm -f /usr/local/bin/$(BINARY_NAME); \
|
||||
echo "$(GREEN)✓ Удалено из /usr/local/bin/$(BINARY_NAME)$(NC)"; \
|
||||
else \
|
||||
echo "Удаление поддерживается только на Linux и macOS"; \
|
||||
fi
|
||||
|
||||
deps: ## Установить зависимости
|
||||
@echo "$(GREEN)Установка зависимостей...$(NC)"
|
||||
$(GO) mod download
|
||||
$(GO) mod tidy
|
||||
@echo "$(GREEN)✓ Зависимости установлены$(NC)"
|
||||
|
||||
fmt: ## Форматировать код
|
||||
@echo "$(GREEN)Форматирование кода...$(NC)"
|
||||
$(GO) fmt ./...
|
||||
@echo "$(GREEN)✓ Код отформатирован$(NC)"
|
||||
|
||||
lint: ## Проверить код линтером
|
||||
@echo "$(GREEN)Проверка кода...$(NC)"
|
||||
@if command -v golangci-lint > /dev/null; then \
|
||||
golangci-lint run; \
|
||||
else \
|
||||
echo "golangci-lint не установлен. Установите: https://golangci-lint.run/usage/install/"; \
|
||||
fi
|
||||
|
||||
dev: ## Режим разработки (сборка и запуск при изменениях)
|
||||
@echo "$(GREEN)Режим разработки...$(NC)"
|
||||
@if command -v air > /dev/null; then \
|
||||
air; \
|
||||
else \
|
||||
echo "air не установлен. Установите: go install github.com/cosmtrek/air@latest"; \
|
||||
echo "Запуск обычной сборки..."; \
|
||||
$(MAKE) run; \
|
||||
fi
|
||||
|
||||
size: build ## Показать размер бинарника
|
||||
@echo "$(GREEN)Размер бинарника:$(NC)"
|
||||
@ls -lh $(BINARY_NAME) | awk '{print $$5 " " $$9}'
|
||||
|
||||
version: ## Показать версию Go
|
||||
@$(GO) version
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
180
README.md
Normal file
180
README.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# VPN Client (Go)
|
||||
|
||||
VPN клиент на Golang с поддержкой VLESS протокола и подписок.
|
||||
|
||||
## Возможности
|
||||
|
||||
- ✅ Поддержка WireGuard
|
||||
- ✅ Поддержка VLESS протокола через Xray
|
||||
- ✅ Управление подписками
|
||||
- ✅ Автоматическое обновление конфигураций из подписок
|
||||
- ✅ Тестирование серверов (ping)
|
||||
- ✅ Детальное логирование
|
||||
- ✅ Статистика трафика для WireGuard
|
||||
- ✅ Кроссплатформенность (Windows, Linux, macOS)
|
||||
|
||||
## Требования
|
||||
|
||||
- Go 1.21 или выше
|
||||
- Xray-core (автоматически используется из папки `xray`)
|
||||
- WireGuard (для Windows: https://www.wireguard.com/install/, для Linux: `apt install wireguard` или `yum install wireguard-tools`)
|
||||
|
||||
## Установка
|
||||
|
||||
### Сборка из исходников
|
||||
|
||||
```bash
|
||||
cd vpn_client_go
|
||||
go mod download
|
||||
go build -o vpn-client main.go
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
build.bat
|
||||
```
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
```bash
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
vpn-client.exe
|
||||
|
||||
# Linux/macOS
|
||||
./vpn-client
|
||||
```
|
||||
|
||||
### Основные функции
|
||||
|
||||
1. **WireGuard**
|
||||
- Список конфигураций
|
||||
- Добавить конфиг (вручную)
|
||||
- Добавить конфиг (из файла)
|
||||
- Удалить конфиг
|
||||
- Подключиться
|
||||
- Статистика трафика
|
||||
|
||||
2. **VLESS**
|
||||
- Список конфигураций
|
||||
- Добавить конфиг
|
||||
- Удалить конфиг
|
||||
- Подключиться
|
||||
- Тестировать конфиг (пинг)
|
||||
|
||||
3. **Управление подписками**
|
||||
- Список подписок
|
||||
- Добавить подписку
|
||||
- Удалить подписку
|
||||
- Обновить конфиги из подписки
|
||||
- Показать конфиги из подписки
|
||||
- Тестировать конфиги из подписки
|
||||
|
||||
4. **Статус подключения**
|
||||
- Показать детальный статус
|
||||
- Время подключения
|
||||
- Информация о прокси
|
||||
- Статистика трафика (для WireGuard)
|
||||
|
||||
5. **Отключение от VPN**
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
vpn_client_go/
|
||||
├── main.go # Точка входа
|
||||
├── go.mod # Зависимости
|
||||
├── internal/
|
||||
│ ├── cli/ # CLI интерфейс
|
||||
│ │ └── cli.go
|
||||
│ ├── config/ # Управление конфигурацией
|
||||
│ │ └── config.go
|
||||
│ ├── wireguard/ # WireGuard протокол
|
||||
│ │ └── wireguard.go
|
||||
│ ├── vless/ # VLESS протокол
|
||||
│ │ └── vless.go
|
||||
│ ├── subscription/ # Управление подписками
|
||||
│ │ └── subscription.go
|
||||
│ ├── vpn/ # Управление VPN
|
||||
│ │ └── vpn.go
|
||||
│ └── logger/ # Логирование
|
||||
│ └── logger.go
|
||||
├── .vpn_client/ # Конфигурационные файлы
|
||||
│ ├── configs.json
|
||||
│ ├── subscriptions.json
|
||||
│ └── state.json
|
||||
└── logs/ # Логи
|
||||
├── wireguard.log
|
||||
├── vless.log
|
||||
├── vless_access.log
|
||||
├── vless_error.log
|
||||
└── vless_traffic_*.log
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Все конфигурационные файлы хранятся в папке `.vpn_client/`:
|
||||
|
||||
- `configs.json` - конфигурации VLESS
|
||||
- `subscriptions.json` - подписки
|
||||
- `state.json` - текущее состояние подключения
|
||||
|
||||
## Логи
|
||||
|
||||
Логи сохраняются в папке `logs/`:
|
||||
|
||||
- `vless.log` - основной лог
|
||||
- `vless_access.log` - лог доступа (IP, подключения)
|
||||
- `vless_error.log` - лог ошибок
|
||||
- `vless_traffic_*.log` - логи трафика для каждого подключения
|
||||
|
||||
## Прокси
|
||||
|
||||
После подключения к VLESS серверу, SOCKS5 прокси доступен по адресу:
|
||||
```
|
||||
127.0.0.1:10808
|
||||
```
|
||||
|
||||
Настройте браузер или систему на использование этого прокси.
|
||||
|
||||
## Отличия от Python версии
|
||||
|
||||
- ✅ Более быстрая работа
|
||||
- ✅ Один исполняемый файл без зависимостей
|
||||
- ✅ Меньшее потребление памяти
|
||||
- ✅ Нативная кроссплатформенность
|
||||
- ✅ Полная поддержка WireGuard
|
||||
- ✅ Полная поддержка VLESS
|
||||
- ⚠️ Нет GUI версии
|
||||
- ⚠️ Нет автоматической настройки системного прокси (пока)
|
||||
|
||||
## Разработка
|
||||
|
||||
### Добавление новых функций
|
||||
|
||||
1. Создайте новый пакет в `internal/`
|
||||
2. Реализуйте функциональность
|
||||
3. Интегрируйте в CLI (`internal/cli/cli.go`)
|
||||
|
||||
### Тестирование
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
|
||||
## Автор
|
||||
|
||||
Портировано с Python на Go
|
||||
17
build.bat
Normal file
17
build.bat
Normal file
@@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
echo Building VPN Client for Windows...
|
||||
|
||||
go mod download
|
||||
go build -ldflags="-s -w" -o vpn-client.exe main.go
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo Build successful!
|
||||
echo Executable: vpn-client.exe
|
||||
) else (
|
||||
echo.
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pause
|
||||
17
build.sh
Normal file
17
build.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Building VPN Client..."
|
||||
|
||||
go mod download
|
||||
go build -ldflags="-s -w" -o vpn-client main.go
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo "Build successful!"
|
||||
echo "Executable: vpn-client"
|
||||
chmod +x vpn-client
|
||||
else
|
||||
echo ""
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module vpn-client
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/fatih/color v1.16.0
|
||||
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
)
|
||||
11
go.sum
Normal file
11
go.sum
Normal file
@@ -0,0 +1,11 @@
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
11
init.bat
Normal file
11
init.bat
Normal file
@@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
echo Initializing Go modules...
|
||||
|
||||
go mod tidy
|
||||
go get github.com/fatih/color@v1.16.0
|
||||
go get golang.org/x/sys@v0.16.0
|
||||
|
||||
echo.
|
||||
echo Dependencies installed!
|
||||
echo Now you can run: go build -ldflags="-s -w" -o vpn-client.exe main.go
|
||||
pause
|
||||
11
init.sh
Normal file
11
init.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Initializing Go modules..."
|
||||
|
||||
go mod tidy
|
||||
go get github.com/fatih/color@v1.16.0
|
||||
go get golang.org/x/sys@v0.16.0
|
||||
|
||||
echo ""
|
||||
echo "Dependencies installed!"
|
||||
echo "Now you can run: go build -ldflags=\"-s -w\" -o vpn-client main.go"
|
||||
864
internal/cli/cli.go
Normal file
864
internal/cli/cli.go
Normal file
@@ -0,0 +1,864 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"vpn-client/internal/config"
|
||||
"vpn-client/internal/subscription"
|
||||
"vpn-client/internal/vless"
|
||||
"vpn-client/internal/vpn"
|
||||
"vpn-client/internal/wireguard"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var (
|
||||
green = color.New(color.FgGreen).SprintFunc()
|
||||
red = color.New(color.FgRed).SprintFunc()
|
||||
yellow = color.New(color.FgYellow).SprintFunc()
|
||||
cyan = color.New(color.FgCyan).SprintFunc()
|
||||
bold = color.New(color.Bold).SprintFunc()
|
||||
)
|
||||
|
||||
// Run запускает CLI интерфейс
|
||||
func Run() error {
|
||||
for {
|
||||
clearScreen()
|
||||
showMainMenu()
|
||||
|
||||
choice := readInput("Выберите действие: ")
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
wireguardMenu()
|
||||
case "2":
|
||||
vlessMenu()
|
||||
case "3":
|
||||
subscriptionsMenu()
|
||||
case "4":
|
||||
if err := vpn.ShowStatus(); err != nil {
|
||||
fmt.Printf("%s %v\n", red("✗"), err)
|
||||
}
|
||||
pause()
|
||||
case "5":
|
||||
if err := vpn.Disconnect(config.LogsDir); err != nil {
|
||||
fmt.Printf("%s %v\n", red("✗"), err)
|
||||
}
|
||||
pause()
|
||||
case "0":
|
||||
fmt.Println("До свидания!")
|
||||
return nil
|
||||
default:
|
||||
fmt.Printf("%s Неверный выбор\n", red("✗"))
|
||||
pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showMainMenu() {
|
||||
state, _ := vpn.GetStatus()
|
||||
|
||||
statusIcon := "○"
|
||||
statusText := "Не подключено"
|
||||
if state != nil && state.Connected {
|
||||
statusIcon = "✓"
|
||||
statusText = fmt.Sprintf("Подключено: %s", state.ConfigName)
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 50))
|
||||
fmt.Println(bold("VPN Клиент (Go)"))
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
fmt.Printf("Статус: %s %s\n", statusIcon, statusText)
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
fmt.Println("1. WireGuard")
|
||||
fmt.Println("2. VLESS")
|
||||
fmt.Println("3. Управление подписками")
|
||||
fmt.Println("4. Показать статус подключения")
|
||||
fmt.Println("5. Отключиться от VPN")
|
||||
fmt.Println("0. Выход")
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
}
|
||||
|
||||
func vlessMenu() {
|
||||
for {
|
||||
clearScreen()
|
||||
fmt.Println("\n" + strings.Repeat("=", 50))
|
||||
fmt.Println(bold("VLESS"))
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
fmt.Println("1. Список конфигураций")
|
||||
fmt.Println("2. Добавить конфиг")
|
||||
fmt.Println("3. Удалить конфиг")
|
||||
fmt.Println("4. Подключиться")
|
||||
fmt.Println("5. Тестировать конфиг (пинг)")
|
||||
fmt.Println("0. Назад")
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
|
||||
choice := readInput("\nВыберите действие: ")
|
||||
|
||||
switch choice {
|
||||
case "0":
|
||||
return
|
||||
case "1":
|
||||
listVLESSConfigs()
|
||||
pause()
|
||||
case "2":
|
||||
addVLESSConfig()
|
||||
pause()
|
||||
case "3":
|
||||
deleteVLESSConfig()
|
||||
pause()
|
||||
case "4":
|
||||
connectVLESS()
|
||||
pause()
|
||||
case "5":
|
||||
testVLESSConfig()
|
||||
pause()
|
||||
default:
|
||||
fmt.Printf("%s Неверный выбор\n", red("✗"))
|
||||
pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func listVLESSConfigs() {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== VLESS конфиги ===")
|
||||
if len(configs.VLESS) == 0 {
|
||||
fmt.Println("Нет конфигов")
|
||||
return
|
||||
}
|
||||
|
||||
for i, cfg := range configs.VLESS {
|
||||
protocol := cfg.Protocol
|
||||
if protocol == "" {
|
||||
protocol = "VLESS"
|
||||
}
|
||||
fmt.Printf("%d. [%s] %s\n", i+1, protocol, cfg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func addVLESSConfig() {
|
||||
name := readInput("Имя конфига: ")
|
||||
if name == "" {
|
||||
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
vlessURL := readInput("VLESS URL: ")
|
||||
if vlessURL == "" {
|
||||
fmt.Printf("%s URL не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
configs.VLESS = append(configs.VLESS, config.VLESSConfig{
|
||||
Name: name,
|
||||
URL: vlessURL,
|
||||
Protocol: "VLESS",
|
||||
})
|
||||
|
||||
if err := config.SaveConfigs(configs); err != nil {
|
||||
fmt.Printf("%s Ошибка сохранения конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s VLESS конфиг '%s' добавлен\n", green("✓"), name)
|
||||
}
|
||||
|
||||
func deleteVLESSConfig() {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(configs.VLESS) == 0 {
|
||||
fmt.Println("Нет конфигов для удаления")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== VLESS конфиги ===")
|
||||
for i, cfg := range configs.VLESS {
|
||||
fmt.Printf("%d. %s\n", i+1, cfg.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер конфига для удаления: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(configs.VLESS) {
|
||||
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
name := configs.VLESS[num-1].Name
|
||||
configs.VLESS = append(configs.VLESS[:num-1], configs.VLESS[num:]...)
|
||||
|
||||
if err := config.SaveConfigs(configs); err != nil {
|
||||
fmt.Printf("%s Ошибка сохранения конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s Конфиг '%s' удален\n", green("✓"), name)
|
||||
}
|
||||
|
||||
func connectVLESS() {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(configs.VLESS) == 0 {
|
||||
fmt.Println("Нет конфигов для подключения")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== VLESS конфиги ===")
|
||||
for i, cfg := range configs.VLESS {
|
||||
protocol := cfg.Protocol
|
||||
if protocol == "" {
|
||||
protocol = "VLESS"
|
||||
}
|
||||
fmt.Printf("%d. [%s] %s\n", i+1, protocol, cfg.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер конфига для подключения: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(configs.VLESS) {
|
||||
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
configName := configs.VLESS[num-1].Name
|
||||
|
||||
fmt.Printf("\nПодключение к '%s' через Xray...\n", configName)
|
||||
|
||||
if err := vless.Connect(configName, config.LogsDir, config.XrayDir); err != nil {
|
||||
fmt.Printf("%s Ошибка подключения: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testVLESSConfig() {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(configs.VLESS) == 0 {
|
||||
fmt.Println("Нет конфигов для тестирования")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== VLESS конфиги ===")
|
||||
for i, cfg := range configs.VLESS {
|
||||
fmt.Printf("%d. %s\n", i+1, cfg.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер конфига для тестирования: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(configs.VLESS) {
|
||||
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
cfg := configs.VLESS[num-1]
|
||||
|
||||
fmt.Printf("\nТестирование '%s'...\n", cfg.Name)
|
||||
fmt.Println("Проверка доступности сервера...")
|
||||
|
||||
success, ping, err := vless.PingServer(cfg.URL, 5000000000) // 5 секунд
|
||||
if err != nil || !success {
|
||||
fmt.Printf("%s Сервер недоступен\n", red("✗"))
|
||||
if err != nil {
|
||||
fmt.Printf(" Ошибка: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s Сервер доступен\n", green("✓"))
|
||||
fmt.Printf(" Пинг: %.2f мс\n", ping)
|
||||
|
||||
// Оценка качества
|
||||
var quality string
|
||||
if ping < 50 {
|
||||
quality = green("Отлично")
|
||||
} else if ping < 100 {
|
||||
quality = cyan("Хорошо")
|
||||
} else if ping < 200 {
|
||||
quality = yellow("Средне")
|
||||
} else {
|
||||
quality = red("Плохо")
|
||||
}
|
||||
fmt.Printf(" Качество: %s\n", quality)
|
||||
}
|
||||
|
||||
func subscriptionsMenu() {
|
||||
for {
|
||||
clearScreen()
|
||||
fmt.Println("\n" + strings.Repeat("=", 50))
|
||||
fmt.Println(bold("Управление подписками"))
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
fmt.Println("1. Список подписок")
|
||||
fmt.Println("2. Добавить подписку")
|
||||
fmt.Println("3. Удалить подписку")
|
||||
fmt.Println("4. Обновить конфиги из подписки")
|
||||
fmt.Println("5. Показать конфиги из подписки")
|
||||
fmt.Println("6. Тестировать конфиги из подписки (пинг)")
|
||||
fmt.Println("0. Назад")
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
|
||||
choice := readInput("\nВыберите действие: ")
|
||||
|
||||
switch choice {
|
||||
case "0":
|
||||
return
|
||||
case "1":
|
||||
listSubscriptions()
|
||||
pause()
|
||||
case "2":
|
||||
addSubscription()
|
||||
pause()
|
||||
case "3":
|
||||
deleteSubscription()
|
||||
pause()
|
||||
case "4":
|
||||
updateSubscription()
|
||||
pause()
|
||||
case "5":
|
||||
showSubscriptionConfigs()
|
||||
pause()
|
||||
case "6":
|
||||
testSubscriptionConfigs()
|
||||
pause()
|
||||
default:
|
||||
fmt.Printf("%s Неверный выбор\n", red("✗"))
|
||||
pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func listSubscriptions() {
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nПодписки:")
|
||||
if len(subs.Subscriptions) == 0 {
|
||||
fmt.Println("Нет подписок")
|
||||
return
|
||||
}
|
||||
|
||||
for i, sub := range subs.Subscriptions {
|
||||
fmt.Printf("%d. %s - %s\n", i+1, sub.Name, sub.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func addSubscription() {
|
||||
name := readInput("Имя подписки: ")
|
||||
if name == "" {
|
||||
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
url := readInput("URL подписки: ")
|
||||
if url == "" {
|
||||
fmt.Printf("%s URL не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
subs.Subscriptions = append(subs.Subscriptions, config.Subscription{
|
||||
Name: name,
|
||||
URL: url,
|
||||
})
|
||||
|
||||
if err := config.SaveSubscriptions(subs); err != nil {
|
||||
fmt.Printf("%s Ошибка сохранения подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s Подписка '%s' добавлена\n", green("✓"), name)
|
||||
}
|
||||
|
||||
func deleteSubscription() {
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(subs.Subscriptions) == 0 {
|
||||
fmt.Println("Нет подписок для удаления")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nПодписки:")
|
||||
for i, sub := range subs.Subscriptions {
|
||||
fmt.Printf("%d. %s\n", i+1, sub.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер подписки для удаления: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(subs.Subscriptions) {
|
||||
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
name := subs.Subscriptions[num-1].Name
|
||||
subs.Subscriptions = append(subs.Subscriptions[:num-1], subs.Subscriptions[num:]...)
|
||||
|
||||
if err := config.SaveSubscriptions(subs); err != nil {
|
||||
fmt.Printf("%s Ошибка сохранения подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s Подписка '%s' удалена\n", green("✓"), name)
|
||||
}
|
||||
|
||||
func updateSubscription() {
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(subs.Subscriptions) == 0 {
|
||||
fmt.Println("Нет подписок")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nПодписки:")
|
||||
for i, sub := range subs.Subscriptions {
|
||||
fmt.Printf("%d. %s\n", i+1, sub.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер подписки для обновления: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(subs.Subscriptions) {
|
||||
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
name := subs.Subscriptions[num-1].Name
|
||||
|
||||
if err := subscription.UpdateSubscription(name, config.LogsDir); err != nil {
|
||||
fmt.Printf("%s Ошибка обновления: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func showSubscriptionConfigs() {
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(subs.Subscriptions) == 0 {
|
||||
fmt.Println("Нет подписок")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nПодписки:")
|
||||
for i, sub := range subs.Subscriptions {
|
||||
fmt.Printf("%d. %s\n", i+1, sub.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер подписки: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(subs.Subscriptions) {
|
||||
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
subName := subs.Subscriptions[num-1].Name
|
||||
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Конфиги из подписки '%s' ===\n", subName)
|
||||
count := 0
|
||||
for _, cfg := range configs.VLESS {
|
||||
if cfg.Subscription == subName {
|
||||
count++
|
||||
protocol := cfg.Protocol
|
||||
if protocol == "" {
|
||||
protocol = "Unknown"
|
||||
}
|
||||
displayName := strings.TrimPrefix(cfg.Name, fmt.Sprintf("[%s] ", subName))
|
||||
fmt.Printf("%d. [%s] %s\n", count, protocol, displayName)
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
fmt.Println("Нет конфигов из этой подписки")
|
||||
fmt.Println("Сначала обновите конфиги из подписки (пункт 4)")
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscriptionConfigs() {
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(subs.Subscriptions) == 0 {
|
||||
fmt.Println("Нет подписок")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nПодписки:")
|
||||
for i, sub := range subs.Subscriptions {
|
||||
fmt.Printf("%d. %s\n", i+1, sub.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер подписки: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(subs.Subscriptions) {
|
||||
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
subName := subs.Subscriptions[num-1].Name
|
||||
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
var subConfigs []config.VLESSConfig
|
||||
for _, cfg := range configs.VLESS {
|
||||
if cfg.Subscription == subName {
|
||||
subConfigs = append(subConfigs, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(subConfigs) == 0 {
|
||||
fmt.Println("Нет конфигов из этой подписки")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Тестирование конфигов из '%s' ===\n", subName)
|
||||
fmt.Printf("Найдено конфигов: %d\n\n", len(subConfigs))
|
||||
|
||||
type result struct {
|
||||
name string
|
||||
protocol string
|
||||
ping float64
|
||||
success bool
|
||||
}
|
||||
|
||||
var results []result
|
||||
|
||||
for i, cfg := range subConfigs {
|
||||
protocol := cfg.Protocol
|
||||
if protocol == "" {
|
||||
protocol = "Unknown"
|
||||
}
|
||||
displayName := strings.TrimPrefix(cfg.Name, fmt.Sprintf("[%s] ", subName))
|
||||
|
||||
fmt.Printf("%d/%d Тестирование [%s] %s...", i+1, len(subConfigs), protocol, truncate(displayName, 50))
|
||||
|
||||
success, ping, _ := vless.PingServer(cfg.URL, 3000000000) // 3 секунды
|
||||
|
||||
if success {
|
||||
fmt.Printf(" %s %.2f мс\n", green("✓"), ping)
|
||||
results = append(results, result{
|
||||
name: displayName,
|
||||
protocol: protocol,
|
||||
ping: ping,
|
||||
success: true,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf(" %s Недоступен\n", red("✗"))
|
||||
results = append(results, result{
|
||||
name: displayName,
|
||||
protocol: protocol,
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Сортируем по пингу
|
||||
var successful []result
|
||||
var failed []result
|
||||
for _, r := range results {
|
||||
if r.success {
|
||||
successful = append(successful, r)
|
||||
} else {
|
||||
failed = append(failed, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Простая сортировка пузырьком
|
||||
for i := 0; i < len(successful); i++ {
|
||||
for j := i + 1; j < len(successful); j++ {
|
||||
if successful[i].ping > successful[j].ping {
|
||||
successful[i], successful[j] = successful[j], successful[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
if len(successful) > 0 {
|
||||
fmt.Printf("\n%s Доступно: %d\n", green("✓"), len(successful))
|
||||
fmt.Println("\nЛучшие серверы:")
|
||||
for i, r := range successful {
|
||||
if i >= 5 {
|
||||
break
|
||||
}
|
||||
fmt.Printf(" %d. [%s] %s - %.2f мс\n", i+1, r.protocol, truncate(r.name, 35), r.ping)
|
||||
}
|
||||
}
|
||||
|
||||
if len(failed) > 0 {
|
||||
fmt.Printf("\n%s Недоступно: %d\n", red("✗"), len(failed))
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
}
|
||||
|
||||
func clearScreen() {
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd := exec.Command("cmd", "/c", "cls")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
} else {
|
||||
cmd := exec.Command("clear")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func readInput(prompt string) string {
|
||||
fmt.Print(prompt)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
func pause() {
|
||||
fmt.Print("\nНажмите Enter для продолжения...")
|
||||
bufio.NewReader(os.Stdin).ReadBytes('\n')
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
|
||||
func wireguardMenu() {
|
||||
for {
|
||||
clearScreen()
|
||||
fmt.Println("\n" + strings.Repeat("=", 50))
|
||||
fmt.Println(bold("WireGuard"))
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
fmt.Println("1. Список конфигураций")
|
||||
fmt.Println("2. Добавить конфиг (вручную)")
|
||||
fmt.Println("3. Добавить конфиг (из файла)")
|
||||
fmt.Println("4. Удалить конфиг")
|
||||
fmt.Println("5. Подключиться")
|
||||
fmt.Println("0. Назад")
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
|
||||
choice := readInput("\nВыберите действие: ")
|
||||
|
||||
switch choice {
|
||||
case "0":
|
||||
return
|
||||
case "1":
|
||||
listWireGuardConfigs()
|
||||
pause()
|
||||
case "2":
|
||||
addWireGuardConfigManual()
|
||||
pause()
|
||||
case "3":
|
||||
addWireGuardConfigFromFile()
|
||||
pause()
|
||||
case "4":
|
||||
deleteWireGuardConfig()
|
||||
pause()
|
||||
case "5":
|
||||
connectWireGuard()
|
||||
pause()
|
||||
default:
|
||||
fmt.Printf("%s Неверный выбор\n", red("✗"))
|
||||
pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func listWireGuardConfigs() {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== WireGuard конфиги ===")
|
||||
if len(configs.WireGuard) == 0 {
|
||||
fmt.Println("Нет конфигов")
|
||||
return
|
||||
}
|
||||
|
||||
for i, cfg := range configs.WireGuard {
|
||||
fmt.Printf("%d. %s\n", i+1, cfg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func addWireGuardConfigManual() {
|
||||
name := readInput("Имя конфига: ")
|
||||
if name == "" {
|
||||
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Вставьте конфиг WireGuard (завершите пустой строкой):")
|
||||
|
||||
var lines []string
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
line, _ := reader.ReadString('\n')
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
if len(lines) == 0 {
|
||||
fmt.Printf("%s Конфиг не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
configText := strings.Join(lines, "\n")
|
||||
|
||||
if err := wireguard.AddConfig(name, configText); err != nil {
|
||||
fmt.Printf("%s Ошибка добавления конфига: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s WireGuard конфиг '%s' добавлен\n", green("✓"), name)
|
||||
}
|
||||
|
||||
func addWireGuardConfigFromFile() {
|
||||
name := readInput("Имя конфига: ")
|
||||
if name == "" {
|
||||
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
filePath := readInput("Путь к файлу конфига: ")
|
||||
if filePath == "" {
|
||||
fmt.Printf("%s Путь не может быть пустым\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := wireguard.AddConfigFromFile(name, filePath); err != nil {
|
||||
fmt.Printf("%s Ошибка добавления конфига: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s WireGuard конфиг '%s' добавлен из файла\n", green("✓"), name)
|
||||
}
|
||||
|
||||
func deleteWireGuardConfig() {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(configs.WireGuard) == 0 {
|
||||
fmt.Println("Нет конфигов для удаления")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== WireGuard конфиги ===")
|
||||
for i, cfg := range configs.WireGuard {
|
||||
fmt.Printf("%d. %s\n", i+1, cfg.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер конфига для удаления: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(configs.WireGuard) {
|
||||
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
name := configs.WireGuard[num-1].Name
|
||||
|
||||
if err := wireguard.DeleteConfig(name); err != nil {
|
||||
fmt.Printf("%s Ошибка удаления конфига: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s Конфиг '%s' удален\n", green("✓"), name)
|
||||
}
|
||||
|
||||
func connectWireGuard() {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(configs.WireGuard) == 0 {
|
||||
fmt.Println("Нет конфигов для подключения")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== WireGuard конфиги ===")
|
||||
for i, cfg := range configs.WireGuard {
|
||||
fmt.Printf("%d. %s\n", i+1, cfg.Name)
|
||||
}
|
||||
|
||||
numStr := readInput("\nНомер конфига для подключения: ")
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil || num < 1 || num > len(configs.WireGuard) {
|
||||
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
|
||||
return
|
||||
}
|
||||
|
||||
configName := configs.WireGuard[num-1].Name
|
||||
|
||||
if err := wireguard.Connect(configName, config.LogsDir); err != nil {
|
||||
fmt.Printf("%s Ошибка подключения: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
172
internal/config/config.go
Normal file
172
internal/config/config.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigDir string
|
||||
SubscriptionsFile string
|
||||
ConfigsFile string
|
||||
StateFile string
|
||||
LogsDir string
|
||||
XrayDir string
|
||||
)
|
||||
|
||||
type WireGuardConfig struct {
|
||||
Name string `json:"name"`
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
type VLESSConfig struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Subscription string `json:"subscription,omitempty"`
|
||||
}
|
||||
|
||||
type Configs struct {
|
||||
WireGuard []WireGuardConfig `json:"wireguard"`
|
||||
VLESS []VLESSConfig `json:"vless"`
|
||||
}
|
||||
|
||||
type Subscription struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Subscriptions struct {
|
||||
Subscriptions []Subscription `json:"subscriptions"`
|
||||
}
|
||||
|
||||
type ConnectionState struct {
|
||||
Connected bool `json:"connected"`
|
||||
ConfigName string `json:"config_name"`
|
||||
ConfigType string `json:"config_type"`
|
||||
StartTime string `json:"start_time"`
|
||||
Interface string `json:"interface"`
|
||||
ProcessPID int `json:"process_pid"`
|
||||
LogFile string `json:"log_file"`
|
||||
}
|
||||
|
||||
// Init инициализирует конфигурационные директории и файлы
|
||||
func Init() error {
|
||||
// Получаем рабочую директорию
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ConfigDir = filepath.Join(workDir, ".vpn_client")
|
||||
SubscriptionsFile = filepath.Join(ConfigDir, "subscriptions.json")
|
||||
ConfigsFile = filepath.Join(ConfigDir, "configs.json")
|
||||
StateFile = filepath.Join(ConfigDir, "state.json")
|
||||
LogsDir = filepath.Join(workDir, "logs")
|
||||
XrayDir = filepath.Join(workDir, "xray")
|
||||
|
||||
// Создаем директории
|
||||
if err := os.MkdirAll(ConfigDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(LogsDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Инициализируем файлы если их нет
|
||||
if err := initFileIfNotExists(SubscriptionsFile, Subscriptions{Subscriptions: []Subscription{}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initFileIfNotExists(ConfigsFile, Configs{WireGuard: []WireGuardConfig{}, VLESS: []VLESSConfig{}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initFileIfNotExists(StateFile, ConnectionState{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initFileIfNotExists(path string, defaultData interface{}) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
data, err := json.MarshalIndent(defaultData, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfigs загружает конфигурации
|
||||
func LoadConfigs() (*Configs, error) {
|
||||
data, err := os.ReadFile(ConfigsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var configs Configs
|
||||
if err := json.Unmarshal(data, &configs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &configs, nil
|
||||
}
|
||||
|
||||
// SaveConfigs сохраняет конфигурации
|
||||
func SaveConfigs(configs *Configs) error {
|
||||
data, err := json.MarshalIndent(configs, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(ConfigsFile, data, 0644)
|
||||
}
|
||||
|
||||
// LoadSubscriptions загружает подписки
|
||||
func LoadSubscriptions() (*Subscriptions, error) {
|
||||
data, err := os.ReadFile(SubscriptionsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var subs Subscriptions
|
||||
if err := json.Unmarshal(data, &subs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &subs, nil
|
||||
}
|
||||
|
||||
// SaveSubscriptions сохраняет подписки
|
||||
func SaveSubscriptions(subs *Subscriptions) error {
|
||||
data, err := json.MarshalIndent(subs, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(SubscriptionsFile, data, 0644)
|
||||
}
|
||||
|
||||
// LoadState загружает состояние подключения
|
||||
func LoadState() (*ConnectionState, error) {
|
||||
data, err := os.ReadFile(StateFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var state ConnectionState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// SaveState сохраняет состояние подключения
|
||||
func SaveState(state *ConnectionState) error {
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(StateFile, data, 0644)
|
||||
}
|
||||
34
internal/logger/logger.go
Normal file
34
internal/logger/logger.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogMessage записывает сообщение в лог-файл
|
||||
func LogMessage(logFile, message string) error {
|
||||
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
|
||||
|
||||
_, err = f.WriteString(logEntry)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLogPath возвращает путь к лог-файлу
|
||||
func GetLogPath(logsDir, logType string) string {
|
||||
return filepath.Join(logsDir, fmt.Sprintf("%s.log", logType))
|
||||
}
|
||||
|
||||
// GetTrafficLogPath возвращает путь к лог-файлу трафика с временной меткой
|
||||
func GetTrafficLogPath(logsDir string) string {
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
return filepath.Join(logsDir, fmt.Sprintf("vless_traffic_%s.log", timestamp))
|
||||
}
|
||||
184
internal/subscription/subscription.go
Normal file
184
internal/subscription/subscription.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"vpn-client/internal/config"
|
||||
"vpn-client/internal/logger"
|
||||
)
|
||||
|
||||
// FetchConfigs загружает конфигурации из подписки
|
||||
func FetchConfigs(subscriptionURL, logsDir string) ([]config.VLESSConfig, error) {
|
||||
logFile := logger.GetLogPath(logsDir, "subscription")
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Начало загрузки подписки: %s", subscriptionURL))
|
||||
|
||||
// Создаем HTTP клиент с таймаутом
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get(subscriptionURL)
|
||||
if err != nil {
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Ошибка загрузки подписки: %v", err))
|
||||
return nil, fmt.Errorf("ошибка загрузки подписки: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ошибка HTTP: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||
}
|
||||
|
||||
// Пытаемся декодировать base64
|
||||
content := string(body)
|
||||
if decoded, err := base64.StdEncoding.DecodeString(content); err == nil {
|
||||
content = string(decoded)
|
||||
}
|
||||
|
||||
// Парсим конфигурации
|
||||
var configs []config.VLESSConfig
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var protocol string
|
||||
var name string
|
||||
var configURL string
|
||||
|
||||
// VLESS конфиги
|
||||
if strings.HasPrefix(line, "vless://") {
|
||||
protocol = "VLESS"
|
||||
configURL = line
|
||||
if parsed, err := url.Parse(line); err == nil && parsed.Fragment != "" {
|
||||
name, _ = url.QueryUnescape(parsed.Fragment)
|
||||
} else {
|
||||
name = fmt.Sprintf("VLESS_%d", len(configs)+1)
|
||||
}
|
||||
configs = append(configs, config.VLESSConfig{
|
||||
Name: name,
|
||||
URL: configURL,
|
||||
Protocol: protocol,
|
||||
})
|
||||
} else if strings.HasPrefix(line, "vmess://") {
|
||||
protocol = "VMess"
|
||||
configURL = line
|
||||
if parsed, err := url.Parse(line); err == nil && parsed.Fragment != "" {
|
||||
name, _ = url.QueryUnescape(parsed.Fragment)
|
||||
} else {
|
||||
name = fmt.Sprintf("VMess_%d", len(configs)+1)
|
||||
}
|
||||
configs = append(configs, config.VLESSConfig{
|
||||
Name: name,
|
||||
URL: configURL,
|
||||
Protocol: protocol,
|
||||
})
|
||||
} else if strings.HasPrefix(line, "trojan://") {
|
||||
protocol = "Trojan"
|
||||
configURL = line
|
||||
if parsed, err := url.Parse(line); err == nil && parsed.Fragment != "" {
|
||||
name, _ = url.QueryUnescape(parsed.Fragment)
|
||||
} else {
|
||||
name = fmt.Sprintf("Trojan_%d", len(configs)+1)
|
||||
}
|
||||
configs = append(configs, config.VLESSConfig{
|
||||
Name: name,
|
||||
URL: configURL,
|
||||
Protocol: protocol,
|
||||
})
|
||||
} else if strings.HasPrefix(line, "ss://") {
|
||||
protocol = "Shadowsocks"
|
||||
configURL = line
|
||||
if parsed, err := url.Parse(line); err == nil && parsed.Fragment != "" {
|
||||
name, _ = url.QueryUnescape(parsed.Fragment)
|
||||
} else {
|
||||
name = fmt.Sprintf("SS_%d", len(configs)+1)
|
||||
}
|
||||
configs = append(configs, config.VLESSConfig{
|
||||
Name: name,
|
||||
URL: configURL,
|
||||
Protocol: protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Успешно загружено %d конфигов", len(configs)))
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// UpdateSubscription обновляет конфигурации из подписки
|
||||
func UpdateSubscription(subscriptionName, logsDir string) error {
|
||||
// Загружаем подписки
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки подписок: %w", err)
|
||||
}
|
||||
|
||||
// Ищем подписку
|
||||
var sub *config.Subscription
|
||||
for i := range subs.Subscriptions {
|
||||
if subs.Subscriptions[i].Name == subscriptionName {
|
||||
sub = &subs.Subscriptions[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sub == nil {
|
||||
return fmt.Errorf("подписка '%s' не найдена", subscriptionName)
|
||||
}
|
||||
|
||||
fmt.Printf("Загрузка конфигов из '%s'...\n", subscriptionName)
|
||||
|
||||
// Загружаем конфиги
|
||||
newConfigs, err := FetchConfigs(sub.URL, logsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигов: %w", err)
|
||||
}
|
||||
|
||||
if len(newConfigs) == 0 {
|
||||
return fmt.Errorf("не удалось загрузить конфиги")
|
||||
}
|
||||
|
||||
// Загружаем текущие конфигурации
|
||||
allConfigs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигураций: %w", err)
|
||||
}
|
||||
|
||||
// Удаляем старые конфиги из этой подписки
|
||||
var filteredConfigs []config.VLESSConfig
|
||||
for _, cfg := range allConfigs.VLESS {
|
||||
if cfg.Subscription != subscriptionName {
|
||||
filteredConfigs = append(filteredConfigs, cfg)
|
||||
}
|
||||
}
|
||||
allConfigs.VLESS = filteredConfigs
|
||||
|
||||
// Добавляем новые конфиги с префиксом подписки
|
||||
for _, cfg := range newConfigs {
|
||||
cfg.Name = fmt.Sprintf("[%s] %s", subscriptionName, cfg.Name)
|
||||
cfg.Subscription = subscriptionName
|
||||
allConfigs.VLESS = append(allConfigs.VLESS, cfg)
|
||||
}
|
||||
|
||||
// Сохраняем
|
||||
if err := config.SaveConfigs(allConfigs); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения конфигураций: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Обновлено %d конфигов из подписки\n", len(newConfigs))
|
||||
return nil
|
||||
}
|
||||
493
internal/vless/vless.go
Normal file
493
internal/vless/vless.go
Normal file
@@ -0,0 +1,493 @@
|
||||
package vless
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"vpn-client/internal/config"
|
||||
"vpn-client/internal/logger"
|
||||
)
|
||||
|
||||
// XrayConfig представляет конфигурацию Xray
|
||||
type XrayConfig struct {
|
||||
Log LogConfig `json:"log"`
|
||||
Inbounds []InboundConfig `json:"inbounds"`
|
||||
Outbounds []OutboundConfig `json:"outbounds"`
|
||||
Stats interface{} `json:"stats"`
|
||||
Policy PolicyConfig `json:"policy"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Loglevel string `json:"loglevel"`
|
||||
Access string `json:"access"`
|
||||
Error string `json:"error"`
|
||||
DNSLog bool `json:"dnsLog"`
|
||||
}
|
||||
|
||||
type InboundConfig struct {
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
Settings InboundSettings `json:"settings"`
|
||||
Sniffing SniffingConfig `json:"sniffing"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type InboundSettings struct {
|
||||
UDP bool `json:"udp"`
|
||||
Auth string `json:"auth"`
|
||||
}
|
||||
|
||||
type SniffingConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DestOverride []string `json:"destOverride"`
|
||||
MetadataOnly bool `json:"metadataOnly"`
|
||||
}
|
||||
|
||||
type OutboundConfig struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Tag string `json:"tag"`
|
||||
Settings OutboundSettings `json:"settings"`
|
||||
StreamSettings StreamSettings `json:"streamSettings"`
|
||||
}
|
||||
|
||||
type OutboundSettings struct {
|
||||
Vnext []VnextConfig `json:"vnext"`
|
||||
}
|
||||
|
||||
type VnextConfig struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Users []UserConfig `json:"users"`
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
ID string `json:"id"`
|
||||
Encryption string `json:"encryption"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
}
|
||||
|
||||
type StreamSettings struct {
|
||||
Network string `json:"network"`
|
||||
Security string `json:"security,omitempty"`
|
||||
TLSSettings *TLSSettings `json:"tlsSettings,omitempty"`
|
||||
RealitySettings *RealitySettings `json:"realitySettings,omitempty"`
|
||||
WSSettings *WSSettings `json:"wsSettings,omitempty"`
|
||||
GRPCSettings *GRPCSettings `json:"grpcSettings,omitempty"`
|
||||
HTTPSettings *HTTPSettings `json:"httpSettings,omitempty"`
|
||||
}
|
||||
|
||||
type TLSSettings struct {
|
||||
ServerName string `json:"serverName"`
|
||||
AllowInsecure bool `json:"allowInsecure"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
type RealitySettings struct {
|
||||
ServerName string `json:"serverName"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Show bool `json:"show"`
|
||||
PublicKey string `json:"publicKey,omitempty"`
|
||||
ShortID string `json:"shortId,omitempty"`
|
||||
SpiderX string `json:"spiderX,omitempty"`
|
||||
}
|
||||
|
||||
type WSSettings struct {
|
||||
Path string `json:"path"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
type GRPCSettings struct {
|
||||
ServiceName string `json:"serviceName"`
|
||||
MultiMode bool `json:"multiMode"`
|
||||
}
|
||||
|
||||
type HTTPSettings struct {
|
||||
Path []string `json:"path"`
|
||||
Host []string `json:"host"`
|
||||
}
|
||||
|
||||
type PolicyConfig struct {
|
||||
Levels map[string]LevelPolicy `json:"levels"`
|
||||
System SystemPolicy `json:"system"`
|
||||
}
|
||||
|
||||
type LevelPolicy struct {
|
||||
StatsUserUplink bool `json:"statsUserUplink"`
|
||||
StatsUserDownlink bool `json:"statsUserDownlink"`
|
||||
}
|
||||
|
||||
type SystemPolicy struct {
|
||||
StatsInboundUplink bool `json:"statsInboundUplink"`
|
||||
StatsInboundDownlink bool `json:"statsInboundDownlink"`
|
||||
StatsOutboundUplink bool `json:"statsOutboundUplink"`
|
||||
StatsOutboundDownlink bool `json:"statsOutboundDownlink"`
|
||||
}
|
||||
|
||||
// ParseVLESSURL парсит VLESS URL и создает конфигурацию Xray
|
||||
func ParseVLESSURL(vlessURL, logsDir string) (*XrayConfig, error) {
|
||||
// Убираем префикс vless://
|
||||
urlStr := strings.TrimPrefix(vlessURL, "vless://")
|
||||
|
||||
// Разделяем на части
|
||||
var name string
|
||||
if idx := strings.Index(urlStr, "#"); idx != -1 {
|
||||
name = urlStr[idx+1:]
|
||||
urlStr = urlStr[:idx]
|
||||
}
|
||||
|
||||
// Парсим параметры
|
||||
var paramsStr string
|
||||
var connection string
|
||||
if idx := strings.Index(urlStr, "?"); idx != -1 {
|
||||
connection = urlStr[:idx]
|
||||
paramsStr = urlStr[idx+1:]
|
||||
} else {
|
||||
connection = urlStr
|
||||
}
|
||||
|
||||
// Парсим connection (uuid@server:port)
|
||||
parts := strings.Split(connection, "@")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("неверный формат VLESS URL")
|
||||
}
|
||||
uuid := parts[0]
|
||||
serverPort := parts[1]
|
||||
|
||||
// Парсим server:port
|
||||
var server string
|
||||
var port int
|
||||
if strings.Contains(serverPort, "[") {
|
||||
// IPv6
|
||||
endIdx := strings.Index(serverPort, "]")
|
||||
server = serverPort[1:endIdx]
|
||||
portStr := strings.TrimPrefix(serverPort[endIdx+1:], ":")
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
} else {
|
||||
lastColon := strings.LastIndex(serverPort, ":")
|
||||
server = serverPort[:lastColon]
|
||||
portStr := serverPort[lastColon+1:]
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
}
|
||||
|
||||
if port == 0 {
|
||||
port = 443
|
||||
}
|
||||
|
||||
// Парсим параметры
|
||||
params := make(map[string]string)
|
||||
if paramsStr != "" {
|
||||
for _, param := range strings.Split(paramsStr, "&") {
|
||||
kv := strings.SplitN(param, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
key, _ := url.QueryUnescape(kv[0])
|
||||
value, _ := url.QueryUnescape(kv[1])
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем базовую конфигурацию
|
||||
cfg := &XrayConfig{
|
||||
Log: LogConfig{
|
||||
Loglevel: "debug",
|
||||
Access: filepath.Join(logsDir, "vless_access.log"),
|
||||
Error: filepath.Join(logsDir, "vless_error.log"),
|
||||
DNSLog: true,
|
||||
},
|
||||
Inbounds: []InboundConfig{
|
||||
{
|
||||
Port: 10808,
|
||||
Protocol: "socks",
|
||||
Settings: InboundSettings{
|
||||
UDP: true,
|
||||
Auth: "noauth",
|
||||
},
|
||||
Sniffing: SniffingConfig{
|
||||
Enabled: true,
|
||||
DestOverride: []string{"http", "tls"},
|
||||
MetadataOnly: false,
|
||||
},
|
||||
Tag: "socks-in",
|
||||
},
|
||||
},
|
||||
Outbounds: []OutboundConfig{
|
||||
{
|
||||
Protocol: "vless",
|
||||
Tag: "proxy",
|
||||
Settings: OutboundSettings{
|
||||
Vnext: []VnextConfig{
|
||||
{
|
||||
Address: server,
|
||||
Port: port,
|
||||
Users: []UserConfig{
|
||||
{
|
||||
ID: uuid,
|
||||
Encryption: getParam(params, "encryption", "none"),
|
||||
Flow: params["flow"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
StreamSettings: StreamSettings{
|
||||
Network: getParam(params, "type", "tcp"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Stats: struct{}{},
|
||||
Policy: PolicyConfig{
|
||||
Levels: map[string]LevelPolicy{
|
||||
"0": {
|
||||
StatsUserUplink: true,
|
||||
StatsUserDownlink: true,
|
||||
},
|
||||
},
|
||||
System: SystemPolicy{
|
||||
StatsInboundUplink: true,
|
||||
StatsInboundDownlink: true,
|
||||
StatsOutboundUplink: true,
|
||||
StatsOutboundDownlink: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Удаляем пустой flow
|
||||
if cfg.Outbounds[0].Settings.Vnext[0].Users[0].Flow == "" {
|
||||
cfg.Outbounds[0].Settings.Vnext[0].Users[0].Flow = ""
|
||||
}
|
||||
|
||||
// Настройки безопасности
|
||||
security := params["security"]
|
||||
if security == "tls" {
|
||||
cfg.Outbounds[0].StreamSettings.Security = "tls"
|
||||
cfg.Outbounds[0].StreamSettings.TLSSettings = &TLSSettings{
|
||||
ServerName: getParam(params, "sni", server),
|
||||
AllowInsecure: params["allowInsecure"] == "1",
|
||||
Fingerprint: params["fp"],
|
||||
}
|
||||
} else if security == "reality" {
|
||||
cfg.Outbounds[0].StreamSettings.Security = "reality"
|
||||
cfg.Outbounds[0].StreamSettings.RealitySettings = &RealitySettings{
|
||||
ServerName: getParam(params, "sni", server),
|
||||
Fingerprint: getParam(params, "fp", "chrome"),
|
||||
Show: false,
|
||||
PublicKey: params["pbk"],
|
||||
ShortID: params["sid"],
|
||||
SpiderX: params["spx"],
|
||||
}
|
||||
}
|
||||
|
||||
// Настройки транспорта
|
||||
network := getParam(params, "type", "tcp")
|
||||
if network == "ws" {
|
||||
headers := make(map[string]string)
|
||||
if host := params["host"]; host != "" {
|
||||
headers["Host"] = host
|
||||
}
|
||||
cfg.Outbounds[0].StreamSettings.WSSettings = &WSSettings{
|
||||
Path: getParam(params, "path", "/"),
|
||||
Headers: headers,
|
||||
}
|
||||
} else if network == "grpc" {
|
||||
cfg.Outbounds[0].StreamSettings.GRPCSettings = &GRPCSettings{
|
||||
ServiceName: getParam(params, "serviceName", params["path"]),
|
||||
MultiMode: params["mode"] == "multi",
|
||||
}
|
||||
} else if network == "h2" || network == "http" {
|
||||
cfg.Outbounds[0].StreamSettings.Network = "http"
|
||||
cfg.Outbounds[0].StreamSettings.HTTPSettings = &HTTPSettings{
|
||||
Path: []string{getParam(params, "path", "/")},
|
||||
Host: []string{getParam(params, "host", server)},
|
||||
}
|
||||
}
|
||||
|
||||
// Логируем информацию
|
||||
logFile := logger.GetLogPath(logsDir, "vless")
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Создание конфига для сервера: %s:%d", server, port))
|
||||
logger.LogMessage(logFile, fmt.Sprintf("UUID: %s", uuid))
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Транспорт: %s", network))
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Безопасность: %s", security))
|
||||
if name != "" {
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Имя: %s", name))
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getParam(params map[string]string, key, defaultValue string) string {
|
||||
if val, ok := params[key]; ok && val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Connect подключается к VLESS серверу
|
||||
func Connect(configName string, logsDir, xrayDir string) error {
|
||||
// Загружаем конфигурации
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигураций: %w", err)
|
||||
}
|
||||
|
||||
// Ищем конфиг
|
||||
var vlessConfig *config.VLESSConfig
|
||||
for i := range configs.VLESS {
|
||||
if configs.VLESS[i].Name == configName {
|
||||
vlessConfig = &configs.VLESS[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if vlessConfig == nil {
|
||||
return fmt.Errorf("конфиг '%s' не найден", configName)
|
||||
}
|
||||
|
||||
// Создаем конфигурацию Xray
|
||||
xrayConfig, err := ParseVLESSURL(vlessConfig.URL, logsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка парсинга VLESS URL: %w", err)
|
||||
}
|
||||
|
||||
// Сохраняем конфигурацию
|
||||
configPath := filepath.Join(config.ConfigDir, "xray_config.json")
|
||||
data, err := json.MarshalIndent(xrayConfig, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка сериализации конфига: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения конфига: %w", err)
|
||||
}
|
||||
|
||||
// Путь к xray
|
||||
xrayExe := "xray"
|
||||
if runtime.GOOS == "windows" {
|
||||
xrayExe = "xray.exe"
|
||||
}
|
||||
xrayPath := filepath.Join(xrayDir, xrayExe)
|
||||
|
||||
if _, err := os.Stat(xrayPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("xray не найден в %s", xrayDir)
|
||||
}
|
||||
|
||||
// Создаем лог-файл трафика
|
||||
trafficLog := logger.GetTrafficLogPath(logsDir)
|
||||
logFile, err := os.Create(trafficLog)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка создания лог-файла: %w", err)
|
||||
}
|
||||
|
||||
// Записываем заголовок
|
||||
fmt.Fprintf(logFile, "=== VPN Подключение начато: %s ===\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(logFile, "Конфиг: %s\n", configName)
|
||||
fmt.Fprintf(logFile, "Клиент: Xray\n")
|
||||
fmt.Fprintf(logFile, "Прокси: 127.0.0.1:10808\n")
|
||||
fmt.Fprintf(logFile, "%s\n\n", strings.Repeat("=", 60))
|
||||
|
||||
// Запускаем xray
|
||||
cmd := exec.Command(xrayPath, "run", "-c", configPath)
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return fmt.Errorf("ошибка запуска xray: %w", err)
|
||||
}
|
||||
|
||||
// Ждем немного для проверки запуска
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Проверяем, что процесс еще работает
|
||||
if err := cmd.Process.Signal(os.Signal(nil)); err != nil {
|
||||
logFile.Close()
|
||||
return fmt.Errorf("процесс xray завершился с ошибкой")
|
||||
}
|
||||
|
||||
// Сохраняем состояние
|
||||
state := &config.ConnectionState{
|
||||
Connected: true,
|
||||
ConfigName: configName,
|
||||
ConfigType: "vless",
|
||||
StartTime: time.Now().Format(time.RFC3339),
|
||||
Interface: "xray",
|
||||
ProcessPID: cmd.Process.Pid,
|
||||
LogFile: trafficLog,
|
||||
}
|
||||
|
||||
if err := config.SaveState(state); err != nil {
|
||||
cmd.Process.Kill()
|
||||
logFile.Close()
|
||||
return fmt.Errorf("ошибка сохранения состояния: %w", err)
|
||||
}
|
||||
|
||||
// Логируем успешное подключение
|
||||
logPath := logger.GetLogPath(logsDir, "vless")
|
||||
logger.LogMessage(logPath, fmt.Sprintf("Успешно подключено к '%s' (PID: %d, Лог: %s)", configName, cmd.Process.Pid, trafficLog))
|
||||
|
||||
fmt.Printf("✓ Подключено к '%s'\n", configName)
|
||||
fmt.Printf("SOCKS5 прокси: 127.0.0.1:10808\n")
|
||||
fmt.Printf("\nЛоги трафика:\n")
|
||||
fmt.Printf(" - Основной: %s\n", trafficLog)
|
||||
fmt.Printf(" - Доступ (IP): %s\n", filepath.Join(logsDir, "vless_access.log"))
|
||||
fmt.Printf(" - Ошибки: %s\n", filepath.Join(logsDir, "vless_error.log"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PingServer проверяет доступность VLESS сервера
|
||||
func PingServer(vlessURL string, timeout time.Duration) (bool, float64, error) {
|
||||
// Парсим URL для получения адреса сервера
|
||||
urlStr := strings.TrimPrefix(vlessURL, "vless://")
|
||||
|
||||
if idx := strings.Index(urlStr, "#"); idx != -1 {
|
||||
urlStr = urlStr[:idx]
|
||||
}
|
||||
|
||||
if idx := strings.Index(urlStr, "?"); idx != -1 {
|
||||
urlStr = urlStr[:idx]
|
||||
}
|
||||
|
||||
parts := strings.Split(urlStr, "@")
|
||||
if len(parts) != 2 {
|
||||
return false, 0, fmt.Errorf("неверный формат URL")
|
||||
}
|
||||
|
||||
serverPort := parts[1]
|
||||
var server string
|
||||
var port string
|
||||
|
||||
if strings.Contains(serverPort, "[") {
|
||||
endIdx := strings.Index(serverPort, "]")
|
||||
server = serverPort[1:endIdx]
|
||||
port = strings.TrimPrefix(serverPort[endIdx+1:], ":")
|
||||
} else {
|
||||
lastColon := strings.LastIndex(serverPort, ":")
|
||||
server = serverPort[:lastColon]
|
||||
port = serverPort[lastColon+1:]
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
|
||||
// Измеряем время подключения
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(server, port), timeout)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
return true, float64(elapsed.Milliseconds()), nil
|
||||
}
|
||||
143
internal/vpn/vpn.go
Normal file
143
internal/vpn/vpn.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package vpn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"vpn-client/internal/config"
|
||||
"vpn-client/internal/logger"
|
||||
"vpn-client/internal/wireguard"
|
||||
)
|
||||
|
||||
// Disconnect отключает VPN
|
||||
func Disconnect(logsDir string) error {
|
||||
// Загружаем состояние
|
||||
state, err := config.LoadState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки состояния: %w", err)
|
||||
}
|
||||
|
||||
if !state.Connected {
|
||||
return fmt.Errorf("VPN не подключен")
|
||||
}
|
||||
|
||||
fmt.Printf("Отключение от '%s'...\n", state.ConfigName)
|
||||
|
||||
// Логируем отключение
|
||||
var logFile string
|
||||
if state.ConfigType == "wireguard" {
|
||||
logFile = logger.GetLogPath(logsDir, "wireguard")
|
||||
} else if state.ConfigType == "vless" {
|
||||
logFile = logger.GetLogPath(logsDir, "vless")
|
||||
}
|
||||
|
||||
if logFile != "" {
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Начало отключения от '%s'", state.ConfigName))
|
||||
}
|
||||
|
||||
// Останавливаем процесс в зависимости от типа
|
||||
if state.ConfigType == "wireguard" {
|
||||
// Отключаем WireGuard
|
||||
if err := wireguard.Disconnect(state.Interface, logsDir); err != nil {
|
||||
fmt.Printf("%s Ошибка отключения WireGuard: %v\n", "⚠", err)
|
||||
}
|
||||
} else if state.ProcessPID > 0 {
|
||||
// Останавливаем процесс VLESS
|
||||
process, err := os.FindProcess(state.ProcessPID)
|
||||
if err == nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
// На Windows используем taskkill
|
||||
process.Kill()
|
||||
} else {
|
||||
// На Unix используем SIGTERM
|
||||
process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
||||
// Ждем завершения процесса
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if logFile != "" {
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Отключено от '%s' (PID: %d)", state.ConfigName, state.ProcessPID))
|
||||
}
|
||||
|
||||
if state.LogFile != "" && logFile != "" {
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Лог трафика сохранен: %s", state.LogFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сбрасываем состояние
|
||||
newState := &config.ConnectionState{
|
||||
Connected: false,
|
||||
ConfigName: "",
|
||||
ConfigType: "",
|
||||
StartTime: "",
|
||||
Interface: "",
|
||||
ProcessPID: 0,
|
||||
LogFile: "",
|
||||
}
|
||||
|
||||
if err := config.SaveState(newState); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения состояния: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Отключено от VPN")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStatus возвращает текущий статус подключения
|
||||
func GetStatus() (*config.ConnectionState, error) {
|
||||
return config.LoadState()
|
||||
}
|
||||
|
||||
// ShowStatus отображает детальный статус подключения
|
||||
func ShowStatus() error {
|
||||
state, err := GetStatus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения статуса: %w", err)
|
||||
}
|
||||
|
||||
if !state.Connected {
|
||||
fmt.Println("\n❌ VPN не подключен")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("\n" + "==================================================")
|
||||
fmt.Println("📊 Статус VPN")
|
||||
fmt.Println("==================================================")
|
||||
fmt.Printf("Статус: ✓ Подключено\n")
|
||||
fmt.Printf("Конфиг: %s\n", state.ConfigName)
|
||||
fmt.Printf("Тип: %s\n", state.ConfigType)
|
||||
|
||||
if state.StartTime != "" {
|
||||
startTime, err := time.Parse(time.RFC3339, state.StartTime)
|
||||
if err == nil {
|
||||
duration := time.Since(startTime)
|
||||
hours := int(duration.Hours())
|
||||
minutes := int(duration.Minutes()) % 60
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
fmt.Printf("Время подключения: %02d:%02d:%02d\n", hours, minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
if state.ConfigType == "vless" {
|
||||
fmt.Printf("Прокси: 127.0.0.1:10808\n")
|
||||
if state.LogFile != "" {
|
||||
fmt.Printf("Лог трафика: %s\n", state.LogFile)
|
||||
}
|
||||
} else if state.ConfigType == "wireguard" {
|
||||
// Получаем статистику WireGuard
|
||||
stats, err := wireguard.GetStats(state.Interface)
|
||||
if err == nil {
|
||||
fmt.Printf("\nСтатистика трафика:\n")
|
||||
fmt.Printf(" Получено: %s\n", stats["rx"])
|
||||
fmt.Printf(" Отправлено: %s\n", stats["tx"])
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("==================================================")
|
||||
return nil
|
||||
}
|
||||
301
internal/wireguard/wireguard.go
Normal file
301
internal/wireguard/wireguard.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package wireguard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"vpn-client/internal/config"
|
||||
"vpn-client/internal/logger"
|
||||
)
|
||||
|
||||
// Connect подключается к WireGuard серверу
|
||||
func Connect(configName string, logsDir string) error {
|
||||
// Загружаем конфигурации
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигураций: %w", err)
|
||||
}
|
||||
|
||||
// Ищем конфиг
|
||||
var wgConfig *config.WireGuardConfig
|
||||
for i := range configs.WireGuard {
|
||||
if configs.WireGuard[i].Name == configName {
|
||||
wgConfig = &configs.WireGuard[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if wgConfig == nil {
|
||||
return fmt.Errorf("конфиг '%s' не найден", configName)
|
||||
}
|
||||
|
||||
// Создаем файл конфига
|
||||
interfaceName := strings.ReplaceAll(configName, " ", "_")
|
||||
configPath := filepath.Join(config.ConfigDir, interfaceName+".conf")
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(wgConfig.Config), 0600); err != nil {
|
||||
return fmt.Errorf("ошибка создания файла конфига: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Подключение к WireGuard '%s'...\n", configName)
|
||||
|
||||
logFile := logger.GetLogPath(logsDir, "wireguard")
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Начало подключения к '%s'", configName))
|
||||
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Для Windows используем wireguard.exe
|
||||
wgPaths := []string{
|
||||
`C:\Program Files\WireGuard\wireguard.exe`,
|
||||
`C:\Program Files (x86)\WireGuard\wireguard.exe`,
|
||||
}
|
||||
|
||||
var wgExe string
|
||||
for _, path := range wgPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
wgExe = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if wgExe == "" {
|
||||
return fmt.Errorf("WireGuard не найден. Установите WireGuard для Windows\nСкачать: https://www.wireguard.com/install/")
|
||||
}
|
||||
|
||||
// Импортируем туннель
|
||||
cmd = exec.Command(wgExe, "/installtunnelservice", configPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Ошибка подключения к '%s': %s", configName, string(output)))
|
||||
return fmt.Errorf("ошибка подключения: %s", string(output))
|
||||
}
|
||||
|
||||
// Сохраняем состояние
|
||||
state := &config.ConnectionState{
|
||||
Connected: true,
|
||||
ConfigName: configName,
|
||||
ConfigType: "wireguard",
|
||||
StartTime: time.Now().Format(time.RFC3339),
|
||||
Interface: interfaceName,
|
||||
ProcessPID: 0, // WireGuard на Windows работает как служба
|
||||
LogFile: "",
|
||||
}
|
||||
|
||||
if err := config.SaveState(state); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения состояния: %w", err)
|
||||
}
|
||||
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Успешно подключено к '%s'", configName))
|
||||
fmt.Printf("✓ Подключено к '%s'\n", configName)
|
||||
|
||||
} else {
|
||||
// Для Linux/Mac используем wg-quick
|
||||
cmd = exec.Command("sudo", "wg-quick", "up", configPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Ошибка подключения к '%s': %s", configName, string(output)))
|
||||
return fmt.Errorf("ошибка подключения: %s", string(output))
|
||||
}
|
||||
|
||||
// Сохраняем состояние
|
||||
state := &config.ConnectionState{
|
||||
Connected: true,
|
||||
ConfigName: configName,
|
||||
ConfigType: "wireguard",
|
||||
StartTime: time.Now().Format(time.RFC3339),
|
||||
Interface: interfaceName,
|
||||
ProcessPID: 0,
|
||||
LogFile: "",
|
||||
}
|
||||
|
||||
if err := config.SaveState(state); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения состояния: %w", err)
|
||||
}
|
||||
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Успешно подключено к '%s'", configName))
|
||||
fmt.Printf("✓ Подключено к '%s'\n", configName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect отключается от WireGuard
|
||||
func Disconnect(interfaceName, logsDir string) error {
|
||||
logFile := logger.GetLogPath(logsDir, "wireguard")
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Начало отключения от '%s'", interfaceName))
|
||||
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
wgPaths := []string{
|
||||
`C:\Program Files\WireGuard\wireguard.exe`,
|
||||
`C:\Program Files (x86)\WireGuard\wireguard.exe`,
|
||||
}
|
||||
|
||||
var wgExe string
|
||||
for _, path := range wgPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
wgExe = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if wgExe != "" {
|
||||
cmd = exec.Command(wgExe, "/uninstalltunnelservice", interfaceName)
|
||||
cmd.Run()
|
||||
}
|
||||
} else {
|
||||
configPath := filepath.Join(config.ConfigDir, interfaceName+".conf")
|
||||
cmd = exec.Command("sudo", "wg-quick", "down", configPath)
|
||||
cmd.Run()
|
||||
}
|
||||
|
||||
logger.LogMessage(logFile, fmt.Sprintf("Отключено от '%s'", interfaceName))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats получает статистику WireGuard
|
||||
func GetStats(interfaceName string) (map[string]string, error) {
|
||||
stats := map[string]string{
|
||||
"rx": "N/A",
|
||||
"tx": "N/A",
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Для Windows пытаемся получить статистику через wg.exe
|
||||
wgPaths := []string{
|
||||
`C:\Program Files\WireGuard\wg.exe`,
|
||||
`C:\Program Files (x86)\WireGuard\wg.exe`,
|
||||
}
|
||||
|
||||
var wgExe string
|
||||
for _, path := range wgPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
wgExe = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if wgExe != "" {
|
||||
cmd := exec.Command(wgExe, "show", interfaceName, "transfer")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(output) > 0 {
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 3 {
|
||||
rx := parseBytes(parts[1])
|
||||
tx := parseBytes(parts[2])
|
||||
stats["rx"] = formatBytes(rx)
|
||||
stats["tx"] = formatBytes(tx)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Для Linux/Mac
|
||||
cmd := exec.Command("wg", "show", interfaceName, "transfer")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(output) > 0 {
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(lines) > 0 {
|
||||
parts := strings.Fields(lines[0])
|
||||
if len(parts) >= 2 {
|
||||
rx := parseBytes(parts[0])
|
||||
tx := parseBytes(parts[1])
|
||||
stats["rx"] = formatBytes(rx)
|
||||
stats["tx"] = formatBytes(tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// AddConfig добавляет WireGuard конфигурацию
|
||||
func AddConfig(name, configText string) error {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигураций: %w", err)
|
||||
}
|
||||
|
||||
configs.WireGuard = append(configs.WireGuard, config.WireGuardConfig{
|
||||
Name: name,
|
||||
Config: configText,
|
||||
})
|
||||
|
||||
if err := config.SaveConfigs(configs); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения конфигураций: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddConfigFromFile добавляет WireGuard конфигурацию из файла
|
||||
func AddConfigFromFile(name, filePath string) error {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка чтения файла: %w", err)
|
||||
}
|
||||
|
||||
return AddConfig(name, string(data))
|
||||
}
|
||||
|
||||
// DeleteConfig удаляет WireGuard конфигурацию
|
||||
func DeleteConfig(name string) error {
|
||||
configs, err := config.LoadConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигураций: %w", err)
|
||||
}
|
||||
|
||||
var filtered []config.WireGuardConfig
|
||||
found := false
|
||||
for _, cfg := range configs.WireGuard {
|
||||
if cfg.Name != name {
|
||||
filtered = append(filtered, cfg)
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("конфиг '%s' не найден", name)
|
||||
}
|
||||
|
||||
configs.WireGuard = filtered
|
||||
|
||||
if err := config.SaveConfigs(configs); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения конфигураций: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBytes(s string) int64 {
|
||||
var val int64
|
||||
fmt.Sscanf(s, "%d", &val)
|
||||
return val
|
||||
}
|
||||
|
||||
func formatBytes(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
23
main.go
Normal file
23
main.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"vpn-client/internal/cli"
|
||||
"vpn-client/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Инициализация конфигурации
|
||||
if err := config.Init(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Ошибка инициализации: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Запуск CLI
|
||||
if err := cli.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user