π΄ offline
[!WARNING]
This project is retired and no longer maintained. If you need something
for this, fence runs commands in a
lightweight, container-free sandbox with network and filesystem restrictions.
It is a separate project, actively developed, and worth a look.

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 .
# 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)
$ echo $?
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.
A non-zero exit with nothing on stderr
Not an offline failure β that is the wrapped program's own status, passed
through untouched. curl returning 7 ("failed to connect") is the sandbox
working. offline writes to stderr only when it is itself the thing that
failed, so anything on there is worth reading.
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