Инициализация проекта
Всем привет :)
This commit is contained in:
93
README.md
Normal file
93
README.md
Normal file
@@ -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/)
|
BIN
build/wg_serf
Normal file
BIN
build/wg_serf
Normal file
Binary file not shown.
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module wg-panel
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
2
go.sum
Normal file
2
go.sum
Normal file
@@ -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=
|
43
internal/database/config.go
Normal file
43
internal/database/config.go
Normal file
@@ -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)
|
||||||
|
}
|
28
internal/database/database.go
Normal file
28
internal/database/database.go
Normal file
@@ -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)
|
||||||
|
}
|
33
internal/database/helpers.go
Normal file
33
internal/database/helpers.go
Normal file
@@ -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
|
||||||
|
}
|
121
internal/database/pidfile.go
Normal file
121
internal/database/pidfile.go
Normal file
@@ -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
|
||||||
|
}
|
59
internal/database/types.go
Normal file
59
internal/database/types.go
Normal file
@@ -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"`
|
||||||
|
}
|
144
internal/database/utils.go
Normal file
144
internal/database/utils.go
Normal file
@@ -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()
|
||||||
|
}
|
86
internal/database/validation.go
Normal file
86
internal/database/validation.go
Normal file
@@ -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
|
||||||
|
}
|
548
internal/server/handlers.go
Normal file
548
internal/server/handlers.go
Normal file
@@ -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)
|
||||||
|
}
|
34
internal/server/routes.go
Normal file
34
internal/server/routes.go
Normal file
@@ -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)
|
||||||
|
}
|
6
internal/server/static.go
Normal file
6
internal/server/static.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var TemplatesFS embed.FS
|
1122
internal/server/templates/index.html
Normal file
1122
internal/server/templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
130
internal/server/templates/login.html
Normal file
130
internal/server/templates/login.html
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WireGuard Panel - Вход</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #fee;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
color: #c33;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>WireGuard Panel</h1>
|
||||||
|
<p>Панель управления VPN</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.get('error')) {
|
||||||
|
document.write('<div class="error">Неверный логин или пароль</div>');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Логин</label>
|
||||||
|
<input type="text" id="username" name="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Пароль</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Войти</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
163
internal/wireguard/client.go
Normal file
163
internal/wireguard/client.go
Normal file
@@ -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
|
||||||
|
}
|
72
internal/wireguard/iptables.go
Normal file
72
internal/wireguard/iptables.go
Normal file
@@ -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
|
||||||
|
}
|
177
internal/wireguard/portforward.go
Normal file
177
internal/wireguard/portforward.go
Normal file
@@ -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
|
||||||
|
}
|
196
internal/wireguard/server.go
Normal file
196
internal/wireguard/server.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
254
internal/wireguard/sync.go
Normal file
254
internal/wireguard/sync.go
Normal file
@@ -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
|
||||||
|
}
|
76
internal/wireguard/wg_iptables.go
Normal file
76
internal/wireguard/wg_iptables.go
Normal file
@@ -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
|
||||||
|
}
|
666
main.go
Normal file
666
main.go
Normal file
@@ -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))
|
||||||
|
}
|
Reference in New Issue
Block a user