# IPC Transport & Protocol Framing

> `rmux-ipc` owns endpoint naming and raw byte transport only (Unix sockets on Linux/macOS, Named Pipes on Windows with a per-user mutex) — it knows nothing about request shapes. `rmux-proto` owns the typed frame contracts: `Request` and `Response` enums covering all 90 tmux-compatible commands plus SDK-only extensions (session leases, pane output subscriptions, snapshot refs, SDK wait IDs). The codec layer in `rmux-proto::codec` length-frames and encodes requests; `rmux-client::connection` drives this over the blocking stream from `rmux-ipc`. The key boundary invariant: `rmux-ipc` must remain protocol-agnostic so it can be swapped or extended without touching the request/response contracts in `rmux-proto`.

- Repository: Helvesec/rmux
- GitHub: https://github.com/Helvesec/rmux
- Human wiki: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181
- Complete Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/llms-full.txt

## Source Files

- `crates/rmux-ipc/src/lib.rs`
- `crates/rmux-ipc/src/endpoint.rs`
- `crates/rmux-ipc/src/stream.rs`
- `crates/rmux-proto/src/lib.rs`
- `crates/rmux-proto/src/request.rs`
- `crates/rmux-client/src/connection.rs`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [crates/rmux-ipc/src/lib.rs](crates/rmux-ipc/src/lib.rs)
- [crates/rmux-ipc/src/endpoint.rs](crates/rmux-ipc/src/endpoint.rs)
- [crates/rmux-ipc/src/stream.rs](crates/rmux-ipc/src/stream.rs)
- [crates/rmux-ipc/src/windows_mutex.rs](crates/rmux-ipc/src/windows_mutex.rs)
- [crates/rmux-proto/src/lib.rs](crates/rmux-proto/src/lib.rs)
- [crates/rmux-proto/src/request.rs](crates/rmux-proto/src/request.rs)
- [crates/rmux-proto/src/response.rs](crates/rmux-proto/src/response.rs)
- [crates/rmux-proto/src/codec.rs](crates/rmux-proto/src/codec.rs)
- [crates/rmux-proto/src/envelope.rs](crates/rmux-proto/src/envelope.rs)
- [crates/rmux-client/src/connection.rs](crates/rmux-client/src/connection.rs)
</details>

# IPC Transport & Protocol Framing

The rmux IPC stack is divided into two tightly scoped crates with a strict dependency direction: `rmux-ipc` owns the OS-level transport boundary (endpoint naming and raw byte streams), while `rmux-proto` owns the typed protocol boundary (request/response contracts and wire framing). `rmux-client::connection` is the integration point that drives `rmux-proto`'s codec over the blocking stream vended by `rmux-ipc`.

This separation is the key architectural invariant: `rmux-ipc` knows nothing about request shapes and can be replaced or extended on a new platform without touching a single type in `rmux-proto`. Conversely, the protocol layer never directly opens sockets — it only consumes abstract byte streams.

---

## `rmux-ipc`: Endpoint Naming and Raw Transport

### Responsibility

`rmux-ipc` is documented explicitly as owning "endpoint naming and local transport handles" and transporting bytes only. The crate's top-level doc comment states: *"It deliberately transports bytes only; the RMUX request/response protocol stays in `rmux-proto`."*

Sources: [crates/rmux-ipc/src/lib.rs:1-7]()

### Endpoint Naming

`LocalEndpoint` wraps a `PathBuf` that represents either a Unix domain socket path or a Windows named pipe path. The same struct serves both platforms by keeping separate accessor methods:

| Method | Platform | Returns |
|---|---|---|
| `as_path()` | Unix | `&Path` (socket path) |
| `as_pipe_name()` | Windows | `&OsStr` (named pipe name) |
| `into_path()` | Both | `PathBuf` |

Sources: [crates/rmux-ipc/src/endpoint.rs:37-68]()

**Endpoint resolution priority** (highest to lowest):

1. `-S <path>` explicit socket path
2. `-L <label>` socket label
3. `$RMUX` environment variable
4. Default (label `"default"`)

Sources: [crates/rmux-ipc/src/endpoint.rs:117-135]()

#### Unix Socket Path Format

On Unix, the path is assembled as:

```
<socket_root>/rmux-<uid>/<label>
```

where `<socket_root>` is resolved from `$RMUX_TMPDIR`, falling back to `/tmp`. The UID-scoped directory means no two users share a socket namespace even in the same `/tmp`.

