NetBSD desktop pt.3: simple stateful firewall with NPF


#1

/etc/npf.conf


# Associate a dynamic list of IPs, both IPv4 and IPv6, to default WLAN interface[1]
   $wifi_if = ifaddrs(athn0)

# Introduce 2 container lists for blacklisted IPs[2]
   table <blacklist> type hash file "/etc/npf_blacklist"       
   table <suspicious> type tree dynamic

# Introduce 2 variables to list opened TCP and UDP ports[3]
   $services_tcp = { http, https, smtp, smtps, domain, 587, 6000 }
   $services_udp = { domain, ntp, 6000, 51413 }

 # Variable $LAN represents range of IPs for the local network 
  $LAN = { 192.168.1.0/24 }

# Load ICMP application-level gateway[4]
   alg "icmp"

# Introduce a pseudo-device for logging events[5]
  procedure "log" {
        log: npflog0
  }

# Introduce a set of 'normalization' options[6]
  procedure "norm" {
        normalize: "random-id", "min-ttl" 512, "max-mss" 1432,  "no-df"
  }


  group default {
        #Pass everything on loop interface
              pass final on lo0 all

        #Block blacklisted IPs
              block in final from <blacklist>
              
         #Block IPs marked as 'suspicious'
              block in final from <suspicious>     
        
        #Allow all outgoing traffic
              pass stateful out final all
             
        #Only allow selected ICMP types
              pass in final proto icmp icmp-type timxceed all
 	      pass in final proto icmp icmp-type unreach all
 	      pass in final proto icmp icmp-type echoreply all
 	      pass in final proto icmp icmp-type sourcequench all
 	      pass in final proto icmp icmp-type paramprob all
        
        # Allow SSH/(T)FTP/MPD/TigerVNC \ 
        # connections on LAN and log them [8]
             pass stateful in final proto tcp from \
                       $LAN to $wifi_if port ftp apply "log"
             pass stateful in final proto tcp from \
                       $LAN to $wifi_if port ssh apply "log"
             pass stateful in final proto udp from \
                        $LAN to $wifi_if port tftp apply "log"
             pass stateful in final proto tcp from \
                        $LAN to $wifi_if port 6600 apply "log"
             pass stateful in final proto tcp from \
                        $LAN to $wifi_if port 6600 apply "log"           
             pass stateful in final proto tcp from \
                        $LAN to $wifi_if port 5901  apply "log"
                        
        # Allow DHCP requests 
	      pass out final proto udp from any port \
	                bootpc to any port bootps
	      pass in final proto udp from any port \
	                 bootps to any port bootpc
	      pass in final proto udp from any port \
	                 bootps to 255.255.255.0 port bootpc

        #Allow incoming TCP/UDP packets \
        # on selected ports applying "norm" procedure
              pass stateful in final proto tcp to $wifi_if \
                         port $services_tcp apply "norm"
              pass stateful in final proto udp to $wifi_if \
                         port $services_udp apply  "norm"
         
        # Allow Traceroute 
              pass stateful in final proto udp to $wifi_if \
                        port 33434-33600  apply"norm"   
              
        
        # Reject everything else [9]
               block return-rst in final proto tcp all apply "log"
               block return-icmp in final proto udp all apply "log"
               block return in final all apply "log"
  }

Notes

a) Rule are processed from top to bottom, meaning a packet is kept waiting until a matching rule is found; if no pass criteria matches a packet, final rules shall block it (see above)

b) group default has to be set mandatory; we could have defined a second “home” group on wifi_if containing most rules, while letting group default only pass on lo0, but this just sounded redundant, as we only need a single group for this config

[1] ifaddrs() is needed for DHCP; NPF will capture the runtime list of addresses, reflecting
any changes to the interface, including attachig/detaching. inet4/6 functions are meant for static IP use. family keyword can be used in combination of a filtering rule to explicitly select an IP address type.

[2]

  • table <blacklist>
    type hash provides amortised O(1) lookup time; a good option for sets which do not change significantly (e.g. a listed inside a file, kept across sessions )

echo any potentially harmful IP address to /etc/npf_blacklist and notify NPF of changes by running
$ npfctl table "blacklist" flush

  • table <suspicious>
    tree is a good option when the set changes often and requires prefix matching.
    dynamic indicates table is emptied every time configuration is reloaded. ideal to temporary block suspicious addresses, which you can do by
    $ npfctl table "suspicious" add 10.0.1.0/24

[3] 587 is SMTP over TLS, 6000 is X server, 51413 is Transmission torrent. Consult services(5) man page and /etc/services database

[4] Allows to find an active connection by looking at the ICMP payload, and to perform NAT translation of the ICMP payload. Good to keep loaded for when in need of dynamic mapping as certain application layer protocols are not compatible with NAT and require translation outside layers 3 and 4. no mapping is implied in the current config

[5] The npflog0 interface will be auto-created once the configuration is loaded. The log packets will be written to /var/log/npflog0.pcap file by npfd(8) daemon.

