commit 0e93af1d8c43bad5ad53ccaf06ba89df67b58851 Author: Falknat Date: Thu Oct 16 16:27:36 2025 +0700 Инициализация проекта Всем привет :) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fea9bb5 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# WG_SERF - WireGuard Server Panel + +Современная веб-панель для управления WireGuard VPN серверами. Всё в одном файле. + +## 🎯 Что умеет + +- 🌐 **Создавайте VPN серверы** - несколько серверов на одной машине (wg0, wg1...) +- 👥 **Управляйте клиентами** - добавляйте пользователей в пару кликов +- 📱 **QR коды** - клиент отсканирует и подключится за 10 секунд +- 🔀 **Проброс портов** - открывайте порты клиентам (SSH, RDP, игры) +- 📊 **Мониторинг в реальном времени** - кто онлайн, сколько трафика +- 🌓 **Темная и Светлая тема** - приятно работать ночью и днём +- ⚙️ **Автонастройка** - подсети, порты, IP - всё автоматически + +## 🚀 Быстрый старт + +**Скачайте и установите одной командой:** + +1. Нужно зайти на сервер под root + +```bash +wget https://vserf.ru/download/wgserf/wg_serf && chmod +x wg_serf && ./wg_serf +``` + +## 📋 Команды + +```bash +wg_serf # Показать информацию и статус +wg_serf install # Установить (только один раз) +wg_serf start # Запустить +wg_serf stop # Остановить +wg_serf restart # Перезапустить +wg_serf status # Статус сервиса +wg_serf delete # Полностью удалить +``` + +## 🔧 Разработка + +**Структура проекта:** +``` +WG_Serv/ +├── main.go # Точка входа + CLI +├── go.mod / go.sum +├── build/ +│ └── wg_serf # Скомпилированный бинарник +└── internal/ + ├── server/ # HTTP + handlers + embed HTML + ├── wireguard/ # Логика WireGuard + iptables + └── database/ # БД, конфиг, типы, утилиты +``` + +**Компиляция:** +```bash +# PowerShell (Windows) +$env:GOOS="linux"; $env:GOARCH="amd64"; go build -ldflags="-s -w" -o build/wg_serf . + +# Bash (Linux/Mac) +GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o build/wg_serf . +``` + +**Зависимости:** +- `github.com/skip2/go-qrcode` - генерация QR кодов +- Всё остальное - встроенные библиотеки Go + +## 🔄 Как это работает + +1. **Синхронизация:** При запуске БД синхронизируется с WireGuard (удаляет лишние интерфейсы, создает нужные) +2. **Статистика:** Обновляется каждые 5 секунд +3. **Онлайн/офлайн:** Клиент онлайн если handshake < 30 секунд (PersistentKeepalive = 10 сек) +4. **Автоперезапуск:** При сбое systemd автоматически перезапустит +5. **Пробросы портов:** Применяются автоматически через iptables +6. **Автоматический IPtables** Автоматически очищает и заполняет при старте сервера IpTables + +## 📁 Файлы +После установки в `/opt/wg_serf/`: +- `wg_serf` - бинарник +- `config.json` - настройки (порт, логин, пароль) +- `db.json` - база данных (серверы, клиенты) +- `wg_serf.pid` - PID запущенного процесса + +## 🛡️ Безопасность + +- Cookie-based авторизация (24 часа) +- Проверка уникальности портов и подсетей +- Безопасные имена файлов +- Работает только под root + +## 👨‍💻 Автор + +Создано для удобного управления WireGuard VPN + +**Разработчик:** [voxsel.com](https://voxsel.com/) +**Отличный WebServer для Windows:** [vserf.ru](https://vserf.ru/) \ No newline at end of file diff --git a/build/wg_serf b/build/wg_serf new file mode 100644 index 0000000..86173ee Binary files /dev/null and b/build/wg_serf differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b429776 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module wg-panel + +go 1.21 + +require github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e99b5b9 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= diff --git a/internal/database/config.go b/internal/database/config.go new file mode 100644 index 0000000..82daa81 --- /dev/null +++ b/internal/database/config.go @@ -0,0 +1,43 @@ +package database + +import ( + "encoding/json" + "os" +) + +const configFile = "/opt/wg_serf/config.json" + +// LoadConfig загружает конфигурацию из config.json +func LoadConfig() (*Config, error) { + if _, err := os.Stat(configFile); os.IsNotExist(err) { + // Создаем дефолтную конфигурацию + config := Config{ + Port: "8080", + Address: "0.0.0.0", + Username: "admin", + Password: "admin", + } + if err := SaveConfig(&config); err != nil { + return nil, err + } + return &config, nil + } + + data, err := os.ReadFile(configFile) + if err != nil { + return nil, err + } + + var config Config + err = json.Unmarshal(data, &config) + return &config, err +} + +// SaveConfig сохраняет конфигурацию в config.json +func SaveConfig(config *Config) error { + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + return os.WriteFile(configFile, data, 0644) +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..4693b23 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,28 @@ +package database + +import ( + "encoding/json" + "os" +) + +const dbFile = "/opt/wg_serf/db.json" + +// LoadDatabase загружает базу данных из db.json +func LoadDatabase() (*Database, error) { + var db Database + data, err := os.ReadFile(dbFile) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &db) + return &db, err +} + +// SaveDatabase сохраняет базу данных в db.json +func SaveDatabase(db *Database) error { + data, err := json.MarshalIndent(db, "", " ") + if err != nil { + return err + } + return os.WriteFile(dbFile, data, 0644) +} diff --git a/internal/database/helpers.go b/internal/database/helpers.go new file mode 100644 index 0000000..975cf06 --- /dev/null +++ b/internal/database/helpers.go @@ -0,0 +1,33 @@ +package database + +import ( + "regexp" + "strings" +) + +// SanitizeFilename очищает имя файла от недопустимых символов +func SanitizeFilename(name string) string { + // Заменяем пробелы на подчеркивания + name = strings.ReplaceAll(name, " ", "_") + + // Заменяем тире и другие символы на подчеркивания + name = strings.ReplaceAll(name, "-", "_") + + // Удаляем все символы кроме букв, цифр и подчеркиваний + reg := regexp.MustCompile(`[^a-zA-Z0-9_а-яА-ЯёЁ]`) + name = reg.ReplaceAllString(name, "_") + + // Убираем множественные подчеркивания + reg = regexp.MustCompile(`_+`) + name = reg.ReplaceAllString(name, "_") + + // Убираем подчеркивания в начале и конце + name = strings.Trim(name, "_") + + // Если имя пустое - возвращаем дефолтное + if name == "" { + name = "client" + } + + return name +} diff --git a/internal/database/pidfile.go b/internal/database/pidfile.go new file mode 100644 index 0000000..3f6df19 --- /dev/null +++ b/internal/database/pidfile.go @@ -0,0 +1,121 @@ +package database + +import ( + "fmt" + "log" + "os" + "os/exec" + "strconv" + "strings" + "syscall" +) + +const pidFile = "/opt/wg_serf/wg_serf.pid" + +// CheckAndKillOldProcess проверяет и завершает старый процесс +func CheckAndKillOldProcess() error { + // Проверяем существует ли PID файл + if _, err := os.Stat(pidFile); os.IsNotExist(err) { + // Файла нет - это первый запуск + return nil + } + + // Читаем PID из файла + data, err := os.ReadFile(pidFile) + if err != nil { + log.Println("⚠️ Не удалось прочитать PID файл:", err) + return nil + } + + oldPID, err := strconv.Atoi(strings.TrimSpace(string(data))) + if err != nil { + log.Println("⚠️ Некорректный PID в файле:", err) + os.Remove(pidFile) + return nil + } + + // Проверяем существует ли процесс + if !processExists(oldPID) { + log.Println("📝 Старый процесс не найден, очищаю PID файл") + os.Remove(pidFile) + return nil + } + + // Проверяем что это действительно wg-panel + if !isWGPanelProcess(oldPID) { + log.Println("⚠️ PID принадлежит другому процессу, очищаю файл") + os.Remove(pidFile) + return nil + } + + // Завершаем старый процесс + log.Printf("🔄 Обнаружена запущенная версия (PID: %d), завершаю...", oldPID) + if err := killProcess(oldPID); err != nil { + return fmt.Errorf("не удалось завершить старый процесс: %v", err) + } + + log.Println("✅ Старый процесс завершен") + os.Remove(pidFile) + return nil +} + +// WritePIDFile записывает PID текущего процесса в файл +func WritePIDFile() error { + pid := os.Getpid() + return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", pid)), 0644) +} + +// RemovePIDFile удаляет PID файл +func RemovePIDFile() { + os.Remove(pidFile) +} + +// processExists проверяет существует ли процесс с данным PID +func processExists(pid int) bool { + process, err := os.FindProcess(pid) + if err != nil { + return false + } + + // Отправляем сигнал 0 для проверки существования процесса + err = process.Signal(syscall.Signal(0)) + return err == nil +} + +// isWGPanelProcess проверяет что процесс действительно wg-panel +func isWGPanelProcess(pid int) bool { + // Читаем командную строку процесса + cmdline, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + if err != nil { + return false + } + + // Проверяем что в командной строке есть wg-panel + return strings.Contains(string(cmdline), "wg-panel") +} + +// killProcess завершает процесс +func killProcess(pid int) error { + process, err := os.FindProcess(pid) + if err != nil { + return err + } + + // Сначала пробуем мягко (SIGTERM) + if err := process.Signal(syscall.SIGTERM); err != nil { + return err + } + + // Ждем немного + cmd := exec.Command("sleep", "1") + cmd.Run() + + // Проверяем завершился ли процесс + if processExists(pid) { + // Если нет - убиваем принудительно (SIGKILL) + log.Println("⚠️ Процесс не завершился, использую SIGKILL") + return process.Signal(syscall.SIGKILL) + } + + return nil +} diff --git a/internal/database/types.go b/internal/database/types.go new file mode 100644 index 0000000..7888b66 --- /dev/null +++ b/internal/database/types.go @@ -0,0 +1,59 @@ +package database + +import "time" + +// Config структура для конфигурации приложения +type Config struct { + Port string `json:"port"` + Address string `json:"address"` + Username string `json:"username"` + Password string `json:"password"` +} + +// Server структура для WireGuard сервера +type Server struct { + ID string `json:"id"` + Name string `json:"name"` + Interface string `json:"interface"` + PrivateKey string `json:"private_key"` + PublicKey string `json:"public_key"` + Address string `json:"address"` + ListenPort int `json:"listen_port"` + DNS string `json:"dns"` + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"created_at"` + PostUp string `json:"post_up"` + PostDown string `json:"post_down"` + NextClientIP int `json:"next_client_ip"` +} + +// PortForward структура для проброса порта +type PortForward struct { + Port int `json:"port"` // Порт (одинаковый внешний и внутренний) + Protocol string `json:"protocol"` // tcp, udp или both + Description string `json:"description"` // Описание +} + +// Client структура для клиента WireGuard +type Client struct { + ID string `json:"id"` + ServerID string `json:"server_id"` + Name string `json:"name"` + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` + Address string `json:"address"` + Enabled bool `json:"enabled"` + Comment string `json:"comment"` + CreatedAt time.Time `json:"created_at"` + RxBytes int64 `json:"rx_bytes"` + TxBytes int64 `json:"tx_bytes"` + LastHandshake time.Time `json:"last_handshake"` + Endpoint string `json:"endpoint"` // IP:Port клиента + PortForwards []PortForward `json:"port_forwards"` +} + +// Database структура для хранения данных +type Database struct { + Servers []Server `json:"servers"` + Clients []Client `json:"clients"` +} diff --git a/internal/database/utils.go b/internal/database/utils.go new file mode 100644 index 0000000..af72789 --- /dev/null +++ b/internal/database/utils.go @@ -0,0 +1,144 @@ +package database + +import ( + "fmt" + "io" + "log" + "net" + "net/http" + "os/exec" + "strings" +) + +// CheckWireGuardInstalled проверяет установлен ли WireGuard +func CheckWireGuardInstalled() bool { + cmd := exec.Command("which", "wg") + err := cmd.Run() + return err == nil +} + +// GenerateKeys генерирует пару ключей WireGuard +func GenerateKeys() (privateKey, publicKey string, err error) { + // Генерируем приватный ключ + cmd := exec.Command("wg", "genkey") + privateKeyBytes, err := cmd.Output() + if err != nil { + return "", "", err + } + privateKey = strings.TrimSpace(string(privateKeyBytes)) + + // Генерируем публичный ключ из приватного + cmd = exec.Command("wg", "pubkey") + cmd.Stdin = strings.NewReader(privateKey) + publicKeyBytes, err := cmd.Output() + if err != nil { + return "", "", err + } + publicKey = strings.TrimSpace(string(publicKeyBytes)) + + return privateKey, publicKey, nil +} + +// GetNextClientIP возвращает следующий доступный IP адрес для клиента +func GetNextClientIP(server *Server) string { + // Парсим адрес сервера (например 10.0.0.1/24) + parts := strings.Split(server.Address, "/") + if len(parts) != 2 { + return "" + } + + ipParts := strings.Split(parts[0], ".") + if len(ipParts) != 4 { + return "" + } + + // Формируем IP клиента + ip := fmt.Sprintf("%s.%s.%s.%d", ipParts[0], ipParts[1], ipParts[2], server.NextClientIP) + server.NextClientIP++ + return ip +} + +// GetServerEndpoint получает внешний IP адрес сервера +func GetServerEndpoint() string { + // Пробуем получить внешний IP + resp, err := http.Get("https://api.ipify.org") + if err == nil { + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err == nil { + return string(body) + } + } + + // Если не получилось, возвращаем локальный IP + return GetLocalIP() +} + +// GetLocalIP получает локальный IP адрес +func GetLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "127.0.0.1" + } + + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + + return "127.0.0.1" +} + +// GetDefaultInterface получает основной сетевой интерфейс для интернета +func GetDefaultInterface() string { + // Пытаемся определить через маршруты + cmd := exec.Command("sh", "-c", "ip route | grep default | awk '{print $5}' | head -n1") + output, err := cmd.Output() + if err == nil && len(output) > 0 { + iface := strings.TrimSpace(string(output)) + if iface != "" { + return iface + } + } + + // Пробуем альтернативный способ + cmd = exec.Command("sh", "-c", "ip -4 route ls | grep default | grep -Po '(?<=dev )\\S+' | head -n1") + output, err = cmd.Output() + if err == nil && len(output) > 0 { + iface := strings.TrimSpace(string(output)) + if iface != "" { + return iface + } + } + + // Возвращаем eth0 по умолчанию + log.Println("⚠️ Не удалось определить сетевой интерфейс, использую eth0") + return "eth0" +} + +// EnableIPForwarding включает IP forwarding в системе +func EnableIPForwarding() error { + // Временно включаем + cmd := exec.Command("sysctl", "-w", "net.ipv4.ip_forward=1") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("sysctl failed: %v, output: %s", err, string(output)) + } + + // Проверяем что включилось + cmd = exec.Command("cat", "/proc/sys/net/ipv4/ip_forward") + output, err = cmd.Output() + if err == nil { + value := strings.TrimSpace(string(output)) + if value != "1" { + return fmt.Errorf("IP forwarding не включился, значение: %s", value) + } + } + + // Делаем постоянным + cmd = exec.Command("sh", "-c", "grep -q 'net.ipv4.ip_forward' /etc/sysctl.conf || echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf") + return cmd.Run() +} diff --git a/internal/database/validation.go b/internal/database/validation.go new file mode 100644 index 0000000..1630911 --- /dev/null +++ b/internal/database/validation.go @@ -0,0 +1,86 @@ +package database + +import ( + "fmt" + "strings" +) + +// GetNextAvailableNetwork возвращает следующую доступную подсеть +func GetNextAvailableNetwork(db *Database) string { + // Начинаем с 10.0.0.1/24 + for i := 10; i < 255; i++ { + network := fmt.Sprintf("%d.0.0.1/24", i) + if IsNetworkAvailable(db, network) { + return network + } + } + return "10.0.0.1/24" // fallback +} + +// GetNextAvailablePort возвращает следующий доступный порт +func GetNextAvailablePort(db *Database) int { + // Начинаем с 50000 + for port := 50000; port < 65535; port++ { + if IsPortAvailableForServer(db, port) { + return port + } + } + return 50000 // fallback +} + +// IsNetworkAvailable проверяет свободна ли подсеть +func IsNetworkAvailable(db *Database, network string) bool { + // Извлекаем второй октет для сравнения + // 10.0.0.1/24 -> 10 + // 11.0.0.1/24 -> 11 + parts := strings.Split(network, ".") + if len(parts) < 2 { + return false + } + + networkPrefix := parts[0] + "." + parts[1] + + for _, server := range db.Servers { + serverParts := strings.Split(server.Address, ".") + if len(serverParts) < 2 { + continue + } + serverPrefix := serverParts[0] + "." + serverParts[1] + + if networkPrefix == serverPrefix { + return false + } + } + + return true +} + +// IsPortAvailableForServer проверяет свободен ли порт для сервера +func IsPortAvailableForServer(db *Database, port int) bool { + for _, server := range db.Servers { + if server.ListenPort == port { + return false + } + } + return true +} + +// ValidateServerConfig проверяет корректность конфигурации сервера +func ValidateServerConfig(db *Database, address string, port int) error { + // Проверяем формат подсети + if !strings.HasSuffix(address, "/24") { + return fmt.Errorf("подсеть должна быть /24") + } + + // Проверяем что подсеть свободна + if !IsNetworkAvailable(db, address) { + return fmt.Errorf("подсеть уже используется") + } + + // Проверяем что порт свободен + if !IsPortAvailableForServer(db, port) { + return fmt.Errorf("порт %d уже используется", port) + } + + return nil +} diff --git a/internal/server/handlers.go b/internal/server/handlers.go new file mode 100644 index 0000000..2818566 --- /dev/null +++ b/internal/server/handlers.go @@ -0,0 +1,548 @@ +package server + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "strconv" + "strings" + + "wg-panel/internal/database" + "wg-panel/internal/wireguard" +) + +var ( + DB *database.Database + Config *database.Config +) + +// authMiddleware проверяет авторизацию +func authMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Проверяем cookie + cookie, err := r.Cookie("auth") + if err != nil || cookie.Value != "authenticated" { + if r.URL.Path == "/" { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next(w, r) + } +} + +// handleLogin обрабатывает страницу входа +func HandleLogin(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + tmplData, err := TemplatesFS.ReadFile("templates/login.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmpl, err := template.New("login").Parse(string(tmplData)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmpl.Execute(w, nil) + return + } + + if r.Method == "POST" { + username := r.FormValue("username") + password := r.FormValue("password") + + if username == Config.Username && password == Config.Password { + http.SetCookie(w, &http.Cookie{ + Name: "auth", + Value: "authenticated", + Path: "/", + MaxAge: 86400, // 24 часа + HttpOnly: true, + }) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + http.Redirect(w, r, "/login?error=1", http.StatusSeeOther) + } +} + +// handleLogout обрабатывает выход +func HandleLogout(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{ + Name: "auth", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + }) + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + +// handleIndex обрабатывает главную страницу +func HandleIndex(w http.ResponseWriter, r *http.Request) { + tmplData, err := TemplatesFS.ReadFile("templates/index.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmpl, err := template.New("index").Parse(string(tmplData)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmpl.Execute(w, nil) +} + +// === ОБРАБОТЧИКИ СЕРВЕРОВ === + +// HandleServers возвращает список серверов +func HandleServers(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if DB.Servers == nil { + json.NewEncoder(w).Encode([]database.Server{}) + } else { + json.NewEncoder(w).Encode(DB.Servers) + } +} + +// HandleCreateServer создает новый WireGuard сервер +func HandleCreateServer(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + name := r.FormValue("name") + address := r.FormValue("address") + portStr := r.FormValue("port") + dns := r.FormValue("dns") + + if name == "" || address == "" || portStr == "" { + http.Error(w, "Missing required fields", http.StatusBadRequest) + return + } + + // Добавляем /24 если не указано + if !strings.Contains(address, "/") { + address = address + "/24" + } + + port, err := strconv.Atoi(portStr) + if err != nil { + http.Error(w, "Invalid port", http.StatusBadRequest) + return + } + + // Валидируем конфигурацию + if err := database.ValidateServerConfig(DB, address, port); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + server, err := wireguard.CreateServer(DB, name, address, port, dns) + if err != nil { + http.Error(w, "Failed to create server: "+err.Error(), http.StatusInternalServerError) + return + } + + DB.Servers = append(DB.Servers, *server) + database.SaveDatabase(DB) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(server) +} + +// HandleUpdateServer обновляет настройки сервера +func HandleUpdateServer(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + id := r.FormValue("id") + name := r.FormValue("name") + portStr := r.FormValue("port") + dns := r.FormValue("dns") + + for i, server := range DB.Servers { + if server.ID == id { + if name != "" { + DB.Servers[i].Name = name + } + if portStr != "" { + port, err := strconv.Atoi(portStr) + if err == nil { + DB.Servers[i].ListenPort = port + } + } + if dns != "" { + DB.Servers[i].DNS = dns + } + + // Обновляем конфиг файл + wireguard.UpdateServerConfig(&DB.Servers[i], DB) + + database.SaveDatabase(DB) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(DB.Servers[i]) + return + } + } + + http.Error(w, "Server not found", http.StatusNotFound) +} + +// HandleDeleteServer удаляет сервер +func HandleDeleteServer(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + id := r.FormValue("id") + + for i, server := range DB.Servers { + if server.ID == id { + // Удаляем сервер + if err := wireguard.DeleteServer(&server); err != nil { + http.Error(w, "Failed to delete server", http.StatusInternalServerError) + return + } + + // Удаляем всех клиентов этого сервера + var newClients []database.Client + for _, client := range DB.Clients { + if client.ServerID != id { + newClients = append(newClients, client) + } + } + DB.Clients = newClients + + // Удаляем сервер из базы + DB.Servers = append(DB.Servers[:i], DB.Servers[i+1:]...) + database.SaveDatabase(DB) + + w.WriteHeader(http.StatusOK) + return + } + } + + http.Error(w, "Server not found", http.StatusNotFound) +} + +// HandleToggleServer включает/выключает сервер +func HandleToggleServer(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + id := r.FormValue("id") + + for i, server := range DB.Servers { + if server.ID == id { + if err := wireguard.ToggleServer(&DB.Servers[i]); err != nil { + http.Error(w, "Failed to toggle server: "+err.Error(), http.StatusInternalServerError) + return + } + + database.SaveDatabase(DB) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(DB.Servers[i]) + return + } + } + + http.Error(w, "Server not found", http.StatusNotFound) +} + +// === ОБРАБОТЧИКИ КЛИЕНТОВ === + +// HandleClients возвращает список клиентов +func HandleClients(w http.ResponseWriter, r *http.Request) { + serverID := r.URL.Query().Get("server_id") + + var clients []database.Client + for _, client := range DB.Clients { + if serverID == "" || client.ServerID == serverID { + clients = append(clients, client) + } + } + + w.Header().Set("Content-Type", "application/json") + if clients == nil { + json.NewEncoder(w).Encode([]database.Client{}) + } else { + json.NewEncoder(w).Encode(clients) + } +} + +// HandleCreateClient создает новый конфиг клиента +func HandleCreateClient(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + serverID := r.FormValue("server_id") + name := r.FormValue("name") + comment := r.FormValue("comment") + + if serverID == "" || name == "" { + http.Error(w, "Missing required fields", http.StatusBadRequest) + return + } + + client, err := wireguard.CreateClient(DB, serverID, name, comment) + if err != nil { + http.Error(w, "Failed to create client: "+err.Error(), http.StatusInternalServerError) + return + } + + DB.Clients = append(DB.Clients, *client) + database.SaveDatabase(DB) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(client) +} + +// HandleDeleteClient удаляет клиента +func HandleDeleteClient(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + id := r.FormValue("id") + + for i, client := range DB.Clients { + if client.ID == id { + // Удаляем клиента + if err := wireguard.DeleteClient(DB, &client); err != nil { + http.Error(w, "Failed to delete client", http.StatusInternalServerError) + return + } + + // Удаляем из базы + DB.Clients = append(DB.Clients[:i], DB.Clients[i+1:]...) + database.SaveDatabase(DB) + + w.WriteHeader(http.StatusOK) + return + } + } + + http.Error(w, "Client not found", http.StatusNotFound) +} + +// HandleToggleClient включает/выключает клиента +func HandleToggleClient(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + id := r.FormValue("id") + + for i, client := range DB.Clients { + if client.ID == id { + if err := wireguard.ToggleClient(DB, &DB.Clients[i]); err != nil { + http.Error(w, "Failed to toggle client", http.StatusInternalServerError) + return + } + + database.SaveDatabase(DB) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(DB.Clients[i]) + return + } + } + + http.Error(w, "Client not found", http.StatusNotFound) +} + +// HandleUpdateClient обновляет имя и комментарий клиента +func HandleUpdateClient(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + id := r.FormValue("id") + name := r.FormValue("name") + comment := r.FormValue("comment") + + for i, client := range DB.Clients { + if client.ID == id { + if name != "" { + DB.Clients[i].Name = name + } + DB.Clients[i].Comment = comment + database.SaveDatabase(DB) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(DB.Clients[i]) + return + } + } + + http.Error(w, "Client not found", http.StatusNotFound) +} + +// HandleAddPortForward добавляет проброс порта +func HandleAddPortForward(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + clientID := r.FormValue("client_id") + portStr := r.FormValue("port") + protocol := r.FormValue("protocol") + description := r.FormValue("description") + + port, err := strconv.Atoi(portStr) + if err != nil { + http.Error(w, "Invalid port", http.StatusBadRequest) + return + } + + if protocol != "tcp" && protocol != "udp" && protocol != "both" { + http.Error(w, "Protocol must be tcp, udp or both", http.StatusBadRequest) + return + } + + for i, client := range DB.Clients { + if client.ID == clientID { + if err := wireguard.AddPortForward(DB, &DB.Clients[i], port, protocol, description); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + database.SaveDatabase(DB) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(DB.Clients[i]) + return + } + } + + http.Error(w, "Client not found", http.StatusNotFound) +} + +// HandleRemovePortForward удаляет проброс порта +func HandleRemovePortForward(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + clientID := r.FormValue("client_id") + portStr := r.FormValue("port") + protocol := r.FormValue("protocol") + + port, err := strconv.Atoi(portStr) + if err != nil { + http.Error(w, "Invalid port", http.StatusBadRequest) + return + } + + for i, client := range DB.Clients { + if client.ID == clientID { + if err := wireguard.RemovePortForward(&DB.Clients[i], port, protocol); err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + database.SaveDatabase(DB) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(DB.Clients[i]) + return + } + } + + http.Error(w, "Client not found", http.StatusNotFound) +} + +// HandleDownloadConfig скачивает конфиг клиента +func HandleDownloadConfig(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + + for _, client := range DB.Clients { + if client.ID == id { + // Находим сервер + var server *database.Server + for i := range DB.Servers { + if DB.Servers[i].ID == client.ServerID { + server = &DB.Servers[i] + break + } + } + + if server == nil { + http.Error(w, "Server not found", http.StatusNotFound) + return + } + + config := wireguard.GenerateClientConfig(client, server) + + // Создаем безопасное имя файла (без пробелов и спецсимволов) + safeName := database.SanitizeFilename(client.Name) + + w.Header().Set("Content-Type", "application/x-wireguard-profile") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.conf", safeName)) + w.Write([]byte(config)) + return + } + } + + http.Error(w, "Client not found", http.StatusNotFound) +} + +// HandleQRCode генерирует QR код для конфига +func HandleQRCode(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + + for _, client := range DB.Clients { + if client.ID == id { + // Находим сервер + var server *database.Server + for i := range DB.Servers { + if DB.Servers[i].ID == client.ServerID { + server = &DB.Servers[i] + break + } + } + + if server == nil { + http.Error(w, "Server not found", http.StatusNotFound) + return + } + + config := wireguard.GenerateClientConfig(client, server) + + // Генерируем QR код + png, err := wireguard.GenerateQRCode(config) + if err != nil { + http.Error(w, "Failed to generate QR code", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "image/png") + w.Write(png) + return + } + } + + http.Error(w, "Client not found", http.StatusNotFound) +} + +// HandleStats возвращает статистику +func HandleStats(w http.ResponseWriter, r *http.Request) { + wireguard.UpdateStats(DB) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(DB.Clients) +} diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..f09db9b --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,34 @@ +package server + +import ( + "net/http" +) + +// SetupRoutes настраивает маршруты HTTP сервера +func SetupRoutes() { + // Главная страница + http.HandleFunc("/", authMiddleware(HandleIndex)) + + // API для серверов + http.HandleFunc("/api/servers", authMiddleware(HandleServers)) + http.HandleFunc("/api/server/create", authMiddleware(HandleCreateServer)) + http.HandleFunc("/api/server/update", authMiddleware(HandleUpdateServer)) + http.HandleFunc("/api/server/delete", authMiddleware(HandleDeleteServer)) + http.HandleFunc("/api/server/toggle", authMiddleware(HandleToggleServer)) + + // API для клиентов + http.HandleFunc("/api/clients", authMiddleware(HandleClients)) + http.HandleFunc("/api/client/create", authMiddleware(HandleCreateClient)) + http.HandleFunc("/api/client/delete", authMiddleware(HandleDeleteClient)) + http.HandleFunc("/api/client/toggle", authMiddleware(HandleToggleClient)) + http.HandleFunc("/api/client/update", authMiddleware(HandleUpdateClient)) + http.HandleFunc("/api/client/download", authMiddleware(HandleDownloadConfig)) + http.HandleFunc("/api/client/qr", authMiddleware(HandleQRCode)) + http.HandleFunc("/api/client/portforward/add", authMiddleware(HandleAddPortForward)) + http.HandleFunc("/api/client/portforward/remove", authMiddleware(HandleRemovePortForward)) + http.HandleFunc("/api/stats", authMiddleware(HandleStats)) + + // Авторизация + http.HandleFunc("/login", HandleLogin) + http.HandleFunc("/logout", HandleLogout) +} diff --git a/internal/server/static.go b/internal/server/static.go new file mode 100644 index 0000000..ce8c735 --- /dev/null +++ b/internal/server/static.go @@ -0,0 +1,6 @@ +package server + +import "embed" + +//go:embed templates/* +var TemplatesFS embed.FS diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html new file mode 100644 index 0000000..b3e2de7 --- /dev/null +++ b/internal/server/templates/index.html @@ -0,0 +1,1122 @@ + + + + + + WireGuard Panel + + + +
+

