armor 1.2.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, AuditCallback audit = null, DecisionCallback decide = null)` -- 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.
When a connection would be denied by the policy, the optional decide callback is consulted before refusing. Returning true allows the connection anyway, letting an embedder round-trip an interactive prompt (the callback runs on the connection's own worker thread, so it may block). An override is logged as a connection_overridden audit event rather than connection_allowed, keeping the audit trail honest. The callback is honoured only in SOCKS5 mode; transparent TUN denials are always final.
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.
- 1.2.0 released 19 days 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 5 versions1.2.0 2026-May-21 1.1.1 2026-Apr-08 1.1.0 2026-Apr-02 1.0.0 2026-Mar-23 ~main 2026-May-21 - Download Stats:
-
-
0 downloads today
-
2 downloads this week
-
7 downloads this month
-
28 downloads total
-
- Score:
- 2.1
- Short URL:
- armor.dub.pm