Merge pull request 'Say the login session a mesh-cli line comes from, and pin the SDK at go/v0.1.9 (hq ADR 0272)' (#70) from feat/272-a-login-session-and-the-tagged-sdk into main
This commit was merged in pull request #70.
This commit is contained in:
@@ -159,6 +159,11 @@ controller decides what the line may do, and the engine runs nothing it receives
|
||||
connection's deadline set the moment it is accepted, two lines at once per account and eight for the machine,
|
||||
"busy" beyond that.
|
||||
|
||||
**Only a login session can be the terminal.** The engine reads the asking process's cgroup and sends its login
|
||||
session's scope (`session-<id>.scope` under the account's own `user-<uid>.slice`), or nothing; the controller makes
|
||||
nothing else the terminal. A service running as the operator — the tool runner, a unit of the account's user
|
||||
manager — is at most an ordinary call.
|
||||
|
||||
**A process outside the machine's own namespaces is refused**, whatever its user id: the peer's mount and user
|
||||
namespace must be the engine's. So `mesh-cli` run inside a flatpak, a toolbox or distrobox container, or any other
|
||||
container — even one running as the operator's own id — is refused with the reason; run it from the machine's own
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/novox/mesh-host
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009103748-a5a040c9049b
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.9
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/sys v0.48.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009103748-a5a040c9049b h1:HmasScQ02QeFlAFDnmRCOEserbtDJJtutK+acBmGlNs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009103748-a5a040c9049b/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.9 h1:2zq9tbwoXEYi+dDBxPoeNmn39KdRRKRp8CK33F4LWd0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.9/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -83,7 +83,7 @@ func AskTheController(ctx context.Context, ask AskFunc, asked Asked) cli.Answer
|
||||
return cli.NotKnown(call, fmt.Sprintf("no answer within the time mesh-cli waits (%s)", cli.AskWithin))
|
||||
case <-time.After(followEvery):
|
||||
}
|
||||
body, _ := json.Marshal(Asked{Follow: call, Account: asked.Account, UID: asked.UID})
|
||||
body, _ := json.Marshal(Asked{Follow: call, Account: asked.Account, UID: asked.UID, Session: asked.Session})
|
||||
if reply, err = ask(ctx, body); err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -35,6 +36,11 @@ type Asked struct {
|
||||
UID uint32 `json:"uid"`
|
||||
// Follow is a call a line runs as, asked about again by the account that asked it (AskTheController).
|
||||
Follow string `json:"follow,omitempty"`
|
||||
// Session is the login session the asking process runs in — its systemd scope, `session-<id>.scope` under the
|
||||
// account's own slice — as the kernel's cgroup says it; empty for anything else: a system service, a user unit,
|
||||
// a process another account's session started. Only a login session is a person at a terminal (novox/hq ADR
|
||||
// 0272): the controller makes nothing else the terminal.
|
||||
Session string `json:"session,omitempty"`
|
||||
}
|
||||
|
||||
// Forward asks the controller and follows the line to what came of it (AskTheController): what it printed, that
|
||||
@@ -84,6 +90,35 @@ func (s *slots) give(uid uint32) {
|
||||
// test can say what the kernel would; it is the only source of the account.
|
||||
var peerOf = peerCredentials
|
||||
|
||||
// sessionOf reads the login session a process runs in, from the kernel. A variable so a test can say.
|
||||
var sessionOf = func(pid int32, uid uint32) string {
|
||||
raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pid))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return loginSessionIn(string(raw), uid)
|
||||
}
|
||||
|
||||
// loginSessionIn is the login session scope a process's cgroup names, for its own account: the unified hierarchy's
|
||||
// line, `0::/user.slice/user-<uid>.slice/session-<id>.scope`, and nothing below or beside it. Anything else is no
|
||||
// login session — a service under system.slice, a unit under the account's user manager (user@<uid>.service), a
|
||||
// scope of another account's.
|
||||
func loginSessionIn(cgroup string, uid uint32) string {
|
||||
for _, line := range strings.Split(cgroup, "\n") {
|
||||
path, ok := strings.CutPrefix(line, "0::")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
prefix := fmt.Sprintf("/user.slice/user-%d.slice/", uid)
|
||||
scope, ok := strings.CutPrefix(path, prefix)
|
||||
if !ok || strings.Contains(scope, "/") || !strings.HasPrefix(scope, "session-") || !strings.HasSuffix(scope, ".scope") {
|
||||
return ""
|
||||
}
|
||||
return scope
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// sameNamespaces refuses a peer whose mount or user namespace is not the engine's. A variable so a test can say.
|
||||
var sameNamespaces = namespacesOf
|
||||
|
||||
@@ -204,5 +239,5 @@ func answer(ctx context.Context, conn *net.UnixConn, held *slots, forward Forwar
|
||||
_ = conn.SetDeadline(time.Now().Add(cli.AskWithin))
|
||||
asking, cancel := context.WithTimeout(ctx, cli.AskWithin-30*time.Second)
|
||||
defer cancel()
|
||||
reply(forward(asking, Asked{Line: req.Line, Account: account, UID: uid}))
|
||||
reply(forward(asking, Asked{Line: req.Line, Account: account, UID: uid, Session: sessionOf(pid, uid)}))
|
||||
}
|
||||
|
||||
@@ -126,6 +126,11 @@ func TestTheRequestToTheControllerKeepsItsFieldNames(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -216,3 +221,35 @@ func TestAPeerInAnotherNamespaceIsRefused(t *testing.T) {
|
||||
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 := loginSessionIn(string(raw), uint32(os.Getuid())); got.Session != want {
|
||||
t.Fatalf("sent session %q, the kernel says %q", got.Session, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user