Read the tunnel through sudo where the tool runner is not root (hq issue 522)
mesh/merge-gate pass: builds netcheck → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered

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.
This commit is contained in:
2026-10-12 00:05:04 +02:00
parent d2dff99bcf
commit b1280f81a2
2 changed files with 25 additions and 1 deletions
+16 -1
View File
@@ -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 {
@@ -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)
}
}