Files
wireguard-ui-multi/wireguard/service.go
T
sysopsandClaude Sonnet 5 83b1da291f Add per-server NAT egress, ip_forward auto-enable, OPNsense import review checklist
- ServerSetting gains WanInterface/EgressSNATIP for optional per-server
  masquerade/SNAT of client traffic, isolated in each server's own
  nftables table
- wireguard.Start/Restart now ensure net.ipv4.ip_forward and
  net.ipv6.conf.all.forwarding are enabled before bringing an interface up
- OPNsense config.xml import now parses staticroutes/filter/nat rules and
  surfaces them as a manual-review checklist in the preview UI (never
  auto-applied)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATVUwTa4Pqwq26orW5BcDW
2026-07-29 13:08:52 +02:00

113 lines
3.6 KiB
Go

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@<iface>.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@<iface>.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@<iface>.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 <verb> <unit>` 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)
}