Unix Domain Sockets

Unix domain sockets are the Swiss Army knife of Linux IPC. They use the familiar socket API (socket, bind, listen, accept, connect) but communicate within the same machine, without network overhead. They support both stream and datagram modes, can pass file descriptors between processes, and can verify the identity of the peer. If you only learn one IPC mechanism, make it this one.

Creating a Unix Domain Socket

The key difference from network sockets: AF_UNIX instead of AF_INET, and struct sockaddr_un instead of struct sockaddr_in.

/* uds_server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/my_uds.sock"

int main(void) {
    int srv = socket(AF_UNIX, SOCK_STREAM, 0);
    if (srv == -1) {
        perror("socket");
        return 1;
    }

    /* Remove any leftover socket file */
    unlink(SOCKET_PATH);

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        return 1;
    }

    listen(srv, 5);
    printf("Server listening on %s\n", SOCKET_PATH);

    int client = accept(srv, NULL, NULL);
    if (client == -1) {
        perror("accept");
        return 1;
    }

    char buf[256];
    ssize_t n = read(client, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("Server received: %s\n", buf);

    const char *reply = "Hello from server";
    write(client, reply, strlen(reply));

    close(client);
    close(srv);
    unlink(SOCKET_PATH);
    return 0;
}
/* uds_client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/my_uds.sock"

int main(void) {
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd == -1) {
        perror("socket");
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("connect");
        return 1;
    }

    const char *msg = "Hello from client";
    write(fd, msg, strlen(msg));

    char buf[256];
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("Client received: %s\n", buf);

    close(fd);
    return 0;
}

Run the server in one terminal, the client in another. The socket appears as a file:

$ ls -la /tmp/my_uds.sock
srwxrwxr-x 1 user user 0 ... /tmp/my_uds.sock

The s at the start of the permissions indicates a socket file.

SOCK_STREAM vs SOCK_DGRAM

SOCK_STREAM (like TCP):
  - Connection-oriented
  - Reliable, ordered byte stream
  - Must listen/accept/connect

SOCK_DGRAM (like UDP):
  - Connectionless
  - Message boundaries preserved
  - No listen/accept needed
  - Reliable (unlike UDP -- no network to drop packets)

A datagram example:

/* uds_dgram_server.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SERVER_PATH "/tmp/uds_dgram_srv.sock"

int main(void) {
    int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    unlink(SERVER_PATH);

    struct sockaddr_un addr = {0};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SERVER_PATH, sizeof(addr.sun_path) - 1);
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));

    printf("Datagram server waiting on %s\n", SERVER_PATH);

    char buf[256];
    struct sockaddr_un client_addr;
    socklen_t len = sizeof(client_addr);

    ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
                         (struct sockaddr *)&client_addr, &len);
    buf[n] = '\0';
    printf("Server got: %s\n", buf);

    /* Send reply back to client */
    const char *reply = "ACK";
    sendto(fd, reply, strlen(reply), 0,
           (struct sockaddr *)&client_addr, len);

    close(fd);
    unlink(SERVER_PATH);
    return 0;
}
/* uds_dgram_client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SERVER_PATH "/tmp/uds_dgram_srv.sock"
#define CLIENT_PATH "/tmp/uds_dgram_cli.sock"

int main(void) {
    int fd = socket(AF_UNIX, SOCK_DGRAM, 0);

    /* Client must bind too, so server can reply */
    unlink(CLIENT_PATH);
    struct sockaddr_un client_addr = {0};
    client_addr.sun_family = AF_UNIX;
    strncpy(client_addr.sun_path, CLIENT_PATH,
            sizeof(client_addr.sun_path) - 1);
    bind(fd, (struct sockaddr *)&client_addr, sizeof(client_addr));

    struct sockaddr_un server_addr = {0};
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SERVER_PATH,
            sizeof(server_addr.sun_path) - 1);

    const char *msg = "Hello datagram";
    sendto(fd, msg, strlen(msg), 0,
           (struct sockaddr *)&server_addr, sizeof(server_addr));

    char buf[256];
    ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0, NULL, NULL);
    buf[n] = '\0';
    printf("Client got reply: %s\n", buf);

    close(fd);
    unlink(CLIENT_PATH);
    return 0;
}

