Unix Domain Sockets

Unix Domain Sockets

The dominant IPC mechanism for local daemons on Unix. Same API as TCP sockets (socket, bind, listen, accept, connect, read, write), but communication stays inside the kernel — no network stack, no TCP overhead, no port number. The address is a filesystem path.

mxr uses them. lazydap uses them. Docker, systemd, X11, dbus, postgres, mysql, ssh-agent — all use them. If two local processes need to talk and one of them is a long-lived service, the answer is almost always a Unix socket.

The API

Same as TCP, with AF_UNIX instead of AF_INET:

int sock = socket(AF_UNIX, SOCK_STREAM, 0);

struct sockaddr_un addr = {
    .sun_family = AF_UNIX,
    .sun_path = "/tmp/mydaemon.sock",
};
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
listen(sock, 128);

// Server accept loop:
while (1) {
    int client = accept(sock, NULL, NULL);
    handle_client(client);  // blocks if not async
}

Client side:

int sock = socket(AF_UNIX, SOCK_STREAM, 0);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
write(sock, "hello\n", 6);

In Rust + Tokio:

let listener = UnixListener::bind("/tmp/mydaemon.sock")?;
loop {
    let (stream, _) = listener.accept().await?;
    tokio::spawn(handle_client(stream));
}
let mut stream = UnixStream::connect("/tmp/mydaemon.sock").await?;
stream.write_all(b"hello\n").await?;

The familiar socket dance, but the address is a path.

Path-based vs abstract addresses

Two flavours:

For mxr / lazydap (cross-platform, want filesystem permissions), path-based is the right choice.

Permissions

Critical for security. The socket file has Unix permissions like any other file:

let listener = UnixListener::bind(path)?;
std::fs::set_permissions(path, Permissions::from_mode(0o700))?;  // owner-only

0700 (rwx for owner, nothing for anyone else) is the standard for per-user daemons. 0660 (group-readable) for daemons that serve a group. World-accessible (0666) is almost never right.

The directory containing the socket also matters: /tmp is world-writable, so anyone can create symlinks. Prefer $XDG_RUNTIME_DIR (Linux) or ~/Library/Application Support (macOS) — both are owner-only.

Cleanup

Path-based sockets persist after the daemon exits. The next start fails with EADDRINUSE if the file is still there from a previous run.

Standard fix: at startup, unlink(path) before bind. Two daemons trying to start simultaneously can race; use flock-based PID files to enforce single-instance.

Abstract sockets (Linux) auto-clean — no action needed.

Why faster than TCP

Unix sockets bypass:

Throughput on a modern machine: 5–10 GB/s for streaming, 1M+ messages/sec for small request/response. TCP loopback is ~3× slower.

For local IPC, there's no benefit to using TCP loopback over Unix sockets unless you genuinely might want to expose the service to the network later.

Datagram vs stream

Two socket types:

Stream sockets need a framing layer because the byte stream has no inherent message boundaries. Datagram sockets get message boundaries for free.

mxr and lazydap use streams + length-prefix framing.

Passing file descriptors

Unique to Unix sockets: you can pass open file descriptors between processes. Sender includes them in the ancillary data of sendmsg(2); receiver picks them up via recvmsg(2). The receiving process gets a usable fd that aliases the sender's.

Used by:

Not common for application-level IPC; useful when you need it.

What mxr and lazydap do

Both use:

Socket paths:

mxr:     ~/.cache/mxr/mxr.sock
lazydap: ~/Library/Application Support/lazydap/instance-<hash>/lazydap.sock

(macOS uses Application Support; Linux uses $XDG_RUNTIME_DIR or ~/.cache.)

When NOT to use Unix sockets

Otherwise: yes, use Unix sockets.

See also