diff options
| author | Ahmed AbdelHalim <[email protected]> | 2025-09-06 23:55:19 +0200 |
|---|---|---|
| committer | Ahmed AbdelHalim <[email protected]> | 2025-09-07 02:20:46 +0200 |
| commit | 17168a489392847794de0aed15fe395230a38261 (patch) | |
| tree | 60914d9df4b9b37852e54dcf72818b0df1a958fb /internal | |
Initial commit
This cleaned up and added linting/readme to the already created web app
that I have been using to control vpn on my raspberry setup
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config.go | 56 | ||||
| -rw-r--r-- | internal/wireguard.go | 172 |
2 files changed, 228 insertions, 0 deletions
diff --git a/internal/config.go b/internal/config.go new file mode 100644 index 0000000..8d9d961 --- /dev/null +++ b/internal/config.go @@ -0,0 +1,56 @@ +package internal + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type ServerConfig struct { + Host string `yaml:"host"` + Port string `yaml:"port"` + Title string `yaml:"title"` +} + +type Config struct { + Server ServerConfig `yaml:"server"` +} + +// Default configuration values +func DefaultConfig() *Config { + config := &Config{} + config.Server.Host = "0.0.0.0" + config.Server.Port = "8080" + config.Server.Title = "WireGuard Gateway Portal" + return config +} + +// LoadConfig loads configuration from file, falls back to defaults if file doesn't exist +func LoadConfig(configPath string) (*Config, error) { + config := DefaultConfig() + + // Check if config file exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + // Config file doesn't exist, use defaults + return config, nil + } + + // Read config file + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + // Parse YAML + if err := yaml.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + return config, nil +} + +// GetAddress returns the server address in host:port format +func (c *Config) GetAddress() string { + return fmt.Sprintf("%s:%s", c.Server.Host, c.Server.Port) +} diff --git a/internal/wireguard.go b/internal/wireguard.go new file mode 100644 index 0000000..ebfa0bb --- /dev/null +++ b/internal/wireguard.go @@ -0,0 +1,172 @@ +package internal + +import ( + "fmt" + "log" + "os/exec" + "path/filepath" + "regexp" + "slices" + "strings" + + "github.com/samber/lo" +) + +var interfaceRegex = regexp.MustCompile(`^interface:\s+(.+)$`) + +type WireGuardConnection struct { + Name string `json:"name"` + Active bool `json:"active"` +} + +func GetStatus() (string, error) { + output, err := showStatus() + if err != nil { + return "", err + } + status := lo.FilterMap(strings.Split(string(output), "\n"), func(line string, _ int) (string, bool) { + line = strings.TrimSpace(line) + if strings.Contains(line, "interface") { + return fmt.Sprintf("Connection: %s", strings.TrimPrefix(line, "interface:")), true + } + if strings.Contains(line, "latest handshake") { + return fmt.Sprintf("Latest Handshake: %s", strings.TrimPrefix(line, "latest handshake:")), true + } + if strings.Contains(line, "transfer") { + return fmt.Sprintf("Transfer: %s", strings.TrimPrefix(line, "transfer:")), true + } + return "", false + }) + // This is a simple check on whether a connection started or not. + // Instead of complex logic on looping on connections and figuring which connection might be missing info. + // NOTE: This doesn't handle if 3x connections were started and none of them is still active. + // NOTE: ToggleConnection stops all active connections and activate one. + // to avoid issues with multiple VPNs configuring the same iptable that could happen with default wireguard configs + if len(status)%3 != 0 { + status = append(status, "Connection starting...") + } + return strings.Join(status, "\n"), nil +} + +func GetConnections() ([]*WireGuardConnection, error) { + activeConnection, err := getActiveConnections() + if err != nil { + return nil, err + } + allConnections, err := getAllConnections() + if err != nil { + return nil, err + } + + connections := make([]*WireGuardConnection, 0, len(allConnections)) + for _, i := range allConnections { + connections = append(connections, &WireGuardConnection{ + Name: i, + Active: slices.Contains(activeConnection, i), + }) + } + return connections, nil +} + +func ToggleConnection(name string) ([]byte, error) { + allConnections, err := GetConnections() + if err != nil { + return nil, err + } + activeConnections := lo.Filter(allConnections, func(i *WireGuardConnection, _ int) bool { + return i.Active + }) + connection, err := getConnection(name) + if err != nil { + return nil, err + } + output, err := stopActiveConnections(activeConnections) + if err != nil { + return nil, err + } + startOutput, err := startConnection(connection) + if err != nil { + return nil, err + } + output = append(output, startOutput...) + return output, nil +} + +func stopActiveConnections(activeConnections []*WireGuardConnection) ([]byte, error) { + var output []byte + for _, activeConnection := range activeConnections { + log.Printf("Stopping connection %s.", activeConnection.Name) + cmd := exec.Command("sudo", "wg-quick", "down", activeConnection.Name) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + output = append(output, out...) + log.Printf("Successfully stopped connection %s.", activeConnection.Name) + } + return output, nil +} + +func startConnection(connection *WireGuardConnection) ([]byte, error) { + if connection.Active { + return nil, nil + } + log.Printf("Starting connection %s.", connection.Name) + cmd := exec.Command("sudo", "wg-quick", "up", connection.Name) + output, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + log.Printf("Successfully started connection %s.", connection.Name) + return output, nil +} + +// Get the list of all wireguard connections using config files +func getAllConnections() ([]string, error) { + files, err := filepath.Glob("/etc/wireguard/*.conf") + if err != nil { + return nil, err + } + files = lo.Map(files, func(f string, _ int) string { + return strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)) + }) + return files, nil +} + +// Get the list of active wireguard connections using wg show command +func getActiveConnections() ([]string, error) { + var activeConnections []string + status, err := showStatus() + if err != nil { + return nil, err + } + for line := range strings.SplitSeq(string(status), "\n") { + if matches := interfaceRegex.FindStringSubmatch(strings.TrimSpace(line)); len(matches) > 1 { + activeConnections = append(activeConnections, matches[1]) + } + } + return activeConnections, nil +} + +func getConnection(name string) (*WireGuardConnection, error) { + allConnections, err := GetConnections() + if err != nil { + return nil, err + } + connection, ok := lo.Find(allConnections, func(dev *WireGuardConnection) bool { + return dev.Name == name + }) + if !ok { + return nil, fmt.Errorf("failed to find connection: %s", name) + } + return connection, nil +} + +func showStatus() ([]byte, error) { + cmd := exec.Command("sudo", "wg", "show") + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to execute wg show: %w", err) + } + return output, nil +} |
