Network Model
Outbound traffic is gated by a list of rules naming protocol, destination, and port. The default is deny, and the shape of the rules you write determines whether the kernel or the supervisor enforces them.
The defaults
With no network rules and no HTTP ACL flags:
- Landlock denies every TCP
connect(). - UDP, ICMP, and raw socket creation are denied at the seccomp layer, so the workload cannot even open the socket.
- There is no on-behalf path active, so nothing is intercepted and nothing is proxied.
Unrestricted TCP and UDP egress is opt-in and explicit: --net-allow '*'. ICMP is never implied by it and needs its own --net-allow 'icmp://*'.
Rule grammar
--net-allow (allowlist) and --net-deny (denylist) share one grammar and are mutually exclusive. Both flags repeat.
<spec> repeatable; the port is optional (a bare target = all ports)
target host | <ip> | <cidr> | * (`*` or empty target = any IP)
forms target[:port[,port,...]] · :port · host:* · :* · *:*
[<ipv6|cidr>]:port (bracket IPv6 when a port follows)
scheme none = tcp + udp · tcp:// · udp:// (`udp://*` = any UDP) · icmp:// (no port)
--net-allow target may also be a hostname, resolved via DNS at start
--net-deny target must be a literal IP/CIDR (no hostnames; use --http-deny)
A few consequences worth stating plainly.
A scheme-less rule covers both TCP and UDP. It expands into one rule per protocol at parse time. Naming a scheme pins the rule to that one protocol. ICMP is never implied and always needs icmp://.
A comma groups ports within one rule. host:80,443 is a single rule covering two ports. To express several independent rules, repeat the flag.
IP and CIDR targets never touch DNS. They are matched by containment, with an IP literal being a /32 or /128. Only hostnames resolve.
Rules are OR'd. A destination is permitted if some rule matches the socket's protocol, plus the destination IP and port. Port is not applicable for ICMP.
Allowlist examples
# One host, one port
$ sandlock run --net-allow api.openai.com:443 \
-r /usr -r /lib -r /etc -- python3 agent.py
# Two ports for one host, plus a separate any-IP port
$ sandlock run --net-allow github.com:22,443 --net-allow :8080 \
-r /usr -r /lib -r /etc -- python3 agent.py
# A bare host (or host:*) permits every port
$ sandlock run --net-allow github.com \
-r /usr -r /lib -r /etc -- ssh user@github.com
# IP, CIDR, or bracketed IPv6 literal; matched by containment
$ sandlock run --net-allow 10.0.0.0/8:443 \
--net-allow '[2606:4700::/32]:443' \
-r /usr -r /lib -r /etc -- python3 agent.py
# Pin protocols: UDP DNS to one resolver, TCP HTTPS anywhere
$ sandlock run --net-allow udp://1.1.1.1:53 --net-allow tcp://:443 \
-r /usr -r /lib -r /etc -- ./client
# Ping: kernel ping socket, gated by net.ipv4.ping_group_range
$ sandlock run --net-allow icmp://github.com \
-r /usr -r /lib -r /etc -- ping github.com
# Fully open TCP and UDP; ICMP still needs its own rule
$ sandlock run --net-allow '*' \
-r /usr -r /lib -r /etc -- ./client
Denylist
--net-deny inverts the model: networking is default-allow and the listed targets are blocked. The grammar is identical, with one restriction: targets must be literal IPs or CIDRs. Hostnames are rejected, because a name-based denial would be trivially bypassed by resolving the name yourself. For domain-level denial use --http-deny.
--net-deny 10.0.0.0/8 # all ports on a CIDR (TCP and UDP)
--net-deny 169.254.169.254:80 # one IP, one port (TCP and UDP)
--net-deny 169.254.169.254:80,443 # comma-separated ports in one rule
--net-deny '*' # any IP, all ports (TCP and UDP)
--net-deny 'udp://192.168.0.0/16' # UDP only, to a CIDR
--net-deny 'tcp://10.0.0.1:22' # TCP only, one IP and port
Denylists are a weaker posture. An allowlist fails closed when you forget something; a denylist fails open. The canonical use for --net-deny is blocking the cloud metadata endpoint and RFC 1918 space for a workload that otherwise needs general internet access. Prefer --net-allow whenever you can enumerate the destinations.
Protocol gating
Which protocols the workload can use at all falls out of which schemes appear in the rules. This is not a separate setting.
| Condition | Effect |
|---|---|
| No UDP rule | UDP socket creation is denied at the seccomp layer. A scheme-less rule counts as a UDP rule. |
| No ICMP rule | Kernel ping socket creation (SOCK_DGRAM + IPPROTO_ICMP) is denied at the seccomp layer. |
| Raw ICMP | Never exposed, under any policy. Packet crafting is out of scope. |
| TCP | Always permitted at the syscall level; destinations are governed by Landlock or the on-behalf path. |
Workloads that need ping should rely on the host's net.ipv4.ping_group_range and use the datagram path with --net-allow icmp://....
Name resolution
Only hostname targets touch DNS. They are resolved once, at sandbox start, and pinned in a synthetic /etc/hosts across every protocol. The workload therefore cannot make the sandbox reach a different address by manipulating resolution later.
The synthetic file replaces the real one only when at least one rule names a concrete hostname. A policy made entirely of IPs, CIDRs, :port rules, udp://*, or icmp://* leaves the real /etc/hosts and the host's DNS configuration visible.
Wildcards
Hostnames are matched literally. --net-allow *.example.com:443 is not supported. List each domain you need, or use a CIDR or IP target to cover an address range.
The * token is allowed in two places: as the target, where it is an alias for empty (*:port is the same as :port), and as the port for TCP and UDP rules (host:*, :*, *:*).
Because the port is optional, host and host:* are the same rule, and *, :*, and *:* are all the same rule. Mixing the wildcard with concrete ports, as in host:80,*, is rejected.
All-ports wildcards change the enforcement path. Landlock cannot express "every port" without enumerating 65535 rules, so when any TCP rule uses the all-ports wildcard, Landlock stops filtering TCP connect and the on-behalf path becomes the sole enforcer. For the bare :* case it short-circuits to allow-all.
Enforcement paths
Sandlock picks the cheapest path that can express your rules. You do not choose this; it follows from the policy.
Direct path
Selected for pure :port TCP policies, meaning any IP and no concrete host, IP, or CIDR, with no HTTP ACL. Landlock enforces the TCP port allowlist in the kernel, and there is no per-syscall overhead. UDP and ICMP are never covered by Landlock and always take the other path when they are allowed at all.
On-behalf path
Selected for any host, IP, or CIDR target, any HTTP ACL rule, or any UDP or ICMP rule, because the destination IP has to be checked and Landlock cannot do that.
Seccomp traps connect(), sendto(), sendmsg(), and sendmmsg(). The supervisor duplicates the child's descriptor, queries getsockopt(SOL_SOCKET, SO_PROTOCOL) to learn whether the socket is TCP, UDP, or ICMP, checks the destination against that protocol's resolved allowlist, and then performs the syscall itself. Acting on the duplicated descriptor rather than on an address read out of the workload's memory is what makes this TOCTOU-safe. The HTTP and HTTPS proxy redirect, when configured, happens here.
Binding ports
Server-side bind() is governed separately from outbound traffic. --net-allow-bind is a default-deny allowlist and has no relationship to --net-allow.
Each value is a comma-separated list of single ports or inclusive lo-hi ranges, and the flag repeats. The '*' wildcard allows binding any port, including an ephemeral bind(0); it cannot be mixed with port lists, though repeating the bare wildcard is fine.
$ sandlock run --net-allow-bind 8080,9000-9005 \
-r /usr -r /lib -r /etc -- python3 server.py
Landlock enforces the allowlist, TCP only; UDP bind() is not separately gated. The wildcard simply leaves Landlock's BIND_TCP hook unhandled.
--net-deny-bind is the inverse: binding is default-allow and the listed TCP ports are denied. Same port syntax, mutually exclusive with --net-allow-bind. Because Landlock is allowlist-only, a deny-bind relaxes the BIND_TCP hook and enforces the denylist on the on-behalf seccomp bind() path instead.
Port virtualization
With --port-remap, each sandbox gets a full virtual port space, so several sandboxes can bind the same port without colliding. The supervisor performs the bind() on behalf of the child using pidfd_getfd, which is TOCTOU-safe. When a port is already taken, a different real port is allocated transparently, and /proc/net/tcp inside the sandbox is filtered to show only that sandbox's own ports.
$ sandlock run --name api.local --port-remap --net-allow-bind 6379 \
-r /usr -r /lib -r /etc -- redis-server --port 6379 &
$ sandlock run --name web.local --port-remap --net-allow-bind 8080 \
-r /usr -r /lib -r /etc -- python3 server.py &
$ sandlock ps
NAME PID UPTIME CMD
api.local 12345 5m redis-server --port 6379
web.local 12346 3m python3 server.py
Naming a sandbox with --name gives it a stable virtual hostname, which is what lets an external reverse proxy such as nginx or envoy route by name to the correct real port. From Python, sb.ports() returns the {virtual_port: real_port} mapping while the sandbox is running.
UNIX sockets
Abstract AF_UNIX sockets are governed by Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (ABI v6), which is independent of --net-allow entirely. The scope prevents a sandbox from connecting to abstract sockets belonging to processes outside it. Filesystem-backed UNIX sockets are governed by the filesystem rules, like any other path.
Policy fields
| Python | TOML | Default | Description |
|---|---|---|---|
net_allow | allow | () | Outbound endpoint allowlist. Empty denies all outbound. |
net_allow_bind | allow_bind | () | TCP ports the sandbox may bind. Default-deny allowlist. |
net_deny_bind | deny_bind | () | TCP ports the sandbox may not bind. Default-allow denylist. |
port_remap | port_remap | False | Transparent TCP port virtualization. |
See HTTP ACL and Credentials for rules above the transport layer, and the reference for the complete field list.