Per-server Start/Stop/Restart via systemd wg-quick@<iface>.service units plus live status badge, and a table/list-view toggle with checkbox bulk-delete for both the server list and per-server client list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PvrfUytqd74H6WcQkRzFM4
89 lines
2.8 KiB
Go
89 lines
2.8 KiB
Go
package wireguard
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
|
|
"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 {
|
|
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 {
|
|
return runSystemctl(ctx, "restart", iface)
|
|
}
|
|
|
|
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)
|
|
}
|