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