carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package unixdial is the one way the edge and the agent dial a unix
2// socket. Two checks ride on every dial (isolated-exec design §5.4):
3//
4// - the reserved control endpoint is refused BEFORE connect(): a registry
5// row pointing rt.Socket at the root control channel must never be
6// proxied to, however it got there;
7// - the listener must answer to the path that was dialled: after
8// connect() and before a byte is sent, getpeername() names the path
9// the LISTENER bound, which is its real path however it was reached.
10// A symlink at /run/carlos/<host>.sock pointing at a sibling's socket
11// or at the control socket therefore yields a mismatch, and the
12// connection is closed unused.
13//
14// The first check is purely lexical: it filepath.Cleans the dialled
15// string and compares it to the control directory, before any connect.
16// A relative path, or a symlinked parent directory that only resolves to
17// the control directory at connect time, is not caught here — it is
18// still caught by the second check, once getpeername() names the
19// control listener's real bound path after connect. A listener bound by
20// a relative path is refused by the second check for the same reason:
21// the kernel reports the bind string verbatim, and a relative name can
22// never filepath.Clean-equal the absolute path Checked was dialled with.
23//
24// Credentials are deliberately not consulted: a listener systemd created
25// and passed to a DynamicUser service reports root in SO_PEERCRED, and
26// that shape (carlos-app@.socket) must keep working (codex R4-1).
27package unixdial
28
29import (
30 "context"
31 "errors"
32 "fmt"
33 "net"
34 "path/filepath"
35 "strings"
36
37 "github.com/carlosframework/platform/internal/controlsock"
38)
39
40// ErrControlEndpoint is returned, before any connect, for a path under
41// provision.ControlSocketDir (internal/controlsock.Dir; provision
42// re-exports it — see internal/controlsock's doc comment for why this
43// package imports controlsock directly rather than provision).
44var ErrControlEndpoint = errors.New("unixdial: refusing to dial the instance control channel as an upstream")
45
46// ErrPeerMismatch is returned, after connect and before any byte is sent,
47// when the listener's bound path is not the path that was dialled.
48var ErrPeerMismatch = errors.New("unixdial: listener is bound to a different path than the one dialled")
49
50// Checked dials path as a unix stream socket with d and applies both
51// checks. Connect errors (ENOENT, ECONNREFUSED) are returned unwrapped so
52// callers' retry logic keeps seeing them.
53func Checked(ctx context.Context, d *net.Dialer, path string) (net.Conn, error) {
54 if controlsock.IsEndpoint(path) {
55 return nil, fmt.Errorf("%w: %s", ErrControlEndpoint, path)
56 }
57 if d == nil {
58 d = &net.Dialer{}
59 }
60 conn, err := d.DialContext(ctx, "unix", path)
61 if err != nil {
62 return nil, err
63 }
64 if err := verifyPeer(conn, path); err != nil {
65 conn.Close()
66 return nil, err
67 }
68 return conn, nil
69}
70
71// verifyPeer compares the peer address of a connected unix socket with the
72// dialled path. net's UnixConn fills RemoteAddr from getpeername(2) at
73// connect time, which is the listener's bound address.
74//
75// Only the DIALLED side is cleaned (want, below); the peer name the kernel
76// reports must equal want BYTE FOR BYTE. Cleaning got as well would let a
77// listener whose bound path merely normalizes to the same string (a
78// different-but-equivalent spelling of it) pass as a match, which is more
79// than getpeername() actually promised: the kernel reports the bind string
80// verbatim, not a normalized one, so an exact match is the only comparison
81// that means what this check claims to mean. A peer name containing a ".."
82// path component is refused outright for the same reason a relative bind
83// path already is: it is never a path this package's callers bind by, so
84// its presence means the socket was reached some other way than intended.
85func verifyPeer(conn net.Conn, dialled string) error {
86 ra := conn.RemoteAddr()
87 ua, ok := ra.(*net.UnixAddr)
88 if !ok || ua == nil {
89 return fmt.Errorf("%w: peer address %v is not a unix address", ErrPeerMismatch, ra)
90 }
91 want := filepath.Clean(dialled)
92 got := ua.Name
93 if got == "" || got[0] == '@' || hasDotDotComponent(got) || got != want {
94 return fmt.Errorf("%w: dialled %s, listener is bound to %q", ErrPeerMismatch, want, got)
95 }
96 return nil
97}
98
99// hasDotDotComponent reports whether name has a ".." path component,
100// checked without cleaning it first: cleaning would resolve the very
101// traversal this exists to catch before it could ever be seen.
102func hasDotDotComponent(name string) bool {
103 for _, part := range strings.Split(name, string(filepath.Separator)) {
104 if part == ".." {
105 return true
106 }
107 }
108 return false
109}
110