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

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"); } 
FieldTypeDescription
landlockAbiintLandlock ABI version (0 = unavailable, 1-5+)
landlockNetworkbooltrue if landlockAbi >= 4 (network port rules)
seccompSupportedbooltrue on x86_64 and AArch64
tunAvailablebool/dev/net/tun is accessible
namespacesAvailableboolUser 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; 
FieldDefaultDescription
maxProxyConnections128Max concurrent SOCKS5 proxy connections
listenBacklog128TCP/Unix listener backlog
maxTunConnections1024Max concurrent TUN relay connections
fallbackDnsAddr0x01010101 (1.1.1.1)Fallback DNS server if /etc/resolv.conf is unreadable (network byte order)
dnsTimeoutSec2DNS query timeout in seconds
relayTimeoutSec30Data relay idle timeout in seconds
tunPollIntervalUs10,000TUN event loop poll interval in microseconds

Platform Support

FeatureLinuxmacOS
Filesystem sandboxingLandlock (kernel 5.13+)Seatbelt
Network port blockingLandlock ABI v4 / seccomp (x86_64, AArch64 only)Seatbelt
Network hostname filteringTUN/lwIP transparent proxyLayered proxy (see below)
Resource limitssetrlimitsetrlimit
Process privilege restrictionPRSETNONEWPRIVSSeatbelt
Environment enforcementPOSIX unsetenv/setenvPOSIX 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 
LayerCoversRequires
SeatbeltBlocks everything (security guarantee)Nothing, always on
Proxy env varsHTTP clientsNothing
DYLD hookRaw TCP from user binariesNon-SIP binary
PF redirectAll binaries including SIPRoot

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:

FlagDescription
--ro PATHRead-only path (repeatable)
--rw PATHRead-write path (repeatable)
--root PATHSandbox root directory (read-write)
--no-workdirDo 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:

FlagDescription
--mem BYTESMemory limit (RLIMIT_AS)
--cpu SECONDSCPU time limit
--fsize BYTESMax file size
--nproc NMax processes
--nofile NMax 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:

FlagDescription
--allow-read PATHRead-only path (repeatable)
--allow-rw PATHRead-write path (repeatable)
--sandbox-root PATHSandbox root directory
--no-workdirExclude cwd from read-write paths
--block-networkBlock all network
--allow-host HOSTAllowed hostname (repeatable)
--allow-port PORTAllowed port (repeatable)
--mem-limit BYTESMemory limit
--cpu-limit SECONDSCPU time limit
--fsize-limit BYTESMax file size
--proc-limit NMax processes
--nofile-limit NMax open files
--deny-env VARStrip env var (repeatable)
--force-env K=VForce 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:

FlagDescription
--sandbox-root DIRSandbox root (read-write)
--allow-read PATHRead-only path (repeatable)
--allow-rw PATHRead-write path (repeatable)
--deny-env VARStrip 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:

FlagDescription
--allow-host HOSTAllowed hostname (repeatable, supports *.domain.com)
--allow-port PORTAllowed port (repeatable)
--pf-redirectEnable 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.

Authors:
  • Bogdan Szabo
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:
1.0.0 2026-Mar-23
~main 2026-Mar-23
Show all 2 versions
Download Stats:
  • 0 downloads today

  • 1 downloads this week

  • 1 downloads this month

  • 1 downloads total

Score:
1.5
Short URL:
armor.dub.pm