OpenBSD PF ====================================================================== (2017, updated 2021) - http://www.openbsd.org/faq/pf/ - https://man.openbsd.org/pf - https://man.openbsd.org/pf.conf.5 PF (packet filter) is the OpenBSD firewall. PF is enabled by default. PF selectively passes or blocks data packets on a network interface based on the Layer 3 (IPv4 and IPv6) and Layer 4 (TCP, UDP, ICMP, and ICMPv6) headers. The most often used criteria are source and destination address, source and destination port, and protocol. A series of rules specify matching criteria and the action block or pass. PF is a last-matching-rule-wins firewall. An implicit `pass all` at the beginning of the ruleset means that if a packet does not match any filter rule the packet passes. A best practice is to add an explicit `block all` as the first rule of a ruleset. The syntax for rules is, roughly: action [direction] [log] [quick] [on interface] [af] [proto protocol] [from src_addr [port src_port]] [to dst_addr [port dst_port]] [flags tcp_flags] [state] - action: "pass" or "block" - direction: "in" or "out" - quick: if matched, don't evaluate further rules - interface: interface or interface group. The kernel automatically creates an `egress` group for the interface(s) that hold the default route(s). - af: address family "inet" or "inet6" - protocol: "tcp", "udp", "icmp", "icmp6", an entry from `/etc/protocols`, or a list - src_addr, dst_addr: an IPv4 or IPv6 address, a CIDR block, a network interface or group, a table, a list, or "any" - src_port, dst_port: port number or name from `/etc/services`, a list, or a range (e.g. "1000:1200", "> 10000", "!= 80") - tcp_flags: e.g. "FIN", "SYN", "ACK", etc. By default, PF sets `flags S/SA` for TCP rules. - state: "no state", "keep state", "modulate state", or "synproxy state" PF is stateful. A state table stores info about each connection, so PF knows if a packet belongs to an already established connection. If the packet belongs to an established state, it skips further rule evaluation. When a rule creates a state, all further packets match the state, _as well as all replies_. By default, all "pass" rules create a state upon a match (which can be disabled with "no state"). ICMP traffic associated with an established TCP state also match. Although UPD is a "stateless" protocol, PF creates states for UPD connections that last for a limited lifetime. Address "spoofing" is when a malicious user fakes the source IP address in packets they transmit. PF offers some protection with the `antispoof` keyword. Use `antispoof` only on interfaces with an IP address, and skip to loopback interfaces (really, skip all filterng on loopback anyhow). antispoof [log] [quick] for interface [af] set skip on lo0 antispoof for em0 inet PF reads its configuration rules from pf.conf(5) at boot time, as loaded by the rc scripts. The pf.conf file has multiple parts: - Lists and Macros: user-defined variables that hold IP addresses, interface names, etc. - Tables: structures that hold lists of IP addresses - Options: options that control how PF works - Filter Rules: selective filtering or blocking of packets as they pass through any interface, including network address translation and packet redirection - Comments: comment lines start with "#" Lists group multiple things in a rule, like protocols, port numbers, and addresses. Lists are defined in curly braces. They can be used in-line or assigned to variables. Lists can be nested. block out on fxp0 from { 192.168.0.1, 10.5.32.6 } to any trusted = "{ 192.168.1.2 192.168.5.36 }" pass in inet proto tcp from { 10.10.0.0/24 $trusted } to port { 22 80 443 } Avoid negated lists (e.g. "{ 10.0.0.0/8, !10.1.2.3 }"), because each list item expands to add another rule. For example, "pass in on fxp0 from { 10.0.0.0/8, !10.1.2.3 }" expands to the undesirable and unintended: pass in on fxp0 from 10.0.0.0/8 pass in on fxp0 from !10.1.2.3 Macros are user-defined variables that can hold IP addresses, port numbers, interface names, etc. Macros (as seen above) can expand to lists. Macors can be defined recursively. ext_if = "em0" block in on $ext_if from any to any host1 = "192.168.1.1" host2 = "192.168.1.2" all_hosts = "{" $host1 $host2 "}" Tables hold IP addresses. Enclose table names in carrots. Lookups are faster against tables than against lists, so tables are best for holding large address lists. Create tables with the `table` directive. Populate tables from files with `persist`. Unlike lists, it's safe to use negation in tables (e.g. "table { 192.0.2.0/24, !192.0.2.5 }"), since table entries don't expand to multiple rules. Hosts may also be specified by their hostname. When the hostname is resolved, all resulting IPv4 and IPv6 addresses are placed in the table. IP addresses can also be entered into a table by specifying a valid interface name, interface group, or the `self` keyword; the table then contains all IP addresses assigned to that interface or group, or to the machine (including loopback addresses). table { 192.0.2.0/24 } table const { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 } table persist block in on fxp0 from { , } to any pass in on fxp0 from to any table persist file "/etc/spammers" block in on fxp0 from to any NAT and Redirection ---------------------------------------------------------------------- # echo 'net.inet.ip.forwarding=1' >> /etc/sysctl.conf # echo 'net.inet6.ip6.forwarding=1' >> /etc/sysctl.conf NAT is specified as an optional `nat-to` parameter to an outbound pass rule. Often, rather than being set directly on the `pass` rule, a `match` rule is used. match out on interface [af] \ from src_addr to dst_addr \ nat-to ext_addr [pool_type] [static-port] [...] pass out [log] on interface [af] [proto protocol] \ from ext_addr [port src_port] \ to dst_addr [port dst_port] - match: any optional parameters are remembered for future use (made "sticky"). - nat-to: may only be specified for outbound packets Ports may be redirected to internal hosts: pass in on egress proto tcp from any to any port 80 rdr-to 192.168.1.20 pfctl ---------------------------------------------------------------------- The pfctl utility configures rulesets and parameters, and retrieves status info from PF. pfctl -vnf /etc/pf.conf Check /etc/pf.conf for errors, but do not load ruleset. pfctl -F all -f /etc/pf.conf Flush all NAT, filter, state, and table rules and reload /etc/pf.conf. pfctl -e Enable PF. pfctl -d Disable PF. pfctl -s [ rules | nat | states ] Report on the filter rules, NAT rules, or state table. pfctl -k host Kill all state entries originating from "host" pfctl -s states -vv Show state ID's, ages, and rule numbers pfctl -s rules -vv Show rules with stats and rule numbers pfctl -s Tables List tables pfctl -s info Show filter stats and counters pfctl -s all Show everything pfctl -t foo -T show Show the contents of table "foo" pfctl -t foo -T add xx.xx.xx.xx Add address "xx.xx.xx.xx" to table "foo" pfctl -t foo -T delete xx.xx.xx.xx Delete address "xx.xx.xx.xx" from table "foo" Example Ruleset ---------------------------------------------------------------------- In this example, our firewall NAT's traffic for two internal networks. int_if = "{ axen0 axen1 }" lan_net = "{ 10.0.1.0/24 10.0.2.0/24 }" table { xxx.0.107.153, xxx.148.204.214, xxx.191.51.254 } table const { self } table { 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 224.0.0.0/3 192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 } tcp_pass_out = "{ bootps, bootpc, dhcpv6-client, dhcpv6-server, domain, https, ipp, nicname, ntp, ssh, www, 6667, 6697, 11371, 44422 }" udp_pass_out = "{ bootps, bootpc, dhcpv6-client, dhcpv6-server, domain, nicname, ntp, 11371, 60000:61000 }" set block-policy drop set loginterface egress set skip on lo0 match in all scrub (no-df) match out on egress inet from !(egress:network) to any nat-to (egress:0) block in quick on egress from to any block return out quick on egress from any to block in quick from urpf-failed block log # Pass traffic to and from the local network. pass in on $int_if proto tcp from $lan_net to port $tcp_pass_out pass in on $int_if proto udp from $lan_net to port $udp_pass_out pass in on $int_if proto icmp from $lan_net pass in on $int_if proto icmp6 pass out on $int_if to $lan_net # Pass tcp, udp, and icmp out on the external (Internet) interface. # TCP connections will be modulated, udp/icmp will be tracked statefully. pass out on egress proto { tcp udp icmp icmp6 } all modulate state # Outside access pass in log on egress from pass on egress proto { tcp udp } to self port { 67 68 } Packet Capture ---------------------------------------------------------------------- For rules marked `log`, PF makes any matching packets available on a `pflog` network pseudo-device (see `[pflog(4)](https://man.openbsd.org/pflog)`). With `pf` and `pflogd` enabled, OpenBSD creates one such device by default: `pflog0`. ``` $ ifconfig pflog0 pflog0: flags=141 mtu 33136 index 4 priority 0 llprio 3 groups: pflog ``` The `tcpdump` utility can monitor `pflog0` in (nearly) realtime: # tcpdump -n -e -ttt -i pflog0 However, reading directly from the interface is not always the best approach. [`pflogd`](https://man.openbsd.org/pflogd.8) records packets in a pcap log file (`/var/log/pflog`). # tcpdump -n -e -ttt -r /var/log/pflog port 80 and host 192.168.1.3 N.B. — for stateful rules, PF only logs the first packet! To log all packets, either tag the rule `no state` or specify `log (all)`. It's possible (and not unreasonable) to create an additional pflog interface for a particular use: # ifconfig pflog1 up # tcpdump -n -e -ttt -i pflog1 With a PF rule like: pass in log (all, to pflog1) on egress proto tcp from to any port 443 Links ---------------------------------------------------------------------- - https://www.openbsd.org/faq/pf/ - https://www.openbsd.org/faq/pf/example1.html - https://www.nostarch.com/pf3 (The Book of PF) - http://man.openbsd.org/pf.conf - https://home.nuug.no/~peter/pf/en/