PF NAT redirect not working

Hello.

I've been using FreeBSD and PF for a while now and it's really nice :)

I have one problem though.. I have a FreeBSD as a gateway with NAT and firewall and then in my local LAN I have more computers behind. The computers are running various type of services.

So now I have a NAS behind my FW which I want to be able to reach via SSH from outside, so I have set this rule:
Code:
rdr pass on $ext_if proto tcp from any to any port 2222 -> 172.16.8.12

The NAS have IP 172.16.8.12 ofc and ssh are running on port 2222. Port 22 is taken from the outside world by the FreeBSD gateway itself, hence the other port.

It is possible to ssh to the NAS from the gateway but not from the outside, then I get "Connection refused". I have also tried to remove the "pass" keyword and add
a separate pass in rule to just open the port.

I have also the sysctl inet forwarding flag set to 1.

Any suggestions why this won't work?

I have other services running behind NAT but for that I use nginx as a reverse proxy but it seems overkill to do that in this case when I just want to expose ssh from the NAS.
 
You're redirecting to port 2222 on the NAS. I assume the SSH there is running on the standard port 22. Hence the connection refused. You can just do this:
Code:
rdr pass on $ext_if proto tcp from any to any port 2222 -> 172.16.8.12 port 22
This will redirect the outside port 2222 to the inside port 22 of the NAS. There's really no need to change the port on the NAS where SSH is running on.

I have also tried to remove the "pass" keyword and add
a separate pass in rule to just open the port.
Pass rules (or redirects) don't "open" ports. They just pass along packets, and in the case of a redirection those packets have been translated by changing their destination IP and/or port. You'll get a "connection refused" if the port responds with a RST packet. That means that destination port is closed, i.e. there's nothing listening on it. If there are issues with the pass rules you typically get a "connection timed out", in other words, you don't receive an answer at all.

I have also the sysctl inet forwarding flag set to 1.
Don't do that. Set gateway_enable="YES" in rc.conf. This will do the exact same thing but in a way other FreeBSD users expect.

Note that 2222 is a popular alternative port for 22, so expect bruteforce attacks coming in on your NAS. Make sure its firmware is fully up to date and doesn't have any default usernames/passwords lingering around, or it will get p0wned pretty quickly. You might want to rethink opening this up from the internet, lots of NAS are getting hacked and abused as spam or DDoS relays these days.
 
So what I did now was to change the port on the NAS to 22 just because there's no need to change it as you suggested (it was previously set to 2222).
I have
Code:
gateway_enable="YES"
in rc.conf set.

I have this PF rules now:

Code:
rdr pass on $ext_if proto tcp from any to any port 2222 -> 172.16.8.12 port 22
pass in on $ext_if inet proto tcp from any to any port 2222 keep state

So if I try ssh nasuser@172.16.8.12 from the gateway I successfully get the password prompt.
But if I instead do ssh nasuser@externalip -p 2222 I then get
Code:
ssh: connect to host externalip port 2222: Connection refused

Not sure what is wrong here? (I did replace the real nas user and external ip in this examples).
 
Code:
rdr pass on $ext_if proto tcp from any to any port 2222 -> 172.16.8.12 port 22 
pass in on $ext_if inet proto tcp from any to any port 2222 keep state
Your pass rule is evaluated after the redirection has already happened. So the pass rule's destination port is 22 and the destination IP is 172.16.8.12:
Code:
pass in on $ext_if inet proto tcp from any to 172.16.8.12 port 22 keep state

But if I instead do ssh nasuser@externalip -p 2222
Do this from the outside of your network. Connecting to the external address from your internal network requires a trick called hairpinning. PF will not allow this and requires additional configuration to make hairpinning work.
 
Code:
pass in on $ext_if inet proto tcp from any to 172.16.8.12 port 22 keep state
Ah thanks! That did the trick! Stupid of me to not realize that :)

Do this from the outside of your network. Connecting to the external address from your internal network requires a trick called hairpinning. PF will not allow this and requires additional configuration to make hairpinning work.
Alright! It seems to work to do this now when my pass rules are okay though ;)
 
