Инициализация проекта

Всем привет :)
This commit is contained in:
2025-10-16 16:27:36 +07:00
commit 0e93af1d8c
23 changed files with 4058 additions and 0 deletions

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

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

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

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

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

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

View File

@@ -0,0 +1,6 @@
package server
import "embed"
//go:embed templates/*
var TemplatesFS embed.FS

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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