TL;DR: On Mojave (haven't tested on others), when creating rdr
rules in pf
, the rules work for a while (with some issues accessing the target port directly), but after a while the rules stop working, though I can't make pfctl
report anything differently in a way that makes it obvious that they no longer apply.
I've created /Library/LaunchDaemons/dev.up.pfctl.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Disabled</key>
<false/>
<key>Label</key>
<string>dev.up.pfctl</string>
<key>WorkingDirectory</key>
<string>/var/run</string>
<key>Program</key>
<string>/sbin/pfctl</string>
<key>ProgramArguments</key>
<array>
<string>pfctl</string>
<string>-e</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
...and /Library/LaunchDaemons/dev.up.loopbackalias.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>dev.up.loopbackalias.plist</string>
<key>RunAtLoad</key>
<true/>
<key>ProgramArguments</key>
<array>
<string>/sbin/ifconfig</string>
<string>lo0</string>
<string>alias</string>
<string>127.0.0.42</string>
</array>
</dict>
</plist>
I've added the following to /etc/pf.conf
:
# allow nginx to bind on :20080 instead of :80, and :20443 instead of :443.
# This allows us to run it as the non-root user, and thus not require
# auth to update/restart it.
rdr pass on lo0 inet proto tcp from any to 127.0.0.42 port 80 -> 127.0.0.42 port 20080
rdr pass on lo0 inet proto tcp from any to 127.0.0.42 port 443 -> 127.0.0.42 port 20443
...and I have nginx running and bound to 127.0.0.42:20080
and 127.0.0.42:20443
.
When I apply the configuration by rebooting or running pfctl -f /etc/pf.conf
, things work:
$ curl http://127.0.0.42:80/services/ping
ok
However...
Problem 1
While this rule is active, accessing the target port (20080/20443) directly has some interesting issues. There's a delay in accessing it that seems to grow quickly with the number of (recent?) accesses:
$ time curl -s -o /dev/null http://127.0.0.42:20080/services/ping
(0.2 seconds)
$ time curl -s -o /dev/null http://127.0.0.42:20080/services/ping
(1.6 seconds)
$ time curl -s -o /dev/null http://127.0.0.42:20080/services/ping
(27 seconds)
$ time curl -s -o /dev/null http://127.0.0.42:20080/services/ping
(timeout)
(however, throughout, accessing :80
—the port redirected by pf
to :20080
—works and takes <25ms.)
Problem 2
After a while, the redirect rule completely stops applying, as if pf
were not enabled at all or the rule was not loaded, however, it still claims to be enabled and I still see it reported by pf
:
$ pfctl -s nat
No ALTQ support in kernel
ALTQ related functions disabled
nat-anchor "com.apple/*" all
rdr-anchor "com.apple/*" all
rdr pass on lo0 inet proto tcp from any to 127.0.0.42 port = 80 -> 127.0.0.42 port 20080
rdr pass on lo0 inet proto tcp from any to 127.0.0.42 port = 443 -> 127.0.0.42 port 20443
$ pfctl -s info | grep Enabled
No ALTQ support in kernel
ALTQ related functions disabled
Status: Enabled for 6 days 14:56:42 Debug: Urgent
$ curl -s http://127.0.0.42:80/services/ping
curl: (7) Failed to connect to 127.0.0.42 port 80: Connection refused
$ curl -s http://127.0.0.42:20080/services/ping
ok
I'm able to trigger this pretty reliably by disconnecting from WiFi. I think this likely only happens when network changes happen.