Try It: Modify the datagram server to loop and handle multiple messages from different clients. Each client should bind to a unique path (e.g., /tmp/client_PID.sock).

Abstract Socket Namespace

Linux supports an abstract namespace that does not create a filesystem entry. Set sun_path[0] = '\0':

/* uds_abstract.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>

int main(void) {
    int srv = socket(AF_UNIX, SOCK_STREAM, 0);

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    /* Abstract: first byte is \0, rest is the name */
    const char *name = "\0my_abstract_socket";
    memcpy(addr.sun_path, name, 20);

    socklen_t addr_len = offsetof(struct sockaddr_un, sun_path) + 20;
    bind(srv, (struct sockaddr *)&addr, addr_len);
    listen(srv, 1);

    pid_t pid = fork();
    if (pid == 0) {
        /* Child: connect */
        close(srv);
        int cli = socket(AF_UNIX, SOCK_STREAM, 0);
        connect(cli, (struct sockaddr *)&addr, addr_len);
        write(cli, "abstract!", 9);
        close(cli);
        _exit(0);
    }

    int client = accept(srv, NULL, NULL);
    char buf[64];
    ssize_t n = read(client, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("Received via abstract socket: %s\n", buf);

    close(client);
    close(srv);
    wait(NULL);
    return 0;
}

Advantages of abstract sockets:

  • No filesystem cleanup needed (no unlink required).
  • No permission issues with the socket file.
  • Automatically vanishes when all file descriptors are closed.

Caution: Abstract sockets are Linux-specific. They do not exist on macOS or FreeBSD. The address length matters -- you must pass the exact length, not sizeof(addr), because the name may contain null bytes.

Passing File Descriptors (SCM_RIGHTS)

This is the killer feature. One process can send an open file descriptor to another process over a Unix domain socket. The kernel creates a new file descriptor in the receiver's file descriptor table pointing to the same underlying file.

/* fd_sender.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/fd_pass.sock"

int send_fd(int sock, int fd_to_send) {
    char buf[1] = {'F'};
    struct iovec iov = { .iov_base = buf, .iov_len = 1 };

    /* Ancillary data buffer */
    union {
        char buf[CMSG_SPACE(sizeof(int))];
        struct cmsghdr align;
    } cmsg_buf;

    struct msghdr msg = {0};
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = cmsg_buf.buf;
    msg.msg_controllen = sizeof(cmsg_buf.buf);

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
    memcpy(CMSG_DATA(cmsg), &fd_to_send, sizeof(int));

    return sendmsg(sock, &msg, 0);
}

int main(void) {
    int srv = socket(AF_UNIX, SOCK_STREAM, 0);
    unlink(SOCKET_PATH);

    struct sockaddr_un addr = {0};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    bind(srv, (struct sockaddr *)&addr, sizeof(addr));
    listen(srv, 1);

    printf("Sender: waiting for connection...\n");
    int client = accept(srv, NULL, NULL);

    /* Open a file and send the fd to the other process */
    int file_fd = open("/etc/hostname", O_RDONLY);
    if (file_fd == -1) {
        perror("open");
        return 1;
    }

    printf("Sender: sending fd %d for /etc/hostname\n", file_fd);
    send_fd(client, file_fd);

    close(file_fd);
    close(client);
    close(srv);
    unlink(SOCKET_PATH);
    return 0;
}
/* fd_receiver.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/fd_pass.sock"

int recv_fd(int sock) {
    char buf[1];
    struct iovec iov = { .iov_base = buf, .iov_len = 1 };

    union {
        char buf[CMSG_SPACE(sizeof(int))];
        struct cmsghdr align;
    } cmsg_buf;

    struct msghdr msg = {0};
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = cmsg_buf.buf;
    msg.msg_controllen = sizeof(cmsg_buf.buf);

    if (recvmsg(sock, &msg, 0) <= 0)
        return -1;

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg && cmsg->cmsg_level == SOL_SOCKET
             && cmsg->cmsg_type == SCM_RIGHTS) {
        int fd;
        memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
        return fd;
    }
    return -1;
}

int main(void) {
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);

    struct sockaddr_un addr = {0};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    connect(fd, (struct sockaddr *)&addr, sizeof(addr));

    int received_fd = recv_fd(fd);
    printf("Receiver: got fd %d\n", received_fd);

    /* Read from the received fd */
    char buf[256];
    ssize_t n = read(received_fd, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("Receiver: read from passed fd: %s", buf);

    close(received_fd);
    close(fd);
    return 0;
}

