Инициализация проекта
Всем привет :)
This commit is contained in:
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
|
||||
}
|
Reference in New Issue
Block a user