[6] Modify packets according to the specified normalization options. What those options actually do respectively is

  • Randomize the IPv4 ID parameter.

  • Enforce a minimum value for the IPv4 Time To
    Live (TTL) parameter.

  • Enforce a maximum value for the MSS on TCP
    packets. Typically, for “MSS clamping”.

  • Remove the Don’t Fragment (DF) flag from IPv4
    packets.

[8] Better to disable it on a public network, especially Open WEP

[9] returning TCP RESET or ICMP proto/port UNREACHABLE messages on TCP connection and UDP streams respectively, while logging rejected connections to npflog0*

Enabling services

touch /etc/npf_blacklist

cat <<EOT>> /etc/rc.conf
> npf=YES
> npfd=YES
> npfd_flags="-d 60"
> EOT 

service npf start
service npfd start

testing config

npfctl show
table <blacklist> type hash
table <suspicious> type tree

procedure "norm"
procedure "log"

group # id="1" 
        pass final on lo0 all # id="2" 
        block in final from <blacklist> # id="3" 
        block in final from <suspicious> # id="4" 
        pass stateful out final all # id="5" 
        pass in final proto icmp icmp-type 11 # id="6" 
        pass in final proto icmp icmp-type 3 # id="7" 
        pass in final proto icmp icmp-type 0 # id="8" 
        pass in final proto icmp icmp-type 4 # id="9" 
        pass in final proto icmp icmp-type 12 # id="a" 
        pass stateful in final family inet4 proto tcp flags S/FSRA from 192.168.1.0/24 to <.ifnet-athn0> port 21 apply "log" # id="b" 
        pass stateful in final family inet4 proto tcp flags S/FSRA from 192.168.1.0/24 to <.ifnet-athn0> port 22 apply "log" # id="c" 
        pass stateful in final family inet4 proto udp from 192.168.1.0/24 to <.ifnet-athn0> port 69 apply "log" # id="d" 
        pass stateful in final family inet4 proto tcp flags S/FSRA from 192.168.1.0/24 to <.ifnet-athn0> port 6600 apply "log" # id="e" 
        pass stateful in final family inet4 proto tcp flags S/FSRA from 192.168.1.0/24 to <.ifnet-athn0> port 6600 apply "log" # id="f" 
        pass stateful in final family inet4 proto tcp flags S/FSRA from 192.168.1.0/24 to <.ifnet-athn0> port 5901 apply "log" # id="10" 
        pass out final proto udp from any port 68 to any port 67 # id="11" 
        pass in final proto udp from any port 67 to any port 68 # id="12" 
        pass in final family inet4 proto udp from any port 67 to 255.255.255.0 port 68 # id="13" 
        pass stateful in final proto tcp flags S/FSRA to <.ifnet-athn0> { port 80, port 443, port 25, port 465, port 53, port 587, port 6000 } apply "norm" # id="14" 
        pass stateful in final proto udp to <.ifnet-athn0> { port 53, port 123, port 6000, port 51413 } apply "norm" # id="15" 
        pass stateful in final proto udp to <.ifnet-athn0> port 33434:33600 apply "norm" # id="16" 
        block return-rst in final all apply "log" # id="17" 
        block return-icmp in final all apply "log" # id="18" 
        block return in final all apply "log" # id="19"

Display real time logs of inbound packets that were blocked/passed on $wifi_if

tcpdump -enr /var/log/npflog0.pcap

keeping in mind the rule telling ‘allow FTP connections from LAN’ was 11th on the list…

02:05:06.493690 rule 11.rules.0/0(match): pass in on ???: 192.168.1.13.21 > 192.168.1.2.36542: Flags [P.], seq 1785:1835, ack 67, win 4197, options [nop,nop,TS val 1 ecr 8800950], length 50: FTP: [!ftp]

This is me just connecting on port 21 from Android with AndFTP (smartphone connected to home router)

further reading


#2

Why NPF?

a) you already chose NPF the moment you chose NetBSD, as NPF is where development currently focused on; the PF fork was left there stagnating, and IPFilter is obsolete

b) NPF is an interesting L3/L4 Firewall designed with simplicity, performance and ease at customization in mind. Tables, variables, groups, algs, procedures provide a high level of abstraction, especially when including rules built around pcap-filter(7). Its intuitive syntax makes it easy and even fun to use.
Built-in directional static/dynamic NAT mapping, dynamic IP filtering, live modification/flushing without having to reload whole config, stateful packet inspection, proto flags, return options and npfd integration with syslogd, are all nice to have features (most modern firewall have equivalents though). Documentation is concise and well organized. The most interesting thing is that, like the rest of the NetBSD kernel ext_npf() is Lua-scriptable (I’m sure @pin will like this). Lua extensions give close to infinite possibilities of development, as they can be embedded quickly into ext_npf module with luactl(8) without having to recompile kernel, although they’re burdened by a 10% performance loss too, compared with standard NPF rules.
A recent major rework migrated libnpf from Apple’s proplib API to FreeBSD’s nvlist;


#3

Amazing… :heart_eyes: NPF is missed from most desktop setup guides so this will be helpful to all new users… Thanks :sunglasses:


#4

This is great! I was debating on what to use, PF or NPF. I was leaning towards PF because of the abundance of resource material. Your article convinced me to head towards NPF. Thank you.