Run the sender first, then the receiver. The receiver reads /etc/hostname using a file descriptor it never opened -- the sender passed it over the socket.

FD passing flow:

Sender process:                   Receiver process:
  fd 3 -> /etc/hostname
      |
      +-- sendmsg(SCM_RIGHTS) --> recvmsg() --> fd 4 -> /etc/hostname
                                                        (same file, new fd #)

Caution: The received file descriptor number will be different from the sender's. The kernel allocates the lowest available fd number in the receiver's table. The underlying file description (offset, flags) is shared.

Passing Credentials (SCM_CREDENTIALS)

Unix domain sockets can also verify the peer's PID, UID, and GID.

/* cred_server.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/cred_check.sock"

int main(void) {
    int srv = socket(AF_UNIX, SOCK_STREAM, 0);
    unlink(SOCKET_PATH);

    struct sockaddr_un addr = {0};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    bind(srv, (struct sockaddr *)&addr, sizeof(addr));
    listen(srv, 1);

    int client = accept(srv, NULL, NULL);

    /* Enable credential passing */
    int optval = 1;
    setsockopt(client, SOL_SOCKET, SO_PASSCRED, &optval, sizeof(optval));

    char buf[1];
    struct iovec iov = { .iov_base = buf, .iov_len = 1 };

    union {
        char buf[CMSG_SPACE(sizeof(struct ucred))];
        struct cmsghdr align;
    } cmsg_buf;

    struct msghdr msg = {0};
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = cmsg_buf.buf;
    msg.msg_controllen = sizeof(cmsg_buf.buf);

    recvmsg(client, &msg, 0);

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg && cmsg->cmsg_level == SOL_SOCKET
             && cmsg->cmsg_type == SCM_CREDENTIALS) {
        struct ucred cred;
        memcpy(&cred, CMSG_DATA(cmsg), sizeof(cred));
        printf("Peer PID: %d\n", cred.pid);
        printf("Peer UID: %d\n", cred.uid);
        printf("Peer GID: %d\n", cred.gid);
    }

    close(client);
    close(srv);
    unlink(SOCKET_PATH);
    return 0;
}

This is how D-Bus, systemd, and many daemons authenticate their clients without passwords.

Driver Prep: Unix domain sockets are used heavily in the Linux ecosystem. systemd socket activation passes pre-opened sockets to services. D-Bus uses Unix domain sockets for desktop IPC. Container runtimes pass file descriptors for namespace setup. Understanding sendmsg/recvmsg with ancillary data is essential for systems programming.

Rust: UnixStream and UnixListener

Rust's standard library includes Unix domain socket support:

// uds_server.rs
use std::os::unix::net::UnixListener;
use std::io::{Read, Write};

fn main() {
    let path = "/tmp/rust_uds.sock";
    let _ = std::fs::remove_file(path);

    let listener = UnixListener::bind(path).expect("bind failed");
    println!("Server listening on {}", path);

    let (mut stream, _addr) = listener.accept().expect("accept failed");

    let mut buf = [0u8; 256];
    let n = stream.read(&mut buf).expect("read failed");
    let msg = std::str::from_utf8(&buf[..n]).unwrap();
    println!("Server received: {}", msg);

    stream.write_all(b"Hello from Rust server").expect("write failed");

    std::fs::remove_file(path).ok();
}
// uds_client.rs
use std::os::unix::net::UnixStream;
use std::io::{Read, Write};