Sources: [crates/rmux-ipc/src/endpoint.rs:102-115]()

#### Windows Named Pipe Format

On Windows, the path follows:

```
\\.\pipe\rmux-<SID>-il-<integrity_level>-<label>
```

The integrity level is read from the running process token via `GetTokenInformation(TokenIntegrityLevel)` and mapped to one of `untrusted | low | medium | high | system`. This prevents a low-integrity process from connecting to a medium-integrity daemon through the named pipe.

Sources: [crates/rmux-ipc/src/endpoint.rs:86-100](), [crates/rmux-ipc/src/endpoint.rs:266-348]()

#### Safety: Endpoint Ownership Validation

Both platforms enforce an `socket_path_is_rmux_owned` check before accepting a path from `$RMUX` or `-S`. On Unix the parent directory name must start with `rmux-`; on Windows the pipe name must start with `\\.\pipe\rmux-`. This prevents a malicious process from redirecting the client to an unrelated socket.

Sources: [crates/rmux-ipc/src/endpoint.rs:191-212]()

### Stream Types

`rmux-ipc` exposes two stream type aliases that are platform-specific:

| Type alias | Unix | Windows |
|---|---|---|
| `LocalStream` | `tokio::net::UnixStream` | Windows async pipe (via `stream_windows.rs`) |
| `BlockingLocalStream` | `std::os::unix::net::UnixStream` | Windows blocking pipe |

`LocalStream` (async) is used by the server runtime; `BlockingLocalStream` (sync) is used by the CLI client. This means the server is async-tokio while the CLI is fully blocking — the codec and connection layers in `rmux-client` are written entirely without async.

Sources: [crates/rmux-ipc/src/stream.rs:40-45]()

### Blocking Connect

`connect_blocking` creates a non-blocking Unix socket for the connect syscall (so it can use a poll-based timeout), then flips it back to blocking mode after the connection is confirmed. On Linux, `SOCK_CLOEXEC | SOCK_NONBLOCK` are set atomically at socket creation; on other Unix platforms they are applied post-creation via `fcntl`.

Sources: [crates/rmux-ipc/src/stream.rs:171-223]()

### Peer Disconnect Detection

`wait_for_peer_close` is an async utility that avoids consuming protocol bytes while detecting a peer disappearing. It uses `recv(PEEK)` to observe a 0-byte readable event and supplements with `poll(POLLRDHUP)` on Linux/FreeBSD/illumos where the flag is available, falling back to `POLLHUP | POLLERR` on other platforms.

Sources: [crates/rmux-ipc/src/stream.rs:48-82](), [crates/rmux-ipc/src/stream.rs:99-129]()

### Windows Named Mutex (Daemon Startup Race)

Windows daemon bootstrap requires a per-endpoint serialization point across processes. `rmux-ipc` provides `acquire_named_mutex` — a safe RAII wrapper around Win32 `CreateMutexExW`. The mutex is created with a DACL (`D:P(A;;0x1F0001;;;{SID})`) that restricts access to the current user's SID, ensuring a peer running under a different identity cannot acquire it. The `NamedMutexGuard` releases via `ReleaseMutex + CloseHandle` in `Drop`.

A critical thread-affinity invariant: Win32 mutexes are owned per-thread. Dropping the guard on a different thread calls `ReleaseMutex` with `ERROR_NOT_OWNER`, leaving the kernel mutex held until the original thread terminates. The SDK bootstrap layer avoids this with a dedicated holder thread.

Sources: [crates/rmux-ipc/src/windows_mutex.rs:1-135](), [crates/rmux-ipc/src/windows_mutex.rs:192-276]()

---

## `rmux-proto`: Typed Frame Contracts

### Responsibility

`rmux-proto` owns all typed request/response contracts plus the codec that serializes them into framed byte sequences. It has `#![forbid(unsafe_code)]` and no I/O dependency — it never opens a socket.

Sources: [crates/rmux-proto/src/lib.rs:1-3]()

### The `Request` Enum

`Request` is a flat enum covering all supported command and RPC variants. It derives `Serialize + Deserialize` via serde (encoded by bincode on the wire). A `command_name()` method returns the stable routing string for each variant:

```rust
// crates/rmux-proto/src/request.rs:97-100
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
    NewSession(NewSessionRequest),
    // ... 90+ variants
}
```

Sources: [crates/rmux-proto/src/request.rs:94-322]()

