Pf firewall notes
A great deal of this is from http://calomel.org but I have altered and condensed it down a bit to suit my own purposes.
Parse the pf.conf
file but don't apply it:
# pfctl -n -v -f /etc/pf.conf
Apply the rules currently in /etc/pf.conf
:
# pfctl -f /etc/pf.conf
Disable pf altogether:
# pfctl -d
Re-enable pf and read in config:
# pfctl -e -f /etc/pf.conf
Logs are stored in a binary format, must be read with tcpdump
or similar:
# tcpdump -n -e -ttt -r /var/log/pflog
Watch things happening in real time:
# tcpdump -n -e -ttt -i pflog0
pf.conf
This is a sample configuration for a firewall with three interfaces: A LAN, an Internet connection and a Voice network. Valid for OpenBSD version 4.6. The syntax for some of the NAT rules has changed in 4.7 or 4.8, so look out for that.
Packets are matched against the last rule encountered unless the quick
option is used. Specifying quick
for everything would turn it into a first-match-wins firewall.
Rules have to be in the order macros, tables (not shown), options, scrub (not shown), queues, NAT, filter like so:
### Macros ## Interfaces # Be ADSL internet feed ext_if="vr0" # Inside network int_if="vr1" # Voice network voi_if="vr2" # The PTP interface for the he.net IPv6 tunnel 6_if="gif0" ## Hosts # Internal www2_alt2_v4_int = "192.168.0.212" mbp_v4_int = "192.168.0.44" www2_alt1_v4_int = "192.168.0.120" ns1_v4_int = "192.168.0.104" ns1_alt1_v4_int = "192.168.0.145" proxy1_v4_int = "192.168.0.127" mx_v4_int = "192.168.0.200" www_v4_int ="192.168.0.211" ns2_v4_int = "192.168.0.225" www2_v4_int = "192.168.0.128" torrent_v4_int = "192.168.0.229" lc475_v4_int = "192.168.0.182" fw_v4_int = "192.168.0.254" pabx_v4_voi = "192.168.1.22" # IPv4 External gen_nat1_v4_ext = "78.105.x.x" mx_v4_ext = "78.105.x.x" www2_v4_ext = "78.105.x.x" www_v4_ext = "78.105.x.x" lc475_v4_ext = "78.105.x.x" pabx_v4_ext = "78.105.x.x" # IPv6 mbp_v6_spruce = "2001:470:903f::xxx" www2_v6_spruce = "2001:470:903f::xxx" lc475_v6_spruce = "2001:470:903f::xxx" mx_v6_spruce = "2001:470:903f::xxx" ns1_v6_spruce = "2001:470:903f::xxx" ns2_v6_spruce = "2001:470:903f::xxx" ## Networks spruce_v6 = "2001:470:903f::/48" ## Port groups web_pg = "{ http, https }" sprucehosts_tcp_pg = "{ ssh, http, https }" sprucehosts_udp_pg = "{ ntp, 5999 }" iax_pg = 4569 smtp_pg = "{ 25, 587 }" bittor_pg = "6881:6999" ### End Macros set skip on lo0 set loginterface $ext_if set loginterface $6_if ### QOS # Queue for uploads on the Be ADSL link - gets saturated at about 950kb, so backed off to 900kb altq on $ext_if bandwidth 900Kb hfsc queue { iax, ack, dns, ssh, web, ip6, mail, bulk, bittor } # ack must always be highest priority; without ack going out no data can be received queue ack bandwidth 30% priority 9 qlimit 500 hfsc (realtime 20%) queue iax bandwidth 10% priority 8 qlimit 0 hfsc (realtime 128Kb) queue dns bandwidth 5% priority 7 qlimit 500 hfsc (realtime 2%) queue ssh bandwidth 10% priority 6 qlimit 500 hfsc (realtime 5%) {ssh_login, ssh_bulk} queue ssh_login bandwidth 50% priority 6 qlimit 500 hfsc queue ssh_bulk bandwidth 50% priority 5 qlimit 500 hfsc #queue web bandwidth 5% priority 5 qlimit 500 hfsc (realtime (10%, 2000, 5%) ) queue web bandwidth 5% priority 5 qlimit 500 hfsc (realtime (5%, 2000, 2%) ) queue ip6 bandwidth 5% priority 5 qlimit 500 hfsc (realtime 5%) queue bulk bandwidth 20% priority 4 qlimit 500 hfsc (realtime 2% default) queue mail bandwidth 5% priority 3 qlimit 500 hfsc # bittorrent can never exceed 50% of outgoing bandwidth capacity queue bittor bandwidth 1% priority 2 qlimit 500 hfsc (upperlimit 50%) # Queue for downloads on Be ADSL link. Tops out at around 7Mb. # Set even lower than usual to guarantee a little bandwidth is available for voice network $voi_if altq on $int_if bandwidth 6.5Mb hfsc queue { ack_$int_if, ssh_$int_if, ip6_$int_if, web_$int_if, bulk_$int_if, mail_$int_if, dns_$int_if, bittor_$int_if } queue ack_$int_if bandwidth 1Mb priority 8 qlimit 500 hfsc (realtime 0.5Mb) # can't put iax here because it leaves on $voi_if queue ssh_$int_if bandwidth 1Mb priority 5 qlimit 500 hfsc (realtime 0.5Mb) {ssh_login_$int_if, ssh_bulk_$int_if } queue ssh_login_$int_if bandwidth 50% priority 5 qlimit 500 hfsc queue ssh_bulk_$int_if bandwidth 50% priority 4 qlimit 500 hfsc queue web_$int_if bandwidth 1Mb priority 4 qlimit 500 hfsc (realtime 0.5Mb) queue dns_$int_if bandwidth 0.25Mb priority 6 qlimit 500 hfsc (realtime 0.25Mb) queue ip6_$int_if bandwidth 1Mb priority 3 qlimit 500 hfsc (realtime 0.5Mb) queue bulk_$int_if bandwidth 1Mb priority 3 qlimit 500 hfsc (realtime 0.5Mb default) queue mail_$int_if bandwidth 0.5Mb priority 2 qlimit 500 hfsc (realtime 0.25Mb) queue bittor_$int_if bandwidth 0.1Mb priority 1 qlimit 500 hfsc (upperlimit 4Mb) ### END QOS ### FTP stuff # Proxy FTP connections for users on the internal network nat-anchor "ftp-proxy/*" rdr-anchor "ftp-proxy/*" rdr on $int_if inet proto tcp from $int_if:network to any port ftp -> lo0 port 8021 ### END FTP ### NAT rules ## rdr (redirect) rules are the port forwarding rules rdr on $ext_if inet proto tcp from any to $gen_nat1_v4_ext port $bittor_pg -> $torrent_v4_int rdr on $ext_if inet proto { tcp, udp } from any to $gen_nat1_v4_ext port 50000 -> $torrent_v4_int rdr on $ext_if inet proto { tcp, udp } from any to $gen_nat1_v4_ext port domain -> $ns1_v4_int rdr on $ext_if inet proto tcp from any to $gen_nat1_v4_ext port 80 -> $www_v4_int ## nat rules are 'dynamic' NAT, one to many # Allow hosts from 192.168.0.0/24 to nat through to the internet nat on $ext_if inet from $int_if:network to any -> $gen_nat1_v4_ext ## These are the 'static' 1:1 NAT rules binat on $ext_if from $lc475_v4_int to any -> $lc475_v4_ext binat on $ext_if from $mx_v4_int to any -> $mx_v4_ext binat on $ext_if from $www2_alt1_v4_int to any -> $www2_v4_ext binat on $ext_if from $www_v4_int to any -> $www_v4_ext binat on $ext_if from $pabx_v4_voi to any -> $pabx_v4_ext ### END NAT ### pass and block statements; connections we want # If we don't specify otherwise later, block it and log it block log all ## Use an FTP proxy on this box anchor "ftp-proxy/*" pass in on $int_if proto tcp from $int_if:network to lo0 port 8021 ## IPv4 connections through to internal hosts # Internal LAN pass in on $ext_if inet proto tcp from any to $mx_v4_int port imaps pass in on $ext_if inet proto tcp from any to $mx_v4_int port $smtp_pg pass in on $ext_if inet proto { tcp, udp } from any to $ns1_v4_int port domain pass in on $ext_if inet proto { tcp, udp } from any to $ns2_v4_int port domain pass in on $ext_if inet proto tcp from any to $www2_alt1_v4_int port $web_pg pass in on $ext_if inet proto tcp from any to $www_v4_int port $web_pg pass in on $ext_if inet proto tcp from any to $lc475_v4_int port $web_pg ## Voice rules pass in on $voi_if inet proto udp from $pabx_v4_voi to { !$voi_if, !$int_if:network } port 4569 pass in on $voi_if inet proto { tcp, udp } from $voi_if:network to { $ns2_v4_int, $ns1_alt1_v4_int } port domain # DHCP from voice network pass in on $voi_if inet proto { tcp, udp } from any to any port { 67, 68 } pass in on $voi_if inet proto tcp from $pabx_v4_voi to $mx_v4_int port $smtp_pg pass in on $voi_if inet proto tcp from $pabx_v4_voi to $proxy1_v4_int port { 8080, 3128 } ## IPv6 connections through to internal hosts pass in on $6_if inet6 proto tcp from any to $www2_v6_spruce port $web_pg pass in on $6_if inet6 proto tcp from any to $lc475_v6_spruce port $web_pg pass in on $6_if inet6 proto tcp from any to $mx_v6_spruce port $smtp_pg pass in on $6_if inet6 proto tcp from any to $mx_v6_spruce port imaps pass in on $6_if inet6 proto { tcp, udp } from any to { $ns2_v6_spruce, $ns1_v6_spruce } port domain ## connections to the firewall # IPv6 tunnel pass in on $ext_if inet proto 41 from any to $gen_nat1_v4_ext ## Pass IPv4 and IPv6 traffic from the local network to elsewhere pass in on $int_if inet proto tcp from $int_if:network to !$int_if port $sprucehosts_tcp_pg pass in on $int_if inet proto udp from $int_if:network to !$int_if port $sprucehosts_udp_pg pass in on $int_if inet6 proto tcp from $int_if:network to !$int_if port $sprucehosts_tcp_pg pass in on $int_if inet6 proto udp from $int_if:network to !$int_if port $sprucehosts_udp_pg pass in on $int_if inet proto tcp from $torrent_v4_int to !$int_if port $bittor_pg # Due to !$int_if, need to explicitly allow ssh to fw even though it is allowed 'out'. pass in on $int_if inet proto tcp from $int_if:network to $int_if port ssh # Permit all traffic going out of an interface (not the router as a whole) pass out inet pass out inet6 # Allow ICMP types neccessary for MTU path discovery to work: pass inet proto icmp all icmp-type unreach code needfrag #Allow ICMP echo requests icmp_types = "{ echoreq, unreach }" pass inet proto icmp all icmp-type $icmp_types pass quick proto icmp6 all ## pass out statements for altq qos ## these pass out statements do nothing in terms of security because of the ## above pass out inet/inet6 rule; they just tag traffic for the QoS rules ## Upstream traffic - this is to 'any' because all traffic must be shaped # ACK pass out on $ext_if inet proto tcp from ($ext_if) to any flags S/SA modulate state queue (bulk, ack) # Can't be more granular because it's a tunnel pass out on $ext_if inet proto 41 from ($ext_if) to any queue (ip6) pass out on $ext_if inet proto tcp from ($ext_if) to port ssh queue (ssh_bulk, ssh_login) pass out on $ext_if inet proto tcp from ($ext_if) to any port $web_pg queue (web) pass out on $ext_if inet proto tcp from ($ext_if) to any port $smtp_pg queue (mail) pass out on $ext_if inet proto udp from ($ext_if) to any port $iax_pg queue (iax) pass out on $ext_if inet proto { udp, tcp } from ($ext_if) to any port domain queue (dns) pass out on $ext_if inet from $torrent_v4_int to any queue (bittor) ## Downstream traffic is shaped going out of the $int_if, because it must be so. ## From !$voi_if:network because traffic from one LAN to another has plenty ## of bandwidth available pass out on $int_if inet proto tcp from !$voi_if:network to any flags S/SA modulate state queue (bulk_$int_if, ack_$int_if) pass out on $int_if inet proto tcp from !$voi_if:network to any port ssh queue (ssh_$int_if) pass out on $int_if inet proto tcp from !$voi_if:network to any port $web_pg queue (web_$int_if) pass out on $int_if inet proto tcp from !$voi_if:network to any port $smtp_pg queue (mail_$int_if) pass out on $int_if inet proto udp from !$voi_if:network to any port domain queue (dns_$int_if) pass out on $int_if inet from any to $torrent_v4_int queue (bittor_$int_if) pass out on $int_if inet6 from any to any queue (ip6_$int_if) ### END pass and block rules antispoof for $ext_if antispoof for $int_if antispoof for $voi_if antispoof for lo0
pfstat
Stats package for pfctl. I don't run pfstat on the firewall itself because it needs the xbase set, so I install pfstat on an OpenBSD virtual machine. If you don't have xbase, it can be added easily. Fetch the xbase46.tgz (or whatever corresponds to the version of OpenBSD you are running) set from an OpenBSD mirror and place it in the root of your filesystem. Then:
vm# cd / vm# tar xzvphf xbase46.tgz vm# ldconfig -m /usr/X11R6/lib
pfstat
can be installed with pkg_add
(dont forget to set $PKG_PATH
if it's a new system):
vm# pkg_add -r pfstat
On the firewall itself, add just the pfstatd
daemon (which doesn't need the xbase set):
firewall# pkg_add -r pfstatd
Start pfstatd
and add it to rc.conf.local
(where 192.168.0.254 is your firewall's internal IP address):
firewall# pfstatd -a 192.168.0.254 firewall# echo "pfstatd_flags="-a 192.168.0.254"" >> /etc/rc.conf.local
Finally, place a loginterface
directive in pf.conf
so that pfstatd
has something to look at:
set loginterface vr0
fw# pfctl -f /etc/pf.conf
pfstatd
just trundles away monitoring /dev/pf
and sets up a listener on tcp 9999. pfstat
connects to this and performs the analysis as normal. Let's do that. Here's a simple pfstat.conf
:
vm# vi /etc/pfstat.conf # ATTN! REPLACE 'vr0' WITH YOUR EXTERNAL INTERFACE! # e.g :%s/vr0/vic0/g # # Bytes in and out per second and states (connections) # per second # collect 1 = interface "vr0" pass bytes in ipv4 diff collect 2 = interface "vr0" pass bytes out ipv4 diff collect 3 = global states entries image "/var/www/htdocs/pfstat/pfstat_day.png" { from 1 days to now type png width 1200 height 350 left graph 1 "in" "bytes/s" color 0 192 0 filled, graph 2 "out" "bytes/s" color 0 0 255 right graph 3 "states" "entries" color 192 192 0 } image "/var/www/htdocs/pfstat/pfstat_week.png" { from 1 weeks to now type png width 1200 height 350 left graph 1 "in" "bytes/s" color 0 192 0 filled, graph 2 "out" "bytes/s" color 0 0 255 right graph 3 "states" "entries" color 192 192 0 } image "/var/www/htdocs/pfstat/pfstat_month.png" { from 1 months to now type png width 1200 height 350 left graph 1 "in" "bytes/s" color 0 192 0 filled, graph 2 "out" "bytes/s" color 0 0 255 right graph 3 "states" "entries" color 192 192 0 } # # Matches against pass/block statements per second # collect 4 = interface "vr0" pass packets in ipv4 diff collect 5 = interface "vr0" pass packets out ipv4 diff collect 6 = interface "vr0" block packets in ipv4 diff collect 7 = interface "vr0" block packets out ipv4 diff image "/var/www/htdocs/pfstat/pfstat-packets_day.png" { from 1 days to now type png width 1200 height 350 left graph 4 "pass in" "packets/s" color 0 192 0 filled, graph 5 "pass out" "packets/s" color 0 0 255 right graph 6 "block in" "packets/s" color 255 0 0, graph 7 "block out" "packets/s" color 192 192 0 } image "/var/www/htdocs/pfstat/pfstat-packets_week.png" { from 1 weeks to now type png width 1200 height 350 left graph 4 "pass in" "packets/s" color 0 192 0 filled, graph 5 "pass out" "packets/s" color 0 0 255 right graph 6 "block in" "packets/s" color 255 0 0, graph 7 "block out" "packets/s" color 192 192 0 } image "/var/www/htdocs/pfstat/pfstat-packets_month.png" { from 1 months to now type png width 1200 height 350 left graph 4 "pass in" "packets/s" color 0 192 0 filled, graph 5 "pass out" "packets/s" color 0 0 255 right graph 6 "block in" "packets/s" color 255 0 0, graph 7 "block out" "packets/s" color 192 192 0 }
Finally, we can generate some graphs:
vm# mkdir -p /var/www/htdocs/pfstat vm# pfstat -q -d /var/db/pfstat.db -r 192.168.0.254 vm# pfstat -p -d /var/db/pfstat.db
You should see a load of graphs in the directory you created:
vm# ls -l /var/www/htdocs/pfstat/ total 204 -rw-r--r-- 1 root daemon 8268 Dec 8 10:00 pfstat-packets_day.png -rw-r--r-- 1 root daemon 4710 Dec 8 10:00 pfstat-packets_month.png -rw-r--r-- 1 root daemon 4368 Dec 8 10:00 pfstat-packets_week.png -rw-r--r-- 1 root daemon 8921 Dec 8 10:00 pfstat_day.png -rw-r--r-- 1 root daemon 5049 Dec 8 10:00 pfstat_month.png -rw-r--r-- 1 root daemon 4549 Dec 8 10:00 pfstat_week.png
Assuming that this was successful, you'll need to re-generate these graphs periodically. I do this with cron:
# Query pfstatd every minute * * * * * /usr/local/bin/pfstat -q -d /var/db/pfstat.db -r 192.168.0.254 # Use the collected data to draw graphs every 5 minutes */5 * * * * /usr/local/bin/pfstat -p -d /var/db/pfstat.db # Once per day remove any data older than 30 days 25 3 * * * /usr/local/bin/pfstat -t 30 -d /var/db/pfstat.db
You'll probably want the facility to view your graphs. Conveniently, Apache is part of the base install in OpenBSD.
vm# vi /var/www/htdocs/pfstat/index.html <html> <body> <h2>Bytes Per Second and Connections Tracked Per Second in the Past 24 Hours</h2> <img src="pfstat_day.png" /> <h2>Bytes Per Second and Connections Tracked Per Second in the Past Week</h2> <img src="pfstat_week.png" /> <h2>Bytes Per Second and Connections Tracked Per Second in the Past Month</h2> <img src="pfstat_month.png" /> <h2>Passed/Blocked Packets Per Second in the Past 24 Hours</h2> <img src="pfstat-packets_day.png" /> <h2>Passed/Blocked Packets Per Second in the Past Week</h2> <img src="pfstat-packets_week.png" /> <h2>Passed/Blocked Packets Per Second in the Past Month</h2> <img src="pfstat-packets_month.png" /> </body> </html>
Start Apache:
vm# echo "httpd_flags=\"\"" >> /etc/rc.conf.local vm# apachectl start
Browse to http://vm/pfstat/ and you'll see graphs.