Files
mesh-host/internal/meshcli/meshcli_test.go
T
jochen bf3f45e769
mesh/merge-gate pass: builds mesh-host → 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
mesh/delivery delivered
A login session is the account's own, of class user, read at accept (hq ADR 0272)
The cgroup names a scope; only logind's record of it says whose session it
is. The engine now takes a session only when /run/systemd/sessions/<id>
says it is this account's (UID), of class user (not a greeter's or the
user manager's), and of this very scope. And it reads the session the
moment the connection is accepted, with the namespaces, before the line,
so the peer is judged as it was when it connected.

Tests: session-9.scope with a user record is taken; x.scope, a different
uid, another account's session, a greeter's, a record naming another
scope, no record, and a path with dots are not; the session is read
before the line is forwarded.
2026-10-09 13:55:45 +02:00

315 lines
12 KiB
Go

package meshcli
import (
"context"
"encoding/json"
"errors"
"net"
"os"
"os/user"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/cli"
)
// serving starts Serve on a socket of the test's own, and stops it when the test ends.
func serving(t *testing.T, forward Forward) string {
t.Helper()
socket := filepath.Join(t.TempDir(), "run", "cli.sock")
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() { done <- Serve(ctx, socket, forward, func(string) {}) }()
t.Cleanup(func() {
cancel()
if err := <-done; err != nil {
t.Errorf("Serve ended with %v", err)
}
})
for i := 0; i < 200; i++ {
if _, err := os.Stat(socket); err == nil {
return socket
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("the socket never appeared")
return ""
}
// The account sent is the one the kernel gives for the connecting process, and the answer comes back as it was.
func TestTheAccountAskingIsTheKernelsAndTheAnswerComesBack(t *testing.T) {
var got Asked
socket := serving(t, func(_ context.Context, asked Asked) cli.Answer {
got = asked
return cli.Answer{Stdout: []byte("done\n"), Exit: 0, Terminal: true, Why: "the terminal"}
})
a, err := cli.Ask(context.Background(), socket, []string{"settings", "show", "zsh"})
if err != nil {
t.Fatal(err)
}
if string(a.Stdout) != "done\n" || !a.Terminal || a.Refused != "" {
t.Fatalf("the answer came back as %+v", a)
}
me, err := user.Current()
if err != nil {
t.Skipf("this machine cannot name the test's account: %v", err)
}
if int(got.UID) != os.Getuid() || got.Account != me.Username {
t.Fatalf("asked as %d %q, and the kernel says %d %q", got.UID, got.Account, os.Getuid(), me.Username)
}
if strings.Join(got.Line, " ") != "settings show zsh" {
t.Fatalf("the line was sent as %q", got.Line)
}
if info, err := os.Stat(socket); err != nil || info.Mode().Perm() != 0o666 {
t.Fatalf("the socket is not open to every account: %v %v", info.Mode(), err)
}
}
// Credentials the kernel did not give are no account: refused, and nothing is asked.
func TestAConnectionWhoseAccountCannotBeReadIsNeverAskedAsAnyone(t *testing.T) {
before := peerOf
peerOf = func(*net.UnixConn) (uint32, int32, error) { return 0, 0, errors.New("no credentials") }
t.Cleanup(func() { peerOf = before })
asked := false
socket := serving(t, func(context.Context, Asked) cli.Answer {
asked = true
return cli.Answer{}
})
a, err := cli.Ask(context.Background(), socket, []string{"status"})
if err != nil {
t.Fatal(err)
}
if asked || a.Refused == "" || a.Exit == 0 {
t.Fatalf("a connection with no account was asked (%v) or not refused: %+v", asked, a)
}
}
// A request over the bound is refused and never asked.
func TestARequestOverTheBoundIsRefused(t *testing.T) {
asked := false
socket := serving(t, func(context.Context, Asked) cli.Answer {
asked = true
return cli.Answer{}
})
a, err := cli.Ask(context.Background(), socket, []string{strings.Repeat("x", cli.MaxRequest)})
if err != nil {
t.Fatal(err)
}
if asked || a.Refused == "" {
t.Fatalf("a request over the bound was asked (%v): %+v", asked, a)
}
}
// A file at the socket's path that is not a socket is never removed.
func TestAFileThatIsNotASocketIsKept(t *testing.T) {
dir := t.TempDir()
socket := filepath.Join(dir, "cli.sock")
if err := os.WriteFile(socket, []byte("mine"), 0o644); err != nil {
t.Fatal(err)
}
err := Serve(context.Background(), socket, nil, nil)
if err == nil || !strings.Contains(err.Error(), "not a socket") {
t.Fatalf("a file at the socket's path was not refused: %v", err)
}
if body, _ := os.ReadFile(socket); string(body) != "mine" {
t.Fatal("the file at the socket's path was touched")
}
}
// The request the controller reads holds these field names; the controller's side holds the same list
// (mesh-controller, the mesh-cli request's test).
func TestTheRequestToTheControllerKeepsItsFieldNames(t *testing.T) {
body, err := json.Marshal(Asked{Line: []string{"status"}, Account: "a", UID: 1})
if err != nil {
t.Fatal(err)
}
body, _ = json.Marshal(Asked{Line: []string{"status"}, Account: "a", UID: 1, Session: "session-3.scope"})
if got := keysOf(t, body); got != "account line session uid" {
t.Fatalf("the request's fields are %q", got)
}
body, _ = json.Marshal(Asked{Line: []string{"status"}, Account: "a", UID: 1})
if got := keysOf(t, body); got != "account line uid" {
t.Fatalf("the request's fields are %q", got)
}
body, _ = json.Marshal(Asked{Follow: "call-1", Account: "a", UID: 1})
if got := keysOf(t, body); got != "account follow uid" {
t.Fatalf("a follow's fields are %q", got)
}
// What the controller answers as a line's result, and as a running line's: the same names its side holds.
body, err = json.Marshal(cli.Answer{Stdout: []byte("o"), Stderr: []byte("e"), Exit: 1, Terminal: true, Why: "w",
Refused: "r", Cut: true})
if err != nil {
t.Fatal(err)
}
if got := keysOf(t, body); got != "cut exit refused stderr stdout terminal why" {
t.Fatalf("the answer's fields are %q", got)
}
body, _ = json.Marshal(runningResult{Running: true, Call: "call-1"})
if got := keysOf(t, body); got != "call running" {
t.Fatalf("a running answer's fields are %q", got)
}
}
func keysOf(t *testing.T, body []byte) string {
t.Helper()
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
t.Fatal(err)
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return strings.Join(keys, " ")
}
// **One account cannot hold the socket** (review of ADR 0272): its lines beyond its own bound are answered busy at
// once, nothing is asked, and the engine goes on accepting.
func TestOneAccountsLinesBeyondItsBoundAreAnsweredBusy(t *testing.T) {
was := perAccount
perAccount = 1
t.Cleanup(func() { perAccount = was })
release := make(chan struct{})
started := make(chan struct{}, 2)
socket := serving(t, func(context.Context, Asked) cli.Answer {
started <- struct{}{}
<-release
return cli.Answer{}
})
// Released before the engine is stopped: cleanups run last first, and the engine waits for its lines.
t.Cleanup(func() { close(release) })
go func() { _, _ = cli.Ask(context.Background(), socket, []string{"push"}) }()
<-started
done := make(chan cli.Answer, 1)
go func() {
a, _ := cli.Ask(context.Background(), socket, []string{"status"})
done <- a
}()
select {
case a := <-done:
if !strings.Contains(a.Refused, "busy") {
t.Fatalf("a second line from the same account reads %+v", a)
}
case <-time.After(5 * time.Second):
t.Fatal("a second line from the same account waited instead of being answered busy")
}
if len(started) != 0 {
t.Fatal("the busy line was asked")
}
}
// **A process in another namespace is not this machine's account** (review of ADR 0272): a container running with
// the operator's user id shares the id and not the machine; refused, and nothing is asked.
func TestAPeerInAnotherNamespaceIsRefused(t *testing.T) {
before := sameNamespaces
sameNamespaces = func(pid int32) error { return errors.New("its mount namespace is not the engine's") }
t.Cleanup(func() { sameNamespaces = before })
asked := false
socket := serving(t, func(context.Context, Asked) cli.Answer {
asked = true
return cli.Answer{}
})
a, err := cli.Ask(context.Background(), socket, []string{"status"})
if err != nil {
t.Fatal(err)
}
if asked || !strings.Contains(a.Refused, "namespace") {
t.Fatalf("a peer in another namespace was asked (%v): %+v", asked, a)
}
}
// **The session a line comes from is the kernel's word, and only a login session counts** (review of ADR 0272): a
// service running as the operator — the tool runner, a user unit — is not a person at a terminal. The engine reads
// the peer's cgroup and sends the login session's scope, or nothing; the controller makes no other terminal.
func TestTheLoginSessionAskingIsReadFromTheKernel(t *testing.T) {
for _, c := range []struct{ cgroup, session string }{
{"0::/user.slice/user-1000.slice/session-3.scope\n", "session-3.scope"},
{"0::/user.slice/user-1000.slice/session-c1.scope\n", "session-c1.scope"},
{"0::/user.slice/user-1000.slice/user@1000.service/app.slice/tmux.scope\n", ""},
{"0::/system.slice/node-tools.service\n", ""},
{"0::/user.slice/user-1001.slice/session-3.scope\n", ""}, // another account's session
{"12:pids:/user.slice\n0::/user.slice/user-1000.slice/session-9.scope/sub\n", ""},
{"", ""},
} {
if got := loginSessionIn(c.cgroup, 1000); got != c.session {
t.Errorf("%q read as %q, want %q", c.cgroup, got, c.session)
}
}
// And it is sent: the test process's own, whatever it is, as the kernel says it.
var got Asked
socket := serving(t, func(_ context.Context, asked Asked) cli.Answer {
got = asked
return cli.Answer{}
})
if _, err := cli.Ask(context.Background(), socket, []string{"status"}); err != nil {
t.Fatal(err)
}
raw, _ := os.ReadFile("/proc/self/cgroup")
if want := userSessionIn(string(raw), uint32(os.Getuid())); got.Session != want {
t.Fatalf("sent session %q, the kernel says %q", got.Session, want)
}
}
// **A login session is a person's: systemd's own record says so** (review of ADR 0272): the scope the cgroup names
// must be a session logind keeps for this very account, of class `user` — not a greeter's, not the user manager's,
// not one whose record names another account — and read at accept, with the namespaces, before anything else.
func TestALoginSessionIsAUsersByItsOwnRecord(t *testing.T) {
dir := t.TempDir()
was := sessionsDir
sessionsDir = dir
t.Cleanup(func() { sessionsDir = was })
write := func(id, body string) {
if err := os.WriteFile(filepath.Join(dir, id), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
write("9", "UID=1000\nUSER=op\nCLASS=user\nSCOPE=session-9.scope\nACTIVE=1\n")
write("3", "UID=1000\nCLASS=greeter\nSCOPE=session-3.scope\n")
write("4", "UID=1001\nCLASS=user\nSCOPE=session-4.scope\n")
write("5", "UID=1000\nCLASS=user\nSCOPE=session-6.scope\n")
for _, c := range []struct {
cgroup string
uid uint32
session string
}{
{"0::/user.slice/user-1000.slice/session-9.scope\n", 1000, "session-9.scope"},
{"0::/user.slice/user-1000.slice/x.scope\n", 1000, ""},
{"0::/user.slice/user-1000.slice/session-9.scope\n", 1001, ""}, // a different uid than the scope's slice
{"0::/user.slice/user-1001.slice/session-4.scope\n", 1000, ""}, // another account's session
{"0::/user.slice/user-1000.slice/session-3.scope\n", 1000, ""}, // a greeter's
{"0::/user.slice/user-1000.slice/session-5.scope\n", 1000, ""}, // a record naming another scope
{"0::/user.slice/user-1000.slice/session-7.scope\n", 1000, ""}, // no record at all
{"0::/user.slice/user-1000.slice/session-../9.scope\n", 1000, ""},
} {
if got := userSessionIn(c.cgroup, c.uid); got != c.session {
t.Errorf("%q as uid %d read as %q, want %q", c.cgroup, c.uid, got, c.session)
}
}
}
// The session is read at accept, with the namespaces, and before the line: a peer that execs or exits while its
// line is read is judged as it was when it connected.
func TestTheSessionIsReadBeforeTheLine(t *testing.T) {
var order []string
beforeNS, beforeSession := sameNamespaces, sessionOf
sameNamespaces = func(int32) error { order = append(order, "namespaces"); return nil }
sessionOf = func(int32, uint32) string { order = append(order, "session"); return "session-1.scope" }
t.Cleanup(func() { sameNamespaces, sessionOf = beforeNS, beforeSession })
var got Asked
socket := serving(t, func(_ context.Context, asked Asked) cli.Answer {
order = append(order, "forward")
got = asked
return cli.Answer{}
})
if _, err := cli.Ask(context.Background(), socket, []string{"status"}); err != nil {
t.Fatal(err)
}
if strings.Join(order, ",") != "namespaces,session,forward" || got.Session != "session-1.scope" {
t.Fatalf("read in the order %v, sent %q", order, got.Session)
}
}