**Request categories:**

| Category | Examples |
|---|---|
| Session lifecycle | `NewSession`, `HasSession`, `KillSession`, `RenameSession`, `ListSessions` |
| Window management | `NewWindow`, `KillWindow`, `SelectWindow`, `MoveWindow`, `SwapWindow`, `RotateWindow` |
| Pane management | `SplitWindow`, `KillPane`, `ResizePane`, `SelectPane`, `SwapPane`, `JoinPane` |
| Client operations | `AttachSession`, `DetachClient`, `SwitchClient`, `ListClients` |
| Buffers & clipboard | `SetBuffer`, `PasteBuffer`, `CapturePane`, `LoadBuffer`, `SaveBuffer` |
| Options & hooks | `SetOption`, `ShowOptions`, `SetHook`, `ShowHooks`, `SetEnvironment` |
| Keys & bindings | `SendKeys`, `BindKey`, `UnbindKey`, `ListKeys` |
| SDK extensions | `CreateSessionLease`, `RenewSessionLease`, `SubscribePaneOutput`, `SdkWaitForOutput`, `PaneInput`, `PaneBroadcastInput` |
| Internal/protocol | `Handshake`, `ControlMode`, `ResolveTarget` |

The enum also has "extended" variants (`NewSessionExt`, `AttachSessionExt2`, `SwitchClientExt3`, etc.) that map to the same `command_name` as their base form but carry additional fields introduced in later protocol versions.

Sources: [crates/rmux-proto/src/request.rs:327-441]()

### The `Response` Enum

`Response` is the symmetric counterpart. Every `Request` variant has a matching `*Response` variant. Error results are represented as `Response::Error(RmuxError)` — they are returned in the `Ok` arm so callers can pattern-match them without distinguishing transport failure from application-level failure.

Sources: [crates/rmux-proto/src/response.rs:69-100](), [crates/rmux-client/src/connection.rs:189-197]()

### Codec: Wire Frame Format

The codec module (`rmux-proto::codec`) implements length-prefixed bincode framing. Every frame on the wire has the following layout:

```text
Byte offset  Field
─────────────────────────────────────────────────
0            Magic byte: 0x52 ('R')
1..N         Wire version: unsigned LEB128 varint
N..N+4       Payload length: u32 little-endian
N+4..end     Payload: bincode-serialized T
```

**Magic byte** `RMUX_FRAME_MAGIC = 0x52` identifies the stream as rmux wire protocol (not tmux or any other Unix socket listener).

**Wire version** `RMUX_WIRE_VERSION = 1` is encoded as unsigned LEB128 (up to 5 bytes for a u32). Currently only version 1 is in the supported range `1..=1`.

**Payload length** is a 4-byte little-endian `u32`, capped at `DEFAULT_MAX_FRAME_LENGTH = 1 MiB`.

Sources: [crates/rmux-proto/src/codec.rs:1-44](), [crates/rmux-proto/src/envelope.rs:6-13]()

#### `encode_frame`

```rust
// crates/rmux-proto/src/codec.rs:16-45
pub fn encode_frame<T: Serialize>(value: &T) -> Result<Vec<u8>, RmuxError> {
    let payload = bincode::serialize(value)?;
    // validates non-empty and <= 1 MiB
    let mut frame = Vec::with_capacity(1 + 5 + 4 + payload.len());
    frame.push(RMUX_FRAME_MAGIC);
    encode_varint_u32(RMUX_WIRE_VERSION, &mut frame);
    frame.extend_from_slice(&frame_length.to_le_bytes());
    frame.extend_from_slice(&payload);
    Ok(frame)
}
```

Sources: [crates/rmux-proto/src/codec.rs:16-45]()

#### `FrameDecoder`: Incremental Streaming Decoder

The blocking connection reads in 8 KiB chunks, so partial frames are common. `FrameDecoder` maintains an internal byte buffer and implements a push-parse loop:

1. `push_bytes(chunk)` appends raw bytes.
2. `next_frame::<T>()` checks if the buffer contains a complete frame by reading the length field without consuming bytes. Returns `Ok(None)` when more data is needed, `Ok(Some(T))` on success, or `Err` on a framing violation.
3. On decode success, `drain(..required)` removes the consumed frame bytes from the buffer, leaving any pipelining bytes intact.

Sources: [crates/rmux-proto/src/codec.rs:107-193]()

---