When you are using "rdr pass on" the packages matching the redirect rule are passed without inspecting filtering rules.

Instead of using
Code:
rdr pass on $ext_if proto tcp from any to any port 2222 -> 172.16.8.12 port 22

You should use:
Code:
rdr pass on $ext_if proto tcp from any to <Your Public IP Address> port 2222 -> 172.16.8.12 port 22

OR rdr without pass
Code:
rdr on $ext_if proto tcp from any to <Your Public IP Address> port 2222 -> 172.16.8.12 port 22
#filtering rules
block all
pass in on $ext_if proto tcp from any to 172.16.8.12 port 2222 keep state

Note:
When you are connecting from inside network you use:
ssh user@172.16.8.12 -p 22

When you are connecting from internet:
ssh user@<your public ip address> -p 2222
You cant test the rdr rule while you are inside the network, you have to be outside the network in order to connect on port 2222.

Note2:
It's better to limit the range of IP addresses that are allowed to connect and insted of using
pass in on $ext_if proto tcp from any to 172.16.8.12 port 2222 keep state

You can use
Code:
white_nets = "{ 203.0.113.0/24, 198.51.100.0/24 }"
rdr on $ext_if proto tcp from any to <Your Public IP Address> port 2222 -> 172.16.8.12 port 22
pass in on $ext_if proto tcp from $white_nets to 172.16.8.12 port 2222 keep state
This will allow only listed subnets to be able to connect from internet to your ssh.

For more examples you can read pf.conf(5)
 
Last edited:
OR rdr without pass
Code:
rdr on $ext_if proto tcp from any to <Your Public IP Address> port 2222 -> 172.16.8.12 port 22
#filtering rules
block all
pass in on $ext_if proto tcp from any to <Your Public IP address> port 2222 keep state
This is wrong. The translations happen on on the packets before the rules are evaluated. The pass rule I provided is correct.

Code:
     Since translation occurs before filtering the filter engine will see
     packets as they look after any addresses and ports have been translated.
     Filter rules will therefore have to filter based on the translated
     address and port number.  Packets that match a translation rule are only
     automatically passed if the pass modifier is given, otherwise they are
     still subject to block and pass rules.


You are correct about the rdr pass, I didn't spot that one.
 
Thanks for the clarification VladiBG and SirDice! I guess it's a good idea to specify whitelisted IPs rather than open it up for the whole world! I'll fix! 👍

One more thing though.. is it really neccessary to specify my external IP in the rule? I mean.. it's the only IP address they can come in from anyway?
 
One more thing though.. is it really neccessary to specify my external IP in the rule? I mean.. it's the only IP address they can come in from anyway?
Still a good idea to restrict things intentionally. It's much clearer the rule and the redirect belong together and it prevents accidental mistakes (redirecting to the wrong machine for example). It's also a good idea to set the IP in a variable, in case the IP address of the NAS changes you only have to edit that one variable instead of having to go through the entire pf.conf. With a small rule-list this is doable but when it gets larger it's easy to overlook something.

Code:
nas_IP="172.16.8.12"

rdr on $ext_if proto tcp from any to ($ext_if) port 2222 -> $nas_IP port 22 
pass in on $ext_if inet proto tcp from any to $nas_IP port 22 keep state
 
Still a good idea to restrict things intentionally. It's much clearer the rule and the redirect belong together and it prevents accidental mistakes (redirecting to the wrong machine for example). It's also a good idea to set the IP in a variable, in case the IP address of the NAS changes you only have to edit that one variable instead of having to go through the entire pf.conf. With a small rule-list this is doable but when it gets larger it's easy to overlook something.

Code:
nas_IP="172.16.8.12"

rdr on $ext_if proto tcp from any to ($ext_if) port 2222 -> $nas_IP port 22
pass in on $ext_if inet proto tcp from any to $nas_IP port 22 keep state
Thanks for the clarification! Yeah I was planning on using variables but for the sake of the example I omitted it :) But thanks again for your help! Really appreciated!
 
Back
Top