1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
package hostcontract
import (
"encoding/json"
"fmt"
"io"
"reflect"
"slices"
"sort"
"strconv"
"strings"
"syscall"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/strs"
)
type Role string
const (
RoleGateway Role = "gateway"
RoleBidder Role = "bidder"
RoleRequester Role = "requester"
)
const StagedRootEnv = "SOCIALWEB_STAGED_ROOT"
func ParseRole(s string) (Role, error) {
switch Role(s) {
case RoleGateway, RoleBidder, RoleRequester:
return Role(s), nil
}
return "", fmt.Errorf("hostcontract: role %q: want %s, %s or %s", s, RoleGateway, RoleBidder, RoleRequester)
}
type Status string
const (
StatusOK Status = "ok"
StatusFail Status = "fail"
StatusInfo Status = "info"
)
const (
CheckAccount = "account"
CheckSystemdVersion = "systemd-version"
CheckProbeProperties = "probe-unit-properties"
CheckCgroupV2 = "cgroup-v2"
CheckDelegation = "user-manager-delegation"
CheckLinger = "linger"
CheckSocketPath = "runtime-dir-socket-length"
CheckBootID = "boot-id"
CheckKVM = "dev-kvm"
CheckManagerKVM = "user-manager-kvm-group"
CheckTun = "dev-net-tun"
CheckPastaUserns = "pasta-userns"
CheckAppArmor = "apparmor-pasta-profile"
CheckSubids = "subuid-subgid-range"
CheckUIDMapHelpers = "newuidmap-newgidmap"
CheckUIDMapNamespace = "newuidmap-first-vm-range"
CheckStateACL = "state-dir-posix-acl"
CheckOomd = "systemd-oomd"
CheckFDStore = "user-manager-fd-store"
CheckSlotHostname = "k3s-slot-hostname"
CheckTzdata = "tzdata"
VMSlotIDs = 65536
MaxSocketPath = 108
LongestRuntimeSocket = "/socialweb-vm-0123456789-abcdefghijklmnopqrstuvwxyz012345/fc.sock"
ProbePrefix = "socialweb-probe-"
AppArmorProfileName = "socialweb-pasta"
DelegatedControllers = "cpu memory pids"
InstallCommandTemplate = "socialweb-computer install %s (as root)"
)
type Result struct {
Name string `json:"name"`
Status Status `json:"status"`
Detail string `json:"detail"`
Fix string `json:"fix,omitempty"`
}
type Helper struct {
Name string `json:"name"`
Path string `json:"path"`
Setuid bool `json:"setuidRoot"`
Caps bool `json:"fileCaps"`
Err string `json:"err,omitempty"`
}
type Facts struct {
Role Role
Account string
UID int
Euid int
StateDir string
SystemdVersion string
VersionErr string
ProbeSent map[string]any
ProbeReadBack map[string]any
ProbeErr string
CgroupV2 bool
ManagerControllers []string
ManagerControllersErr string
Linger bool
RuntimeDir string
BootID string
KVMErr string
KVMMissing bool
KVMGid int
AccountInKVM bool
ManagerPID int
ManagerGroups []int
ManagerErr string
TunErr string
PastaChecked bool
PastaErr string
AppArmorRestricts bool
AppArmorLoaded bool
AppArmorErr string
Subuid string
Subgid string
Helpers []Helper
UIDMapChecked bool
UIDMapErr string
ACLChecked bool
ACLErr string
OomdActive bool
Tzdata bool
}
func installFix(role Role) string { return fmt.Sprintf(InstallCommandTemplate, role) }
func Evaluate(f Facts) []Result {
out := []Result{account(f), systemdVersion(f), probeProperties(f), cgroupV2(f), delegation(f), linger(f),
socketPath(f), bootID(f)}
if f.Role == RoleBidder {
out = append(out, kvm(f), managerKVM(f), tun(f), pastaUserns(f), apparmor(f), subids(f), uidmapHelpers(f),
uidmapNamespace(f), stateACL(f))
}
out = append(out, oomd(f))
if f.Role == RoleGateway {
out = append(out, fdStore(f), slotHostname(), tzdata(f))
}
return out
}
func EvaluateRequesterGateway(f Facts) []Result {
if f.VersionErr != "" {
return []Result{fail(CheckCgroupV2, f.VersionErr, "run run requester --with-gateway on Linux")}
}
return []Result{cgroupV2(f), delegation(f), subids(f), uidmapHelpers(f), tzdata(f)}
}
func Failed(rs []Result) []string {
var out []string
for _, r := range rs {
if r.Status == StatusFail {
out = append(out, r.Name)
}
}
return out
}
func Write(w io.Writer, rs []Result, asJSON bool) {
if asJSON {
b, _ := json.MarshalIndent(rs, "", " ")
fmt.Fprintln(w, string(b))
return
}
for _, r := range rs {
fmt.Fprintf(w, "%-5s %-28s %s\n", strings.ToUpper(string(r.Status)), r.Name, r.Detail)
if r.Fix != "" {
fmt.Fprintf(w, " %-28s fix: %s\n", "", r.Fix)
}
}
}
func ok(name, detail string) Result { return Result{Name: name, Status: StatusOK, Detail: detail} }
func fail(name, detail, fix string) Result {
return Result{Name: name, Status: StatusFail, Detail: detail, Fix: fix}
}
func account(f Facts) Result {
if f.Euid == 0 {
return fail(CheckAccount, "running as root; the host contract is the account's view",
fmt.Sprintf("run check-host as the account (install runs it as the account after %s)", installFix(f.Role)))
}
return ok(CheckAccount, fmt.Sprintf("%s (uid %d)", f.Account, f.UID))
}
func systemdVersion(f Facts) Result {
if f.VersionErr != "" {
return fail(CheckSystemdVersion, "the user manager's private socket did not answer: "+f.VersionErr,
"enable linger for the account so user@<uid> runs: "+installFix(f.Role))
}
n, err := strs.LeadingInt(f.SystemdVersion)
if err != nil || n < hostsvc.MinSystemdVersion {
return fail(CheckSystemdVersion,
fmt.Sprintf("the user manager reports systemd %q; %d or newer is required (GetUnitByPIDFD)", f.SystemdVersion, hostsvc.MinSystemdVersion),
fmt.Sprintf("upgrade the host to a distribution shipping systemd >= %d", hostsvc.MinSystemdVersion))
}
return ok(CheckSystemdVersion, "systemd "+f.SystemdVersion)
}
func ProbeSpec(role Role, suffix string, exec []string, outputFile string) (hostsvc.UnitSpec, map[string]any) {
spec := hostsvc.UnitSpec{
Name: ProbePrefix + suffix + hostsvc.ServiceSuffix,
Description: "socialweb-computer check-host property probe",
Exec: exec,
Env: []string{"SOCIALWEB_PROBE=1"},
Output: hostsvc.Output{StdoutFile: outputFile, StderrFile: outputFile},
Limits: hostsvc.Limits{MemoryMax: 64 << 20, CPUWeight: 50, CPUQuotaPercent: 50, TasksMax: 16},
OOMPolicy: "kill",
KillSignal: syscall.SIGTERM,
StopTimeout: 5 * time.Second,
After: []string{"basic.target"},
Wants: []string{"basic.target"},
Slice: hostsvc.DefaultSlice,
}
extra := map[string]any{}
if role == RoleGateway {
extra["FileDescriptorStoreMax"] = uint32(1)
}
return spec, extra
}
var readBackName = map[string]string{
"StandardOutputFileToAppend": "StandardOutput",
"StandardErrorFileToAppend": "StandardError",
}
func PropertyMismatches(sent, back map[string]any) []string {
var bad []string
for name, want := range sent {
readName := name
if rn, ok := readBackName[name]; ok {
readName = rn
}
got, present := back[readName]
if !present || got == nil {
bad = append(bad, name+" (not read back)")
continue
}
if !propertyMatches(name, want, got) {
bad = append(bad, fmt.Sprintf("%s (sent %v, read back %v)", name, want, got))
}
}
sort.Strings(bad)
return bad
}
func propertyMatches(name string, want, got any) bool {
switch name {
case "StandardOutputFileToAppend", "StandardErrorFileToAppend":
return fmt.Sprint(got) == "append"
case "ExecStart":
return execPath(want) != "" && strings.Contains(fmt.Sprint(got), execPath(want))
case "After", "Wants", "Environment":
return containsAll(got, want)
}
return numberOrString(want) == numberOrString(got)
}
func execPath(v any) string {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Slice || rv.Len() == 0 {
return ""
}
first := rv.Index(0)
if first.Kind() == reflect.Struct {
if p := first.FieldByName("Path"); p.IsValid() {
return p.String()
}
}
return fmt.Sprint(first.Interface())
}
func stringsOf(v any) []string {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Slice {
return nil
}
out := make([]string, 0, rv.Len())
for i := 0; i < rv.Len(); i++ {
out = append(out, fmt.Sprint(rv.Index(i).Interface()))
}
return out
}
func containsAll(got, want any) bool {
have := map[string]bool{}
for _, s := range stringsOf(got) {
have[s] = true
}
for _, s := range stringsOf(want) {
if !have[s] {
return false
}
}
return true
}
func numberOrString(v any) string {
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(rv.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(rv.Uint(), 10)
}
return fmt.Sprint(v)
}
func probeProperties(f Facts) Result {
if f.ProbeErr != "" {
return fail(CheckProbeProperties, "the probe unit did not start: "+f.ProbeErr,
"read the probe's start error above; every runtime property must be accepted by the user manager")
}
if len(f.ProbeSent) == 0 {
return fail(CheckProbeProperties, "no probe unit was started", "re-run check-host as the account")
}
if bad := PropertyMismatches(f.ProbeSent, f.ProbeReadBack); len(bad) > 0 {
return fail(CheckProbeProperties, "the user manager did not keep: "+strings.Join(bad, ", "),
fmt.Sprintf("use a host whose systemd (>= %d) supports every property the runtime sets", hostsvc.MinSystemdVersion))
}
names := make([]string, 0, len(f.ProbeSent))
for n := range f.ProbeSent {
names = append(names, n)
}
sort.Strings(names)
return ok(CheckProbeProperties, fmt.Sprintf("%d properties read back: %s", len(names), strings.Join(names, " ")))
}
func cgroupV2(f Facts) Result {
if !f.CgroupV2 {
return fail(CheckCgroupV2, "/sys/fs/cgroup is not the cgroup v2 unified hierarchy",
"boot with systemd.unified_cgroup_hierarchy=1 (the default on current distributions)")
}
return ok(CheckCgroupV2, "/sys/fs/cgroup is cgroup2")
}
func delegation(f Facts) Result {
if f.ManagerControllersErr != "" {
return fail(CheckDelegation, "reading user@"+strconv.Itoa(f.UID)+".service cgroup.controllers: "+f.ManagerControllersErr,
"enable linger for the account: "+installFix(f.Role))
}
have := map[string]bool{}
for _, c := range f.ManagerControllers {
have[c] = true
}
var missing []string
for c := range strings.FieldsSeq(DelegatedControllers) {
if !have[c] {
missing = append(missing, c)
}
}
if len(missing) > 0 {
return fail(CheckDelegation, fmt.Sprintf("user@%d.service is not delegated %s (has %s): CPUQuota would be accepted but unenforced",
f.UID, strings.Join(missing, " "), strings.Join(f.ManagerControllers, " ")),
fmt.Sprintf("%s writes /etc/systemd/system/user@%d.service.d/socialweb-delegate.conf (Delegate=%s); then restart user@%d (stops every VM) or reboot",
installFix(f.Role), f.UID, DelegatedControllers, f.UID))
}
return ok(CheckDelegation, "user@"+strconv.Itoa(f.UID)+".service controllers: "+strings.Join(f.ManagerControllers, " "))
}
func linger(f Facts) Result {
if !f.Linger {
return fail(CheckLinger, "linger is off for "+f.Account+": the user manager stops at the last logout and every VM with it",
installFix(f.Role)+" enables linger once the account, its groups and the delegation drop-in are in place")
}
return ok(CheckLinger, "linger on for "+f.Account)
}
func socketPath(f Facts) Result {
if f.RuntimeDir == "" {
return fail(CheckSocketPath, "XDG_RUNTIME_DIR is unset", "run as the lingering account so /run/user/<uid> exists")
}
n := len(f.RuntimeDir) + len(LongestRuntimeSocket)
if n >= MaxSocketPath {
return fail(CheckSocketPath, fmt.Sprintf("the longest runtime socket path is %d bytes under %s; the limit is %d", n, f.RuntimeDir, MaxSocketPath-1),
"use the default /run/user/<uid> runtime directory")
}
return ok(CheckSocketPath, fmt.Sprintf("longest runtime socket %d bytes (< %d) under %s", n, MaxSocketPath, f.RuntimeDir))
}
func bootID(f Facts) Result {
if f.BootID == "" {
return fail(CheckBootID, "/proc/sys/kernel/random/boot_id is not readable", "mount /proc normally; reboot detection needs boot_id")
}
return ok(CheckBootID, f.BootID)
}
func kvm(f Facts) Result {
switch {
case f.KVMMissing:
return fail(CheckKVM, "/dev/kvm does not exist", "enable hardware virtualization and load kvm_intel or kvm_amd (root); never chmod /dev/kvm")
case f.KVMErr != "":
return fail(CheckKVM, "/dev/kvm is not read-write for "+f.Account+": "+f.KVMErr,
installFix(RoleBidder)+" adds the account to the kvm group before linger; for a foreground run bidder --host-backend proc, add the account to group kvm (root: usermod -aG kvm "+f.Account+") and log in again; never chmod /dev/kvm")
}
return ok(CheckKVM, "/dev/kvm read-write for "+f.Account)
}
func managerKVM(f Facts) Result {
if f.ManagerErr != "" {
return fail(CheckManagerKVM, "reading user@"+strconv.Itoa(f.UID)+"'s groups: "+f.ManagerErr,
"enable linger for the account: "+installFix(RoleBidder))
}
if slices.Contains(f.ManagerGroups, f.KVMGid) {
return ok(CheckManagerKVM, fmt.Sprintf("user@%d (pid %d) runs with kvm (gid %d)", f.UID, f.ManagerPID, f.KVMGid))
}
if f.AccountInKVM {
return fail(CheckManagerKVM,
fmt.Sprintf("the account is in kvm in /etc/group but its user manager (user@%d, pid %d) started before", f.UID, f.ManagerPID),
fmt.Sprintf("restart user@%d - this stops every VM - or reboot; logging in again does not refresh a lingering manager", f.UID))
}
return fail(CheckManagerKVM, fmt.Sprintf("%s is not in the kvm group and user@%d (pid %d) runs without it", f.Account, f.UID, f.ManagerPID),
installFix(RoleBidder)+" adds kvm before the user manager first starts; on an already running manager it refuses and names the restart")
}
func tun(f Facts) Result {
if f.TunErr != "" {
return fail(CheckTun, "/dev/net/tun is not usable: "+f.TunErr, "load the tun module (root): modprobe tun")
}
return ok(CheckTun, "/dev/net/tun opens read-write")
}
func pastaUserns(f Facts) Result {
if !f.PastaChecked {
return fail(CheckPastaUserns, "the embedded pasta was not resolved: "+f.PastaErr,
"give check-host the pasta path (-pasta) in a dev build; a release build extracts it")
}
if f.PastaErr != "" {
fix := "check kernel.unprivileged_userns_clone and user.max_user_namespaces"
if f.AppArmorRestricts {
fix = "sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 (root; persist it in /etc/sysctl.d), or " + installFix(RoleBidder) +
" loads /etc/apparmor.d/" + AppArmorProfileName + " for <state>/runtime/*/pasta"
}
return fail(CheckPastaUserns, "the embedded pasta did not create a user+net namespace and exec through /bin/sh: "+f.PastaErr, fix)
}
return ok(CheckPastaUserns, "the embedded pasta created a user+net namespace and exec'd through /bin/sh")
}
func apparmor(f Facts) Result {
if !f.AppArmorRestricts {
return ok(CheckAppArmor, "AppArmor does not restrict unprivileged user namespaces here; no profile needed")
}
if f.AppArmorErr != "" {
return fail(CheckAppArmor, "reading the loaded AppArmor profiles: "+f.AppArmorErr, installFix(RoleBidder))
}
if !f.AppArmorLoaded {
return fail(CheckAppArmor, "kernel.apparmor_restrict_unprivileged_userns=1 and profile "+AppArmorProfileName+" is not loaded",
installFix(RoleBidder)+" writes /etc/apparmor.d/"+AppArmorProfileName+" and loads it with apparmor_parser -r")
}
return ok(CheckAppArmor, "profile "+AppArmorProfileName+" loaded")
}
type SubidEntry struct {
Owner string
Start uint64
Count uint64
Line int
}
func (e SubidEntry) End() uint64 { return e.Start + e.Count }
func ParseSubids(content string) []SubidEntry {
var out []SubidEntry
for i, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
f := strings.Split(line, ":")
if len(f) != 3 {
continue
}
start, err1 := strconv.ParseUint(f[1], 10, 64)
count, err2 := strconv.ParseUint(f[2], 10, 64)
if err1 != nil || err2 != nil {
continue
}
out = append(out, SubidEntry{Owner: f[0], Start: start, Count: count, Line: i})
}
return out
}
func owns(e SubidEntry, account string, uid int) bool {
return e.Owner == account || e.Owner == strconv.Itoa(uid)
}
func AccountRange(content, account string, uid int) (SubidEntry, bool) {
for _, e := range ParseSubids(content) {
if owns(e, account, uid) {
return e, true
}
}
return SubidEntry{}, false
}
func Overlaps(a, b SubidEntry) bool { return a.Start < b.End() && b.Start < a.End() }
func SubidProblems(subuid, subgid, account string, uid int) []string {
var problems []string
ranges := map[string]SubidEntry{}
for _, file := range []struct{ name, content string }{{"/etc/subuid", subuid}, {"/etc/subgid", subgid}} {
var mine []SubidEntry
for _, e := range ParseSubids(file.content) {
if owns(e, account, uid) {
mine = append(mine, e)
}
}
switch {
case len(mine) == 0:
problems = append(problems, "no range for "+account+" in "+file.name)
continue
case len(mine) > 1:
problems = append(problems, fmt.Sprintf("%d ranges for %s in %s; install reserves exactly one", len(mine), account, file.name))
}
if mine[0].Count < VMSlotIDs {
problems = append(problems, fmt.Sprintf("%s range of %s holds %d ids, under one VM slot (%d)", file.name, account, mine[0].Count, VMSlotIDs))
}
ranges[file.name] = mine[0]
}
for fname, r := range ranges {
for _, other := range []struct{ name, content string }{{"/etc/subuid", subuid}, {"/etc/subgid", subgid}} {
for _, e := range ParseSubids(other.content) {
if owns(e, account, uid) {
continue
}
if Overlaps(r, e) {
problems = append(problems, fmt.Sprintf("%s range %d-%d of %s overlaps %s's %d-%d in %s",
fname, r.Start, r.End()-1, account, e.Owner, e.Start, e.End()-1, other.name))
}
}
}
}
sort.Strings(problems)
return problems
}
func subids(f Facts) Result {
if p := SubidProblems(f.Subuid, f.Subgid, f.Account, f.UID); len(p) > 0 {
return fail(CheckSubids, strings.Join(p, "; "),
installFix(RoleBidder)+" reserves one contiguous range in both files above every existing entry; or keep exactly one range by hand: usermod --del-subuids/--del-subgids the extra ones, then usermod --add-subuids/--add-subgids <first>-<last> (65536 ids per VM, 16 VMs need 1048576)")
}
r, _ := AccountRange(f.Subuid, f.Account, f.UID)
return ok(CheckSubids, fmt.Sprintf("%d-%d (%d VM slots) in /etc/subuid and /etc/subgid, overlapping nothing", r.Start, r.End()-1, r.Count/VMSlotIDs))
}
func uidmapHelpers(f Facts) Result {
var bad, good []string
for _, h := range f.Helpers {
switch {
case h.Err != "":
bad = append(bad, h.Name+": "+h.Err)
case !h.Setuid && !h.Caps:
bad = append(bad, h.Path+" is neither setuid root nor carries file capabilities")
default:
good = append(good, h.Path)
}
}
if len(f.Helpers) == 0 {
bad = append(bad, "newuidmap and newgidmap were not looked up")
}
if len(bad) > 0 {
return fail(CheckUIDMapHelpers, strings.Join(bad, "; "),
"install the distribution's uidmap (Debian/Ubuntu) or shadow-utils (Fedora/RHEL) package (root); never ship a setuid helper")
}
return ok(CheckUIDMapHelpers, strings.Join(good, " "))
}
func uidmapNamespace(f Facts) Result {
if !f.UIDMapChecked {
return fail(CheckUIDMapNamespace, "not attempted: "+f.UIDMapErr, "fix "+CheckSubids+" and "+CheckUIDMapHelpers+" first")
}
if f.UIDMapErr != "" {
return fail(CheckUIDMapNamespace, "a throwaway user namespace could not be mapped into the first VM sub-range: "+f.UIDMapErr,
"check the account's /etc/subuid and /etc/subgid range and that newuidmap/newgidmap are the distribution's")
}
return ok(CheckUIDMapNamespace, "a throwaway user namespace mapped 0-65535 onto the first VM sub-range")
}
func stateACL(f Facts) Result {
if !f.ACLChecked || f.ACLErr != "" {
return fail(CheckStateACL, "POSIX ACLs on "+f.StateDir+": "+f.ACLErr,
"put the state dir on a filesystem mounted with ACL support (ext4, xfs, btrfs default)")
}
return ok(CheckStateACL, "POSIX ACLs supported on "+f.StateDir)
}
func oomd(f Facts) Result {
if f.OomdActive {
return Result{Name: CheckOomd, Status: StatusInfo,
Detail: "systemd-oomd is active; every unit runs below socialweb.slice, outside the app.slice it watches"}
}
return Result{Name: CheckOomd, Status: StatusInfo, Detail: "systemd-oomd is not active"}
}
func fdStore(f Facts) Result {
want, sent := f.ProbeSent["FileDescriptorStoreMax"]
if !sent {
return fail(CheckFDStore, "the probe did not ask for an fd store", "re-run check-host --role gateway")
}
if got, ok := f.ProbeReadBack["FileDescriptorStoreMax"]; !ok || numberOrString(got) != numberOrString(want) {
return fail(CheckFDStore, "the user manager did not keep FileDescriptorStoreMax on a transient service",
"use a systemd whose user manager keeps an fd store for transient services (the kcp front restarts without a refused-connection window only with it)")
}
return ok(CheckFDStore, "FileDescriptorStoreMax read back from a transient service")
}
func slotHostname() Result {
return Result{Name: CheckSlotHostname, Status: StatusInfo,
Detail: "the k3s slot hostname mechanism is chosen by 0006 M3-CP-SLOTS; probed once it lands"}
}
func tzdata(f Facts) Result {
if !f.Tzdata {
return fail(CheckTzdata, "/usr/share/zoneinfo/UTC is missing: the embedded postgres is built with the system tzdata path",
"install the distribution's tzdata package (root)")
}
return ok(CheckTzdata, "/usr/share/zoneinfo present")
}