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

Всем привет :)
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
}