WireGuard Panel

+
+ + + Выход +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/internal/server/templates/login.html b/internal/server/templates/login.html new file mode 100644 index 0000000..75b1c1c --- /dev/null +++ b/internal/server/templates/login.html @@ -0,0 +1,130 @@ + + + + + + WireGuard Panel - Вход + + + +
+ + + + +
+
+ + +
+
+ + +
+ +
+
+ + + diff --git a/internal/wireguard/client.go b/internal/wireguard/client.go new file mode 100644 index 0000000..3b0f774 --- /dev/null +++ b/internal/wireguard/client.go @@ -0,0 +1,163 @@ +package wireguard + +import ( + "fmt" + "log" + "os/exec" + "time" + + "wg-panel/internal/database" + + "github.com/skip2/go-qrcode" +) + +// CreateClient создает нового клиента +func CreateClient(db *database.Database, serverID, name, comment string) (*database.Client, error) { + // Находим сервер + var server *database.Server + for i := range db.Servers { + if db.Servers[i].ID == serverID { + server = &db.Servers[i] + break + } + } + + if server == nil { + return nil, fmt.Errorf("server not found") + } + + // Генерируем ключи + privateKey, publicKey, err := database.GenerateKeys() + if err != nil { + return nil, err + } + + // Создаем клиента + client := database.Client{ + ID: fmt.Sprintf("%d", time.Now().UnixNano()), + ServerID: serverID, + Name: name, + PublicKey: publicKey, + PrivateKey: privateKey, + Address: database.GetNextClientIP(server), + Enabled: true, + Comment: comment, + CreatedAt: time.Now(), + } + + // Добавляем peer в WireGuard если сервер запущен + if server.Enabled { + log.Printf("➕ Добавляю peer %s в WireGuard...", client.Name) + if err := addPeerToWireGuard(server, client); err != nil { + log.Printf("⚠️ Ошибка добавления peer: %v", err) + } else { + log.Printf("✅ Peer добавлен") + } + } + + // Обновляем конфиг файл + UpdateServerConfig(server, db) + + return &client, nil +} + +// DeleteClient удаляет клиента +func DeleteClient(db *database.Database, client *database.Client) error { + // Находим сервер + var server *database.Server + for j := range db.Servers { + if db.Servers[j].ID == client.ServerID { + server = &db.Servers[j] + break + } + } + + // Удаляем peer из WireGuard + if server != nil && server.Enabled { + removePeerFromWireGuard(server, *client) + } + + // Обновляем конфиг файл + if server != nil { + UpdateServerConfig(server, db) + } + + return nil +} + +// ToggleClient включает/выключает клиента +func ToggleClient(db *database.Database, client *database.Client) error { + client.Enabled = !client.Enabled + + // Находим сервер + var server *database.Server + for j := range db.Servers { + if db.Servers[j].ID == client.ServerID { + server = &db.Servers[j] + break + } + } + + if server != nil && server.Enabled { + if client.Enabled { + addPeerToWireGuard(server, *client) + } else { + removePeerFromWireGuard(server, *client) + } + } + + // Обновляем конфиг файл + if server != nil { + UpdateServerConfig(server, db) + } + + return nil +} + +// GenerateClientConfig генерирует конфиг для клиента +func GenerateClientConfig(client database.Client, server *database.Server) string { + // Получаем endpoint сервера + endpoint := database.GetServerEndpoint() + + config := fmt.Sprintf(`[Interface] +PrivateKey = %s +Address = %s/32 +DNS = %s + +[Peer] +PublicKey = %s +Endpoint = %s:%d +AllowedIPs = 0.0.0.0/0 +PersistentKeepalive = 10 +`, client.PrivateKey, client.Address, server.DNS, server.PublicKey, endpoint, server.ListenPort) + + return config +} + +// GenerateQRCode генерирует QR код для конфига +func GenerateQRCode(config string) ([]byte, error) { + return qrcode.Encode(config, qrcode.Medium, 256) +} + +// addPeerToWireGuard добавляет peer в WireGuard +func addPeerToWireGuard(server *database.Server, client database.Client) error { + cmd := exec.Command("wg", "set", server.Interface, "peer", client.PublicKey, + "allowed-ips", client.Address+"/32") + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Ошибка добавления peer: %s, %v", string(output), err) + return err + } + return nil +} + +// removePeerFromWireGuard удаляет peer из WireGuard +func removePeerFromWireGuard(server *database.Server, client database.Client) error { + cmd := exec.Command("wg", "set", server.Interface, "peer", client.PublicKey, "remove") + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Ошибка удаления peer: %s, %v", string(output), err) + return err + } + return nil +} diff --git a/internal/wireguard/iptables.go b/internal/wireguard/iptables.go new file mode 100644 index 0000000..fa895fc --- /dev/null +++ b/internal/wireguard/iptables.go @@ -0,0 +1,72 @@ +package wireguard + +import ( + "log" + "os/exec" +) + +// CleanIPTables очищает все правила iptables +func CleanIPTables() error { + log.Println("🧹 Очистка iptables...") + + commands := [][]string{ + // Устанавливаем политики по умолчанию в ACCEPT + {"iptables", "-P", "INPUT", "ACCEPT"}, + {"iptables", "-P", "FORWARD", "ACCEPT"}, + {"iptables", "-P", "OUTPUT", "ACCEPT"}, + + // Очищаем все цепочки + {"iptables", "-t", "nat", "-F"}, + {"iptables", "-t", "mangle", "-F"}, + {"iptables", "-t", "filter", "-F"}, + {"iptables", "-t", "raw", "-F"}, + + // Удаляем пользовательские цепочки + {"iptables", "-t", "nat", "-X"}, + {"iptables", "-t", "mangle", "-X"}, + {"iptables", "-t", "filter", "-X"}, + {"iptables", "-t", "raw", "-X"}, + } + + for _, cmdArgs := range commands { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf(" ⚠️ Команда %v: %v (output: %s)", cmdArgs, err, string(output)) + // Продолжаем даже при ошибках + } + } + + log.Println(" ✅ iptables очищен") + return nil +} + +// SetupBasicIPTables настраивает базовые правила iptables +func SetupBasicIPTables() error { + log.Println("🔧 Настройка базовых правил iptables...") + + commands := [][]string{ + // Разрешаем loopback + {"iptables", "-A", "INPUT", "-i", "lo", "-j", "ACCEPT"}, + {"iptables", "-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"}, + + // Разрешаем established и related соединения + {"iptables", "-A", "INPUT", "-m", "state", "--state", "ESTABLISHED,RELATED", "-j", "ACCEPT"}, + {"iptables", "-A", "OUTPUT", "-m", "state", "--state", "ESTABLISHED,RELATED", "-j", "ACCEPT"}, + {"iptables", "-A", "FORWARD", "-m", "state", "--state", "ESTABLISHED,RELATED", "-j", "ACCEPT"}, + + // Разрешаем SSH (чтобы не потерять доступ) + {"iptables", "-A", "INPUT", "-p", "tcp", "--dport", "22", "-j", "ACCEPT"}, + } + + for _, cmdArgs := range commands { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf(" ⚠️ Команда %v: %v (output: %s)", cmdArgs, err, string(output)) + } + } + + log.Println(" ✅ Базовые правила настроены") + return nil +} diff --git a/internal/wireguard/portforward.go b/internal/wireguard/portforward.go new file mode 100644 index 0000000..022150f --- /dev/null +++ b/internal/wireguard/portforward.go @@ -0,0 +1,177 @@ +package wireguard + +import ( + "fmt" + "log" + "os/exec" + + "wg-panel/internal/database" +) + +// isPortAvailable проверяет доступен ли порт +func isPortAvailable(db *database.Database, port int, protocol string) bool { + protocols := []string{protocol} + if protocol == "both" { + protocols = []string{"tcp", "udp", "both"} + } + + for _, client := range db.Clients { + for _, pf := range client.PortForwards { + if pf.Port == port { + // Проверяем конфликт протоколов + if pf.Protocol == "both" || protocol == "both" { + return false + } + for _, p := range protocols { + if pf.Protocol == p { + return false + } + } + } + } + } + return true +} + +// AddPortForward добавляет проброс порта для клиента +func AddPortForward(db *database.Database, client *database.Client, port int, protocol, description string) error { + // Проверяем что порт свободен + if !isPortAvailable(db, port, protocol) { + return fmt.Errorf("порт %d/%s уже используется", port, protocol) + } + + // Добавляем в список + portForward := database.PortForward{ + Port: port, + Protocol: protocol, + Description: description, + } + client.PortForwards = append(client.PortForwards, portForward) + + // Применяем правила iptables если клиент активен + if client.Enabled { + if err := applyPortForwardRules(client, portForward); err != nil { + return err + } + } + + return nil +} + +// updatePortForward обновляет проброс порта +func updatePortForward(client *database.Client, port int, protocol, newDescription string) error { + for i, pf := range client.PortForwards { + if pf.Port == port && pf.Protocol == protocol { + client.PortForwards[i].Description = newDescription + return nil + } + } + return fmt.Errorf("проброс порта не найден") +} + +// RemovePortForward удаляет проброс порта +func RemovePortForward(client *database.Client, port int, protocol string) error { + // Находим и удаляем проброс + for i, pf := range client.PortForwards { + if pf.Port == port && pf.Protocol == protocol { + // Удаляем правила iptables + if client.Enabled { + removePortForwardRules(client, pf) + } + + // Удаляем из списка + client.PortForwards = append(client.PortForwards[:i], client.PortForwards[i+1:]...) + return nil + } + } + + return fmt.Errorf("проброс порта не найден") +} + +// applyPortForwardRules применяет правила iptables для проброса порта +func applyPortForwardRules(client *database.Client, pf database.PortForward) error { + log.Printf(" 🔀 Применяю проброс порта %d (%s)", pf.Port, pf.Protocol) + + netInterface := database.GetDefaultInterface() + + protocols := []string{pf.Protocol} + if pf.Protocol == "both" { + protocols = []string{"tcp", "udp"} + } + + for _, proto := range protocols { + commands := [][]string{ + // DNAT только для пакетов приходящих с внешнего интерфейса + {"iptables", "-t", "nat", "-A", "PREROUTING", "-i", netInterface, "-p", proto, + "--dport", fmt.Sprintf("%d", pf.Port), "-j", "DNAT", + "--to-destination", fmt.Sprintf("%s:%d", client.Address, pf.Port)}, + + // Разрешаем FORWARD для этого порта + {"iptables", "-I", "FORWARD", "1", "-p", proto, "-d", client.Address, + "--dport", fmt.Sprintf("%d", pf.Port), "-j", "ACCEPT"}, + } + + for _, cmdArgs := range commands { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf(" ⚠️ Ошибка: %v (output: %s)", err, string(output)) + return err + } + } + } + + log.Printf(" ✅ Проброс порта настроен") + return nil +} + +// removePortForwardRules удаляет правила iptables для проброса порта +func removePortForwardRules(client *database.Client, pf database.PortForward) error { + netInterface := database.GetDefaultInterface() + + protocols := []string{pf.Protocol} + if pf.Protocol == "both" { + protocols = []string{"tcp", "udp"} + } + + for _, proto := range protocols { + commands := [][]string{ + {"iptables", "-t", "nat", "-D", "PREROUTING", "-i", netInterface, "-p", proto, + "--dport", fmt.Sprintf("%d", pf.Port), "-j", "DNAT", + "--to-destination", fmt.Sprintf("%s:%d", client.Address, pf.Port)}, + + {"iptables", "-D", "FORWARD", "-p", proto, "-d", client.Address, + "--dport", fmt.Sprintf("%d", pf.Port), "-j", "ACCEPT"}, + } + + for _, cmdArgs := range commands { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + cmd.Run() // Игнорируем ошибки при удалении + } + } + + return nil +} + +// ApplyAllPortForwards применяет все пробросы портов для клиента +func ApplyAllPortForwards(client *database.Client) error { + if !client.Enabled { + return nil + } + + for _, pf := range client.PortForwards { + if err := applyPortForwardRules(client, pf); err != nil { + log.Printf(" ⚠️ Ошибка проброса порта %d: %v", pf.Port, err) + } + } + + return nil +} + +// removeAllPortForwards удаляет все пробросы портов для клиента +func removeAllPortForwards(client *database.Client) error { + for _, pf := range client.PortForwards { + removePortForwardRules(client, pf) + } + return nil +} diff --git a/internal/wireguard/server.go b/internal/wireguard/server.go new file mode 100644 index 0000000..6145841 --- /dev/null +++ b/internal/wireguard/server.go @@ -0,0 +1,196 @@ +package wireguard + +import ( + "fmt" + "log" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "wg-panel/internal/database" +) + +// UpdateServerConfig обновляет конфиг файл сервера +func UpdateServerConfig(server *database.Server, db *database.Database) error { + configContent := fmt.Sprintf(`[Interface] +PrivateKey = %s +Address = %s +ListenPort = %d +PostUp = %s +PostDown = %s +`, server.PrivateKey, server.Address, server.ListenPort, server.PostUp, server.PostDown) + + // Добавляем всех клиентов + for _, client := range db.Clients { + if client.ServerID == server.ID && client.Enabled { + configContent += fmt.Sprintf("\n[Peer]\nPublicKey = %s\nAllowedIPs = %s/32\n", + client.PublicKey, client.Address) + } + } + + configPath := fmt.Sprintf("/etc/wireguard/%s.conf", server.Interface) + return os.WriteFile(configPath, []byte(configContent), 0600) +} + +// CreateServer создает новый WireGuard сервер +func CreateServer(db *database.Database, name, address string, port int, dns string) (*database.Server, error) { + // Генерируем ключи + privateKey, publicKey, err := database.GenerateKeys() + if err != nil { + return nil, err + } + + // Определяем имя интерфейса + interfaceName := fmt.Sprintf("wg%d", len(db.Servers)) + + // Получаем основной сетевой интерфейс + netInterface := database.GetDefaultInterface() + log.Printf("📡 Определен сетевой интерфейс для NAT: %s", netInterface) + + // Создаем сервер + server := database.Server{ + ID: fmt.Sprintf("%d", time.Now().UnixNano()), + Name: name, + Interface: interfaceName, + PrivateKey: privateKey, + PublicKey: publicKey, + Address: address, + ListenPort: port, + DNS: dns, + Enabled: true, // Запускаем сразу + CreatedAt: time.Now(), + PostUp: fmt.Sprintf("iptables -I FORWARD 1 -i %%i -j ACCEPT; iptables -I FORWARD 1 -o %%i -j ACCEPT; iptables -t nat -A POSTROUTING -o %s -j MASQUERADE", netInterface), + PostDown: fmt.Sprintf("iptables -D FORWARD -i %%i -j ACCEPT; iptables -D FORWARD -o %%i -j ACCEPT; iptables -t nat -D POSTROUTING -o %s -j MASQUERADE", netInterface), + NextClientIP: 2, + } + + // Включаем IP forwarding + log.Println("🔧 Включаю IP forwarding...") + if err := database.EnableIPForwarding(); err != nil { + log.Println("⚠️ Предупреждение: не удалось включить IP forwarding:", err) + } else { + log.Println("✅ IP forwarding включен") + } + + // Создаем конфиг файл + log.Printf("📝 Создаю конфиг %s...", interfaceName) + configContent := fmt.Sprintf(`[Interface] +PrivateKey = %s +Address = %s +ListenPort = %d +PostUp = %s +PostDown = %s +`, server.PrivateKey, server.Address, server.ListenPort, server.PostUp, server.PostDown) + + configPath := fmt.Sprintf("/etc/wireguard/%s.conf", interfaceName) + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { + return nil, err + } + + // Запускаем интерфейс сразу (так как Enabled = true) + log.Printf("🚀 Запускаю интерфейс %s...", interfaceName) + cmd := exec.Command("wg-quick", "up", interfaceName) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("❌ Ошибка запуска: %s", string(output)) + return nil, fmt.Errorf("failed to start interface: %v, output: %s", err, string(output)) + } + log.Printf("✅ Интерфейс %s запущен", interfaceName) + + return &server, nil +} + +// ToggleServer включает/выключает сервер +func ToggleServer(server *database.Server) error { + if server.Enabled { + // Выключаем + cmd := exec.Command("wg-quick", "down", server.Interface) + if err := cmd.Run(); err != nil { + return err + } + server.Enabled = false + } else { + // Включаем IP forwarding перед запуском + database.EnableIPForwarding() + + // Включаем + cmd := exec.Command("wg-quick", "up", server.Interface) + if err := cmd.Run(); err != nil { + return err + } + server.Enabled = true + } + return nil +} + +// DeleteServer удаляет сервер +func DeleteServer(server *database.Server) error { + // Останавливаем интерфейс + if server.Enabled { + exec.Command("wg-quick", "down", server.Interface).Run() + } + + // Удаляем конфиг файл + configPath := fmt.Sprintf("/etc/wireguard/%s.conf", server.Interface) + return os.Remove(configPath) +} + +// UpdateStats обновляет статистику из WireGuard +func UpdateStats(db *database.Database) { + for _, server := range db.Servers { + if !server.Enabled { + continue + } + + cmd := exec.Command("wg", "show", server.Interface, "dump") + output, err := cmd.Output() + if err != nil { + continue + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines[1:] { // Пропускаем первую строку (заголовок) + if line == "" { + continue + } + + fields := strings.Split(line, "\t") + if len(fields) < 8 { + continue + } + + pubKey := fields[0] + endpoint := fields[2] // IP:Port клиента + lastHandshake, _ := strconv.ParseInt(fields[4], 10, 64) + rxBytes, _ := strconv.ParseInt(fields[5], 10, 64) // received + txBytes, _ := strconv.ParseInt(fields[6], 10, 64) // sent + + // Обновляем статистику клиента + for i := range db.Clients { + if db.Clients[i].PublicKey == pubKey && db.Clients[i].ServerID == server.ID { + db.Clients[i].RxBytes = rxBytes + db.Clients[i].TxBytes = txBytes + db.Clients[i].Endpoint = endpoint + if lastHandshake > 0 { + db.Clients[i].LastHandshake = time.Unix(lastHandshake, 0) + } + break + } + } + } + } + + database.SaveDatabase(db) +} + +// UpdateStatsLoop обновляет статистику каждые 5 секунд +func UpdateStatsLoop(db *database.Database) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for range ticker.C { + UpdateStats(db) + } +} diff --git a/internal/wireguard/sync.go b/internal/wireguard/sync.go new file mode 100644 index 0000000..11afe00 --- /dev/null +++ b/internal/wireguard/sync.go @@ -0,0 +1,254 @@ +package wireguard + +import ( + "fmt" + "log" + "os" + "os/exec" + "strconv" + "strings" + + "wg-panel/internal/database" +) + +// SyncWireGuardWithDatabase синхронизирует WireGuard с базой данных +// База данных - единственный источник истины +func SyncWireGuardWithDatabase(db *database.Database) error { + log.Println("🔄 Синхронизация WireGuard с базой данных...") + log.Println("📋 База данных - единственный источник истины") + + // Получаем список всех интерфейсов WireGuard + cmd := exec.Command("wg", "show", "interfaces") + output, err := cmd.Output() + + var activeInterfaces []string + if err == nil && len(output) > 0 { + activeInterfaces = strings.Fields(strings.TrimSpace(string(output))) + } + + // Создаем карту интерфейсов из БД + dbInterfaces := make(map[string]*database.Server) + for i := range db.Servers { + dbInterfaces[db.Servers[i].Interface] = &db.Servers[i] + } + + // Удаляем все интерфейсы которых НЕТ в БД + for _, iface := range activeInterfaces { + if _, exists := dbInterfaces[iface]; !exists { + log.Printf(" ❌ Интерфейс %s не найден в БД - удаление...", iface) + if err := removeInterface(iface); err != nil { + log.Printf(" ⚠️ Ошибка удаления интерфейса: %v", err) + } else { + log.Printf(" ✅ Интерфейс %s удален", iface) + } + } + } + + // Также удаляем конфиг файлы которых нет в БД (но интерфейс не запущен) + cmd = exec.Command("sh", "-c", "ls /etc/wireguard/*.conf 2>/dev/null | xargs -n1 basename | sed 's/.conf$//'") + output, err = cmd.Output() + if err == nil && len(output) > 0 { + configInterfaces := strings.Fields(strings.TrimSpace(string(output))) + for _, iface := range configInterfaces { + if _, exists := dbInterfaces[iface]; !exists { + configPath := fmt.Sprintf("/etc/wireguard/%s.conf", iface) + log.Printf(" 🗑️ Удаление конфига %s (не в БД)...", configPath) + if err := os.Remove(configPath); err != nil { + log.Printf(" ⚠️ Ошибка удаления конфига: %v", err) + } else { + log.Printf(" ✅ Конфиг удален") + } + } + } + } + + // Синхронизируем каждый сервер из БД + for i := range db.Servers { + server := &db.Servers[i] + log.Printf(" 🔧 Синхронизация сервера %s (%s)...", server.Name, server.Interface) + + // Проверяем запущен ли интерфейс + isRunning := false + for _, iface := range activeInterfaces { + if iface == server.Interface { + isRunning = true + break + } + } + + if server.Enabled { + // Сервер должен быть запущен + if isRunning { + // Если уже запущен - перезапускаем (чтобы применить правила iptables после очистки) + log.Printf(" 🔄 Интерфейс уже запущен, перезапускаю для применения правил...") + if err := stopInterface(server.Interface); err != nil { + log.Printf(" ⚠️ Ошибка остановки: %v", err) + } + } + + log.Printf(" 🚀 Запуск интерфейса %s...", server.Interface) + if err := startInterface(server, db); err != nil { + log.Printf(" ❌ Ошибка запуска: %v", err) + server.Enabled = false + continue + } + + // Синхронизируем peers + log.Printf(" 🧹 Очистка всех peers...") + if err := clearAllPeers(server.Interface); err != nil { + log.Printf(" ⚠️ Ошибка очистки: %v", err) + } + + log.Printf(" 📤 Загрузка peers из БД...") + loadedCount := 0 + for j := range db.Clients { + client := &db.Clients[j] + if client.ServerID == server.ID && client.Enabled { + if err := addPeerToWireGuard(server, *client); err != nil { + log.Printf(" ⚠️ Ошибка добавления %s: %v", client.Name, err) + } else { + loadedCount++ + // Применяем пробросы портов + if len(client.PortForwards) > 0 { + log.Printf(" 🔀 Применяю %d пробросов портов для %s...", len(client.PortForwards), client.Name) + ApplyAllPortForwards(client) + } + } + } + } + log.Printf(" ✅ Загружено %d peers", loadedCount) + + } else { + // Сервер должен быть остановлен + if isRunning { + log.Printf(" 🛑 Остановка интерфейса %s...", server.Interface) + if err := stopInterface(server.Interface); err != nil { + log.Printf(" ⚠️ Ошибка остановки: %v", err) + } else { + log.Printf(" ✅ Интерфейс остановлен") + } + } + } + } + + database.SaveDatabase(db) + log.Println("✅ Синхронизация завершена") + return nil +} + +// removeInterface удаляет интерфейс WireGuard +func removeInterface(iface string) error { + // Останавливаем интерфейс + cmd := exec.Command("wg-quick", "down", iface) + cmd.Run() // Игнорируем ошибку если уже остановлен + + // Удаляем конфиг файл + configPath := fmt.Sprintf("/etc/wireguard/%s.conf", iface) + return os.Remove(configPath) +} + +// stopInterface останавливает интерфейс WireGuard +func stopInterface(iface string) error { + cmd := exec.Command("wg-quick", "down", iface) + return cmd.Run() +} + +// startInterface запускает интерфейс WireGuard +func startInterface(server *database.Server, db *database.Database) error { + log.Printf(" 📝 Обновляю конфиг файл...") + // Убеждаемся что конфиг файл существует + if err := UpdateServerConfig(server, db); err != nil { + return fmt.Errorf("ошибка создания конфига: %v", err) + } + + log.Printf(" 🔧 Включаю IP forwarding...") + // Включаем IP forwarding + if err := database.EnableIPForwarding(); err != nil { + log.Printf(" ⚠️ Ошибка IP forwarding: %v", err) + } + + log.Printf(" 🚀 Запускаю wg-quick up %s...", server.Interface) + // Запускаем интерфейс + cmd := exec.Command("wg-quick", "up", server.Interface) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf(" ❌ Вывод wg-quick: %s", string(output)) + return fmt.Errorf("wg-quick up failed: %v, output: %s", err, string(output)) + } + log.Printf(" ✅ Интерфейс запущен успешно") + + // Применяем правила iptables вручную (не полагаемся на PostUp) + if err := ApplyWireGuardIPTablesRules(server); err != nil { + log.Printf(" ⚠️ Ошибка применения правил: %v", err) + } + + return nil +} + +// clearAllPeers очищает все peers из интерфейса WireGuard +func clearAllPeers(iface string) error { + // Получаем список всех peers + cmd := exec.Command("wg", "show", iface, "peers") + output, err := cmd.Output() + if err != nil { + return err + } + + peers := strings.Split(strings.TrimSpace(string(output)), "\n") + removedCount := 0 + + for _, peer := range peers { + peer = strings.TrimSpace(peer) + if peer == "" { + continue + } + + // Удаляем peer + cmd := exec.Command("wg", "set", iface, "peer", peer, "remove") + if err := cmd.Run(); err != nil { + log.Printf(" ⚠️ Не удалось удалить peer %s: %v", peer[:16]+"...", err) + } else { + removedCount++ + } + } + + if removedCount > 0 { + log.Printf(" 🗑️ Удалено %d peers", removedCount) + } + + return nil +} + +// updateNextClientIP обновляет счетчик следующего IP для клиентов +func updateNextClientIP(server *database.Server, db *database.Database) { + // Парсим адрес сервера + parts := strings.Split(server.Address, "/") + if len(parts) != 2 { + server.NextClientIP = 2 + return + } + + ipParts := strings.Split(parts[0], ".") + if len(ipParts) != 4 { + server.NextClientIP = 2 + return + } + + // Находим максимальный IP среди клиентов этого сервера + maxIP := 1 + for _, client := range db.Clients { + if client.ServerID != server.ID { + continue + } + + clientIPParts := strings.Split(client.Address, ".") + if len(clientIPParts) == 4 { + lastOctet, err := strconv.Atoi(clientIPParts[3]) + if err == nil && lastOctet > maxIP { + maxIP = lastOctet + } + } + } + + server.NextClientIP = maxIP + 1 +} diff --git a/internal/wireguard/wg_iptables.go b/internal/wireguard/wg_iptables.go new file mode 100644 index 0000000..72555e9 --- /dev/null +++ b/internal/wireguard/wg_iptables.go @@ -0,0 +1,76 @@ +package wireguard + +import ( + "log" + "os/exec" + + "wg-panel/internal/database" +) + +// ApplyWireGuardIPTablesRules применяет правила iptables для WireGuard сервера +func ApplyWireGuardIPTablesRules(server *database.Server) error { + netInterface := database.GetDefaultInterface() + iface := server.Interface + network := getNetworkFromAddress(server.Address) + + log.Printf(" 🔧 Применяю правила iptables для %s (сеть: %s, интерфейс: %s)...", iface, network, netInterface) + + // Правила FORWARD + commands := [][]string{ + {"iptables", "-I", "FORWARD", "1", "-i", iface, "-j", "ACCEPT"}, + {"iptables", "-I", "FORWARD", "1", "-o", iface, "-j", "ACCEPT"}, + {"iptables", "-t", "nat", "-A", "POSTROUTING", "-s", network, "-o", netInterface, "-j", "MASQUERADE"}, + } + + for i, cmdArgs := range commands { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf(" ⚠️ Команда #%d %v: %v (output: %s)", i+1, cmdArgs, err, string(output)) + } else { + log.Printf(" ✅ Команда #%d выполнена: %v", i+1, cmdArgs) + } + } + + log.Printf(" ✅ Правила iptables применены") + return nil +} + +// RemoveWireGuardIPTablesRules удаляет правила iptables для WireGuard сервера +func RemoveWireGuardIPTablesRules(server *database.Server) error { + netInterface := database.GetDefaultInterface() + iface := server.Interface + + log.Printf(" 🗑️ Удаляю правила iptables для %s...", iface) + + commands := [][]string{ + {"iptables", "-D", "FORWARD", "-i", iface, "-j", "ACCEPT"}, + {"iptables", "-D", "FORWARD", "-o", iface, "-j", "ACCEPT"}, + {"iptables", "-t", "nat", "-D", "POSTROUTING", "-s", getNetworkFromAddress(server.Address), "-o", netInterface, "-j", "MASQUERADE"}, + } + + for _, cmdArgs := range commands { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + cmd.Run() // Игнорируем ошибки + } + + return nil +} + +// getNetworkFromAddress извлекает подсеть из адреса типа "10.0.0.1/24" -> "10.0.0.0/24" +func getNetworkFromAddress(address string) string { + // Простая реализация - заменяем последний октет на 0 + parts := address[:len(address)-1] // убираем последнюю цифру + // Находим последнюю точку + lastDot := -1 + for i := len(parts) - 1; i >= 0; i-- { + if parts[i] == '.' { + lastDot = i + break + } + } + if lastDot > 0 { + return parts[:lastDot+1] + "0" + address[len(address)-3:] // +0 и маска + } + return address +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f9a0ca1 --- /dev/null +++ b/main.go @@ -0,0 +1,666 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + + "wg-panel/internal/database" + "wg-panel/internal/server" + "wg-panel/internal/wireguard" +) + +const version = "1.0.0" + +func main() { + // Обрабатываем аргументы командной строки + if len(os.Args) > 1 { + command := os.Args[1] + + // Команды доступные всегда + switch command { + case "install": + installServer() + return + case "version", "-v", "--version": + fmt.Printf("wg_serf version %s\n", version) + os.Exit(0) + case "help", "-h", "--help": + showHelp() + return + case "serve": + // Прямой запуск сервера (используется systemd) + runServer() + return + } + + // Для остальных команд проверяем: + // 1. Установлена ли служба + // 2. Запущено через PATH (не ./wg_serf) + + if !isInstalled() { + fmt.Println("") + fmt.Println("❌ WG_SERF не установлен!") + fmt.Println("") + fmt.Println("Сначала установите:") + fmt.Println(" sudo wg_serf install") + fmt.Println("") + fmt.Println("Или используйте install.sh:") + fmt.Println(" curl -fsSL https://vserf.ru/download/wgserf/install.sh | sudo bash") + fmt.Println("") + os.Exit(1) + } + + // Проверяем что команда запущена через PATH, а не напрямую + exePath, _ := os.Executable() + if strings.HasPrefix(exePath, "./") || strings.HasPrefix(exePath, "/root/") || strings.HasPrefix(exePath, "/home/") { + // Запущено не через PATH + if exePath != "/opt/wg_serf/wg_serf" { + fmt.Println("") + fmt.Println("⚠️ Команды управления работают только через PATH") + fmt.Println("") + fmt.Println("Используйте:") + fmt.Printf(" wg_serf %s\n", command) + fmt.Println("") + fmt.Println("А не:") + fmt.Printf(" %s %s\n", exePath, command) + fmt.Println("") + os.Exit(1) + } + } + + // Команды доступные только после установки + switch command { + case "start": + startServer() + case "stop": + stopServer() + case "restart": + restartServer() + case "status": + showStatus() + case "uninstall", "delete": + deleteServer() + default: + fmt.Printf("Неизвестная команда: %s\n\n", command) + showHelp() + os.Exit(1) + } + return + } + + // Без аргументов - интерактивный режим + handleNoArgs() +} + +func handleNoArgs() { + // Проверяем запущен ли через PATH или напрямую + exePath, _ := os.Executable() + + // Если не установлено - предлагаем установить + if !isInstalled() { + fmt.Println("") + fmt.Println("╔══════════════════════════════════════════════════════════════╗") + fmt.Println("║ WG_SERF - WireGuard Server Panel ║") + fmt.Println("║ Version " + version + " ║") + fmt.Println("╚══════════════════════════════════════════════════════════════╝") + fmt.Println("") + fmt.Println("❌ WG_SERF не установлен") + fmt.Println("") + if askYesNo("📥 Установить в систему? (yes/no): ") { + installServer() + } else { + fmt.Println("") + fmt.Println("Установка отменена. Установить позже:") + fmt.Println(" sudo " + exePath + " install") + fmt.Println("") + } + return + } + + // Если установлено - показываем информацию + config, _ := database.LoadConfig() + pid, _ := readPIDFile() + running := isRunning() + + fmt.Println("") + fmt.Println("╔══════════════════════════════════════════════════════════════╗") + fmt.Println("║ WG_SERF - WireGuard Server Panel ║") + if running { + // Динамическое форматирование для ровной рамки + line := fmt.Sprintf("Version %s PID: %d", version, pid) + padding := 62 - len(line) + leftPad := padding / 2 + rightPad := padding - leftPad + fmt.Printf("║%s%s%s║\n", strings.Repeat(" ", leftPad), line, strings.Repeat(" ", rightPad)) + } else { + line := fmt.Sprintf("Version %s", version) + padding := 62 - len(line) + leftPad := padding / 2 + rightPad := padding - leftPad + fmt.Printf("║%s%s%s║\n", strings.Repeat(" ", leftPad), line, strings.Repeat(" ", rightPad)) + } + fmt.Println("╚══════════════════════════════════════════════════════════════╝") + fmt.Println("") + + if running { + fmt.Println("✅ Сервис установлен и работает") + if config != nil { + serverIP := database.GetLocalIP() + if serverIP == "127.0.0.1" { + serverIP = database.GetServerEndpoint() + } + fmt.Printf("🌐 Веб-панель: http://%s:%s\n", serverIP, config.Port) + fmt.Printf("👤 Логин: %s\n", config.Username) + fmt.Printf("🔒 Пароль: %s\n", config.Password) + } + } else { + fmt.Println("⚠️ Сервис установлен, но не запущен") + fmt.Println("") + fmt.Println("Запустить: wg_serf start") + } + + fmt.Println("") + fmt.Println("📋 Доступные команды:") + fmt.Println(" wg_serf status # Статус") + fmt.Println(" wg_serf restart # Перезапустить") + fmt.Println(" wg_serf stop # Остановить") + fmt.Println(" wg_serf delete # Удалить") + fmt.Println("") + fmt.Println("📚 Справка: wg_serf help") + fmt.Println("") +} + +func isInstalled() bool { + _, err := os.Stat("/etc/systemd/system/wg_serf.service") + return err == nil +} + +// askYesNo запрашивает подтверждение yes/no (толерантно к вводу) +func askYesNo(prompt string) bool { + fmt.Print(prompt) + var response string + fmt.Scanln(&response) + + // Убираем пробелы и переводим в нижний регистр + response = strings.ToLower(strings.TrimSpace(response)) + + // Проверяем содержит ли yes + if strings.Contains(response, "yes") || strings.Contains(response, "y") { + return true + } + + return false +} + +func showHelp() { + fmt.Println(` +╔══════════════════════════════════════════════════════════════╗ +║ WG_SERF - WireGuard Server Panel ║ +║ Version ` + version + ` ║ +╚══════════════════════════════════════════════════════════════╝ + +📖 ИСПОЛЬЗОВАНИЕ: + wg_serf <команда> + +📋 КОМАНДЫ: + install Установить wg_serf как службу (требуется сначала!) + version Показать версию + help Показать эту справку + +📋 КОМАНДЫ ПОСЛЕ УСТАНОВКИ: + start Запустить сервер + stop Остановить сервер + restart Перезапустить сервер + status Показать статус сервера + delete Удалить wg_serf полностью + +🔧 ПРИМЕРЫ: + sudo wg_serf install # Сначала установить + wg_serf status # Проверить статус + wg_serf restart # Перезапустить + +📡 ВЕБ-ИНТЕРФЕЙС: + После запуска откройте в браузере: http://your-server-ip:8080 + Логин по умолчанию: admin / admin + +💡 Совет: Для работы требуются root права (sudo)`) +} + +func startServer() { + // Проверяем не запущен ли уже + if isRunning() { + fmt.Println("❌ Сервер уже запущен!") + os.Exit(1) + } + + fmt.Println("🚀 Запуск WG_SERF...") + + // Пробуем запустить через systemctl (если установлена служба) + cmd := exec.Command("systemctl", "start", "wg_serf") + output, err := cmd.CombinedOutput() + if err != nil { + // Если systemd не доступен или служба не установлена - запускаем напрямую + if strings.Contains(string(output), "Failed to connect") || + strings.Contains(string(output), "not found") || + strings.Contains(err.Error(), "executable file not found") { + fmt.Println("⚠️ Служба не найдена, запускаю напрямую...") + runServer() + return + } + fmt.Println("❌ Ошибка запуска:", err) + fmt.Println(string(output)) + os.Exit(1) + } + + fmt.Println("✅ Сервер запущен как служба") + fmt.Println("📋 Проверить статус: wg_serf status") + fmt.Println("📋 Просмотр логов: journalctl -u wg_serf -f") +} + +func stopServer() { + fmt.Println("🛑 Остановка сервера...") + + // Останавливаем через systemctl + cmd := exec.Command("systemctl", "stop", "wg_serf") + output, err := cmd.CombinedOutput() + if err != nil { + // Если systemd не доступен, пробуем через PID файл + if strings.Contains(string(output), "Failed to connect") || strings.Contains(err.Error(), "executable file not found") { + stopServerViaPID() + return + } + fmt.Println("❌ Ошибка остановки:", err) + os.Exit(1) + } + + fmt.Println("✅ Сервер остановлен") +} + +func restartServer() { + fmt.Println("🔄 Перезапуск сервера...") + + // Перезапускаем через systemctl + cmd := exec.Command("systemctl", "restart", "wg_serf") + output, err := cmd.CombinedOutput() + if err != nil { + // Если systemd не доступен, делаем вручную + if strings.Contains(string(output), "Failed to connect") || strings.Contains(err.Error(), "executable file not found") { + stopServerViaPID() + startServer() + return + } + fmt.Println("❌ Ошибка перезапуска:", err) + os.Exit(1) + } + + fmt.Println("✅ Сервер перезапущен") +} + +func stopServerViaPID() { + pid, err := readPIDFile() + if err != nil { + fmt.Println("❌ Сервер не запущен или PID файл не найден") + os.Exit(1) + } + + process, err := os.FindProcess(pid) + if err != nil { + fmt.Println("❌ Процесс не найден") + os.Exit(1) + } + + if err := process.Signal(syscall.SIGTERM); err != nil { + fmt.Println("❌ Ошибка остановки сервера:", err) + os.Exit(1) + } + + fmt.Println("✅ Сервер остановлен") +} + +func showStatus() { + if isRunning() { + pid, _ := readPIDFile() + config, err := database.LoadConfig() + if err == nil { + fmt.Printf("✅ Сервер работает (PID: %d)\n", pid) + fmt.Printf("🌐 Веб-интерфейс: http://%s:%s\n", config.Address, config.Port) + fmt.Printf("👤 Логин: %s\n", config.Username) + } else { + fmt.Printf("✅ Сервер работает (PID: %d)\n", pid) + } + } else { + fmt.Println("❌ Сервер не запущен") + os.Exit(1) + } +} + +func isRunning() bool { + pid, err := readPIDFile() + if err != nil { + return false + } + + process, err := os.FindProcess(pid) + if err != nil { + return false + } + + // Проверяем существует ли процесс + err = process.Signal(syscall.Signal(0)) + return err == nil +} + +func installServer() { + fmt.Println("") + fmt.Println("╔══════════════════════════════════════════════════════════════╗") + fmt.Println("║ WG_SERF Installer ║") + fmt.Println("╚══════════════════════════════════════════════════════════════╝") + fmt.Println("") + + // Проверяем установлена ли уже служба + if _, err := os.Stat("/etc/systemd/system/wg_serf.service"); err == nil { + fmt.Println("✅ WG_SERF уже установлен как служба!") + fmt.Println("") + fmt.Println("📋 Доступные команды:") + fmt.Println(" wg_serf status # Проверить статус") + fmt.Println(" wg_serf restart # Перезапустить") + fmt.Println(" wg_serf stop # Остановить") + fmt.Println(" wg_serf delete # Удалить") + fmt.Println("") + return + } + + // Проверяем и устанавливаем WireGuard + fmt.Println("🔍 Проверка WireGuard...") + if !database.CheckWireGuardInstalled() { + fmt.Println("⚠️ WireGuard не установлен. Устанавливаю...") + if err := installWireGuard(); err != nil { + fmt.Println("❌ Не удалось установить WireGuard:", err) + fmt.Println("") + fmt.Println("Установите WireGuard вручную:") + fmt.Println(" apt install wireguard # Debian/Ubuntu") + fmt.Println(" dnf install wireguard-tools # Fedora") + fmt.Println(" yum install wireguard-tools # CentOS") + os.Exit(1) + } + fmt.Println("✅ WireGuard установлен") + } else { + fmt.Println("✅ WireGuard уже установлен") + } + + // Проверяем что бинарник в правильном месте + currentPath, err := os.Executable() + if err != nil { + fmt.Println("❌ Ошибка определения пути к бинарнику:", err) + os.Exit(1) + } + + targetPath := "/opt/wg_serf/wg_serf" + if currentPath != targetPath { + // Создаем директорию + fmt.Println("📁 Создание /opt/wg_serf/...") + os.MkdirAll("/opt/wg_serf", 0755) + + // Копируем бинарник + fmt.Println("📦 Копирование бинарника...") + input, err := os.ReadFile(currentPath) + if err != nil { + fmt.Println("❌ Ошибка чтения:", err) + os.Exit(1) + } + if err := os.WriteFile(targetPath, input, 0755); err != nil { + fmt.Println("❌ Ошибка записи:", err) + os.Exit(1) + } + + // Создаем symlink + fmt.Println("🔗 Создание symlink...") + os.Remove("/usr/local/bin/wg_serf") + os.Symlink(targetPath, "/usr/local/bin/wg_serf") + } + + // Создаем systemd service + fmt.Println("⚙️ Создание systemd service...") + serviceContent := `[Unit] +Description=WG_SERF - WireGuard Server Panel +After=network.target +Wants=network-online.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/wg_serf +ExecStart=/opt/wg_serf/wg_serf serve +Restart=on-failure +RestartSec=5s +KillMode=mixed +KillSignal=SIGTERM +TimeoutStopSec=5s + +# Security +NoNewPrivileges=false +PrivateTmp=false + +[Install] +WantedBy=multi-user.target +` + if err := os.WriteFile("/etc/systemd/system/wg_serf.service", []byte(serviceContent), 0644); err != nil { + fmt.Println("❌ Ошибка создания service:", err) + os.Exit(1) + } + + // Перезагрузка systemd + fmt.Println("🔄 Перезагрузка systemd...") + exec.Command("systemctl", "daemon-reload").Run() + + // Включение автозапуска + fmt.Println("✅ Включение автозапуска...") + exec.Command("systemctl", "enable", "wg_serf").Run() + + // Запуск + fmt.Println("🚀 Запуск wg_serf...") + cmd := exec.Command("systemctl", "start", "wg_serf") + if err := cmd.Run(); err != nil { + fmt.Println("❌ Ошибка запуска:", err) + os.Exit(1) + } + + fmt.Println("") + // Получаем IP и конфиг + serverIP := database.GetLocalIP() + if serverIP == "127.0.0.1" { + serverIP = database.GetServerEndpoint() + } + config, _ := database.LoadConfig() + port := "8080" + if config != nil { + port = config.Port + } + + fmt.Println("╔══════════════════════════════════════════════════════════════╗") + fmt.Println("║ ✅ Установка завершена! ║") + fmt.Println("╚══════════════════════════════════════════════════════════════╝") + fmt.Println("") + fmt.Printf("🌐 Откройте в браузере: http://%s:%s\n", serverIP, port) + if config != nil { + fmt.Printf("👤 Логин: %s\n", config.Username) + fmt.Printf("🔒 Пароль: %s\n", config.Password) + } else { + fmt.Println("👤 Логин: admin") + fmt.Println("🔒 Пароль: admin") + } + fmt.Println("") + fmt.Println("📋 Команды:") + fmt.Println(" wg_serf status # Проверить статус") + fmt.Println(" wg_serf restart # Перезапустить") + fmt.Println("") +} + +func deleteServer() { + fmt.Println("") + fmt.Println("╔══════════════════════════════════════════════════════════════╗") + fmt.Println("║ WG_SERF - Удаление ║") + fmt.Println("╚══════════════════════════════════════════════════════════════╝") + fmt.Println("") + + // Подтверждение + if !askYesNo("⚠️ Вы уверены? Все данные будут удалены! (yes/no): ") { + fmt.Println("❌ Удаление отменено") + os.Exit(0) + } + + // Остановка сервиса + fmt.Println("🛑 Остановка wg_serf...") + exec.Command("systemctl", "stop", "wg_serf").Run() + + // Отключение автозапуска + fmt.Println("🔄 Отключение автозапуска...") + exec.Command("systemctl", "disable", "wg_serf").Run() + + // Удаление service файла + fmt.Println("🗑️ Удаление systemd service...") + os.Remove("/etc/systemd/system/wg_serf.service") + exec.Command("systemctl", "daemon-reload").Run() + + // Удаление symlink + fmt.Println("🗑️ Удаление symlink...") + os.Remove("/usr/local/bin/wg_serf") + + // Удаление директории + fmt.Println("🗑️ Удаление /opt/wg_serf...") + os.RemoveAll("/opt/wg_serf") + + fmt.Println("") + fmt.Println("✅ Удаление завершено!") + fmt.Println("") +} + +func installWireGuard() error { + // Определяем дистрибутив + osRelease, err := os.ReadFile("/etc/os-release") + if err != nil { + return fmt.Errorf("не удалось определить дистрибутив") + } + + osID := "" + for _, line := range strings.Split(string(osRelease), "\n") { + if strings.HasPrefix(line, "ID=") { + osID = strings.Trim(strings.TrimPrefix(line, "ID="), "\"") + break + } + } + + var cmd *exec.Cmd + switch osID { + case "ubuntu", "debian": + fmt.Println("📦 Установка для Debian/Ubuntu...") + exec.Command("apt", "update", "-qq").Run() + cmd = exec.Command("apt", "install", "-y", "wireguard", "wireguard-tools") + case "centos", "rhel": + fmt.Println("📦 Установка для RHEL/CentOS...") + exec.Command("yum", "install", "-y", "epel-release").Run() + cmd = exec.Command("yum", "install", "-y", "wireguard-tools") + case "fedora": + fmt.Println("📦 Установка для Fedora...") + cmd = exec.Command("dnf", "install", "-y", "wireguard-tools") + case "arch", "manjaro": + fmt.Println("📦 Установка для Arch Linux...") + cmd = exec.Command("pacman", "-Sy", "--noconfirm", "wireguard-tools") + default: + return fmt.Errorf("дистрибутив %s не поддерживается", osID) + } + + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("ошибка установки: %v, output: %s", err, string(output)) + } + + // Проверяем что установилось + if !database.CheckWireGuardInstalled() { + return fmt.Errorf("WireGuard не найден после установки") + } + + return nil +} + +func readPIDFile() (int, error) { + data, err := os.ReadFile("/opt/wg_serf/wg_serf.pid") + if err != nil { + return 0, err + } + return strconv.Atoi(strings.TrimSpace(string(data))) +} + +func runServer() { + // Проверяем и завершаем старый процесс если запущен + if err := database.CheckAndKillOldProcess(); err != nil { + log.Fatal("Ошибка при завершении старого процесса:", err) + } + + // Записываем PID текущего процесса + if err := database.WritePIDFile(); err != nil { + log.Fatal("Ошибка записи PID файла:", err) + } + defer database.RemovePIDFile() + + // Проверяем установлен ли WireGuard + if !database.CheckWireGuardInstalled() { + log.Fatal("WireGuard не установлен! Установите WireGuard перед запуском.") + } + + // Загружаем конфигурацию + config, err := database.LoadConfig() + if err != nil { + log.Fatal("Ошибка загрузки конфигурации:", err) + } + server.Config = config + + // Загружаем базу данных + db, err := database.LoadDatabase() + if err != nil { + log.Println("Создаю новую базу данных...") + db = &database.Database{ + Servers: []database.Server{}, + Clients: []database.Client{}, + } + database.SaveDatabase(db) + } + server.DB = db + + // Очищаем iptables (так как сервер только для WireGuard) + if err := wireguard.CleanIPTables(); err != nil { + log.Println("Предупреждение: ошибка очистки iptables:", err) + } + + // Настраиваем базовые правила + if err := wireguard.SetupBasicIPTables(); err != nil { + log.Println("Предупреждение: ошибка настройки базовых правил:", err) + } + + // Включаем IP forwarding + if err := database.EnableIPForwarding(); err != nil { + log.Println("Предупреждение: не удалось включить IP forwarding:", err) + } + + // Синхронизируем WireGuard с базой данных (создаст правила для серверов из БД) + if err := wireguard.SyncWireGuardWithDatabase(db); err != nil { + log.Println("Предупреждение: ошибка синхронизации:", err) + } + + // Настраиваем маршруты + server.SetupRoutes() + + // Обновляем статистику каждые 5 секунд + go wireguard.UpdateStatsLoop(db) + + addr := config.Address + ":" + config.Port + log.Printf("🚀 Сервер запущен на http://%s\n", addr) + log.Printf("👤 Логин: %s\n", config.Username) + log.Printf("🔒 Пароль: %s\n", config.Password) + log.Fatal(http.ListenAndServe(addr, nil)) +}