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) + } +}