· 7 min read ·

What Little Snitch for Linux Exposes About Per-Application Network Monitoring

Source: lobsters

Objective Development announced they are working on a Linux version of Little Snitch, their per-application outbound firewall that has been a staple of macOS security tooling for over two decades. The announcement is worth reading carefully, because the authors are unusually candid about what makes the Linux port difficult. Building something that looks like Little Snitch on Linux turns out to surface a fundamental mismatch between the two operating systems’ network stacks, and the gap goes much deeper than “different API calls.”

How the macOS Version Actually Works

Little Snitch 5 and later runs on top of Apple’s Network Extension framework, specifically the NEFilterDataProvider class introduced in macOS 10.15. Before that, Little Snitch used a kernel extension (KEXT) that hooked into the BSD socket filter layer via sflt_register, calling EJUSTRETURN to hold packets while the user-space daemon made a decision. Apple deprecated KEXTs starting with Catalina and began enforcing their removal in macOS 12.

The NEFilterDataProvider approach is significantly cleaner. You subclass NEFilterDataProvider in a System Extension, which runs in user space but registers with the kernel’s content filter subsystem. The kernel intercepts every new network flow at the socket layer, before any data leaves the machine, and calls your handleNewFlow(_:) method. The kernel holds the connection while your method runs. You return a verdict: .allow, .drop, or a data verdict if you want to inspect payload.

What makes this powerful is the metadata on the flow object. Each NEFilterSocketFlow carries a sourceAppAuditToken, which is a cryptographically signed audit token produced by the XNU kernel at process launch and tied to the process’s code signing identity. It contains the PID, UID, GID, code signing team ID, and bundle identifier. Because the kernel verifies the code signature when the process loads, this identity is tamper-proof in practice. A process cannot lie about its bundle ID.

The interception happens at the socket layer, above the IP layer, so the kernel still has full task context when the hook fires. Process identity is never lost.

Why Linux Makes This Substantially Harder

Linux’s network stack works differently. The primary mechanism for redirecting packets to user space is Netfilter, specifically the NFQUEUE target, which has been in the kernel since 2.6.14 (2005). A rule like:

nft add rule ip filter output queue num 0

redirects matching outbound packets to a user-space daemon via libnetfilter_queue. The daemon receives the packet, inspects it, and issues a verdict.

The problem is that by the time a packet reaches the Netfilter hooks at NF_INET_LOCAL_OUT, the kernel has already discarded the task context. The packet is just a struct sk_buff on the network layer. There is no equivalent of sourceAppAuditToken attached to it. To know which process sent this packet, you have to work backwards.

The standard approach is a two-step inode walk. First, you parse the packet’s source IP and port, then query the kernel via SOCK_DIAG netlink to find the socket’s inode number. Then you iterate over /proc/[pid]/fd/ for every running process, reading each symlink until you find one that resolves to socket:[<inode>]. When the inode matches, you have the PID. Then you read /proc/[pid]/exe for the binary path.

This is inherently racy. Between the moment the packet arrives in NFQUEUE and the moment your daemon completes the fd scan, the process may have exited. PID reuse can cause misattribution. The scan itself is O(n) across all running processes and all their file descriptors. On a system with hundreds of processes each holding many fds, this adds measurable latency to every new connection.

Tools like OpenSnitch and Portmaster both use this basic architecture. OpenSnitch mitigates the race condition by adding an eBPF layer starting in v1.5: it attaches kprobes to tcp_v4_connect and udp_sendmsg, and at syscall time, when the process context is still live, it stores the mapping of socket_ptr -> {pid, comm, uid} in a BPF hash map. When NFQUEUE later delivers the packet, the daemon looks up the map instead of scanning /proc. The race window shrinks dramatically, though it does not disappear entirely.

eBPF Gets You Closer, But Not All the Way

The more direct approach using eBPF is to intercept at the socket layer, the same layer macOS intercepts at. Linux added BPF_CGROUP_INET4_CONNECT in kernel 4.17 (2018). An eBPF program of this type attaches to a cgroup and fires when any process in that cgroup calls connect() for an IPv4 socket. At that point, you have full process context via bpf_get_current_pid_tgid() and bpf_get_current_comm(), and you can see the destination sockaddr. Returning a non-zero value from the program blocks the connection entirely.

