ipc

package
v0.26.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 17 Imported by: 0

README

ipc (internal/service/proc/ipc)

A private socket between processes of one machine. Internal service implementation behind the public pkg/v1/proc/ipc facade — consumers import the facade, not this package.

API

func NewListener(cfg *Config) (*Listener, error)
func NewDialer(cfg *Config) (*Dialer, error)
func Dial(ctx context.Context, cfg *Config) (*coreipc.Conn, error)
func RuntimeDir(app string) string

func (l *Listener) Accept() (*coreipc.Conn, error)
func (l *Listener) Addr() net.Addr
func (l *Listener) Path() string
func (l *Listener) Refused() int64
func (l *Listener) Close() error

func (d *Dialer) Dial(ctx context.Context) (*coreipc.Conn, error)

*Listener and *Dialer implement the Listener and Dialer ports of internal/core/proc/ipc, which also holds PeerValue, Conn and the 0.3.91.* codes (ADR 0160). See CLAUDE.md for the two gates (the directory, the peer's credentials) and ADR 0148 for the decision.

Documentation

Overview

Package ipc — the components ABOVE the socket's directory, and who could steer them: ADR 0083's rule, applied where the directory is the whole of the access control (ADR 0148).

One Lstat sees one component; every lookup crosses all of them

[checkEntry] judges the socket's directory by its own entry: not a link, a directory, this account's, written by nobody else. It cannot see a PARENT, and every lookup this package makes — the Mkdir that creates the directory, the bind that creates the socket, the connect that reaches it — follows a link planted at one. Measured on the code before this file, with a link planted in a 0777|sticky directory, which is what /tmp is:

Config.Path     /tmp/ipc…/pub/app/run/d.sock    pub dtrwxrwxrwx, app -> …/elsewhere
NewListener     accepted: the socket was bound at …/elsewhere/run/d.sock
Dial            accepted
app re-planted  -> …/trap, where another listener waited
Dial            accepted: the trap's listener read the client's first line

The planter owns its link, so the sticky bit lets it replace that link at will: it decides where the directory is created, and later which socket a client reaches. Across accounts it also owns the directory the socket's directory was created in, and on macOS an inheritable access-control entry it puts there is inherited by the 0700 directory Listen creates and by the 0600 socket bound inside — observed with ls -le — which no mode check sees. Outside Linux the peer is not verified, so that entry is a way in.

The rule: who could have created a component, and who can replace it

A link at a parent is not evidence on its own — /tmp -> private/tmp and /var -> private/var on macOS, /var/run -> /run on most Linux distributions — and what separates those from an attack is the directory the component lives in, exactly as internal/service/app/lock reasons (ADR 0083). So every component above the socket's directory is judged by the directory holding it, and only when ANYBODY can write that directory:

  • an indirection is refused ([kindIndirection]): anybody could have planted it. The sticky bit does not exempt it — sticky governs UNLINKING an entry that exists, and planting creates one at a name nobody had taken. This is lock's rule, unchanged.
  • a component owned by neither this process's user nor root is refused ([kindForeign]): anybody could have created it, and whoever did decides everything below it, down to the entries a directory made there inherits. lock has no such rule because a lock is shared between accounts on purpose; a private socket's directory must be this account's, and its gate is that nobody else reaches it.
  • without the sticky bit, any component is refused ([kindReplaceable]): anybody can rename it away and put their own in its place. The socket's directory itself is held to the same rule by [checkHolder].

A directory writable by its GROUP is not judged: a directory shared with a group is a deliberate arrangement, as lock accepts it.

What it does not see

A component this rule accepts can be replaced only by this account, by root, by the owner of the directory holding it or by that directory's group — accounts the deployment chose by placing the socket there. A socket under a tree another account owns therefore trusts that account, by the deployment's own choice, and nothing here refuses it. The audit runs at Listen — before the directory is created and again after — and at every Dial; on Linux the peer's credentials are the gate that holds between them.

Package ipc — the socket directory on Unix: created 0700, refused when another account could write to it, when it is a link, or when another account could steer the path to it (chain_unix.go).

Package ipc is a private socket between processes of one machine: a listener only its own account — and the accounts it names — can reach, a client that refuses to talk to a socket another account planted, and the kernel's word on who is at the other end (ADR 0148).

Two gates, and which one holds where is said rather than assumed:

  • THE DIRECTORY, everywhere. The socket lives in a directory this process's account owns and nobody else may write to; connecting to a Unix socket needs search permission on every directory above it, so a 0700 directory admits its owner and root and nobody else. It is checked at Listen and at Dial, never widened by this package — and so is the PATH to it: a component above it that anybody could have planted or created, or can replace, is refused (PATH_UNSAFE, chain_unix.go), since every lookup follows a link planted at a parent.
  • THE PEER'S CREDENTIALS, where the kernel gives them: SO_PEERCRED on Linux. There the listener refuses a peer whose UID is neither its own nor allowed, whatever the directory says. Elsewhere Peer.Verified is false and the directory is the only gate — stated, not papered over.

Windows has no search permission to gate a socket by its directory: there the endpoint is a named pipe (pipe_windows.go) named after Config.Path, whose own DACL grants this process's account only, which refuses remote clients (PIPE_REJECT_REMOTE_CLIENTS) and which nobody can join once it exists (FILE_FLAG_FIRST_PIPE_INSTANCE); each end reads the other's account from its process token (Peer.SID), and a client refuses a listener of another account before it sends a byte.

The contract is internal/core/proc/ipc (ADR 0160): the peer and the connection that carries it, the Listener and Dialer ports this package's Listener and Dialer implement, and the codes every refusal carries. This package is the engine — and Config, the one configuration both ends share, is the engine's (ADR 0074).

Package ipc — the compile-time proof that the engines satisfy the core ports (ADR 0160): a method renamed or retyped on either engine fails the build here, not in a caller that holds the port.

Package ipc — the peer's credentials on Linux: SO_PEERCRED, what the peer had when it connected or listened.

Package ipc — no peer credentials outside Linux without x/sys: the peer is unverified and the directory is the gate.

Package ipc — the private endpoint on Windows: a named pipe.

A Unix socket there is a file in a directory, but Windows has no search permission to make a directory the gate, so the endpoint is a named pipe, the object Windows secures by itself:

  • its DACL grants this process's account only, protected from inheritance (SDDL "D:P(A;;GA;;;<SID>)"), so no other account can open it — Everyone, Authenticated Users and Administrators included;
  • PIPE_REJECT_REMOTE_CLIENTS refuses a client of another machine;
  • the first instance is created with FILE_FLAG_FIRST_PIPE_INSTANCE: when the name exists — another daemon, or a squatter that made it first — the listen fails (IN_USE) rather than joining an instance somebody else controls, and the next instance is created before a connection is handed out, so the name never lapses while the listener lives;
  • each end reads the other's account from its process token (GetNamedPipeClientProcessId / GetNamedPipeServerProcessId, then OpenProcessToken): a client refuses a server of another account before it sends a byte (ENDPOINT_FOREIGN), and opens the pipe at SECURITY_IDENTIFICATION, so the server can learn who it is but never act as it.

The pipe is named after Config.Path, so one configuration serves every platform: \\.\pipe\ipc-<16 hex digits of the path's SHA-256>-<base name>. Nothing is created in the path's directory.

The handles are opened overlapped and handed to os.NewFile, which puts them on the runtime's poller: reads, writes and deadlines are the os package's. kernel32 and advapi32 are bound with syscall.NewLazyDLL, as the lock domain binds LockFileEx and GetNamedSecurityInfoW — golang.org/x/sys is banned SDK-wide (ADR 0018).

Package ipc — the Unix socket: a 0700 directory, a 0600 socket, a leftover removed only when nobody answers on it and it is ours, and the peer the kernel names where it does.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Dial

func Dial(ctx context.Context, cfg *Config) (*coreipc.Conn, error)

Dial connects to the private socket at cfg.Path, within ctx and dialTimeout. It refuses first — before a byte is sent — a directory another account could write to and a socket file another account owns, so a client never hands a request to an impostor; and where the kernel names the listener, it refuses one that is neither this account nor allowed. On Windows it refuses a pipe whose server runs as another account.

func RuntimeDir

func RuntimeDir(app string) string

RuntimeDir is where an application's private sockets belong on this machine: $RUNTIME_DIRECTORY (systemd's — its first entry when several RuntimeDirectory= make it a colon-separated list), then $XDG_RUNTIME_DIR/<app>, then %LOCALAPPDATA%\<app> on Windows, else <tmp>/<app>-<uid>. It creates nothing; Listen does, at 0700.