fn main() {
    let path = "/tmp/rust_uds.sock";
    let mut stream = UnixStream::connect(path).expect("connect failed");

    stream.write_all(b"Hello from Rust client").expect("write failed");

    let mut buf = [0u8; 256];
    let n = stream.read(&mut buf).expect("read failed");
    let msg = std::str::from_utf8(&buf[..n]).unwrap();
    println!("Client received: {}", msg);
}

Rust: Datagram Sockets

// uds_dgram.rs
use std::os::unix::net::UnixDatagram;

fn main() {
    let server_path = "/tmp/rust_dgram_srv.sock";
    let client_path = "/tmp/rust_dgram_cli.sock";

    let _ = std::fs::remove_file(server_path);
    let _ = std::fs::remove_file(client_path);

    let server = UnixDatagram::bind(server_path).unwrap();
    let client = UnixDatagram::bind(client_path).unwrap();

    client.send_to(b"Hello datagram", server_path).unwrap();

    let mut buf = [0u8; 256];
    let (n, addr) = server.recv_from(&mut buf).unwrap();
    println!("Server got: {}", std::str::from_utf8(&buf[..n]).unwrap());

    server.send_to(b"ACK", addr.as_pathname().unwrap()).unwrap();

    let n = client.recv(&mut buf).unwrap();
    println!("Client got: {}", std::str::from_utf8(&buf[..n]).unwrap());

    std::fs::remove_file(server_path).ok();
    std::fs::remove_file(client_path).ok();
}

For async Unix domain sockets, tokio provides tokio::net::UnixListener and tokio::net::UnixStream with the same API as the sync versions but using .await. See Ch40 for async patterns.

Rust Note: Rust's std::os::unix::net types do not support SCM_RIGHTS directly. For file descriptor passing in Rust, use the nix crate's sendmsg/recvmsg with ControlMessage::ScmRights, or the passfd crate.

Why Unix Domain Sockets Are the Best IPC

+---------------------------+------------------------------------+
| Feature                   | Unix Domain Sockets                |
+---------------------------+------------------------------------+
| Bidirectional             | Yes (SOCK_STREAM)                  |
| Message boundaries        | Yes (SOCK_DGRAM)                   |
| Unrelated processes       | Yes                                |
| File descriptor passing   | Yes (SCM_RIGHTS)                   |
| Credential checking       | Yes (SCM_CREDENTIALS)              |
| Familiar API              | Same as TCP/UDP sockets            |
| Performance               | Faster than TCP loopback           |
| Backpressure              | Yes (kernel buffer limits)         |
| Async-compatible          | Yes (epoll, tokio, etc.)           |
| Easy to upgrade to TCP    | Change AF_UNIX to AF_INET          |
+---------------------------+------------------------------------+

Knowledge Check

  1. What is the difference between a filesystem-path socket and an abstract socket?
  2. How does SCM_RIGHTS work at the kernel level?
  3. Why must a datagram client also bind to a path if it wants to receive replies?

Common Pitfalls

  • Forgetting to unlink the socket file -- the next bind will fail with EADDRINUSE. Always unlink before bind.
  • Using sizeof(addr) for abstract socket addresses -- abstract names can contain null bytes. Pass the exact computed length.
  • Not setting SO_PASSCRED before receiving credentials -- the kernel does not attach credential data by default.
  • Assuming SOCK_DGRAM is unreliable -- unlike UDP, Unix datagram sockets are reliable on the same machine. Messages are never dropped (but the sender blocks if the receiver's buffer is full).
  • Permission issues on the socket file -- the socket file inherits the umask. Use chmod or fchmod if other users need access.
  • Buffer overflow in sun_path -- the path field is only 108 bytes on Linux. Use abstract sockets for long names.