armor 1.0.0
Cross-platform process sandboxing library for D
To use this package, run the following command in your project's root directory:
Manual usage
Put the following dependency into your project's dependences section:
Armor
Cross-platform process sandboxing library for D.
Armor restricts what a process can do -- filesystem access, network connections, resource usage, and environment variables. It uses kernel-level enforcement (Landlock on Linux, Seatbelt on macOS) with a transparent network proxy for hostname-level filtering.
Table of Contents
- Quick Start
- API Reference
- Sandbox (builder)
- SandboxContext (lifecycle)
- Network proxy
- System probing
- Configuration
- Platform Support
- Network filtering architecture
- Examples
- fs-sandbox
- resource-jail
- safe-exec
- plugin-runner
- net-filter-proxy
- Known Issues
- Building
- License
Quick Start
import armor; auto sandbox = Sandbox() .allowRead("/usr") .allowReadWrite("/tmp/myapp") .blockAllNetwork() .limitMemory(256 * 1024 * 1024) .limitCpu(30) .denyEnv("LD_PRELOAD") .forceEnv("SANDBOX=1"); Child Sandbox (Fork + Exec)
import armor.sandbox : Sandbox; import armor.context : build, applyEnv, applyKernel, free; import core.sys.posix.unistd : fork, execvp, _exit; import core.sys.posix.sys.wait : waitpid; auto ctx = build(sandbox); auto pid = fork(); if (pid == 0) { applyEnv(ctx); // set/unset environment variables applyKernel(ctx); // apply Landlock, seccomp, Seatbelt, resource limits execvp(program, argv); _exit(127); } waitpid(pid, &status, 0); free(ctx); // parent cleanup API Reference
Sandbox (builder)
All builder methods return ref Sandbox for chaining.
Filesystem
`allowRead(string[] paths...)` -- Add read-only paths.
auto sb = Sandbox() .allowRead("/usr", "/lib", "/lib64") .allowRead("/etc/ssl/certs"); `allowReadWrite(string[] paths...)` -- Add read-write paths.
auto sb = Sandbox() .allowReadWrite("/tmp/myapp", "/var/log/myapp"); `setSandboxRoot(string path)` -- Set the sandbox root directory (read-write). Paths outside the root are restricted.
auto sb = Sandbox() .setSandboxRoot("/tmp/sandbox") .allowRead("/usr"); All filesystem paths support ~/... expansion via $HOME.
`setIncludeWorkdir(bool v)` -- Include the current working directory as read-write. Default: true. Set to false for stricter isolation.
auto sb = Sandbox() .setSandboxRoot("/tmp/sandbox") .setIncludeWorkdir(false); // cwd is NOT writable Network
`blockAllNetwork()` -- Block all network access except explicitly allowed hosts and ports. On Linux, uses Landlock/seccomp. On macOS, uses Seatbelt.
auto sb = Sandbox() .blockAllNetwork(); // no network at all `allowNetwork(string[] hosts...)` -- Add allowed hostnames. Supports *.domain.com wildcard patterns for subdomain matching.
auto sb = Sandbox() .blockAllNetwork() .allowNetwork("github.com", "*.npmjs.org") .allowNetwork("registry.example.com"); `allowPorts(string[] ports...)` -- Add allowed TCP ports as strings. Only connections to these ports are permitted.
auto sb = Sandbox() .blockAllNetwork() .allowNetwork("github.com") .allowPorts("443", "80"); `setTunFd(int fd)` -- Set a pre-created TUN device file descriptor for transparent network proxy (Linux only). Used internally by the TUN/lwIP proxy layer.
auto tunFd = createTunDevice(); auto sb = Sandbox() .blockAllNetwork() .allowNetwork("github.com") .setTunFd(tunFd); `enablePfRedirect()` -- Enable PF redirect rules for full network interception on macOS. Requires root. Catches all TCP traffic including SIP-protected system binaries. No-op on Linux (TUN already provides transparent interception).
auto sb = Sandbox() .blockAllNetwork() .allowNetwork("github.com") .enablePfRedirect(); // requires root on macOS Resource limits
`limitMemory(ulong bytes)` -- Max address space in bytes (RLIMIT_AS). Child is killed with SIGSEGV/SIGKILL when exceeded.
auto sb = Sandbox() .limitMemory(256 * 1024 * 1024); // 256 MB `limitCpu(ulong seconds)` -- Max CPU time in seconds (RLIMIT_CPU). Child receives SIGXCPU when exceeded.
auto sb = Sandbox() .limitCpu(30); // 30 seconds of CPU time `limitFileSize(ulong bytes)` -- Max file size in bytes (RLIMIT_FSIZE). Write operations that exceed the limit fail with SIGXFSZ.
auto sb = Sandbox() .limitFileSize(10 * 1024 * 1024); // 10 MB max file size `limitProcesses(ulong n)` -- Max number of processes (RLIMIT_NPROC). fork() fails when exceeded.
auto sb = Sandbox() .limitProcesses(16); // max 16 processes `limitOpenFiles(ulong n)` -- Max open file descriptors (RLIMIT_NOFILE). open()/socket() fail when exceeded.
auto sb = Sandbox() .limitOpenFiles(64); // max 64 open fds Environment
`denyEnv(string[] vars...)` -- Strip environment variables from the child process. Removed before exec.
auto sb = Sandbox() .denyEnv("LD_PRELOAD", "LD_LIBRARY_PATH") .denyEnv("AWS_SECRET_ACCESS_KEY"); `forceEnv(string[] pairs...)` -- Force environment variables in the child process as "KEY=VALUE" pairs. Set before exec, overriding any existing values.
auto sb = Sandbox() .forceEnv("SANDBOX=1", "HOME=/tmp/sandbox") .forceEnv("NODE_ENV=production"); SandboxContext (lifecycle)
`build(ref const Sandbox sb)` -- Pre-allocate all enforcement state (Landlock ruleset, seccomp filter, Seatbelt profile, env lists). Call in parent before fork. On macOS with network filtering, also starts the SOCKS5 proxy.
auto sb = Sandbox().allowRead("/usr").blockAllNetwork(); auto ctx = build(sb); // ctx is ready for fork `applyEnv(ref SandboxContext ctx)` -- Apply environment deny/force lists in the child process. Calls unsetenv()/setenv(). NOT safe after vfork() (mutates shared address space). Calls _exit(126) if any env operation fails.
if (pid == 0) { applyEnv(ctx); // must be called before applyKernel applyKernel(ctx); execvp(program, argv); _exit(127); } `applyKernel(ref SandboxContext ctx)` -- Apply kernel-level enforcement: Landlock, seccomp, Seatbelt, resource limits, PR_SET_NO_NEW_PRIVS. Safe after both fork() and vfork(). Uses only raw syscalls (@nogc nothrow). Calls _exit(126) if any enforcement operation fails -- the child must not proceed without the sandbox applied.
if (pid == 0) { applyEnv(ctx); applyKernel(ctx); // after this, the process is restricted execvp(program, argv); _exit(127); } `free(ref SandboxContext ctx)` -- Release all resources held by the context: close Landlock fd, free seccomp filter, free Seatbelt profile, stop proxy, tear down PF rules. Call in parent after waitpid(). Safe to call twice. Note: the TUN fd set via setTunFd() is borrowed, not owned -- free() does NOT close it. The caller is responsible for closing the TUN fd.
int status; waitpid(pid, &status, 0); free(ctx); // cleanup after child exits `ctx.mutatesEnv()` -- Returns true if the context has environment modifications (deny or force lists). Use to decide between fork() and vfork().
auto ctx = build(sb); auto pid = ctx.mutatesEnv() ? fork() : vfork(); `ctx.hasKernelEnforcement()` -- Returns true if the context has kernel-level enforcement active (Landlock, seccomp, or Seatbelt).
auto ctx = build(sb); if (ctx.hasKernelEnforcement()) { writeln("Kernel sandbox is active"); } Network proxy
For hostname-level filtering, Armor runs a SOCKS5 proxy that checks each connection against a NetworkPolicy. On Linux with a TUN fd, it runs a transparent proxy instead. On macOS, it can listen on both a TCP port and a Unix domain socket simultaneously.
`startProxy(NetworkPolicy policy, int tunFd = -1)` -- Start a network proxy. If tunFd >= 0 (Linux), uses transparent TUN mode. Otherwise starts a SOCKS5 proxy on a random localhost port.
import armor.network.policy : NetworkPolicy; import armor.network.proxy : startProxy, stopProxy; NetworkPolicy policy; policy.allowHosts = ["github.com", "*.npmjs.org"]; policy.allowPorts = ["443"]; policy.blockAllOthers = true; auto handle = startProxy(policy); // handle.port contains the assigned TCP port `startDualSocksProxy(NetworkPolicy policy, string unixPath)` -- Start a SOCKS5 proxy listening on both a TCP port (localhost) and a Unix domain socket. The TCP port is used for proxy env vars and PF redirect. The Unix socket is used by the DYLD connect() hook on macOS.
auto handle = startDualSocksProxy(policy, "/tmp/armor-proxy.sock"); // handle.port = TCP port on localhost // Unix socket at /tmp/armor-proxy.sock `stopProxy(ref ProxyHandle handle)` -- Shut down the proxy, close all sockets, join the proxy thread, and clean up the Unix socket file.
stopProxy(handle); // handle.port == 0, handle.serverFd == -1 The proxy validates hostnames twice: once on the SOCKS5 CONNECT target, and again on the TLS SNI hostname if present. This catches hostname mismatches in HTTPS tunneling.
System probing
Detect which enforcement mechanisms are available on the current system before building a sandbox. No side effects, no file descriptors leaked, no forking.
`probeCapabilities()` -- Returns a SystemCapabilities struct describing what the current kernel supports.
import armor.probe : probeCapabilities; auto caps = probeCapabilities(); if (caps.landlockAbi > 0) { writefln("Landlock ABI v%d available", caps.landlockAbi); } if (caps.landlockNetwork) { writeln("Landlock network rules supported (ABI >= 4)"); } if (caps.tunAvailable && caps.namespacesAvailable) { writeln("TUN transparent proxy available"); } if (caps.seccompSupported) { writeln("seccomp filtering supported"); } | Field | Type | Description |
|---|---|---|
landlockAbi | int | Landlock ABI version (0 = unavailable, 1-5+) |
landlockNetwork | bool | true if landlockAbi >= 4 (network port rules) |
seccompSupported | bool | true on x86_64 and AArch64 |
tunAvailable | bool | /dev/net/tun is accessible |
namespacesAvailable | bool | User namespaces enabled in kernel |
Configuration
Armor exposes operational tunables through armor.config.config. Override values before calling build():
import armor.config; config.maxProxyConnections = 256; config.relayTimeoutSec = 60; | Field | Default | Description |
|---|---|---|
maxProxyConnections | 128 | Max concurrent SOCKS5 proxy connections |
listenBacklog | 128 | TCP/Unix listener backlog |
maxTunConnections | 1024 | Max concurrent TUN relay connections |
fallbackDnsAddr | 0x01010101 (1.1.1.1) | Fallback DNS server if /etc/resolv.conf is unreadable (network byte order) |
dnsTimeoutSec | 2 | DNS query timeout in seconds |
relayTimeoutSec | 30 | Data relay idle timeout in seconds |
tunPollIntervalUs | 10,000 | TUN event loop poll interval in microseconds |
Platform Support
| Feature | Linux | macOS |
|---|---|---|
| Filesystem sandboxing | Landlock (kernel 5.13+) | Seatbelt |
| Network port blocking | Landlock ABI v4 / seccomp (x86_64, AArch64 only) | Seatbelt |
| Network hostname filtering | TUN/lwIP transparent proxy | Layered proxy (see below) |
| Resource limits | setrlimit | setrlimit |
| Process privilege restriction | PRSETNONEWPRIVS | Seatbelt |
| Environment enforcement | POSIX unsetenv/setenv | POSIX unsetenv/setenv |
Network filtering architecture
When blockAllNetwork() is set with allowed hosts, Armor enforces hostname-level filtering through a proxy that checks each connection against the policy. The proxy is the same on both platforms -- the difference is how traffic reaches it.
Linux: TUN + lwIP (transparent)
All child traffic is transparently intercepted via a network namespace and TUN device. The child doesn't need to know about the proxy.
Child process (isolated network namespace) | v TUN device (only interface in namespace) | v lwIP userspace TCP/IP stack (parent process) | v Policy check (checkHostPolicy) --> allow/deny | v Real destination macOS: layered proxy
Seatbelt blocks all network at the kernel level. Multiple layers help the child reach the SOCKS5 proxy. If a program can't find the proxy through any layer, it's simply blocked -- the sandbox is always secure.
Layer 1: Proxy env vars (http_proxy, https_proxy, ALL_PROXY) covers: curl, wget, npm, pip, most HTTP clients Layer 2: DYLD connect() hook (DYLD_INSERT_LIBRARIES) covers: raw TCP from user-built binaries limitation: SIP-protected system binaries strip DYLD_INSERT_LIBRARIES Layer 3: PF redirect rules (optional, requires root) covers: everything including SIP-protected binaries limitation: requires root, rules scoped by UID not PID Backstop: Seatbelt kernel sandbox blocks all network except proxy socket -- nothing escapes | Layer | Covers | Requires |
|---|---|---|
| Seatbelt | Blocks everything (security guarantee) | Nothing, always on |
| Proxy env vars | HTTP clients | Nothing |
| DYLD hook | Raw TCP from user binaries | Non-SIP binary |
| PF redirect | All binaries including SIP | Root |
Examples
All examples live in examples/ and can be built with dub build from their directory. Each is a standalone CLI that wraps a command in a sandbox. Use --help on any example to see all options.
fs-sandbox
Filesystem sandbox with read-only and read-write path separation. Demonstrates Landlock (Linux) and Seatbelt (macOS) filesystem enforcement.
cd examples/fs-sandbox && dub build # Allow read to system libs, read-write to output dir ./armor-fs-sandbox --ro /usr --rw /tmp/output -- ls /tmp/output # Block writes everywhere except sandbox root ./armor-fs-sandbox --root /tmp/sandbox --no-workdir -- ./my-program # Show all options ./armor-fs-sandbox --help Options:
| Flag | Description |
|---|---|
--ro PATH | Read-only path (repeatable) |
--rw PATH | Read-write path (repeatable) |
--root PATH | Sandbox root directory (read-write) |
--no-workdir | Do not include cwd as read-write |
resource-jail
Enforce resource limits on a child process. The child is killed by the kernel when a limit is exceeded (e.g. SIGXCPU for CPU time).
cd examples/resource-jail && dub build # 1 second CPU limit ./armor-resource-jail --cpu 1 -- ./cpu-intensive-program # 16 MB memory limit ./armor-resource-jail --mem 16777216 -- ./memory-hungry-program # Combine limits ./armor-resource-jail --cpu 30 --mem 268435456 --nproc 4 -- make -j4 Options:
| Flag | Description |
|---|---|
--mem BYTES | Memory limit (RLIMIT_AS) |
--cpu SECONDS | CPU time limit |
--fsize BYTES | Max file size |
--nproc N | Max processes |
--nofile N | Max open files |
safe-exec
Full-featured sandbox CLI combining filesystem, network, resource, and environment restrictions.
cd examples/safe-exec && dub build # Sandbox a build command ./armor-safe-exec \ --allow-read /usr \ --allow-rw /tmp/build \ --block-network \ --cpu-limit 60 \ --mem-limit 536870912 \ --deny-env LD_PRELOAD \ --force-env SANDBOX=1 \ -- make -j4 # Allow only specific network hosts ./armor-safe-exec \ --allow-host github.com \ --allow-host "*.npmjs.org" \ --allow-port 443 \ -- npm install Options:
| Flag | Description |
|---|---|
--allow-read PATH | Read-only path (repeatable) |
--allow-rw PATH | Read-write path (repeatable) |
--sandbox-root PATH | Sandbox root directory |
--no-workdir | Exclude cwd from read-write paths |
--block-network | Block all network |
--allow-host HOST | Allowed hostname (repeatable) |
--allow-port PORT | Allowed port (repeatable) |
--mem-limit BYTES | Memory limit |
--cpu-limit SECONDS | CPU time limit |
--fsize-limit BYTES | Max file size |
--proc-limit N | Max processes |
--nofile-limit N | Max open files |
--deny-env VAR | Strip env var (repeatable) |
--force-env K=V | Force env var (repeatable) |
plugin-runner
Run multiple executables ("plugins"), each in its own sandboxed child process. Outputs JSON-lines with exit codes. Separate plugins with --.
cd examples/plugin-runner && dub build # Run two plugins in a shared sandbox ./armor-plugin-runner \ --sandbox-root /tmp/plugins \ --allow-read /usr \ -- ./plugin-a --flag \ -- ./plugin-b input.txt # Output: # {"plugin": "./plugin-a", "exit": 0} # {"plugin": "./plugin-b", "exit": 0} Options:
| Flag | Description |
|---|---|
--sandbox-root DIR | Sandbox root (read-write) |
--allow-read PATH | Read-only path (repeatable) |
--allow-rw PATH | Read-write path (repeatable) |
--deny-env VAR | Strip env var (repeatable) |
net-filter-proxy
Hostname-based network filtering with layered proxy enforcement. Blocks all network at the kernel level and routes allowed traffic through a SOCKS5 proxy. On macOS, automatically uses the DYLD connect() hook for non-SIP binaries. Use --pf-redirect (requires root) for full interception including SIP binaries.
cd examples/net-filter-proxy && dub build # Allow only github.com on port 443 ./armor-net-filter \ --allow-host github.com \ --allow-port 443 \ -- curl https://github.com # Allow all subdomains of example.com ./armor-net-filter \ --allow-host "*.example.com" \ -- ./my-program # macOS: full interception including SIP binaries (requires root) sudo ./armor-net-filter \ --allow-host github.com \ --pf-redirect \ -- /usr/bin/curl https://github.com Options:
| Flag | Description |
|---|---|
--allow-host HOST | Allowed hostname (repeatable, supports *.domain.com) |
--allow-port PORT | Allowed port (repeatable) |
--pf-redirect | Enable PF redirect rules for full interception (macOS, requires root) |
Known Issues
No known issues.
Building
dub build dub test On Linux, the build automatically compiles the vendored lwIP C library for TUN-based transparent proxy support. On macOS, it compiles libarmor_netredirect.dylib for the connect() interposer used by the DYLD hook layer.
Running tests with coverage
dub test --config=unittest-cov --coverage Integration tests
DC=ldc2 bash tests/integration/run.sh License
BSD 3-Clause. See LICENSE.
- Registered by Szabo Bogdan
- 1.0.0 released a day ago
- szabobogdan3/armor
- BSD-3-Clause
- Copyright 2026, Bogdan Szabo
- Authors:
- Sub packages:
- armor:armor-fs-sandbox, armor:armor-resource-jail, armor:armor-safe-exec, armor:armor-plugin-runner, armor:armor-net-filter
- Dependencies:
- none
- Versions:
-
Show all 2 versions1.0.0 2026-Mar-23 ~main 2026-Mar-23 - Download Stats:
-
-
0 downloads today
-
1 downloads this week
-
1 downloads this month
-
1 downloads total
-
- Score:
- 1.5
- Short URL:
- armor.dub.pm