Vouch for the account that runs mesh-cli, and ask the controller its line (hq ADR 0272)
The records send the operator to "the controller's terminal", and nothing reaches it (hq issue 343). mesh-cli holds no credential: it asks the engine on its own machine over a socket. The engine reads the asking account from the kernel (SO_PEERCRED), never from the program, and asks the controller on mesh.control.<node>.cli, its own node's subject, so the bus server says which node asked. The controller decides what the line may do; nothing received on the socket is run or applied here, and the socket has no network address (ADR 0004 stands). The local protocol is the SDK's go/cli package, at a pseudo-version of its branch until that merges and is tagged go/v0.1.8.
This commit is contained in:
@@ -1218,6 +1218,9 @@ func runLink(ctx context.Context, opts options) error {
|
||||
}
|
||||
// And the core builds this host placed are judged, whichever host placed them (to-be 45 §8).
|
||||
go watchWhatThisHostPlaced(aside, mine.Node, queue, say)
|
||||
// And mesh-cli is answered on this machine: the account from the kernel, the line asked of the controller
|
||||
// (novox/hq ADR 0272).
|
||||
go serveMeshCLI(aside, queue, say)
|
||||
|
||||
held := link.HoldRoused(aside, membership, queue, say, opts.timeout, rousedBySignal(ctx))
|
||||
// The act in hand finishes and is kept before this process exits: an apply that stood aside with
|
||||
@@ -1364,6 +1367,8 @@ func (w *adoptionWatch) changed(r link.Report) bool {
|
||||
// be a control surface on every machine, reachable by anything that can reach the machine, in
|
||||
// exchange for saving twenty seconds — and the whole security argument rests on there not being
|
||||
// one. A signal is delivered by the service manager to a process it already supervises.
|
||||
// (mesh-cli's socket is no such surface: it has no network address and nothing sent to it acts on
|
||||
// the machine — it is asked of the controller, novox/hq ADR 0272 §3.)
|
||||
//
|
||||
// SIGHUP, because that is the signal a long-running program conventionally reads as *look again*,
|
||||
// and nothing here is being reloaded from a file that a different signal would suit better.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
"github.com/novox/mesh-host/internal/meshcli"
|
||||
)
|
||||
|
||||
// serveMeshCLI answers mesh-cli on this machine for as long as this engine serves (novox/hq ADR 0272): the account
|
||||
// from the kernel, the line asked of the controller over the link open at the time. A socket that cannot be served
|
||||
// is said once, and this engine goes on applying; mesh-cli then says the engine does not answer.
|
||||
func serveMeshCLI(ctx context.Context, queue *link.Queue, say func(string)) {
|
||||
if err := meshcli.Serve(ctx, cli.Socket, forwardOver(queue), say); err != nil {
|
||||
say("mesh-cli is not answered on this machine: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// forwardOver asks the controller over the queue's link, and reads its answer as mesh-cli's.
|
||||
func forwardOver(queue *link.Queue) meshcli.Forward {
|
||||
return func(ctx context.Context, asked meshcli.Asked) (cli.Answer, error) {
|
||||
body, err := json.Marshal(asked)
|
||||
if err != nil {
|
||||
return cli.Answer{}, err
|
||||
}
|
||||
reply, err := queue.AskController(ctx, body)
|
||||
if err != nil {
|
||||
return cli.Answer{}, err
|
||||
}
|
||||
var a cli.Answer
|
||||
if err := json.Unmarshal(reply, &a); err != nil {
|
||||
return cli.Answer{}, fmt.Errorf("the controller's answer is not one: %w", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ module github.com/novox/mesh-host
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009100314-bfe82315f42c
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009100314-bfe82315f42c h1:8hjQ3FYt7mGdZv+HX0Vk+n3pdaS3W5EIlwsrvSXXhRY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009100314-bfe82315f42c/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=
|
||||
|
||||
@@ -67,6 +67,11 @@ func (b OverNATS) Health(ctx context.Context, node string, body []byte) error {
|
||||
return b.Conn.FlushWithContext(flush)
|
||||
}
|
||||
|
||||
// CLISubject is where this node's engine asks the controller a line `mesh-cli` was given on this machine
|
||||
// (novox/hq ADR 0272 §3): its own node's, inside the grant every engine already has (`mesh.control.<node>.>`), so the
|
||||
// bus server is the authority on which node asked. Core NATS, never the CONTROL stream, which names its own subjects.
|
||||
func CLISubject(node string) string { return "mesh.control." + node + ".cli" }
|
||||
|
||||
// ToolSubject is a module's tool on one machine's node tools — the subject a node-engine asks a declared
|
||||
// tool check on (novox/hq ADR 0240, to-be 48 §3), granted it for its own machine's instance and no other.
|
||||
func ToolSubject(module, tool, node string) string {
|
||||
|
||||
@@ -544,6 +544,23 @@ func (q *Queue) AskTool(ctx context.Context, module, tool string) (bool, string,
|
||||
return ReadToolAnswer(reply)
|
||||
}
|
||||
|
||||
// AskController asks the controller a line mesh-cli was given on this machine, on this node's own subject, over
|
||||
// the link open now (novox/hq ADR 0272 §3), and returns its answer as it came. No link open is said, not waited for.
|
||||
func (q *Queue) AskController(ctx context.Context, body []byte) ([]byte, error) {
|
||||
q.init()
|
||||
q.mu.Lock()
|
||||
bus := q.bus
|
||||
q.mu.Unlock()
|
||||
if bus == nil {
|
||||
return nil, errors.New("this machine has no link to the bus open now")
|
||||
}
|
||||
asker, ok := bus.(ToolBus)
|
||||
if !ok {
|
||||
return nil, errors.New("the link to the bus open now cannot ask the controller")
|
||||
}
|
||||
return asker.Ask(ctx, CLISubject(q.Membership.Node), body)
|
||||
}
|
||||
|
||||
// ReadToolAnswer reads a tool's reply envelope — `{result, error}`, as the node tools answer every call —
|
||||
// as a health answer: an error is not healthy, and so is a result that does not say `healthy: true`.
|
||||
func ReadToolAnswer(reply []byte) (bool, string, error) {
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Package meshcli is the node-engine's side of `mesh-cli` (novox/hq ADR 0272): a socket on this machine that any
|
||||
// account may connect to, the account read from the kernel, and the line asked of the controller.
|
||||
//
|
||||
// **The node-engine vouches; it does not decide.** It adds what only it can know — which account on this machine
|
||||
// is asking, from the kernel's peer credentials, never from anything the program says — and sends the request out
|
||||
// over its own link, on its own node's subject. The controller judges whether the line runs as its terminal, as an
|
||||
// ordinary call, or not at all (ADR 0272 §4). Nothing received here is run or applied on the machine.
|
||||
//
|
||||
// **No listening control surface** (novox/hq ADR 0004, ADR 0272 §3): the socket has no network address, so
|
||||
// nothing on another machine reaches it, and nothing sent to it acts on this one.
|
||||
package meshcli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
)
|
||||
|
||||
// Asked is what the node-engine sends the controller on its own node's subject: the line, and the account the
|
||||
// kernel says is asking. The controller's side holds the same field names (a test on each side).
|
||||
type Asked struct {
|
||||
Line []string `json:"line"`
|
||||
// Account is the login of the user id the kernel gave, from this machine's user database.
|
||||
Account string `json:"account"`
|
||||
// UID is the user id itself, so a renamed or duplicated login is still the same id.
|
||||
UID uint32 `json:"uid"`
|
||||
}
|
||||
|
||||
// Forward asks the controller, and is answered. An error is said to the program as a refusal: nothing ran.
|
||||
type Forward func(ctx context.Context, asked Asked) (cli.Answer, error)
|
||||
|
||||
// readWithin bounds how long a connection may take to say its line: a program that connects and says nothing
|
||||
// holds nothing for long.
|
||||
const readWithin = 30 * time.Second
|
||||
|
||||
// atOnce bounds how many lines are asked at the same time. A person types one at a time; more than this is a
|
||||
// program on the machine, and it waits.
|
||||
const atOnce = 8
|
||||
|
||||
// peerOf reads the connecting process's credentials from the kernel. A variable so a test can say what the kernel
|
||||
// would; it is the only source of the account.
|
||||
var peerOf = peerCredentials
|
||||
|
||||
// lookupAccount names a user id from this machine's user database.
|
||||
var lookupAccount = func(uid uint32) (string, error) {
|
||||
u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.Username, nil
|
||||
}
|
||||
|
||||
// Serve listens on socket until ctx ends, and answers each connection: one request, one answer.
|
||||
//
|
||||
// The directory is made 0755 and the socket 0666: every account on the machine may ask, because the account is
|
||||
// what is vouched for and the controller decides what it may do. A file at the socket's path that is not a socket
|
||||
// is never removed: the engine says so and serves nothing.
|
||||
func Serve(ctx context.Context, socket string, forward Forward, say func(string)) error {
|
||||
dir := filepath.Dir(socket)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("mesh-cli's socket directory: %w", err)
|
||||
}
|
||||
// The directory is the engine's: a link, or one another account owns, could put the socket where that
|
||||
// account replaces it with its own. Said, and nothing served.
|
||||
if problem := notOwnDirectory(dir); problem != "" {
|
||||
return fmt.Errorf("mesh-cli is not served on this machine: %s", problem)
|
||||
}
|
||||
if err := os.Chmod(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("mesh-cli's socket directory: %w", err)
|
||||
}
|
||||
if info, err := os.Lstat(socket); err == nil {
|
||||
if info.Mode()&os.ModeSocket == 0 {
|
||||
return fmt.Errorf("%s is there and is not a socket, and the node-engine did not put it there; "+
|
||||
"mesh-cli is not served on this machine until it is moved", socket)
|
||||
}
|
||||
// A socket left by the engine before this one: nothing listens on it any more.
|
||||
if err := os.Remove(socket); err != nil {
|
||||
return fmt.Errorf("the socket a previous engine left at %s: %w", socket, err)
|
||||
}
|
||||
}
|
||||
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listening for mesh-cli at %s: %w", socket, err)
|
||||
}
|
||||
if err := os.Chmod(socket, 0o666); err != nil {
|
||||
l.Close()
|
||||
return fmt.Errorf("opening mesh-cli's socket to every account: %w", err)
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
l.Close()
|
||||
}()
|
||||
slots := make(chan struct{}, atOnce)
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
for {
|
||||
conn, err := l.AcceptUnix()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("mesh-cli's socket: %w", err)
|
||||
}
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-slots }()
|
||||
answer(ctx, conn, forward, say)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// answer serves one connection.
|
||||
func answer(ctx context.Context, conn *net.UnixConn, forward Forward, say func(string)) {
|
||||
defer conn.Close()
|
||||
reply := func(a cli.Answer) {
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(readWithin))
|
||||
if err := cli.WriteAnswer(conn, a); err != nil && say != nil {
|
||||
say(fmt.Sprintf("mesh-cli: the answer could not be written back: %v", err))
|
||||
}
|
||||
}
|
||||
uid, err := peerOf(conn)
|
||||
if err != nil {
|
||||
// Never sent as anyone: an account the kernel did not give is no account at all.
|
||||
reply(cli.Refusal("the node-engine could not read which account is asking, so nothing was asked: " + err.Error()))
|
||||
return
|
||||
}
|
||||
account, err := lookupAccount(uid)
|
||||
if err != nil || account == "" {
|
||||
reply(cli.Refusal(fmt.Sprintf("user id %d is not in this machine's user database, so nothing was asked", uid)))
|
||||
return
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(readWithin))
|
||||
req, err := cli.ReadRequest(conn)
|
||||
if err != nil {
|
||||
reply(cli.Refusal("the request could not be read, so nothing was asked: " + err.Error()))
|
||||
return
|
||||
}
|
||||
asking, cancel := context.WithTimeout(ctx, cli.AskWithin-time.Minute/2)
|
||||
defer cancel()
|
||||
a, err := forward(asking, Asked{Line: req.Line, Account: account, UID: uid})
|
||||
if err != nil {
|
||||
reply(cli.Refusal("the controller was not reached, so nothing ran: " + err.Error()))
|
||||
return
|
||||
}
|
||||
reply(a)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
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, error) {
|
||||
got = asked
|
||||
return cli.Answer{Stdout: []byte("done\n"), Exit: 0, Terminal: true, Why: "the terminal"}, nil
|
||||
})
|
||||
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, error) { return 0, errors.New("no credentials") }
|
||||
t.Cleanup(func() { peerOf = before })
|
||||
asked := false
|
||||
socket := serving(t, func(context.Context, Asked) (cli.Answer, error) {
|
||||
asked = true
|
||||
return cli.Answer{}, nil
|
||||
})
|
||||
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 controller out of reach is said: nothing ran, and why.
|
||||
func TestAControllerOutOfReachIsSaidAsNothingRan(t *testing.T) {
|
||||
socket := serving(t, func(context.Context, Asked) (cli.Answer, error) {
|
||||
return cli.Answer{}, errors.New("this machine has no link to the bus open now")
|
||||
})
|
||||
a, err := cli.Ask(context.Background(), socket, []string{"status"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(a.Refused, "nothing ran") || !strings.Contains(a.Refused, "no link") || a.Exit != 1 {
|
||||
t.Fatalf("an unreached controller was not said: %+v", 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, error) {
|
||||
asked = true
|
||||
return cli.Answer{}, nil
|
||||
})
|
||||
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)
|
||||
}
|
||||
if got := keysOf(t, body); got != "account line uid" {
|
||||
t.Fatalf("the request's fields are %q", got)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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, " ")
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//go:build linux
|
||||
|
||||
package meshcli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// peerCredentials is the user id of the process at the other end, as the kernel recorded it when it connected
|
||||
// (SO_PEERCRED). Nothing the process says is read.
|
||||
func peerCredentials(conn *net.UnixConn) (uint32, error) {
|
||||
raw, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var cred *syscall.Ucred
|
||||
var inner error
|
||||
if err := raw.Control(func(fd uintptr) {
|
||||
cred, inner = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if inner != nil {
|
||||
return 0, inner
|
||||
}
|
||||
if cred == nil {
|
||||
return 0, errors.New("the kernel gave no credentials")
|
||||
}
|
||||
return cred.Uid, nil
|
||||
}
|
||||
|
||||
// notOwnDirectory says what is wrong with the socket's directory, or nothing: it must be a directory, not a
|
||||
// link, owned by the account the engine runs as.
|
||||
func notOwnDirectory(dir string) string {
|
||||
info, err := os.Lstat(dir)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return dir + " is not a directory the engine made"
|
||||
}
|
||||
if st, ok := info.Sys().(*syscall.Stat_t); ok && int(st.Uid) != os.Geteuid() {
|
||||
return fmt.Sprintf("%s belongs to user id %d, not to the engine's account", dir, st.Uid)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build !linux
|
||||
|
||||
package meshcli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
// peerCredentials is unknown where the kernel's way of saying it is not built here: refused, never guessed.
|
||||
func peerCredentials(*net.UnixConn) (uint32, error) {
|
||||
return 0, errors.New("this engine reads a peer's account on Linux only")
|
||||
}
|
||||
|
||||
// notOwnDirectory is never satisfied where peers cannot be read.
|
||||
func notOwnDirectory(string) string { return "not served on this system" }
|
||||
Reference in New Issue
Block a user