Linx

CI

Linux kernel interfaces for Elixir.

A library of low-level Linux primitives — netlink sockets, process and namespace lifecycle, terminal/PTY control, cgroup v2 resource limits, filesystem mounts, user-namespace identity mappings, per-process capability sets, per-thread seccomp filters, kernel-tunable parameters, modern firewalling via nf_tables — exposed as idiomatic Elixir. The aim is to make these feel as natural to drive from the BEAM as anything in the standard library.

Linx is a library of primitives, not a runtime. A container engine, a network orchestrator, or an observability tool is a consumer of Linx; the runtime concepts (images, supervision policies, request routing) live in those projects.

⚠️ 0.x. The API is still settling; minor releases may include breaking changes until 1.0.

Installation

Add linx to your dependencies:

def deps do
[
{:linx, "~> 0.1"}
]
end

Requirements. Linux only — the underlying kernel interfaces don't exist on macOS, BSD, or Windows. Elixir 1.15+ on Erlang/OTP 26+ (the Linx.Tty group-leader attach mode depends on the OTP-26 prim_tty driver). Kernel 6.6 LTS or newer; the nf_tables paths target 6.12 LTS.

Build prerequisites. The kernel-interface NIFs and the process Port are compiled from C (c_src/) at install time, so a C compiler and the relevant headers must be present:

build-essential / base-devel also pull in the libc and Linux UAPI headers the sources include.

The headline composition

Linx's value isn't any single subsystem — it's that they all hook into the same Linx.Processcheckpoint, the window between clone(2) and execve(2) where the child is parked. Inside that window a workload's identity, resource ceiling, network, privileges, and syscall surface are all decided at once, before its first instruction.

alias Linx.{Process, User, Cgroup, Capabilities, Seccomp}
alias Linx.Netlink.Rtnl
{:ok, c} =
Process.spawn(
argv: ["/usr/sbin/nginx"],
namespaces: [:net, :pid, :user],
no_new_privs: true
)
receive do {:linx_process, :ready, _} -> :ok end
{:ok, host_pid} = Process.host_pid(c)
# Identity: root inside ↔ this uid outside.
my_uid = System.cmd("id", ["-u"]) |> elem(0) |> String.trim() |> String.to_integer()
my_gid = System.cmd("id", ["-g"]) |> elem(0) |> String.trim() |> String.to_integer()
:ok = User.setup_maps(host_pid,
uid: [{0, my_uid, 1}], gid: [{0, my_gid, 1}])
# Resources: 256 MiB / half a CPU.
{:ok, cg} = Cgroup.create("/sys/fs/cgroup/myorg/nginx-42")
:ok = Cgroup.set_memory_max(cg, 256 * 1024 * 1024)
:ok = Cgroup.set_cpu_max(cg, {50_000, 100_000})
:ok = Cgroup.add_process(cg, host_pid)
# Network: a macvlan with an address and a default route.
{:ok, host_sock} = Rtnl.open()
:ok = Rtnl.Link.create_macvlan(host_sock, "ct0", "eth0", :bridge)
:ok = Rtnl.Link.move_to_netns(host_sock, "ct0", host_pid)
{:ok, ns} = Rtnl.open({:pid, host_pid})
:ok = Rtnl.Link.set_up(ns, "ct0")
:ok = Rtnl.Address.add(ns, "ct0", "10.0.0.5", 24)
:ok = Rtnl.Route.add_default(ns, "10.0.0.1")
# Firewall: default drop, allow established + ssh; rules vanish when we do.
{:ok, ct_nfnl} = Linx.Netlink.Nfnl.open({:pid, host_pid})
:ok = Linx.Netfilter.push(ct_nfnl, ~NFT"""
table inet guard {
chain input {
type filter hook input priority 0
policy drop
ct state established accept
tcp dport 22 accept
}
}
""")
# Privilege: only cap_net_bind_service.
all = Linx.Capabilities.Constants.all()
:ok = Capabilities.drop_bounding(c,
MapSet.difference(all, MapSet.new([:cap_net_bind_service])))
# Syscalls: only what nginx actually needs.
nginx_syscalls = ~w(read write openat close fstat brk mmap munmap mprotect
socket bind listen accept4 setsockopt getsockopt
rt_sigaction rt_sigprocmask rt_sigreturn exit_group
epoll_pwait epoll_ctl epoll_create1 clock_gettime futex)a
{:ok, filter} = Seccomp.allow_list(nginx_syscalls, default: :kill_process)
:ok = Seccomp.install(c, filter)
# Release the workload. Every constraint above is in force from
# the moment execve(2) runs.
:ok = Process.proceed(c)

The subsystems are independent — you can spawn without namespaces, use netlink without spawning, drop caps without seccomp. They compose cleanly because they share one primitive (the checkpoint), not because there's a framework holding them together. Each subsystem's module doc carries the standalone walkthroughs and the progressively-richer composition recipes; docs/<subsystem>/<subsystem>-examples.md has runnable, copy-paste transcripts.

Subsystems

How Linx is organized

Three kinds of top-level module, named for what they organize:

KindWhenExamples
Mechanism layerA coherent transport with shared infrastructure (codec, framing, error handling, …).Linx.Netlink
Subsystem conceptA grouping of kernel operations that work together for one purpose. Mirrors how Linux man-page section 7 names things.Linx.Process, Linx.Tty, Linx.Cgroup, Linx.Mount, Linx.User, Linx.Capabilities, Linx.Seccomp, Linx.Sysctl, Linx.Netfilter
Value typeA domain primitive that flows through the mechanisms. Top level.Linx.IP, Linx.MAC

Name a module after a mechanism only when the mechanism has shared shape worth factoring out; otherwise name it after the kernel subsystem or concept. Namespace isn't a subsystem — it's a cross-cutting flag on clone(2) — so it doesn't get its own module; the operations live where they belong.

Each subsystem owns its living docs under docs/<subsystem>/: an overview, runnable examples, and external references. Roadmap and forward-compatibility notes live in each subsystem's module doc.

Docs

Generated docs are hosted at hexdocs.pm/linx. Locally, mix docs builds HexDocs-style HTML under _build/docs/; the per-subsystem overview, examples, and references pages are surfaced there alongside the module docs.

License

Linx is released under the MIT License.