package wireguard import ( "context" "fmt" "os" "os/exec" "strings" "github.com/ngoduykhanh/wireguard-ui/util" ) // UnitName returns the systemd unit name managing the given WireGuard // interface via wg-quick, e.g. "wg-quick@wg-home.service". func UnitName(iface string) string { return "wg-quick@" + iface + ".service" } // Start brings up the given WireGuard interface via // `systemctl start wg-quick@.service`. func Start(ctx context.Context, iface string) error { EnsureIPForwarding() return runSystemctl(ctx, "start", iface) } // Stop brings down the given WireGuard interface via // `systemctl stop wg-quick@.service`. func Stop(ctx context.Context, iface string) error { return runSystemctl(ctx, "stop", iface) } // Restart restarts the given WireGuard interface via // `systemctl restart wg-quick@.service`. func Restart(ctx context.Context, iface string) error { EnsureIPForwarding() return runSystemctl(ctx, "restart", iface) } // EnsureIPForwarding turns on IPv4/IPv6 forwarding for the running kernel // (equivalent to `sysctl -w net.ipv4.ip_forward=1`), best-effort. Without // this, any server relying on FORWARD rules or NAT egress silently drops // all forwarded traffic. Persistence across reboots (e.g. // /etc/sysctl.d/*.conf) is left to install-time setup, not this runtime // call - this only guarantees the currently running kernel is correct // whenever a server is (re)started. func EnsureIPForwarding() { setSysctl("/proc/sys/net/ipv4/ip_forward") setSysctl("/proc/sys/net/ipv6/conf/all/forwarding") } func setSysctl(path string) { data, err := os.ReadFile(path) if err == nil && strings.TrimSpace(string(data)) == "1" { return } _ = os.WriteFile(path, []byte("1"), 0644) } func runSystemctl(ctx context.Context, action, iface string) error { if !util.ValidateInterfaceName(iface) { return fmt.Errorf("invalid interface name: %q", iface) } cmd := exec.CommandContext(ctx, "systemctl", action, UnitName(iface)) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("systemctl %s %s failed: %w: %s", action, UnitName(iface), err, string(out)) } return nil } // Status reports whether the given WireGuard interface's wg-quick unit is // currently active and whether it is enabled to start on boot. Only real // exec failures (e.g. systemctl binary missing) are returned as err - a // unit being inactive/disabled/failed is a normal, non-error result // reflected in the returned booleans. func Status(ctx context.Context, iface string) (active bool, enabled bool, err error) { if !util.ValidateInterfaceName(iface) { return false, false, fmt.Errorf("invalid interface name: %q", iface) } unit := UnitName(iface) active, err = systemctlBoolCheck(ctx, "is-active", unit) if err != nil { return false, false, err } enabled, err = systemctlBoolCheck(ctx, "is-enabled", unit) if err != nil { return active, false, err } return active, enabled, nil } // systemctlBoolCheck runs `systemctl ` and interprets a // non-zero exit code as a normal "false" result (inactive/disabled/failed), // not an error. Only a failure to execute systemctl at all (binary missing, // context cancelled, ...) is surfaced as err. func systemctlBoolCheck(ctx context.Context, verb, unit string) (bool, error) { cmd := exec.CommandContext(ctx, "systemctl", verb, unit) err := cmd.Run() if err == nil { return true, nil } if _, ok := err.(*exec.ExitError); ok { // systemctl ran fine and just reported a non-active/non-enabled // state via its exit code - not an execution error. return false, nil } return false, fmt.Errorf("systemctl %s %s failed to execute: %w", verb, unit, err) }