func RuntimeDirOr added in v0.12.11

func RuntimeDirOr(app, fallback string) string

RuntimeDirOr is RuntimeDir with fallback in place of <tmp>/<app>-<uid>: $RUNTIME_DIRECTORY (its first entry), then $XDG_RUNTIME_DIR/<app>, then %LOCALAPPDATA%\<app> on Windows, else fallback, returned as given. A caller that keeps state in a directory of its own names a directory there, and its sockets never land in a tree any account can create first (ADR 0182). It creates nothing; Listen does, at 0700, and refuses a fallback that is not absolute or not private.

Types

type Config

type Config struct {
	// Path is the socket's absolute path. Its directory is created 0700 when
	// missing and refused when another account could write to it.
	Path string
	// AllowUIDs and AllowGIDs admit peers besides this process's own user:
	// an on-call group declared at deployment, say. They are enforced where
	// the kernel names the peer (Peer.Verified); the directory must still
	// let those accounts reach the socket, which is the deployment's to
	// arrange — this package never widens a directory. Windows has no UID:
	// there the pipe's DACL admits this account only, and the lists admit
	// nobody more.
	AllowUIDs, AllowGIDs []int
}

Config is where a private socket lives and who, besides its own account, may use it. The same value serves the listener and its clients.

type Dialer

