π΄ offline

offline is a Linux sandbox wrapper that executes another program with its
network access completely isolated: user/net/mount/IPC/UTS namespaces,
capability dropping, and a seccomp filter blocking network syscalls.
Linux only, and it isolates the network β nothing else. The wrapped
program keeps your filesystem and your environment; what it cannot do is
reach anything off the machine. Unprivileged user namespaces must be
enabled on the host.
./offline curl https://example.com
The wrapped process runs inside a restricted environment where it cannot
access the internet.
How it works
One binary, two stages. The first cannot isolate itself β a running Go program
is multithreaded, and unshare(CLONE_NEWUSER) refuses a multithreaded caller β
so it re-executes itself and passes the flags through the environment:
offline [flags] <program> [args...]
β
β stage 1 β on the host
ββ parse flags
ββ clone(CLONE_NEWUSER|NEWNET|NEWNS|NEWIPC|NEWUTS)
uid/gid mapped 1:1, setgroups off
_AIRGAP_STAGE=1 in the environment
β
βΌ
re-exec of the same binary
β
β stage 2 β inside the namespaces
ββ prctl(PR_SET_NO_NEW_PRIVS) no setuid escape hatch
ββ --keep-loopback ? ifup "lo" before the filter, it needs socket(2)
ββ PR_CAPBSET_DROP 0..63 bounding set emptied
ββ PR_CAP_AMBIENT_CLEAR_ALL ambient set emptied
ββ seccomp: allow by default, deny
β socket(AF_INET|AF_INET6|AF_PACKET)
β connect bind listen accept accept4 send* recv*
ββ exec <program> [args...]
Three layers, so a gap in one is not a gap in the sandbox: the namespace
leaves nothing to route to, the empty capability set leaves nothing to
reconfigure it with, and the filter refuses the syscalls anyway.
Features
- Network namespace isolation, so there is nothing to reach: no physical
interfaces, no routes, no default gateway, no DNS connectivity, no inbound
connections.
offline ip route prints nothing.
- User namespace isolation with a single-entry uid/gid map β the caller is
root inside and nobody outside, which is what buys the other namespaces
without any privilege on the host. Caveat: it needs unprivileged user
namespaces enabled; some hardened distros and Docker profiles turn them off.
- Mount namespace isolation, so mounts made inside the sandbox never reach
the host. Caveat: the host filesystem itself is still visible and writable β
this tool takes away the network, not the disk.
- IPC namespace isolation, so IPC objects are not shared with the host.
No PID namespace: it renumbers processes without a matching
/proc, which
breaks runtimes that look themselves up there, and it never blocked the
network anyway.
- UTS namespace isolation, so the hostname is the sandbox's own.
- Capability dropping, bounding and ambient set, so no capability can be
regained by exec'ing a file that carries one.
- Seccomp filtering as the last layer, allow-by-default and deny-by
-exception: the goal is to stop networking, not to enumerate an allowlist the
wrapped program would trip over. Denials are
EPERM.
--keep-loopback for the local case β 127.0.0.1 works, external
addresses still fail at the routing layer. Caveat: with loopback up the
connect/bind family has to be allowed, so the filter is thinner here;
AF_PACKET stays blocked either way.
--log-external routes denials through a userspace notifier and prints
them. Caveat: it logs the syscall name and pid, not the destination address.
Requirements
Linux kernel with support for user, network, mount, and IPC namespaces,
plus seccomp.
Ubuntu/Debian dependencies:
sudo apt install pkg-config libseccomp-dev
Go dependencies are pinned in go.mod (github.com/seccomp/libseccomp-golang,
golang.org/x/sys) and resolved automatically by go build/go mod tidy.
Install
One-liner (clones/updates a checkout under ~/.local/share/offline and
runs make install):
curl -fsSL https://raw.githubusercontent.com/fabiocicerchia/offline/main/install.sh | bash
Build from source:
go build -o offline offline.go
# or
make build
make install # drops the binary in ~/.local/bin (on your PATH)
make install BINDIR=/usr/local/bin # ...or anywhere else
Usage
$ offline --help
usage: offline [--keep-loopback] [--log-external] <program> [args...]
Runs <program> with its network access fully isolated: private user, network,
mount, IPC and UTS namespaces, an emptied capability set, and a seccomp
filter refusing the network syscalls.
Flags:
-keep-loopback
keep the loopback interface (127.0.0.1) up and reachable
-log-external
log blocked network syscalls to stderr
A real run β the wrapped curl gets no further than the filter:
$ offline --log-external curl -s -m 3 https://example.com
offline: blocked connect (pid 9)
offline: blocked connect (pid 9)
offline: blocked socket (pid 9)
offline: blocked socket (pid 9)
exit status 7
offline curl https://example.com # fails: no network in the sandbox
offline bash # an offline shell
More in docs/getting-started.md.
Common errors
Package libseccomp was not found in the pkg-config search path.
The cgo binding needs the development headers, not just the runtime library:
sudo apt install pkg-config libseccomp-dev (or libseccomp-devel on
Fedora/RHEL).
fork/exec /usr/local/bin/offline: operation not permitted
The host refuses unprivileged user namespaces. Check
sysctl kernel.unprivileged_userns_clone and, on Ubuntu,
sysctl kernel.apparmor_restrict_unprivileged_userns; a container runtime can
also block the clone flags through its own seccomp profile.
exit status N
Not an offline failure β the wrapped program exited non-zero and offline
relays that on stderr. curl returning 7 ("failed to connect") is the sandbox
working.
Documentation
Full docs live in docs/ (mkdocs). Runnable examples live in
examples/.
References
The design is a straight reading of the manual pages; they are the reference,
not this file.
Release cycle
Semantic Versioning. Releases are cut by
release-please from Conventional Commits,
and the tag is the only source of truth for a version.
- Major β a change to what is isolated, i.e. to the security guarantee.
- Minor β new flags or behaviour that leaves the guarantee intact.
- Patch β fixes; only the latest minor gets them.
Every change to the namespace flags, the capability sweep or the seccomp
filter is security-sensitive by definition and is treated as such, whatever
the version number says.
Contributing
See CONTRIBUTING.md. By participating you agree to the
Code of Conduct.
Security
Found a vulnerability, especially a network-isolation bypass? See
SECURITY.md β please don't open a public issue.
Support
Need help implementing this? Get in touch.
License
MIT Β© Fabio Cicerchia