## `rmux-client::connection`: Integration Point

`Connection` is the struct that bridges `rmux-ipc`'s `BlockingLocalStream` with `rmux-proto`'s `FrameDecoder`. It holds both together and provides the `roundtrip` method as the primary client-facing API.

```rust
// crates/rmux-client/src/connection.rs:99-102
pub struct Connection {
    stream: BlockingLocalStream,
    decoder: FrameDecoder,
}
```

Sources: [crates/rmux-client/src/connection.rs:97-102]()

### Timeouts

Three separate timeouts are enforced at the `Connection` level, not at the stream level:

| Constant | Value | Applied to |
|---|---|---|
| `SOCKET_CONNECT_TIMEOUT` | 5 s | `connect_blocking` in `rmux-ipc` |
| `SOCKET_WRITE_TIMEOUT` | 5 s | `stream.set_write_timeout` |
| `SOCKET_RESPONSE_TIMEOUT` | 15 s | `stream.set_read_timeout` |

Scripts that may block indefinitely use `roundtrip_without_read_timeout`, which temporarily removes the read timeout for the duration of the call.

Sources: [crates/rmux-client/src/connection.rs:22-28](), [crates/rmux-client/src/connection.rs:178-212]()

### Request/Response Flow

```
roundtrip(request):
  1. encode_frame(request) → Vec<u8>         [rmux-proto::codec]
  2. stream.write_all(&frame)                 [rmux-ipc BlockingLocalStream]
  3. loop:
       decoder.next_frame::<Response>()       [rmux-proto::codec]
       if None: stream.read(&mut buffer)      [rmux-ipc BlockingLocalStream]
                decoder.push_bytes(...)
       if Some(response): return Ok(response)
       if Err: return Err(ClientError::Protocol)
```

Sources: [crates/rmux-client/src/connection.rs:194-241]()

### Protocol Upgrade Transitions

After a successful `AttachSession` or `ControlMode` response, the `Connection` can be transitioned into a raw stream mode:

- `into_attach_upgrade` → returns `AttachSessionUpgrade`, which exposes the raw `BlockingLocalStream` plus any bytes the `FrameDecoder` had already buffered past the response frame.
- `into_control_upgrade` → returns `ControlModeUpgrade` with the raw stream.

Both transitions clear the read/write timeouts (set to `None`) before handing off the stream, since attach and control-mode traffic is not bounded by the 15-second RPC timeout.

Sources: [crates/rmux-client/src/connection.rs:247-273]()

---

## End-to-End Data Flow

```mermaid
sequenceDiagram
    participant CLI as CLI caller
    participant Conn as rmux-client::Connection
    participant Codec as rmux-proto::codec
    participant IPC as rmux-ipc::BlockingLocalStream
    participant Daemon as rmux daemon

    CLI->>Conn: roundtrip(&Request)
    Conn->>Codec: encode_frame(&request)
    Codec-->>Conn: Vec<u8> (magic | version | length | bincode payload)
    Conn->>IPC: write_all(&frame)
    IPC->>Daemon: bytes over Unix socket / Named Pipe

    Daemon->>IPC: response bytes
    IPC-->>Conn: read(&mut buffer[..8192])
    Conn->>Codec: decoder.push_bytes(chunk)
    Conn->>Codec: decoder.next_frame::<Response>()
    Codec-->>Conn: Ok(Some(Response))
    Conn-->>CLI: Ok(Response)
```

---

## Boundary Invariant and Design Consequences

The explicit invariant documented in `rmux-ipc/src/lib.rs` — that the crate transports bytes only — enforces a dependency rule:

- `rmux-ipc` must **not** import `rmux-proto`.
- `rmux-proto` must **not** import `rmux-ipc`.
- `rmux-client::connection` is the **only** place that joins them.

This means:
- Adding a new platform transport (e.g., abstract Unix namespaces, VSock) only touches `rmux-ipc` and the `Connection` constructor — the 90+ request/response types in `rmux-proto` are untouched.
- Adding a new request variant only touches `rmux-proto` (the enum, serde impls, codec tests) — no transport code changes.
- The codec (bincode + framing) can be swapped in `rmux-proto::codec` without touching socket code, though the wire version in `envelope.rs` would need to advance to signal incompatibility to existing daemons.

Sources: [crates/rmux-ipc/src/lib.rs:3-7](), [crates/rmux-client/src/connection.rs:14-19]()
