Can't access localhost server after pf redirect

After adding some pf rules to redirect certain egress traffic to a localhost server, I can't access that localhost server directly. I can access it through the redirect just fine though.

I basically followed this answer and added the following rules:

rdr pass on lo0 inet proto tcp from any to 169.254.169.254 port 80 -> 127.0.0.1 port 1254
pass out route-to lo0 proto tcp from any to 169.254.169.254 port 80 keep state

After doing this, I can test successfully with curl http://169.254.169.254. However curl http://127.0.0.1:1254 does not work.
If I remove the rules (specifically the rdr rule), 127.0.0.1:1254 works just fine.

When I perform a tcpdump on lo0, I see the following:

08:27:49.659051 IP 127.0.0.1.63994 > 127.0.0.1.1254: Flags [S], seq 677650599, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439529 ecr 0,sackOK,eol], length 0
08:27:49.659177 IP 169.254.169.254.80 > 127.0.0.1.80: Flags [S.], seq 2981033646, ack 677650600, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439529 ecr 1709439529,sackOK,eol], length 0
08:27:49.762036 IP 169.254.169.254.80 > 127.0.0.1.63994: Flags [S.], seq 2981033646, ack 677650600, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439629 ecr 1709439529,sackOK,eol], length 0
08:27:49.762056 IP 127.0.0.1.63994 > 127.0.0.1.1254: Flags [S], seq 677650599, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439629 ecr 0,sackOK,eol], length 0
08:27:49.762089 IP 169.254.169.254.80 > 127.0.0.1.63994: Flags [S.], seq 2981033646, ack 677650600, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439629 ecr 1709439629,sackOK,eol], length 0
08:27:49.866634 IP 127.0.0.1.63994 > 127.0.0.1.1254: Flags [S], seq 677650599, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439729 ecr 0,sackOK,eol], length 0
08:27:49.866690 IP 169.254.169.254.80 > 127.0.0.1.63994: Flags [S.], seq 2981033646, ack 677650600, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439729 ecr 1709439729,sackOK,eol], length 0
08:27:49.967548 IP 169.254.169.254.80 > 127.0.0.1.63994: Flags [S.], seq 2981033646, ack 677650600, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1709439829 ecr 1709439729,sackOK,eol], length 0
...

So we see the original SYN with the correct source & dest. But then we see two responses coming from 169.254.169.254:80, one to 127.0.0.1:80, and another to 127.0.0.1:63994.

When I add log (all) to the rule, I see similar information:

 00:00:00.000000 rule 0/0(match): rdr out on lo0: 169.254.169.254.80 > 127.0.0.1.80: Flags [S.], seq 1734908561, ack 3748497075, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1711591000 ecr 1711591000,sackOK,eol], length 0
 00:00:00.100977 rule 0/0(match): rdr out on lo0: 169.254.169.254.80 > 127.0.0.1.80: Flags [S.], seq 1734908561, ack 3748497075, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1711591101 ecr 1711591000,sackOK,eol], length 0
 00:00:00.000044 rule 0/0(match): rdr out on lo0: 169.254.169.254.80 > 127.0.0.1.80: Flags [S.], seq 1734908561, ack 3748497075, win 65535, options [mss 16344,nop,wscale 5,nop,nop,TS val 1711591101 ecr 1711591101,sackOK,eol], length 0

Why is the rdr rule matching the response traffic and rewriting it? The match rule is defined as to 169.254.169.254 port 80.

This is on High Sierra.


Solution 1:

To make it work you'd have to add that rule:

  • pass quick on lo0

— I typically have it as the first line of filtering rulesets I manage.

To be precise, even this would be enough:

  • pass in quick on lo0

but why bothering with directions if you typically need it both ways on loopback anyways(?)…