This is architecturally similar to handleNewFlow(). The problem is the “for any process in that cgroup” qualifier. On macOS, every application is automatically isolated in its own container with a distinct code-signed identity. On Linux, desktop applications launched by a user typically land in the same session scope cgroup under systemd, something like /sys/fs/cgroup/user.slice/user-1000.slice/session-1.scope. There is no per-application cgroup assignment unless the desktop session manager creates one for each app, which current GNOME and KDE implementations do not do.

For systemd-managed services this works well; each service has its own cgroup and you can attach BPF_CGROUP_INET4_CONNECT to it. For interactive desktop applications, you would need to place each process in its own cgroup at launch, which requires either kernel-level integration with the app launcher or a wrapper that creates the cgroup before exec.

Linux 5.7 added LSM BPF, which lets you attach eBPF programs to Linux Security Module hooks including security_socket_connect. This fires at connect time with full task context and does not require cgroup management. Combined with a BPF ring buffer to signal user space, this comes close to what the macOS NE framework provides. But it requires CONFIG_BPF_LSM=y in the kernel config and the boot parameter lsm=...,bpf, which is present on Ubuntu 22.04+ and Fedora 35+ but not universally.

There is also a deeper problem with the interactive prompt model. NEFilterDataProvider.handleNewFlow() can call resumeFlow(withVerdict:) asynchronously after showing the user a dialog; the kernel holds the SYN packet in a queue while waiting. NFQUEUE supports this same pattern: the user-space daemon can delay issuing a verdict while it shows a prompt. But the eBPF socket hooks cannot sleep or wait for user input. A synchronous block-until-user-responds flow requires the NFQUEUE path for the actual hold-and-wait behavior, which means the eBPF layer becomes a lookup accelerator rather than the blocking mechanism itself.

The practical architecture for a modern Linux tool in this space looks like this: eBPF kprobes or cgroup hooks for low-latency process attribution, NFQUEUE for the actual packet hold-and-verdict flow, a user-space daemon coordinating between them, and a separate IPC channel to the UI. OpenSnitch implements roughly this. It is functional, but it is a significant engineering surface compared to subclassing NEFilterDataProvider.

What the Existing Tools Reveal

OpenSnitch rules are JSON files in /etc/opensnitchd/rules/, evaluated against process path, command-line arguments, UID, destination hostname, port, and protocol. The rule engine is solid. The UI (a Qt application communicating over gRPC) works. The fundamental limitation shows up at the edges: short-lived processes that connect and exit before the inode scan completes, UDP attribution under load, and the absence of anything like code signing to verify that /usr/bin/curl is actually curl and not something that replaced it.

Portmaster takes a different angle on DNS: it runs a local DNS proxy and redirects all DNS queries through it via iptables REDIRECT rules. This means it sees DNS requests with the source socket still live and can do process attribution at DNS time rather than connection time, which is often more reliable. It also feeds into Safing’s optional SPN privacy network. The architectural investment in DNS interception is substantial and reflects how much work is required to replicate features that the macOS NE framework provides for free.

Why This Announcement Matters

Objective Development has been building Little Snitch since 2002. They understand the problem domain in detail, and their blog post about the Linux version is notably honest about the constraints. When a team with that background says building the Linux version requires working around fundamental kernel architecture differences, it is worth taking seriously.

The announcement also arrives at an interesting moment for the Linux kernel. LSM BPF is stable and shipping in major distributions. cgroups v2 is now the default. The eBPF tooling (libbpf, CO-RE, BTF) has matured considerably since kernel 5.2. The pieces for a first-class per-application network monitor exist on Linux in a way they did not five years ago; they just require significantly more assembly than on macOS, and they depend on kernel versions and configuration options that are not yet universal.

For anyone running OpenSnitch or Portmaster today, the underlying kernel machinery is already doing something impressive. The gap is not in what Linux can do; it is in what Linux does automatically, without configuration, for every process. macOS ships with per-application identity as a fundamental primitive. On Linux, that identity has to be constructed from parts, and the seams show.

A Little Snitch-quality tool on Linux is buildable. The approach will almost certainly converge on LSM BPF for attribution and blocking, NFQUEUE for interactive verdicts, and either per-process cgroup management or a daemon that wraps application launches. The result will be more complex than the macOS version by necessity, and its quality will vary based on which kernel version and distribution the user is running. That is the honest state of the art.

Was this interesting?