summaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorAhmed AbdelHalim <[email protected]>2025-09-06 23:55:19 +0200
committerAhmed AbdelHalim <[email protected]>2025-09-07 02:20:46 +0200
commit17168a489392847794de0aed15fe395230a38261 (patch)
tree60914d9df4b9b37852e54dcf72818b0df1a958fb /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.go56
-rw-r--r--internal/wireguard.go172
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
+}