3

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.

0

Well, this isn't a great solution, but I was able to fix this by:

  1. Watching for network changes using something like SCDynamicStoreKeyCreateNetworkInterfaceEntity(NULL, kSCDynamicStoreDomainState, kSCCompAnyRegex, kSCEntNetIPv4);
  2. Waiting about a second for macOS to desync its state from pf
  3. Checking whether 127.0.0.42:80 is ECONNREFUSED
  4. Running /sbin/pfctl -f /etc/pf.conf if it was.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .