Fixing macOS Local Network Privacy for launchd Agents
Apple introduced a new security feature called Local Network Privacy in macOS 15 Sequoia that blocks apps from accessing the local network by default until the user manually allows them. The limit applies to both prebuilt binaries and anything built locally.
This is particularly annoying with Nix, since it’s very easy to end up with many custom binaries and launchd agents, and macOS doesn’t show the “allow LAN access” dialog for them. Furthermore, every time the derivation changes, the custom binaries end up in a different path that needs to be allowlisted again. Fortunately, we can overcome this limitation with a simple proxy script.
Solving it with a local proxy
macOS lets system-bundled binaries access the LAN, probably to avoid breaking internal stuff most users aren’t aware of. The system-bundled perl interpreter (/usr/bin/perl) is one of those binaries allowed to access the LAN. We can write a simple proxy that just forwards whatever request it gets to the requested host:
The proxy script
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;
use IO::Select;
$SIG{PIPE} = "IGNORE";
my $port = shift @ARGV;
die "usage: lan-proxy.pl <listen-port>\n"
unless defined $port && $port =~ /^\d+$/;
my $srv = IO::Socket::INET->new(
LocalAddr => "127.0.0.1",
LocalPort => $port,
Listen => 128,
ReuseAddr => 1,
Proto => "tcp",
) or die "listen on 127.0.0.1:$port: $!\n";
my $sel = IO::Select->new($srv);
my %peer; # socket -> paired socket (tunnel phase)
my %rbuf; # client socket -> accumulated request bytes (CONNECT phase)
sub shut {
my $fh = shift;
my $p = delete $peer{$fh};
delete $peer{$p} if defined $p;
for my $s (grep { defined } ($fh, $p)) {
$sel->remove($s);
close($s);
delete $rbuf{$s};
}
}
while (1) {
for my $fh ($sel->can_read) {
next unless $sel->exists($fh); # may have been closed earlier this pass
if (fileno($fh) == fileno($srv)) {
# New client connection.
my $c = $srv->accept or next;
$sel->add($c);
$rbuf{$c} = "";
}
elsif (exists $peer{$fh}) {
# Established tunnel: relay bytes to the peer.
my $n = sysread($fh, my $b, 65536);
if (!$n) { shut($fh); }
else { defined syswrite($peer{$fh}, $b) or shut($fh); }
}
else {
# CONNECT phase: read the request until the blank line.
my $n = sysread($fh, my $b, 4096);
if (!$n) { $sel->remove($fh); close($fh); delete $rbuf{$fh}; next; }
$rbuf{$fh} .= $b;
next unless $rbuf{$fh} =~ /\r\n\r\n/;
my $req = delete $rbuf{$fh};
if ($req =~ /^CONNECT\s+([^:\s]+):(\d+)/) {
my $up = IO::Socket::INET->new(
PeerAddr => $1,
PeerPort => $2,
Proto => "tcp",
Timeout => 10,
);
if ($up) {
syswrite($fh, "HTTP/1.1 200 Connection established\r\n\r\n");
$sel->add($up);
$peer{$fh} = $up;
$peer{$up} = $fh;
}
else { $sel->remove($fh); close($fh); }
}
else { $sel->remove($fh); close($fh); }
}
}
}Code language: Perl (perl)
The script listens for requests on a given port and forwards them to the requested host.
Wiring it up
We can wire the launchd agents or any custom binaries to this server using whatever mechanism your request library supports. Typically, that means defining some environment variables like HTTPS_PROXY, HTTP_PROXY and NO_PROXY. For instance, I ended up with:
export HTTPS_PROXY=http://127.0.0.1:MY_PORT
export HTTP_PROXY=http://127.0.0.1:MY_PORT
export NO_PROXY=127.0.0.1,localhostCode language: Bash (bash)
Nix integration
I added a(nother) launchd agent for this to my Nix setup:
{ config, lib, ... }:
let
user = config.system.primaryUser;
in
{
options.services.lanProxy.port = lib.mkOption {
type = lib.types.port;
default = 11599;
description = ''
Loopback port the LAN CONNECT proxy listens on. Launchd services reach the
LAN by exporting HTTPS_PROXY=http://127.0.0.1:<port>.
'';
};
config.launchd.user.agents.lan-proxy.serviceConfig = {
ProgramArguments = [
"/usr/bin/perl"
"${./lan-proxy.pl}"
(toString config.services.lanProxy.port)
];
RunAtLoad = true;
KeepAlive = true;
ThrottleInterval = 10;
StandardOutPath = "/Users/${user}/Library/Logs/lan-proxy.log";
StandardErrorPath = "/Users/${user}/Library/Logs/lan-proxy.log";
};
}Code language: Nix (nix)
Now I run a single instance of the Perl proxy, and any custom binary that needs access to the LAN gets it without me having to allowlist it.
No replies on “Fixing macOS Local Network Privacy for launchd Agents”