Softnet is a software networking for Tart which provides better network isolation and alleviates DHCP shortage on production systems.
It is essentially a userspace packet filter which restricts the VM networking and prevents a class of security issues, such as ARP spoofing. By default, the VM will only be able to:
- send traffic from its own MAC-address
- send traffic from the IP-address assigned to it by the DHCP
- send traffic to globally routable IPv4 addresses
- send traffic to gateway IP of the vmnet bridge (this would normally be "bridge100" interface)
- receive any incoming traffic
In addition, Softnet tunes macOS built-in DHCP server to decrease its lease time from the default 86,400 seconds (one day) to 600 seconds (10 minutes). This is especially important when you use Tart to clone and run a lot of ephemeral VMs over a period of one day.
Please check out this blog post for backstory.
Softnet solves two problems:
- VM network isolation
VZNATNetworkDeviceAttachment(the default networking in Tart) enables vmnet's bridge isolation by default and prevents cross-VM traffic, however it's still possible for any VM to spoof the host's ARP-table and capture other VMs traffic by using tools that enable conducting the ARP spoofing attacks (e.g. arpspoof, arpoison and so on)
- DHCP exhaustion
- macOS built-in DHCP-server allocates a
/24subnet with 86400 seconds lease time by default, which only allows for ~253 VMs a day (or 1 VM every ~6 minutes) to be spawned without causing a denial-of-service, which is pretty limiting for CI services like Cirrus CI
- macOS built-in DHCP-server allocates a
And assumes that:
- Tart gives it's VMs unique MAC-addresses
- macOS built-in DHCP-server won't re-use the IP-addresses from it's pool until their lease expire
...otherwise it's possible for two VMs to receive an identical IP-address from the macOS built-in DHCP-server (even in the presence of Softnet's packet filtering) and thus bypass the protections offered by Softnet.
Stateful in/out rules use a bounded authorization cache that records the
direction and exact transport tuple of policy-approved flows, allowing matching
return traffic without treating it as a new flow and thus requiring a separate
policy entry. It is not a complete TCP connection tracker: endpoint transport
stacks remain responsible for validating sequence numbers, receive windows,
resets, and application-level traffic.
This cache deliberately favors security, bounded resource use, and a simple implementation over availability. Only policy-authorized initiator traffic renews an entry; return traffic does not. Softnet does not maintain fairness quotas, eviction heuristics, or complete TCP lifecycle state.
A packet admitted by a stateful rule is denied when Softnet cannot represent its flow, including when the cache is full. High flow churn, long idle connections, or ambiguous retransmissions may therefore interrupt networking and require the affected VM to reconnect.
For TCP, a bare TCP SYN on an existing tuple is deliberately returned to policy because Softnet cannot distinguish a retransmission from tuple reuse without tracking TCP sequence state. If authorized, it may replace the tuple's previous cache lifetime; this can reduce availability but cannot grant traffic that policy did not permit.
For ICMP, stateful flow authorization supports only echo requests and replies.
For proper functioning, Softnet binary requires two things:
- a SUID-bit to be set on the binary or a passwordless sudo to be configured, which effectively gives the binary
rootprivileges- these privileges are needed to create
vmnet.frameworkinterface and perform DHCP-related system tweaks - the privileges will be dropped automatically to that of the calling user (or those represented by the
--userand--groupcommand-line arguments) once all of the initialization is completed
- these privileges are needed to create
- the binary to be available in
PATH- so that the Tart will be able to find it
Softnet is started and managed automatically by Tart if --net-softnet flag is provided when calling tart run.
Softnet can update the running VM's IPv4 policy without restarting the VM. Pass a connected Unix stream socket as --control-fd to enable a newline-delimited JSON-RPC 2.0 control channel. The socket is duplex and must be separate from --vm-fd, which carries VM packets.
The supported methods are softnet.policy.get and softnet.policy.set. A complete policy update looks like this (each request and response occupies one line):
{"jsonrpc":"2.0","id":"42","method":"softnet.policy.set","params":{"allow":["@host","10.0.0.0/8"],"block":["0.0.0.0/0"]}}
{"jsonrpc":"2.0","id":"42","result":{"allow":["10.0.0.0/8","@host"],"block":["0.0.0.0/0"],"ruleCount":3}}Every request must include a non-null string (at most 256 bytes) or non-negative integer id; notifications are rejected so policy changes always have an acknowledgment. Policy updates are atomic: all rules are parsed and a new prefix map is built before the active policy changes. Longest-prefix matching and block precedence for identical rules are preserved. Rules are normalized and deduplicated. A policy update may contain at most 4096 combined allow/block rules, and a request frame may not exceed 1 MiB.
When the normalized allow or block policy changes, Softnet clears the flow table so the new policy applies to established flows immediately. This may interrupt active connections. Repeating the same normalized policy is a no-op and preserves the flow table.
Use block=["0.0.0.0/0"] with specific allow rules for a default-deny egress policy. Closing the control socket leaves the last accepted policy active.