# uds_dist
Erlang distribution over Unix domain sockets via the `:socket` module. Lets Elixir and Erlang nodes connect to each other without opening TCP listeners — useful for releases that need a local `remote` shell but should not expose distribution over the network.
Filesystem-backed sockets work on any Unix-like platform. Linux additionally supports abstract namespace sockets that live in kernel state rather than on disk.
## Requirements
OTP 26 or newer. Erlang distribution protocol version 6 only.
## Installation
```elixir
def deps do
[{:uds_dist, "~> 1.0"}]
end
```
## Configuration
Every node — the listening release and any client connecting in — needs the same `-proto_dist` and EPMD-bypass flags:
```
-proto_dist uds
-no_epmd
```
Path resolution is driven by `:socket_dir` in the `:uds_dist` application environment. The part of the node name before `@` becomes the socket file's name within that directory.
```elixir
# config/runtime.exs
config :uds_dist, socket_dir: "/run/myapp"
```
A node named `myapp@host` then listens at `/run/myapp/myapp.sock`. Anything after `@` is ignored — all traffic is local, so the host part is conventional only.
If `socket_dir` is omitted the default is `"."`, meaning sockets are created relative to the BEAM's working directory. Convenient for ad-hoc testing; not recommended for releases.
### Abstract namespace sockets (Linux only)
A `socket_dir` value beginning with `@` selects the Linux abstract namespace. Abstract sockets have no filesystem entry, no permission bits, and are cleaned up by the kernel when their owner exits.
```elixir
config :uds_dist, socket_dir: "@myapp"
```
A node named `myapp@host` then listens at the abstract path `\0myapp/myapp`.
Configuring an abstract `socket_dir` on a non-Linux platform raises at `listen/1` or `setup/5` time. There is no automatic fallback.
### Listen backlog
The kernel listen backlog defaults to 5 and can be overridden:
```elixir
config :uds_dist, backlog: 128
```
The value is read at `listen/1` time, so setting it in `config/runtime.exs` of a release is sufficient.
## Release integration
`rel/vm.args.eex`:
```
-proto_dist uds
-no_epmd
```
`rel/remote.vm.args.eex` — used by `bin/<rel> remote`:
```
-proto_dist uds
-no_epmd
-dist_listen false
```
`-dist_listen false` tells `net_kernel` to call `address/0` instead of `listen/1`, so the remote shell does not create its own socket file.
Set `socket_dir` in `config/runtime.exs`:
```elixir
import Config
config :uds_dist, socket_dir: "/run/#{System.get_env("RELEASE_NAME", "myapp")}"
```
Make sure the directory exists with the right ownership before the release boots. `rel/env.sh.eex` is a good place:
```sh
mkdir -p "/run/${RELEASE_NAME}"
```
After deployment `bin/<rel> remote` opens a shell via the UDS instead of TCP, with no other changes needed.
## Local development (`iex -S mix`)
The BEAM processes `-proto_dist` before Mix has set up the dependency code path, so `net_kernel` cannot find `uds_dist` if you start distribution at boot from a Mix project. Pass `-pa` explicitly to fix it:
```sh
iex \
--erl "-pa _build/dev/lib/uds_dist/ebin -proto_dist uds -no_epmd" \
--sname server -S mix
```
Set the socket directory for the dev environment so both nodes can find each other:
```elixir
# config/config.exs
import Config
if Mix.env() == :dev do
config :uds_dist, socket_dir: Path.join(System.tmp_dir!(), "uds_dist_dev")
end
```
Start a second node the same way with a different `--sname` and run `Node.connect/1` from either side. Releases do not need this workaround — the boot script populates the code path before `-proto_dist` is consulted.
## How it works
`uds_dist` implements the seven callbacks an Erlang distribution module must export (`listen/1`, `accept/1`, `accept_connection/5`, `setup/5`, `close/1`, `select/1`, `address/0`) against the `:socket` NIF rather than `gen_tcp`. EPMD is bypassed entirely: `setup/5` derives the target's socket path from the node name plus the configured `socket_dir`, so no registry is needed.
Post-handshake each connection has three processes:
- **Output handler** — sole writer, also handles distribution ticks
- **Input handler** — greedy reader; pulls available bytes from the kernel buffer and parses length-prefixed frames out of the accumulator
- **Connection supervisor** — supplied by `dist_util`
Length prefixes are hand-rolled in Erlang since `:socket` has no `{packet, N}` mode: 2 bytes during handshake, 4 bytes after.
The implementation is modelled on OTP's `lib/kernel/examples/erl_uds_dist` example. Notable differences from that reference: `:socket` instead of `gen_tcp`, distribution protocol version 6 only, abstract namespace support, and socket-path resolution driven by application config rather than by the bare node name.
## License
MIT. See [`LICENSE`](LICENSE).