- Add Go TUI with bubbletea for WireGuard management - Implement client CRUD operations with QR code generation - Add configuration and validation modules - Install/update scripts for client setup - Update Makefile to build binaries to bin/ directory - Add .gitignore for Go projects
268 lines
7.4 KiB
Go
268 lines
7.4 KiB
Go
package backup
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Backup represents a backup with metadata
|
|
type Backup struct {
|
|
Name string // Backup name (directory name)
|
|
Path string // Full path to backup directory
|
|
Operation string // Operation that triggered the backup
|
|
Timestamp time.Time // When the backup was created
|
|
Size int64 // Size in bytes
|
|
}
|
|
|
|
// CreateBackup creates a new backup with the specified operation
|
|
func CreateBackup(operation string) error {
|
|
backupDir := "/etc/wg-admin/backups"
|
|
|
|
// Create backup directory if it doesn't exist
|
|
if err := os.MkdirAll(backupDir, 0700); err != nil {
|
|
return fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
// Create timestamped backup directory
|
|
timestamp := time.Now().Format("20060102-150405")
|
|
backupName := fmt.Sprintf("wg-backup-%s-%s", operation, timestamp)
|
|
backupPath := filepath.Join(backupDir, backupName)
|
|
|
|
if err := os.MkdirAll(backupPath, 0700); err != nil {
|
|
return fmt.Errorf("failed to create backup path: %w", err)
|
|
}
|
|
|
|
// Backup entire wireguard directory to maintain structure expected by restore.go
|
|
wgConfigPath := "/etc/wireguard"
|
|
if _, err := os.Stat(wgConfigPath); err == nil {
|
|
backupWgPath := filepath.Join(backupPath, "wireguard")
|
|
if err := exec.Command("cp", "-a", wgConfigPath, backupWgPath).Run(); err != nil {
|
|
return fmt.Errorf("failed to backup wireguard config: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create backup metadata
|
|
metadataPath := filepath.Join(backupPath, "backup-info.txt")
|
|
metadata := fmt.Sprintf("Backup created: %s\nOperation: %s\nTimestamp: %s\n", time.Now().Format(time.RFC3339), operation, timestamp)
|
|
if err := os.WriteFile(metadataPath, []byte(metadata), 0600); err != nil {
|
|
return fmt.Errorf("failed to create backup metadata: %w", err)
|
|
}
|
|
|
|
// Set restrictive permissions on backup directory
|
|
if err := os.Chmod(backupPath, 0700); err != nil {
|
|
return fmt.Errorf("failed to set backup directory permissions: %w", err)
|
|
}
|
|
|
|
// Apply retention policy (keep last 10 backups)
|
|
if err := applyRetentionPolicy(backupDir, 10); err != nil {
|
|
// Log but don't fail on retention errors
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to apply retention policy: %v\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListBackups returns all available backups sorted by creation time (newest first)
|
|
func ListBackups() ([]Backup, error) {
|
|
backupDir := "/etc/wg-admin/backups"
|
|
|
|
// Check if backup directory exists
|
|
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
|
return []Backup{}, nil
|
|
}
|
|
|
|
// Read all entries
|
|
entries, err := os.ReadDir(backupDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read backup directory: %w", err)
|
|
}
|
|
|
|
var backups []Backup
|
|
|
|
// Parse backup directories
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
name := entry.Name()
|
|
|
|
// Check if it's a valid backup directory (starts with "wg-backup-")
|
|
if !strings.HasPrefix(name, "wg-backup-") {
|
|
continue
|
|
}
|
|
|
|
// Parse backup name to extract operation and timestamp
|
|
// Format: wg-backup-{operation}-{timestamp}
|
|
parts := strings.SplitN(name, "-", 4)
|
|
if len(parts) < 4 {
|
|
continue
|
|
}
|
|
|
|
operation := parts[2]
|
|
timestampStr := parts[3]
|
|
|
|
// Parse timestamp
|
|
timestamp, err := time.Parse("20060102-150405", timestampStr)
|
|
if err != nil {
|
|
// If timestamp parsing fails, use directory modification time
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
timestamp = info.ModTime()
|
|
}
|
|
|
|
// Get backup size
|
|
backupPath := filepath.Join(backupDir, name)
|
|
size, err := getBackupSize(backupPath)
|
|
if err != nil {
|
|
size = 0
|
|
}
|
|
|
|
backup := Backup{
|
|
Name: name,
|
|
Path: backupPath,
|
|
Operation: operation,
|
|
Timestamp: timestamp,
|
|
Size: size,
|
|
}
|
|
|
|
backups = append(backups, backup)
|
|
}
|
|
|
|
// Sort by timestamp (newest first)
|
|
sort.Slice(backups, func(i, j int) bool {
|
|
return backups[i].Timestamp.After(backups[j].Timestamp)
|
|
})
|
|
|
|
return backups, nil
|
|
}
|
|
|
|
// getBackupSize calculates the total size of a backup directory
|
|
func getBackupSize(backupPath string) (int64, error) {
|
|
var size int64
|
|
|
|
err := filepath.Walk(backupPath, func(_ string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
size += info.Size()
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return size, err
|
|
}
|
|
|
|
// RestoreBackup restores WireGuard configurations from a backup by name
|
|
func RestoreBackup(backupName string) error {
|
|
backupDir := "/etc/wg-admin/backups"
|
|
backupPath := filepath.Join(backupDir, backupName)
|
|
|
|
// Verify backup exists
|
|
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("backup does not exist: %s", backupName)
|
|
}
|
|
|
|
// Check for wireguard subdirectory
|
|
wgSourcePath := filepath.Join(backupPath, "wireguard")
|
|
if _, err := os.Stat(wgSourcePath); os.IsNotExist(err) {
|
|
return fmt.Errorf("backup does not contain wireguard configuration")
|
|
}
|
|
|
|
// Restore entire wireguard directory
|
|
wgDestPath := "/etc/wireguard"
|
|
if err := os.RemoveAll(wgDestPath); err != nil {
|
|
return fmt.Errorf("failed to remove existing wireguard config: %w", err)
|
|
}
|
|
|
|
if err := exec.Command("cp", "-a", wgSourcePath, wgDestPath).Run(); err != nil {
|
|
return fmt.Errorf("failed to restore wireguard config: %w", err)
|
|
}
|
|
|
|
// Set proper permissions on restored files
|
|
if err := setRestoredPermissions(wgDestPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to set permissions on restored files: %v\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setRestoredPermissions sets appropriate permissions on restored WireGuard files
|
|
func setRestoredPermissions(wgPath string) error {
|
|
// Set 0600 on .conf files
|
|
return filepath.Walk(wgPath, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() && strings.HasSuffix(path, ".conf") {
|
|
if err := os.Chmod(path, 0600); err != nil {
|
|
return fmt.Errorf("failed to chmod %s: %w", path, err)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// applyRetentionPolicy keeps only the last N backups
|
|
func applyRetentionPolicy(backupDir string, keepCount int) error {
|
|
// List all backup directories
|
|
entries, err := os.ReadDir(backupDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Filter backup directories and sort by modification time
|
|
var backups []os.FileInfo
|
|
for _, entry := range entries {
|
|
if entry.IsDir() && len(entry.Name()) > 10 && entry.Name()[:10] == "wg-backup-" {
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
backups = append(backups, info)
|
|
}
|
|
}
|
|
|
|
// If we have more backups than we want to keep, remove the oldest
|
|
if len(backups) > keepCount {
|
|
// Sort by modification time (oldest first)
|
|
for i := 0; i < len(backups); i++ {
|
|
for j := i + 1; j < len(backups); j++ {
|
|
if backups[i].ModTime().After(backups[j].ModTime()) {
|
|
backups[i], backups[j] = backups[j], backups[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove oldest backups
|
|
toRemove := len(backups) - keepCount
|
|
for i := 0; i < toRemove; i++ {
|
|
backupPath := filepath.Join(backupDir, backups[i].Name())
|
|
if err := os.RemoveAll(backupPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to remove old backup %s: %v\n", backupPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BackupConfig is a compatibility wrapper that calls CreateBackup
|
|
func BackupConfig(operation string) (string, error) {
|
|
if err := CreateBackup(operation); err != nil {
|
|
return "", err
|
|
}
|
|
// Return the backup path for compatibility
|
|
timestamp := time.Now().Format("20060102-150405")
|
|
backupName := fmt.Sprintf("wg-backup-%s-%s", operation, timestamp)
|
|
return filepath.Join("/etc/wg-admin/backups", backupName), nil
|
|
}
|