main ece7682

mux.go RawHistoryFolder

1// Private daemonmux Unix carriage.
2//
3// This file exposes the explicit, mux-specific raw Unix carriage that the
4// daemonmux multiplexer (internal/adapters/daemonmux) runs over. It is
5// deliberately separate from the daemon's public Listen/DialContext pair: the
6// mux carriage is bound at a caller-supplied, isolated path, never the live
7// production daemon.sock, so the two endpoints cannot be confused and the
8// daemon's public socket is never reused or inspected here.
9//
10// Both ends of the carriage are authenticated before they are handed out. A
11// listener and a dialer verify that the connected AF_UNIX peer is the current
12// effective user from the kernel's own peer credentials (SO_PEERCRED on
13// Linux), not from filesystem permissions or an X11-style "the directory is
14// private so the peer is trusted" assumption. On a platform or build without a
15// same-user peer-credential check this file fails closed: it refuses the
16// connection rather than admitting an unverified peer (peer_other.go).
17//
18// The carriage reuses the existing IPC framing and transport (NewTransport,
19// streamframe, RecvBounded) and the shared race-safe stale-socket recovery
20// (listenUnixStaleSafe). Listener setup creates or validates an owner-only
21// (0700) parent directory with safedir.EnsurePrivate and tightens the bound
22// socket to 0600; a foreign path that is not a socket is refused without being
23// removed. Close stops Accept, unlinks only the socket inode this listener
24// created, and leaves a caller replacement alone.
25
26package ipc
27
28import (
29	"context"
30	"errors"
31	"fmt"
32	"net"
33	"os"
34	"path/filepath"
35	"sync"
36
37	"github.com/bnema/vev/internal/protocol/wire"
38	"github.com/bnema/vev/pkg/safedir"
39)
40
41const (
42	// muxSocketMode is the owner-only permission a bound mux socket is
43	// tightened to after bind.
44	muxSocketMode = 0o600
45	// muxSocketPathMax is the worst-case AF_UNIX pathname length observed
46	// across the supported platforms (BSD/macOS 103 < Linux 107). A mux path
47	// longer than this is refused on every platform, so a path that binds on
48	// Linux can never be silently truncated on macOS.
49	muxSocketPathMax = 103
50)
51
52// Mux carriage sentinels. They are typed so a caller can classify a path,
53// platform, or peer-credential outcome without matching on message text.
54var (
55	// ErrMuxPath reports an invalid caller-supplied carriage path: empty,
56	// relative, unclean, too long, containing a NUL, or naming a connection
57	// that is not a Unix socket.
58	ErrMuxPath = errors.New("ipc: invalid mux socket path")
59	// ErrMuxForeignPath reports a mux path that already exists and is not a
60	// socket. ListenMux refuses it without removing the caller's file.
61	ErrMuxForeignPath = errors.New("ipc: mux socket path exists and is not a socket")
62	// ErrMuxUnsupported reports a same-user peer-credential check on a
63	// platform or build that has none. The carriage fails closed: it never
64	// admits an unverified peer.
65	ErrMuxUnsupported = errors.New("ipc: same-user peer credential verification is unsupported on this platform")
66	// ErrMuxPeerRejected reports a connected AF_UNIX peer whose kernel
67	// credentials are not the current effective user, or could not be read.
68	ErrMuxPeerRejected = errors.New("ipc: mux peer failed same-user credential verification")
69)
70
71// PeerVerifier checks one connected AF_UNIX peer's kernel credentials. It
72// returns nil only for a peer proven to be the current effective user. It is a
73// function type so a caller (or test) can inject an observable refusal without
74// root; SameUserPeerVerifier returns the platform default.
75type PeerVerifier func(conn *net.UnixConn) error
76
77// muxPeerVerifier is the in-package spelling of PeerVerifier.
78type muxPeerVerifier = PeerVerifier
79
80// SameUserPeerVerifier returns this build's default same-user peer verifier:
81// Linux SO_PEERCRED admission, or a fail-closed refusal on a platform or
82// build without it.
83func SameUserPeerVerifier() PeerVerifier { return verifySameUserUnixPeer }
84
85// MuxListener is the listener half of a private daemonmux Unix carriage. Accept
86// returns a raw bounded transport (Send/RecvBounded/Close) ready for the
87// daemonmux FramedCarrierBridge, and verifies same-user peer credentials before
88// handing it out. Close unblocks a blocked Accept and removes only the socket
89// inode this listener created.
90type MuxListener interface {
91	Accept() (wire.BoundedTransport, error)
92	Close() error
93	Addr() string
94}
95
96// muxListener implements MuxListener over one bound AF_UNIX SOCK_STREAM socket.
97type muxListener struct {
98	ln     *net.UnixListener
99	path   string
100	file   os.FileInfo
101	opts   []Option
102	verify muxPeerVerifier
103
104	closeOnce sync.Once
105	closeErr  error
106}
107
108var _ MuxListener = (*muxListener)(nil)
109
110// ListenMux binds the private daemonmux carriage at exactly path and returns a
111// listener that verifies same-user peer credentials before handing out a raw
112// bounded transport.
113//
114// path is caller-supplied and isolation is the caller's responsibility: it must
115// be an absolute, cleaned path inside an owner-only directory and must not be
116// the daemon's public daemon.sock. ListenMux creates or validates the parent
117// directory as 0700 (safedir.EnsurePrivate), refuses a path that already exists
118// and is not a socket without removing it, recovers race-safely from a stale
119// socket left by a dead owner, and tightens the bound socket to 0600. A live
120// owner already bound at path is reported as ErrDaemonRunning.
121func ListenMux(path string, opts ...Option) (MuxListener, error) {
122	return listenMux(path, verifySameUserUnixPeer, opts...)
123}
124
125// ListenMuxWithPeerVerifier binds the private carriage exactly like ListenMux
126// but admits peers with an injected verifier instead of the platform default.
127// It exists so a caller or test can supply an explicit admission policy (or
128// observe a deterministic refusal without root); a nil verifier falls back to
129// the platform default. Every other security property - owner-only parent,
130// 0600 socket, race-safe stale recovery, foreign-path refusal, inode-scoped
131// unlink on Close - is unchanged.
132func ListenMuxWithPeerVerifier(path string, verify PeerVerifier, opts ...Option) (MuxListener, error) {
133	return listenMux(path, verify, opts...)
134}
135
136// listenMux is ListenMux with an injected peer verifier, so a test can observe
137// a credential refusal deterministically without root. A nil verifier falls
138// back to the platform default.
139func listenMux(path string, verify muxPeerVerifier, opts ...Option) (MuxListener, error) {
140	if err := validateMuxSocketPath(path); err != nil {
141		return nil, err
142	}
143	if verify == nil {
144		verify = verifySameUserUnixPeer
145	}
146	if err := prepareMuxParent(path); err != nil {
147		return nil, err
148	}
149	if err := inspectMuxSocketPath(path); err != nil {
150		return nil, err
151	}
152
153	ln, err := listenUnixStaleSafe(path)
154	if err != nil {
155		return nil, fmt.Errorf("ipc: listen on mux socket %s: %w", path, err)
156	}
157	// The listener must never unlink a pathname it did not create. A caller may
158	// replace the path after bind; Close preserves that replacement.
159	ln.SetUnlinkOnClose(false)
160
161	cleanup := func(cause error) (MuxListener, error) {
162		_ = ln.Close()
163		return nil, cause
164	}
165	if err := os.Chmod(path, muxSocketMode); err != nil {
166		return cleanup(fmt.Errorf("ipc: securing mux socket %s: %w", path, err))
167	}
168	info, err := os.Lstat(path)
169	if err != nil {
170		return cleanup(fmt.Errorf("ipc: stat mux socket %s: %w", path, err))
171	}
172	if info.Mode()&os.ModeSocket == 0 {
173		return cleanup(fmt.Errorf("%w: %s", ErrMuxForeignPath, path))
174	}
175	return &muxListener{ln: ln, path: path, file: info, opts: opts, verify: verify}, nil
176}
177
178// DialMuxContext connects to the private daemonmux carriage at exactly path and
179// returns a raw bounded transport (Send/RecvBounded/Close) ready for the
180// daemonmux FramedCarrierBridge. The peer's kernel credentials are verified as
181// the current effective user before the transport is returned; a peer that
182// cannot be proven same-user is refused and its connection closed.
183//
184// A dial failure (no carriage bound, or a stale path) is wrapped so callers can
185// distinguish it; cancellation and deadline errors are preserved for
186// classification.
187func DialMuxContext(ctx context.Context, path string, opts ...Option) (wire.BoundedTransport, error) {
188	return dialMux(ctx, path, verifySameUserUnixPeer, opts...)
189}
190
191// DialMuxContextWithPeerVerifier connects to the private carriage exactly like
192// DialMuxContext but verifies the accepted peer with an injected verifier
193// instead of the platform default. A nil verifier falls back to the platform
194// default; a peer the verifier refuses is closed without being handed out.
195func DialMuxContextWithPeerVerifier(ctx context.Context, path string, verify PeerVerifier, opts ...Option) (wire.BoundedTransport, error) {
196	return dialMux(ctx, path, verify, opts...)
197}
198
199// dialMux is DialMuxContext with an injected peer verifier, so a test can
200// observe a credential refusal deterministically without root. A nil verifier
201// falls back to the platform default.
202func dialMux(ctx context.Context, path string, verify muxPeerVerifier, opts ...Option) (wire.BoundedTransport, error) {
203	if err := validateMuxSocketPath(path); err != nil {
204		return nil, err
205	}
206	if verify == nil {
207		verify = verifySameUserUnixPeer
208	}
209	var dialer net.Dialer
210	conn, err := dialer.DialContext(ctx, "unix", path)
211	if err != nil {
212		return nil, fmt.Errorf("ipc: dial mux socket %s: %w", path, err)
213	}
214	unixConn, ok := conn.(*net.UnixConn)
215	if !ok {
216		_ = conn.Close()
217		return nil, fmt.Errorf("%w: %s is not a Unix connection", ErrMuxPath, path)
218	}
219	if err := verify(unixConn); err != nil {
220		_ = conn.Close()
221		return nil, err
222	}
223	transport, ok := NewTransport(conn, opts...).(wire.BoundedTransport)
224	if !ok {
225		_ = conn.Close()
226		return nil, fmt.Errorf("%w: %s transport is not bounded", ErrMuxPath, path)
227	}
228	return transport, nil
229}
230
231// Accept waits for the next connection on the bound socket. Before handing a
232// transport out it verifies the peer's same-user kernel credentials; a refused
233// peer is closed and the refusal returned, so an unverified carriage is never
234// admitted. Accept fails once the listener is closed (Close unblocks it).
235func (l *muxListener) Accept() (wire.BoundedTransport, error) {
236	conn, err := l.ln.AcceptUnix()
237	if err != nil {
238		return nil, err
239	}
240	if err := l.verify(conn); err != nil {
241		_ = conn.Close()
242		return nil, err
243	}
244	transport, ok := NewTransport(conn, l.opts...).(wire.BoundedTransport)
245	if !ok {
246		_ = conn.Close()
247		return nil, fmt.Errorf("%w: %s transport is not bounded", ErrMuxPath, l.path)
248	}
249	return transport, nil
250}
251
252// Addr returns the bound filesystem path.
253func (l *muxListener) Addr() string { return l.path }
254
255// Close stops accepting, unblocking a blocked Accept, and removes the socket
256// only if the pathname still names the inode this listener created. A caller
257// replacement is preserved. Close is idempotent and concurrent-safe.
258func (l *muxListener) Close() error {
259	if l == nil {
260		return nil
261	}
262	l.closeOnce.Do(func() {
263		l.closeErr = l.ln.Close()
264		if err := removeMuxSocket(l.path, l.file); err != nil {
265			l.closeErr = errors.Join(l.closeErr, err)
266		}
267	})
268	return l.closeErr
269}
270
271// validateMuxSocketPath refuses a caller path that cannot be a portable private
272// carriage endpoint: it must be absolute, already cleaned, within the
273// cross-platform AF_UNIX pathname limit, free of NUL, and name a real base.
274func validateMuxSocketPath(path string) error {
275	if path == "" || !filepath.IsAbs(path) {
276		return fmt.Errorf("%w: path must be absolute", ErrMuxPath)
277	}
278	if filepath.Clean(path) != path || path == string(filepath.Separator) {
279		return fmt.Errorf("%w: path must be cleaned", ErrMuxPath)
280	}
281	base := filepath.Base(path)
282	if base == "." || base == ".." || base == string(filepath.Separator) {
283		return fmt.Errorf("%w: invalid base name", ErrMuxPath)
284	}
285	if len(path) > muxSocketPathMax {
286		return fmt.Errorf("%w: path exceeds %d bytes", ErrMuxPath, muxSocketPathMax)
287	}
288	for i := 0; i < len(path); i++ {
289		if path[i] == 0 {
290			return fmt.Errorf("%w: path contains a NUL", ErrMuxPath)
291		}
292	}
293	return nil
294}
295
296// prepareMuxParent creates or validates the carriage's parent directory as an
297// owner-only (0700) directory owned by the current user.
298func prepareMuxParent(path string) error {
299	if err := safedir.EnsurePrivate(filepath.Dir(path)); err != nil {
300		return fmt.Errorf("ipc: securing mux socket directory: %w", err)
301	}
302	return nil
303}
304
305// inspectMuxSocketPath refuses an existing path that is not a socket before any
306// bind or liveness probe, so a caller's regular file, directory, or symlink is
307// never mistaken for a stale endpoint and never removed.
308func inspectMuxSocketPath(path string) error {
309	info, err := os.Lstat(path)
310	if err != nil {
311		if errors.Is(err, os.ErrNotExist) {
312			return nil
313		}
314		return fmt.Errorf("ipc: inspecting mux socket %s: %w", path, err)
315	}
316	if info.Mode()&os.ModeSocket == 0 {
317		return fmt.Errorf("%w: %s", ErrMuxForeignPath, path)
318	}
319	return nil
320}
321
322// removeMuxSocket removes path only when it still names expected, so a socket
323// a caller replaced during Close is preserved.
324func removeMuxSocket(path string, expected os.FileInfo) error {
325	if expected == nil {
326		return nil
327	}
328	info, err := os.Lstat(path)
329	if err != nil {
330		if errors.Is(err, os.ErrNotExist) {
331			return nil
332		}
333		return err
334	}
335	if !os.SameFile(info, expected) {
336		return nil
337	}
338	if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
339		return err
340	}
341	return nil
342}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close