type Dialer struct {
	// contains filtered or unexported fields
}

Dialer connects to the private socket a configuration names: the engine behind the core Dialer port. Code that holds the port dials the same endpoint as often as it needs to, and a test hands that code a double.

func NewDialer

func NewDialer(cfg *Config) (*Dialer, error)

NewDialer validates cfg and returns a Dialer for it. The allow-lists are copied, so a caller that later edits its slices does not change who this dialer admits.

func (*Dialer) Dial

func (d *Dialer) Dial(ctx context.Context) (*coreipc.Conn, error)

Dial connects to the dialer's socket, exactly as the package-level Dial does with the same configuration.

type Listener

type Listener struct {
	// contains filtered or unexported fields
}

Listener accepts the connections of admitted peers only; a peer the kernel names and the configuration does not admit is closed and counted. It is the engine behind the core Listener port.

func NewListener

func NewListener(cfg *Config) (*Listener, error)

NewListener opens the private socket at cfg.Path. A socket already there is dialled first: one that answers is refused (IN_USE) and left alone, and so is one whose dial fails any way but a refused connection — a full accept queue, a timeout — since its owner may be alive. One the kernel refuses the connection on (ECONNREFUSED), or that vanished meanwhile, is a leftover of a dead process and is removed — provided it is a socket this account owns. The socket file is made 0600. On Windows the named pipe is created with FILE_FLAG_FIRST_PIPE_INSTANCE: a name that exists is IN_USE, whoever made it.

func (*Listener) Accept

func (l *Listener) Accept() (*coreipc.Conn, error)

Accept returns the next connection of an admitted peer. A peer the kernel names and the configuration does not admit is closed at once, counted (Refused), and never returned.

func (*Listener) Addr

func (l *Listener) Addr() net.Addr

Addr is the listener's address.

func (*Listener) Close

func (l *Listener) Close() error

Close stops accepting and removes the socket file (a named pipe vanishes with its last handle).

func (*Listener) Path

func (l *Listener) Path() string

Path is the socket's path.

func (*Listener) Refused

func (l *Listener) Refused() int64

Refused is how many connections Accept closed because their peer was not admitted.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL