Erlang/OTP: documenting how to bind a distributed node to loopback

September 2026 — a documentation change proposed to Erlang/OTP, erlang/otp#11617 (target maint). Measurements: erts-sched/otp-loopback-node-measurements. Follows from the VS Code Erlang extension RCE.

Why

The VS Code extension bug was one instance of a class: a tool that only needs local Erlang distribution ships with the cluster default instead — the distribution listener on all interfaces, epmd on all interfaces, and a guessable cookie. The same shape produced CVE-2022-24706 in Apache CouchDB (distribution reachable with the default cookie monster).

The fix in each case is the same three settings. But nowhere in the OTP documentation is it written down how to run a node confined to the local host, which of the settings does what, or why the obvious attempts fail. The inet_dist_use_interface entry in kernel_app.md was two sentences and only mentioned hosts with “many network interfaces”. So this is the general lesson of the extension bug, taken upstream — not as a vulnerability report (Erlang distribution is documented as not being for untrusted networks, and the cookie is documented as not being a security mechanism; reporting that returns “by design”), but as the missing page.

The PR does not change any default or behavior. It expands that one entry to explain the recipe, what each setting does and does not do, the IPv6 equivalent, and the honest limit: binding to loopback removes the network exposure, it does not replace the cookie.

How it was measured

The obvious recipe (-kernel inet_dist_use_interface {127,0,0,1}) has traps that only show up when you measure, not when you reason:

Making the analysis reproducible

Every claim in the PR is one row in a table, and every row is a case in a script anyone can run: otp-loopback-node-measurements. It reads sockets from inside the BEAM (inet:sockname/1, gen_tcp:connect/4 against epmd, inet:getifaddrs/0), not with ss, so the only requirements are bash, erl and epmd. It runs on a private epmd port so a system daemon is never touched, and cleans up every node on exit.

./measure.sh                 # every case, on the local OTP
./run-docker.sh 27 28 29     # official erlang:<v> images

Twenty-four cases, run on OTP 27.3.4.17, 28.5.0.6 and 29.0.6 (the version of maint), IPv4 and IPv6, on the host and in containers with and without the host’s firewall. Each “refused” result has a control on the same path that succeeds, so a refusal is attributable to the socket binding and not to filtering — and the whole matrix, except the one row that needs a global IPv6 address, was repeated on OTP 27, 28 and 29 in a container with its own network namespace (no host rules) to confirm it.

The point of the repository is that a reviewer — or anyone, later — does not have to take the table on faith or rebuild the setup by hand. The analysis also states what was not measured (a second physical host, Windows/macOS, TLS distribution) and why, so the boundary of the evidence is explicit.

Status

Open, DCO and CLA signed, mergeable, awaiting review. Whatever the maintainers decide, the gap is now described publicly, with evidence that reproduces.

References