Initial commit

This commit is contained in:
2026-04-05 20:33:30 +06:00
commit 83fbe7afdd
18 changed files with 3038 additions and 0 deletions

864
internal/cli/cli.go Normal file
View 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
View 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
View 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))
}

View 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
View 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
View 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
}

View 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])
}