From b1280f81a2137f3baebd42e0d78acac64e7d77db Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 12 Oct 2026 00:05:04 +0200 Subject: [PATCH] Read the tunnel through sudo where the tool runner is not root (hq issue 522) On novox the tool runner runs as the operator's account, and wg show answered 'Operation not permitted'. Root is the module's concern: the command runs as given when the process is root, else through sudo -n, which the sudo module allows the runtime's account, as nftables does. --- modules/netcheck/cmd/netcheck/tunnel.go | 17 ++++++++++++++++- modules/netcheck/cmd/netcheck/tunnel_test.go | 9 +++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/modules/netcheck/cmd/netcheck/tunnel.go b/modules/netcheck/cmd/netcheck/tunnel.go index 439b7556..d5972757 100644 --- a/modules/netcheck/cmd/netcheck/tunnel.go +++ b/modules/netcheck/cmd/netcheck/tunnel.go @@ -16,6 +16,7 @@ import ( "errors" "fmt" "net" + "os" "os/exec" "sort" "strconv" @@ -118,9 +119,13 @@ var names sync.Map // readTunnel asks wg for the interface's state and names each peer by the mesh's names. func readTunnel(ctx context.Context) (Tunnel, error) { - out, err := exec.CommandContext(ctx, "wg", "show", TunnelInterface, "dump").Output() + program, argv := escalated("wg", []string{"show", TunnelInterface, "dump"}, os.Geteuid()) + out, err := exec.CommandContext(ctx, program, argv...).Output() if err != nil { var ee *exec.ExitError + if program == "sudo" && errors.As(err, &ee) && strings.HasPrefix(strings.TrimSpace(string(ee.Stderr)), "sudo:") { + return Tunnel{}, fmt.Errorf("wg needs root, and sudo refused the runtime's account: %s", strings.TrimSpace(string(ee.Stderr))) + } if errors.As(err, &ee) && strings.Contains(string(ee.Stderr), "No such device") { return Tunnel{}, errNoTunnel } @@ -147,6 +152,16 @@ func readTunnel(ctx context.Context) (Tunnel, error) { return t, nil } +// escalated is the command as it is run: as given when this process is root, else through sudo without a +// prompt, which the mesh's sudo module allows the runtime's account (ADR 0175 ยง4: root is the module's +// concern; the nftables module reads its rules the same way). The tunnel answers only to root. +func escalated(cmd string, args []string, euid int) (string, []string) { + if euid == 0 { + return cmd, args + } + return "sudo", append([]string{"-n", cmd}, args...) +} + // nameOf is the mesh's name for a peer's own address (a single-address allowed ip), or "". func nameOf(ctx context.Context, allowed []string) string { for _, a := range allowed { diff --git a/modules/netcheck/cmd/netcheck/tunnel_test.go b/modules/netcheck/cmd/netcheck/tunnel_test.go index 0bba1f9e..29f85b78 100644 --- a/modules/netcheck/cmd/netcheck/tunnel_test.go +++ b/modules/netcheck/cmd/netcheck/tunnel_test.go @@ -79,3 +79,12 @@ func asString(v any) string { s, _ := v.(string) return s } + +func TestTheTunnelIsReadAsRootOrThroughSudo(t *testing.T) { + if p, a := escalated("wg", []string{"show", "mesh0", "dump"}, 0); p != "wg" || strings.Join(a, " ") != "show mesh0 dump" { + t.Errorf("as root: %s %v", p, a) + } + if p, a := escalated("wg", []string{"show", "mesh0", "dump"}, 1000); p != "sudo" || strings.Join(a, " ") != "-n wg show mesh0 dump" { + t.Errorf("as an account: %s %v", p, a) + } +}