A web server and a mail server on the inside

Time passes, and needs change. Rather frequently, a need to run externally accessible services develops. This quite frequently becomes just a little harder because externally visible addresses are either not available or too expensive, and running several other services on a machine which is primarily a firewall is not a desirable option.

The redirection mechanisms in PF makes it relatively easy to keep servers on the inside. If we assume that we need to run a web server which serves up data in clear text (http) and encrypted (https) and in addition we want a mail server which sends and receives e-mail while letting clients inside and outside the local network use a number of well known submission and retrieval protocols, the following lines may be all that's needed in addition to the rule set we developed earlier:

webserver = "192.168.2.7"
webports = "{ http, https }"
emailserver = "192.168.2.5"
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }"

match in on $ext_if proto tcp to $ext_if port $webports rdr-to $webserver
match in on $ext_if proto tcp to $ext_if port $email rdr-to $emailserver

pass proto tcp from any to $webserver port $webports 
pass proto tcp from any to $emailserver port $email 
pass proto tcp from $emailserver to any port smtp 

The combination of match and pass rules above is very close to the way things were done in pre-OpenBSD 4.7 PF versions, and if you are upgrading from a previous version, this is the kind of quick edit that could bridge the syntax gap quickly. But you could also opt to go for the new style, and write this slightly more compact version instead:

pass in on $ext_if inet proto tcp to $ext_if port $webports rdr-to $webserver
pass in on $ext_if inet proto tcp to $ext_if port $email rdr-to $mailserver
pass on $int_if inet proto tcp to $webserver port $webports
pass on $int_if inet proto tcp to $mailserver port $email

in pre-OpenBSD 4.7 syntax, the equivalent rules are:

webserver = "192.168.2.7"
webports = "{ http, https }"
emailserver = "192.168.2.5"
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }"

rdr on $ext_if proto tcp from any to $ext_if port \
       $webports -> $webserver
rdr on $ext_if proto tcp from any to $ext_if port \
       $email -> $emailserver

pass proto tcp from any to $webserver port $webports 
pass proto tcp from any to $emailserver port $email 
pass proto tcp from $emailserver to any port smtp 

Previous versions of this document had the flag 'synproxy' in the pass rules, indicating that some backends might be in need of assistance during connection setup, with the gateway handling the three way handshake on behalf of your server or client before handing the connection over to the application. The intention was to provide a certain amount of protection against various SYN based attacks. The general recommendation today is rather to keep things simple and fix the back end.

Rule sets for configurations with DMZ networks isolated behind separate network interfaces and in some cases services running on alternative ports will not necessarily be much different from this one.

Taking care of your own - the inside

Everything I've said so far is excellent and correct as long as all you are interested in is getting traffic from hosts outside your local net to reach your servers.

If you want the hosts in your local net to be able to use the services on these machines, you will soon see that the traffic originating in your local network most likely never reaches the external interface. The external interface is where all the redirection and translation happens, and consequently the redirections do not quite work from the inside. The problem is common enough that the PF documentation lists four different solutions to the problem.[1]

The options listed in the PF user guide are

We need to intercept the network packets originating in the local network and handle those connections correctly, making sure any returning traffic is directed to the communication partner who actually originated the connection.

Returning to our previous example, we achieve this by adding special case rules that mirror the ones designed to handle requests from the outside. First, the pass rules with redirections for OpenBSD 4.7 and newer:

pass in on $ext_if inet proto tcp to $ext_if port $webports rdr-to $webserver
pass in on $ext_if inet proto tcp to $ext_if port $email rdr-to $mailserver
pass in log on $int_if inet proto tcp from $int_if:network to $ext_if port $webports rdr-to $webserver
pass in log on $int_if inet proto tcp from $int_if:network to $ext_if port $email rdr-to $mailserver
match out log on $int_if proto tcp from $int_if:network to $webserver port $webports nat-to $int_if
pass on $int_if inet proto tcp to $webserver port $webports
match out log on $int_if proto tcp from $int_if:network to $mailserver port $email nat-to $int_if
pass on $int_if inet proto tcp to $mailserver port $email

The first two rules are identical to the original ones. The next two intercept the traffic from the local network and the rdr-to actions in both rewrite the destination address much as the corresponding rules do for the traffic that originates elsewhere. The pass on $int_if rules serve the same purpose as in the earlier version.

The match rules with nat-to are there as a routing workaround. Without them, the webserver and mailserver hosts would route return traffic for the redirected connections directly back the hosts in the local network, where the traffic would not match any outgoing connection. With the nat-to in place, the servers consider the gateway as the source of the traffic, and will direct return traffic back the same path it came originally. The gateway of course matches the return traffic to the states created by connections from the clients in the local network, and applies the appropriate actions to return the traffic to the correct clients.

The equivalent rules for pre-OpenBSD 4.7 versions are at first sight a bit more confusing, but the end result is the same:

rdr on $int_if proto tcp from $localnet to $ext_if \
       port $webports -> $webserver
rdr on $int_if proto tcp from $localnet to $ext_if \
       port $email -> $emailserver
no nat on $int_if proto tcp from $int_if to $localnet
nat on $int_if proto tcp from $localnet to $webserver \
       port $webports -> $int_if 
nat on $int_if proto tcp from $localnet to $emailserver \
       port $email -> $int_if 

It is well worth noting that we do not need to touch the pass rules at all.

I've had the good fortune to witness via email or IRC the reactions of several network admins at the point when the truth about this five line reconfiguration sank in.

Notes

[1]

See Redirection and Reflection in the PF user guide.