Network Policy
Outbound traffic is gated by a list of rules naming protocol, destination, and port. The default is deny: only traffic that matches your policy is allowed.
The defaults
With no network rules and no HTTP ACL flags:
- TCP connections are denied.
- UDP, ICMP, and raw socket creation are denied.
- No HTTP proxying or credential injection is active unless you configure HTTP ACL flags.
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://*'.
--net-allow and --net-deny
--net-allow and --net-deny both take repeatable network rule strings. They use the same basic syntax, but choose opposite defaults.
| Flag | Default | Rule effect | Targets |
|---|---|---|---|
--net-allow | Deny outbound traffic | Allow destinations that match any rule | Hostnames, IP literals, CIDRs, *, or an empty target such as :443 |
--net-deny | Allow outbound traffic | Deny destinations that match any rule | IP literals, CIDRs, *, or an empty target such as :443; hostnames are rejected |
The two flags are mutually exclusive. Use --net-allow when you can name the destinations a workload needs. Use --net-deny when the workload needs general network access but specific IP ranges must be blocked.
Rule syntax
A network rule has three parts: an optional protocol scheme, a target, and an optional TCP/UDP port list.
[scheme://]target[:port[,port,...]]
[scheme://]:port[,port,...]
| Part | Meaning | Examples |
|---|---|---|
| Scheme | Optional protocol selector. No scheme means TCP and UDP. | tcp://, udp://, icmp:// |
| Target | Destination host, IP, CIDR, or wildcard. An empty target means any IP. | api.example.com, 10.0.0.4, 10.0.0.0/8, *, empty in :443 |
| Ports | Optional TCP/UDP port or comma-separated port list. Omitted means all ports. | :443, :80,443, :* |
These are the most common rule shapes:
| Rule | Matches |
|---|---|
api.example.com:443 | TCP and UDP traffic to api.example.com on port 443. |
tcp://api.example.com:443 | TCP traffic to api.example.com on port 443. |
udp://1.1.1.1:53 | UDP traffic to 1.1.1.1 on port 53. |
:443 | TCP and UDP traffic to any IP on port 443. |
10.0.0.0/8:443 | TCP and UDP traffic to any address in that CIDR on port 443. |
[2606:4700::/32]:443 | TCP and UDP traffic to an IPv6 CIDR on port 443. Brackets are required when an IPv6 target has a port. |
github.com | TCP and UDP traffic to github.com on any port. |
* | TCP and UDP traffic to any IP on any port. |
icmp://github.com | ICMP echo traffic to github.com. ICMP rules do not take ports. |
A few details are 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.
--net-allow rules are OR'd. A destination is permitted if some rule matches the socket's protocol, destination IP, and port. Port is not applicable for ICMP.
--net-deny uses the same matching rules, but blocks matches. Denylist targets must be literal IPs, CIDRs, *, or an empty target such as :443. Hostnames are rejected because a name-based denial can be bypassed by resolving the name yourself. For domain-level denial, use --http-deny.
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: uses the host's net.ipv4.ping_group_range setting
$ 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
These denylist examples use the same rule syntax, but each matching destination is blocked instead of allowed.
--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 | The workload cannot create UDP sockets. A scheme-less rule counts as a UDP rule. |
| No ICMP rule | The workload cannot create ping sockets (SOCK_DGRAM + IPPROTO_ICMP). |
| Raw ICMP | Never exposed, under any policy. Packet crafting is out of scope. |
| TCP | Available for rules that allow it; destinations are still checked against the configured policy. |
Workloads that need ping should rely on the host's net.ipv4.ping_group_range and use --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.
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
The bind allowlist covers TCP only; UDP bind() is not separately gated. The wildcard allows any TCP bind, including bind(0).
--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.
Port virtualization
With --port-remap, each sandbox gets a full virtual port space, so several sandboxes can bind the same port without colliding. When a port is already taken, Sandlock allocates a different real port transparently, and /proc/net/tcp inside the sandbox shows 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.