blog.kanbach.org

IT-Security and stuff

Firewalls under the hood - UFW

This blogpost aims to explain some of the inner workings of the “uncomplicated firewall” (ufw) that is available for Ubuntu installations since 8.04 LTS and for Debian installations since 10.

Before going into detail, ufw is not a firewall but a frontend for iptables. Iptables is a frontend for the netfilter kernel module that is performing packet filtering within the Linux kernel. Therefore all actions that are performed via ufw can be directly queried using the iptables command.

The following sections deal with the default rules that are added by ufw and describes possible implications of these rules. Furthermore some ways are shown how ufw firewalls could be detected.

Default rule set (IPv4)

The setup that is used in this article is based on a standard Linux kernel without any hardening measures and ufw version 0.36.1, released on 19 September 2021.

After enabling ufw with ufw enable let's use iptables to check what happened:

$ iptables -S

-P INPUT DROP
-P FORWARD DROP
-P OUTPUT ACCEPT
-N ufw-after-forward
-N ufw-after-input
-N ufw-after-logging-forward
-N ufw-after-logging-input
-N ufw-after-logging-output
-N ufw-after-output
-N ufw-before-forward
-N ufw-before-input
-N ufw-before-logging-forward
-N ufw-before-logging-input
-N ufw-before-logging-output
-N ufw-before-output
-N ufw-logging-allow
-N ufw-logging-deny
-N ufw-not-local
-N ufw-reject-forward
-N ufw-reject-input
-N ufw-reject-output
-N ufw-skip-to-policy-forward
-N ufw-skip-to-policy-input
-N ufw-skip-to-policy-output
-N ufw-track-forward
-N ufw-track-input
-N ufw-track-output
-N ufw-user-forward
-N ufw-user-input
-N ufw-user-limit
-N ufw-user-limit-accept
-N ufw-user-logging-forward
-N ufw-user-logging-input
-N ufw-user-logging-output
-N ufw-user-output
-A INPUT -j ufw-before-logging-input
-A INPUT -j ufw-before-input
-A INPUT -j ufw-after-input
-A INPUT -j ufw-after-logging-input
-A INPUT -j ufw-reject-input
-A INPUT -j ufw-track-input
-A FORWARD -j ufw-before-logging-forward
-A FORWARD -j ufw-before-forward
-A FORWARD -j ufw-after-forward
-A FORWARD -j ufw-after-logging-forward
-A FORWARD -j ufw-reject-forward
-A FORWARD -j ufw-track-forward
-A OUTPUT -j ufw-before-logging-output
-A OUTPUT -j ufw-before-output
-A OUTPUT -j ufw-after-output
-A OUTPUT -j ufw-after-logging-output
-A OUTPUT -j ufw-reject-output
-A OUTPUT -j ufw-track-output
-A ufw-after-input -p udp -m udp --dport 137 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 138 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 139 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 445 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 67 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 68 -j ufw-skip-to-policy-input
-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
-A ufw-after-logging-forward -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-after-logging-input -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 12 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A ufw-before-forward -j ufw-user-forward
-A ufw-before-input -i lo -j ACCEPT
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
-A ufw-before-input -m conntrack --ctstate INVALID -j DROP
-A ufw-before-input -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 12 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A ufw-before-input -p udp -m udp --sport 67 --dport 68 -j ACCEPT
-A ufw-before-input -j ufw-not-local
-A ufw-before-input -d 224.0.0.251/32 -p udp -m udp --dport 5353 -j ACCEPT
-A ufw-before-input -d 239.255.255.250/32 -p udp -m udp --dport 1900 -j ACCEPT
-A ufw-before-input -j ufw-user-input
-A ufw-before-output -o lo -j ACCEPT
-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-output -j ufw-user-output
-A ufw-logging-allow -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW ALLOW] "
-A ufw-logging-deny -m conntrack --ctstate INVALID -m limit --limit 3/min --limit-burst 10 -j RETURN
-A ufw-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN
-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN
-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
-A ufw-not-local -j DROP
-A ufw-skip-to-policy-forward -j DROP
-A ufw-skip-to-policy-input -j DROP
-A ufw-skip-to-policy-output -j ACCEPT
-A ufw-track-output -p tcp -m conntrack --ctstate NEW -j ACCEPT
-A ufw-track-output -p udp -m conntrack --ctstate NEW -j ACCEPT
-A ufw-user-limit -m limit --limit 3/min -j LOG --log-prefix "[UFW LIMIT BLOCK] "
-A ufw-user-limit -j REJECT --reject-with icmp-port-unreachable
-A ufw-user-limit-accept -j ACCEPT

The output above shows the filter table, as it was populated by ufw. It comprises several new chains like ufw-after-forward, ufw-after-input and many more and a set of rules that are appended to both the builtin and the custom chains.

The listing above only shows the filter table. Besides filter, netfilter also uses the tables raw, mangle, security and nat, which however remain untouched.

The filter table contains the builtin chains INPUT, OUTPUT and FORWARD and these chains are basically the ones that ufw is able to adjust.

Let's first take a look at how incoming network traffic is handled:

Input handling (IPv4)

Every input that is destined to the host itself traverses the INPUT chain. This builtin netfilter chain is populated by ufw with a series of custom chains as shown in the overview image below:

Default ufw rules for incoming IPv4 packets

The first configuration that can be seen in the image above is a DROP policy for incoming packets. This means that every packet that traverses the whole INPUT chain and doesn't match any configured rule would be discarded by the kernel.

ufw-before-logging-input

First, incoming packets are sent to the target ufw-before-logging-input, which doesn't contain any rules.

ufw-before-input

The next chain that incoming packets are sent to is ufw-before-input and a lot is happening there:

-i lo -j ACCEPT

This rule accepts all packets that arrive on interface lo. This rule is in place to ensure that applications on the machine could use local communication via localnet (127.0.0.0/8).

-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-m conntrack --ctstate INVALID -j ufw-logging-deny
-m conntrack --ctstate INVALID -j DROP

These rules examine the state of the identified packets and call the conntrack module for this. The first rule checks if the incoming packet is associated with the states RELATED or ESTABLISHED.

The ESTABLISHED state refers to connections in which traffic is exchanged in both directions. For TCP this is the case once the three-way handshake is completed. In the case of UDP, datagrams are associated with an ESTABLISHED state, once the connection tuple (src, dst, sport, dport) is reversed.

The RELATED state is a bit more complex. The state of a connection is RELATED, when there is a direct relation between a previous connection and the current one. The following example illustrates such a situation:

ICMP response is RELATED to UDP datagram

A UDP datagram with arbitrary content is sent to port 222 of a server that doesn't have any service listing on that port and does not filter it either. Due to the closed port, the server is rejecting the packet with an ICMP packet that has the type 3 (“Destination unreachable”) and code 3 (“Port unreachable”). Although the ICMP error is sent as a response to the UDP datagram, it is a dedicated connection. Since ICMP error messages are common responses they are considered RELATED to the previous connection.

Besides ICMP there are a few protocols like FTP, SIP or H.323 that could initiate new connections in response to existing connections. FTP is a well known example, because it could initiate a dedicated connection to the ftp-data port 20, after requesting a file to download on port 21 (or whatever port the service is running on). Data connections from a FTP server are then considered RELATED to the original connection.

In order to examine specific protocols, netfilter contains some helper modules like nf_conntrack_ftp.ko and nf_conntrack_sip.ko , which when loaded, parse matching packets and set an expect flag, if specific sequences (like the PORT command for FTP) are discovered.

Conntrack helper modules pose a security risk, because they could inadvertently open other ports than the indended ones if not used correctly. [1]

In early kernel versions automatic helper assignment was enabled by default and it was not possible to disable this behaviour.

For this purpose, Linux 3.5 introduced the following sysctl variable:

net.netfilter.nf_conntrack_helper

The default value for this sysctl was “1” until Linux 4.7. It changed to “0” afterwards. [2]

-m conntrack --ctstate INVALID -j ufw-logging-deny
-m conntrack --ctstate INVALID -j DROP

These two rules track packets that are associated with an INVALID connection state. These packets, or more precisely, their connection state, is neither NEW, nor ESTABLISHED or RELATED. First, packets are sent to the ufw-logging-deny chain, which contains the following rules:

-m conntrack --ctstate INVALID -m limit --limit 3/min --limit-burst 10 -j RETURN
-m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "

These two rules basically deal as a rate-limiting mechanism. Unless the rate of INVALID connections exceeds 3 connections per minute, packets are directly sent back to the previous chain. Otherwise packets are logged to syslog, with a prefix of "[UFW BLOCK] ".

The second rule then drops packets associated with an INVALID connection state.

-A ufw-before-input -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 12 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 8 -j ACCEPT

These 4 rules deal with incoming ICMP messages and allow the following ICMP types:

  • Type 3: Destination unreachable
  • Type 11: Time exceeded
  • Type 12: Parameter problem
  • Type 8: Echo

The next rule deals with DHCP traffic:

-A ufw-before-input -p udp -m udp --sport 67 --dport 68 -j ACCEPT

The rule above allows incoming UDP datagrams to port 68 (DHCP client), if they originate from UDP port 67 (DHCP server).

-A ufw-before-input -j ufw-not-local

The next line that is shown above sends packets to the ufw-not-local chain, that is shown below:

-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN
-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN
-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
-A ufw-not-local -j DROP

The first three rules above check incoming packets for their address type. The LOCAL address type does NOT correspond to localnet/localhost but refers to all addresses that are assigned to the host. Directed traffic that originates from other hosts will most likely match this address type.

The MULTICAST and BROADCAST address types correspond to traffic sent to the special purpose multicast and broadcast addresses.

Packets that match these three address types are returned to the previous chain. The remaining packets are first logged (if the rate exceeds 3 packets/min) and then discarded by the fifth rule.

The next rule deals with mDNS traffic:

-A ufw-before-input -d 224.0.0.251/32 -p udp -m udp --dport 5353 -j ACCEPT

This rule accepts all incoming datagrams that are sent to the MDNS multicast address 224.0.0.251 with destination port 5353/UDP.

The next rule deals with UPnP/SSDP traffic:

-A ufw-before-input -d 239.255.255.250/32 -p udp -m udp --dport 1900 -j ACCEPT

This rule accepts all incoming datagrams that are sent to the SSDP multicast address 239.255.255.250 with destination port 1900/UDP.

What we have seen so far is a set of default rules that are always there - even if users haven't configured a single custom rule with ufw. Packets that were not already discarded or accepted by the kernel are now entering the netfilter chain that contains all rules that are manually added by using the ufw command line tool:

-A ufw-before-input -j ufw-user-input

When ufw is first initialized, this chain is empty. This means that even if users decide to configure DROP rules for mDNS or SSDP traffic via ufw, these datagrams are most likely accepted by the previous chains. For more information on this, check section “Fingerprinting systems that use ufw”.

In order to add an allow rule via ufw, users could for example enter the following command:

ufw allow 22

If we take a look again at the ufw-user-input chain, the following rules appeared:

-A ufw-user-input -p tcp -m tcp --dport 22 -j ACCEPT
-A ufw-user-input -p udp -m udp --dport 22 -j ACCEPT

Because no transport protocol like TCP or UDP was specified in the command, rules were added for both protocols.

To allow a specific protocol it could be appended to the port number:

ufw allow 22/tcp

Another notable feature of ufw is the “limit” command. This is similar to the allow rule but combines it with rate-limiting. The following command adds a limit rule for port 22/TCP:

ufw limit 22/tcp

As with the ufw allow command, iptables is populating the ufw-user-input, ufw-user-limit and ufw-user-limit-accept chains behind the scenes with the following set of rules:

-A ufw-user-input -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name DEFAULT --mask 255.255.255.255 --rsource 
-A ufw-user-input -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 30 --hitcount 6 --name DEFAULT --mask 255.255.255.255 --rsource -j ufw-user-limit
-A ufw-user-input -p tcp -m tcp --dport 22 -j ufw-user-limit-accept
-A ufw-user-limit -j REJECT --reject-with icmp-port-unreachable
-A ufw-user-limit-accept -j ACCEPT

These rules are more complex and use many different parameters.

The first rule uses the conntrack module to match NEW connections. Furthermore the module recent is loaded that is able to track senders or receivers over time. To achieve this, a new entry is added with the --set option and associated with the name DEFAULT.

All entries that are added by the recent module could be read from /proc/net/xt_recent/ and in this particular case /proc/net/xt_recent/DEFAULT. The first rule also specifies the option --rsource that stores the source IP address of the incoming packet and the option --mask 255.255.255.255 ensures that only this single IP address is stored, rather than a larger subnet. Think of it as something like <ipaddress>/32.

A sample entry in /proc/net/xt_recent/DEFAULT might look like this:

src=192.168.0.120 ttl: 41 last_seen: 4330647135 oldest_pkt: 2 4330398367, 4330647135

The entry starts with the source address of the incoming packet, as specified in the first rule. In addition to that the recent module also keeps track of the ttl value and when the most recent packet that matched the rule appeared. The entry also contains the number of matching packets, along with the timestamps.

The second rule in the list above defines very specific matches. Like the first rule, it matches NEW connections and then calls the recent module. As opposed to the first rule it then continues with --update to operate on an existing entry within the xr_recent list. The parameter --seconds 30 adds a sliding window of 30 seconds and the parameter --hitcount 6 ensures that the rule only matches, when 6 packets arrived. Chained together, this rule matches if 6 packets, each not older than 30 seconds, are identified that are sent by the source address stored in the entry. If that is the case, the packets are sent to the ufw-user-limit chain. Packets that don't hit these thresholds are sent to the ufw-user-limit-accept chain.

The ufw-user-limit chain immediately instructs the kernel to discard all packets and respond with an ICMP error message that has the type Destination Unreachable and code Port Unreachable. The chain ufw-user-limit-accept directly accepts all packets.

Summarized, the ufw limit command allows access to ports and adds rate-limiting based on 6 hits within the past 30 seconds.

ufw-after-input

After user-defined rules were processed the remaining packets are sent to the ufw-after-input chain that is shown below.

-A ufw-after-input -p udp -m udp --dport 137 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 138 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 139 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 445 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 67 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 68 -j ufw-skip-to-policy-input
-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input

These rules match TCP packets, with destination ports 139 and 445, UDP datagrams with destination ports 137,138, 67 and 68 and traffic with address type BROADCAST and moves them to the ufw-skip-to-policy-input chain, which in this (default) scenario comprises only one rule:

-j DROP

If the default policy for INPUT would be changed to ACCEPT, this rule would change to -j ACCEPT as well.

This is an interesting set of rules as the 6th rule would effectively block UDP datagrams with destination port 68, although they were already accepted in ufw-before-input and thus never reach this rule.

ufw-after-logging-input

Packets or datagrams that were neither accepted nor dropped/rejected so far are now traversing the ufw-after-logging-input chain that containins the following rule:

-m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "

We've seen a rule like this before and it simply logs everything that exceeds the limit of 3 packets per minute.

ufw-reject-input & ufw-track-input

Packets are then entering the chains ufw-reject-input and ufw-track-input which however are both empty.

All remaining packets are now dropped, according to the default DROP rule.

Output handling (IPv4)

Packets and datagrams that are going to be sent out are traversing the builtin OUTPUT chain. Like the INPUT chain ufw populates it with some custom ufw chains as shown below:

Default ufw rules for outgoing IPv4 packets

As opposed to the INPUT chain, the default policy of the OUTPUT chain is ACCEPT.

ufw-before-logging-output

After entering the INPUT chain, traffic is entering the ufw-before-logging-output chain that does not contain any entries.

ufw-before-output

Afterwards the ufw-before-output chain that includes the following rules is traversed:

-o lo -j ACCEPT
-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-j ufw-user-output

The first two rules look familiar and accept packets destined to the lo interface and connections that are either in the ESTABLISHED or RELATED state.

Afterwards the ufw-user-output chain is entered that is empty by default. User-generated rules that are created with the ufw CLI are written to this chain and processed accordingly.

ufw-after-output & ufw-after-logging-output & ufw-reject-output

Afterwards the remaining packets are passing the ufw-after-output, ufw-after-logging-output and ufw-reject-output chains that are all empty.

ufw-track-output

The final chain ufw-track-output contains the following rules:

-p tcp -m conntrack --ctstate NEW -j ACCEPT
-p udp -m conntrack --ctstate NEW -j ACCEPT

These rules accept NEW TCP and UDP connections. While TCP-SYN packets or initial UDP datagrams count as NEW, there are scenarios where also TCP-ACK packets could be regarded as NEW. This behaviour is described in section “Fingerprinting systems that use ufw”.

All remaining packets are accepted by default, as per the default policy.

It can be concluded that all outgoing packets are allowed by default.

Forward handling (IPv4)

If the system running ufw is not the destination or origin of network traffic, packets are likely forwarded and therefore traversing the builtin FORWARD chain.

An overview of the default configuration by ufw is shown below:

Default ufw rules for forwarded IPv4 packets

The default policy for the FORWARD chain is DROP.

ufw-before-logging-forward

The first custom chain to be entered is ufw-before-logging-forward which does not contain any entries.

ufw-before-forward

Next, the ufw-before-forward is traversed that contains the following rules:

-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-p icmp -m icmp --icmp-type 3 -j ACCEPT
-p icmp -m icmp --icmp-type 11 -j ACCEPT
-p icmp -m icmp --icmp-type 12 -j ACCEPT
-p icmp -m icmp --icmp-type 8 -j ACCEPT
-j ufw-user-forward

The first rule accepts packets associated with an ESTABLISHED or RELATED connection. We've already seen this rule for incoming and outgoing packets.

The next four rules accept ICMP packets with ICMP types 3, 11, 12 and 8. These are the same ICMP types as we've seen in the INPUT chain:

  • Type 3: Destination unreachable
  • Type 11: Time exceeded
  • Type 12: Parameter problem
  • Type 8: Echo

Eventually packets are entering the ufw-user-forward chain that contains user-defined rules and is empty by default.

ufw-after-forward

The next chain that is traversed is ufw-after-forward and in the default configuration is empty.

ufw-after-logging-forward

Within the ufw-after-logging-forward chain, a single rule exists:

-m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "

This rule looks familiar and is logging all packets that exceed a rate of 3/min to syslog.

ufw-reject-forward & ufw-track-forward

The next chains ufw-reject-forward and ufw-track-forward are both empty.

At this stage, all remaining packets are discarded, due to the default DROP policy.

Default rule set (IPv6)

When enabling ufw, not only IPv4 rules are generated but also a large set of IPv6 related rules that can be queried using ip6tables:

-P INPUT DROP
-P FORWARD DROP
-P OUTPUT ACCEPT
-N ufw6-after-forward
-N ufw6-after-input
-N ufw6-after-logging-forward
-N ufw6-after-logging-input
-N ufw6-after-logging-output
-N ufw6-after-output
-N ufw6-before-forward
-N ufw6-before-input
-N ufw6-before-logging-forward
-N ufw6-before-logging-input
-N ufw6-before-logging-output
-N ufw6-before-output
-N ufw6-logging-allow
-N ufw6-logging-deny
-N ufw6-reject-forward
-N ufw6-reject-input
-N ufw6-reject-output
-N ufw6-skip-to-policy-forward
-N ufw6-skip-to-policy-input
-N ufw6-skip-to-policy-output
-N ufw6-track-forward
-N ufw6-track-input
-N ufw6-track-output
-N ufw6-user-forward
-N ufw6-user-input
-N ufw6-user-limit
-N ufw6-user-limit-accept
-N ufw6-user-logging-forward
-N ufw6-user-logging-input
-N ufw6-user-logging-output
-N ufw6-user-output
-A INPUT -j ufw6-before-logging-input
-A INPUT -j ufw6-before-input
-A INPUT -j ufw6-after-input
-A INPUT -j ufw6-after-logging-input
-A INPUT -j ufw6-reject-input
-A INPUT -j ufw6-track-input
-A FORWARD -j ufw6-before-logging-forward
-A FORWARD -j ufw6-before-forward
-A FORWARD -j ufw6-after-forward
-A FORWARD -j ufw6-after-logging-forward
-A FORWARD -j ufw6-reject-forward
-A FORWARD -j ufw6-track-forward
-A OUTPUT -j ufw6-before-logging-output
-A OUTPUT -j ufw6-before-output
-A OUTPUT -j ufw6-after-output
-A OUTPUT -j ufw6-after-logging-output
-A OUTPUT -j ufw6-reject-output
-A OUTPUT -j ufw6-track-output
-A ufw6-after-input -p udp -m udp --dport 137 -j ufw6-skip-to-policy-input
-A ufw6-after-input -p udp -m udp --dport 138 -j ufw6-skip-to-policy-input
-A ufw6-after-input -p tcp -m tcp --dport 139 -j ufw6-skip-to-policy-input
-A ufw6-after-input -p tcp -m tcp --dport 445 -j ufw6-skip-to-policy-input
-A ufw6-after-input -p udp -m udp --dport 546 -j ufw6-skip-to-policy-input
-A ufw6-after-input -p udp -m udp --dport 547 -j ufw6-skip-to-policy-input
-A ufw6-after-logging-forward -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw6-after-logging-input -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw6-before-forward -m rt --rt-type 0 -j DROP
-A ufw6-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw6-before-forward -p ipv6-icmp -m icmp6 --icmpv6-type 1 -j ACCEPT
-A ufw6-before-forward -p ipv6-icmp -m icmp6 --icmpv6-type 2 -j ACCEPT
-A ufw6-before-forward -p ipv6-icmp -m icmp6 --icmpv6-type 3 -j ACCEPT
-A ufw6-before-forward -p ipv6-icmp -m icmp6 --icmpv6-type 4 -j ACCEPT
-A ufw6-before-forward -p ipv6-icmp -m icmp6 --icmpv6-type 128 -j ACCEPT
-A ufw6-before-forward -p ipv6-icmp -m icmp6 --icmpv6-type 129 -j ACCEPT
-A ufw6-before-forward -j ufw6-user-forward
-A ufw6-before-input -i lo -j ACCEPT
-A ufw6-before-input -m rt --rt-type 0 -j DROP
-A ufw6-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 129 -j ACCEPT
-A ufw6-before-input -m conntrack --ctstate INVALID -j ufw6-logging-deny
-A ufw6-before-input -m conntrack --ctstate INVALID -j DROP
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 1 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 2 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 3 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 4 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 128 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 133 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 134 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 135 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 136 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 141 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 142 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 130 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 131 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 132 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 143 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 148 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 149 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 151 -m hl --hl-eq 1 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 152 -m hl --hl-eq 1 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 153 -m hl --hl-eq 1 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 144 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 145 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 146 -j ACCEPT
-A ufw6-before-input -p ipv6-icmp -m icmp6 --icmpv6-type 147 -j ACCEPT
-A ufw6-before-input -s fe80::/10 -d fe80::/10 -p udp -m udp --sport 547 --dport 546 -j ACCEPT
-A ufw6-before-input -d ff02::fb/128 -p udp -m udp --dport 5353 -j ACCEPT
-A ufw6-before-input -d ff02::f/128 -p udp -m udp --dport 1900 -j ACCEPT
-A ufw6-before-input -j ufw6-user-input
-A ufw6-before-output -o lo -j ACCEPT
-A ufw6-before-output -m rt --rt-type 0 -j DROP
-A ufw6-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 1 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 2 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 3 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 4 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 128 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 129 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 133 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 136 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 135 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 134 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 141 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 142 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 130 -j ACCEPT
-A ufw6-before-output -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 131 -j ACCEPT
-A ufw6-before-output -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 132 -j ACCEPT
-A ufw6-before-output -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 143 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 148 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -p ipv6-icmp -m icmp6 --icmpv6-type 149 -m hl --hl-eq 255 -j ACCEPT
-A ufw6-before-output -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 151 -m hl --hl-eq 1 -j ACCEPT
-A ufw6-before-output -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 152 -m hl --hl-eq 1 -j ACCEPT
-A ufw6-before-output -s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 153 -m hl --hl-eq 1 -j ACCEPT
-A ufw6-before-output -j ufw6-user-output
-A ufw6-logging-allow -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW ALLOW] "
-A ufw6-logging-deny -m conntrack --ctstate INVALID -m limit --limit 3/min --limit-burst 10 -j RETURN
-A ufw6-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw6-skip-to-policy-forward -j DROP
-A ufw6-skip-to-policy-input -j DROP
-A ufw6-skip-to-policy-output -j ACCEPT
-A ufw6-track-output -p tcp -m conntrack --ctstate NEW -j ACCEPT
-A ufw6-track-output -p udp -m conntrack --ctstate NEW -j ACCEPT
-A ufw6-user-limit -m limit --limit 3/min -j LOG --log-prefix "[UFW LIMIT BLOCK] "

Input handling (IPv6)

The builtin INPUT chain is populated with a set of rules that are shown in the following illustration:

Default ufw rules for incoming IPv6 packets

Like in the IPv4 examples, the ip6tables INPUT chain also has a default policy set to DROP.

ufw6-before-logging-input

The first chain ufw6-before-logging-input is empty.

ufw6-before-input

Next, the chain ufw6-before-input is entered that has a large set of pre-configured rules. These will be described step by step:

-i lo -j ACCEPT
-m rt --rt-type 0 -j DROP

The first rule accepts traffic destined to the lo interface. This allows local communication between applications.

The second rule matches if a IPv6 routing header is found. A certain routing type (Type 0: Source Routing) exists that was deprecated as per RFC-5095, due to its security risks. [3] This header is comparable to the source routing option in IPv4 that could be used to partly or completely control the route that packets take through networks. Firewall evasion attacks were way easier when source routing was allowed.

-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-m conntrack --ctstate INVALID -j ufw6-logging-deny
-m conntrack --ctstate INVALID -j DROP

The three rules above check the state of the connection, like in many previous examples. If the connection is either in a RELATED or ESTABLISHED state, the packets are accepted.

Connections that are INVALID are dropped, but before that, they are sent to the ufw6-logging-deny chain that contains the following rules:

-m conntrack --ctstate INVALID -m limit --limit 3/min --limit-burst 10 -j RETURN
-m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "

This mechanism is identical to the IPv4 counterpart: Packets associated with an INVALID connection are sent back to the previous chain, unless the rate exceeds 3 packets per minute. Packets that hit the threshold are logged to syslog and leave the chain afterwards.

Most of the following rules deal with ICMPv6 traffic. In order to provide a sufficient overview, some relevant ICMPv6 types are introduced:

  • Type 1: Destination unreachable
  • Type 2: Packet Too Big
  • Type 3: Time Exceeded
  • Type 4: Parameter Problem
  • Type 128: Echo Request
  • Type 129: Echo Reply
  • Type 130: Multicast Listener Query
  • Type 131: Multicast Listener Report
  • Type 132: Multicast Listener Done
  • Type 133: Router Solicitation
  • Type 134: Router Advertisement
  • Type 135: Neighbor Solicitation
  • Type 136: Neighbor Advertisement
  • Type 141: Inverse Neighbor Discovery
  • Type 142: Inverse Neighbor Discovery
  • Type 143: Version 2 Multicast Listener Report
  • Type 144: Home Agent Address Discovery Request Message
  • Type 145: Home Agent Address Discovery Reply Message
  • Type 146: Mobile Prefix Solicitation
  • Type 147: Mobile Prefix Advertisement
  • Type 148: Certification Path Solicitation Message
  • Type 149: Certification Path Advertisement Message
  • Type 151: Multicast Router Advertisement
  • Type 152: Multicast Router Solicitation
  • Type 153: Multicast Router Termination

Now with this list in mind, let's take a look at some ICMPv6 rules:

-p ipv6-icmp -m icmp6 --icmpv6-type 129 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 1 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 2 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 3 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 4 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 128 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 144 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 145 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 146 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 147 -j ACCEPT

These rules accept ICMPv6 packets, that match the type specified in the rule. While some of the allowed types are used in error handling (Destination unreachable, Time Exceeded, etc.) there are also other types like Echo Request and Echo Reply that are accepted.

Please note that all these rules are evaluated before the chain with user-defined rules is even traversed.

The next few rules also deal with ICMPv6, but add some little details:

-p ipv6-icmp -m icmp6 --icmpv6-type 133 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 134 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 135 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 136 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 141 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 142 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 148 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 149 -m hl --hl-eq 255 -j ACCEPT

These rules almost look like the previous ones, however they also contain the option -m hl --hl-eq 255 which match if the Hop Limit field is present the IPv6 header and has a value of 255.

So what does this value mean? The Hop Limit header in IPv6 is what the TTL header is in IPv4. It defines the maximum number of hops over which packets can be sent until they are dropped.

Why is it set to 255 for these specific ICMPv6 types? The types in the rules above correspond to Neighbour Discovery and Router Discovery that happen on the link (compared to ARP in IPv4). A limit of 255 ensures, that off-link packets (which would have a hop limit of < 255) are not allowed. This is specified in RFC-2461 section 3.1. [4]

-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 130 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 131 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 132 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 143 -j ACCEPT

The next 4 rules above refer to Multicast Listener and Version 2 Multicast Listener packets. In these rules an additional source address range of fe80::/10 is specified. This is the IP range reserved for IPv6 link-local addresses that are only valid on the link that the host is connected to. These rules ensure that no multicast traffic is accepted that originates from non-link-local addresses.

-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 151 -m hl --hl-eq 1 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 152 -m hl --hl-eq 1 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 153 -m hl --hl-eq 1 -j ACCEPT

The rules above filter for ICMPv6 types Multicast Router Advertisement, Multicast Router Solicitation and Multicast Router Termination. In addition to that packets are only accepted, if the source address is a link-local address and the hop limit is 1. A hop limit of 1 ensures that packets can't travel beyond the link as described in RFC-2710 section 4. [5]

-s fe80::/10 -d fe80::/10 -p udp -m udp --sport 547 --dport 546 -j ACCEPT

The set of IPv4 rules for the INPUT chain included a rule to allow DHCP traffic. The same applies for IPv6 as shown in the rule above.

It accepts packets that originate from and are sent to link-local addresses. Furthermore, it matches if the protocol is UDP, if the source port is equal to 547 (DHCPv6 Server), and if the destination port is equal to 546 (DHCPv6 Client).

-d ff02::fb/128 -p udp -m udp --dport 5353 -j ACCEPT

As IPv4 has a rule to allow mDNS traffic, the rule above allows mDNS6. The packet is accepted, if the destination address is ff02::fb/128, which corresponds to the special purpose link-local scope multicast addresses of mDNS6.

-d ff02::f/128 -p udp -m udp --dport 1900 -j ACCEPT

UPnP is also accepted, if the UDP destination port is 1900 and the destination address is the special purpose link-local scope multicast address ff02::f/128.

All remaining packets are sent to the ufw6-user-input chain that contains user-defined rules. By default it's empty.

ufw6-after-input

The next major chain to be traversed is ufw6-after-input. This chain contains the following rules:

-p udp -m udp --dport 137 -j ufw6-skip-to-policy-input
-p udp -m udp --dport 138 -j ufw6-skip-to-policy-input
-p tcp -m tcp --dport 139 -j ufw6-skip-to-policy-input
-p tcp -m tcp --dport 445 -j ufw6-skip-to-policy-input
-p udp -m udp --dport 546 -j ufw6-skip-to-policy-input
-p udp -m udp --dport 547 -j ufw6-skip-to-policy-input

This set of rules is very similar to the ufw-after-input chain for IPv4. Packets with destination ports for NetBIOS, SMB and DHCPv6 are sent to the ufw6-skip-to-policy-input chain that in the default setting contains a single rule:

-j DROP

ufw6-after-logging-input

Packets that made it this far are now entering the ufw6-after-logging-input chain, that only contains a rule for logging:

-m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "

As before, packets that exceed a rate of 3 packets per minute are logged to syslog.

ufw6-reject-input & ufw6-track-input

Packets are then sent to the next two empty chains ufw6-reject-input and ufw6-track-input and are eventually dropped, as per the default DROP policy.

Output handling (IPv6)

As for IPv4, packets that are about to leave a network interface traverse the builtin OUTPUT chain. It is populated by ufw with the following chains:

Default ufw rules for outgoing IPv6 packets

The default policy for the OUTPUT chain is set to ACCEPT. All packets that are neither dropped nor rejected are accepted by default.

ufw6-before-logging-output

The first chain ufw6-before-logging-output does not contain any entries.

ufw6-before-output

Afterwards the ufw6-before-output chain is entered, which as opposed to the IPv4 version, contains many more rules:

-o lo -j ACCEPT
-m rt --rt-type 0 -j DROP
-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

The first rule again ensures that local communication using the lo interface is possible.

Source routing is also denied for packets leaving the host, so all packets that match the rt-type 0 are dropped immediately.

The third rule accepts all packets that belong to an ESTABLISHED or RELATED connection.

The next rules deal with many ICMPv6 types that we have already seen in the INPUT chain:

-p ipv6-icmp -m icmp6 --icmpv6-type 1 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 2 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 3 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 4 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 128 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 129 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 133 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 136 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 135 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 134 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 141 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 142 -m hl --hl-eq 255 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 130 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 131 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 132 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 143 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 148 -m hl --hl-eq 255 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 149 -m hl --hl-eq 255 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 151 -m hl --hl-eq 1 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 152 -m hl --hl-eq 1 -j ACCEPT
-s fe80::/10 -p ipv6-icmp -m icmp6 --icmpv6-type 153 -m hl --hl-eq 1 -j ACCEPT

As this block of rules is more or less a repitition of the INPUT version, it is not explained in detail. Please refer to the previous section for further information.

Basically these rules allow ICMPv6 packets of different types. For some it depends on the values of Hop Limit and the source address.

Afterwards, packets are sent to the empty ufw6-user-output chain.

ufw6-after-output & ufw6-after-logging-output & ufw6-reject-output

The next three chains ufw6-after-output, ufw6-after-logging-output and ufw6-reject-output are empty.

ufw6-track-output

The final chain ufw6-track-output contains the following rules:

-p tcp -m conntrack --ctstate NEW -j ACCEPT
-p udp -m conntrack --ctstate NEW -j ACCEPT

These rules accept packets that initiate a NEW connection state.

All remaining packets are accepted as per the default policy. This means that only packets that contain a source routing header are discarded by default.

Forward handling (IPv6)

Packets that need to be routed to other systems are traversing the builtin FORWARD chain.

An overview of the default configuration by ufw is shown below:

Default ufw rules for forwarded IPv6 packets

The default policy for the FORWARD chain is DROP.

ufw6-before-logging-forward

Packets that are forwarded first enter the ufw6-before-logging-forward chain that does not contain any rules.

ufw6-before-forward

Afterwards the chain ufw6-before-forward is entered. Some rules are pre-configured by ufw:

-m rt --rt-type 0 -j DROP
-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

The first rule again drops packets with source routing header. The second rule only accepts packets that belong to an ESTABLISHED or RELATED connection state.

Next, some ICMPv6 rules are evaluated:

-p ipv6-icmp -m icmp6 --icmpv6-type 1 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 2 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 3 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 4 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 128 -j ACCEPT
-p ipv6-icmp -m icmp6 --icmpv6-type 129 -j ACCEPT

These ICMPv6 types correspond to typical error handling messages and Echo Request and Echo Reply packets. These are accepted immediately.

Afterwards the empty ufw6-user-forward chain is entered that is used for user-defined FORWARD rules.

ufw6-after-forward

The next chain ufw6-after-forward does not contain any rules.

ufw6-after-logging-forward

The only rule present in the ufw6-after-logging-forward chain performs logging of packets that exceed the rate of 3 packets per minute.

-m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "

ufw6-reject-forward & ufw6-track-forward

The remaining two chains ufw6-reject-forward and ufw6-track-forward are empty.

Packets that were not accepted or dropped so far are now dropped as per the default policy.

Fingerprinting systems that use UFW

If you are port scanning hosts, it might be good to know if packet filtering mechanisms are configured that block or slow down port scans.

Systems that run ufw have some interesting characteristics. Since user-defined rules are placed behind many of the pre-configured rules, some ports might show up as open, even if deny rules were added to the user-defined list.

A good example for this is DHCP. To recall how DHCP is handled, let's take a quick look at the rule within the chain ufw-before-input:

-p udp -m udp --sport 67 --dport 68 -j ACCEPT

As this rule allows incoming packets to UDP port 68 if they originate from UDP port 67, it's possible to detect this in a port scan:

$ sudo nmap -vvv -n -Pn -sU -g 67 --top-ports 20 172.17.0.2
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.93SVN ( https://nmap.org ) at 2022-11-30 21:25 CET
Initiating ARP Ping Scan at 21:25
Scanning 172.17.0.2 [1 port]
Completed ARP Ping Scan at 21:25, 0.06s elapsed (1 total hosts)
Initiating UDP Scan at 21:25
Scanning 172.17.0.2 [20 ports]
Completed UDP Scan at 21:26, 1.45s elapsed (20 total ports)
Nmap scan report for 172.17.0.2
Host is up, received arp-response (0.000071s latency).
Scanned at 2022-12-01 21:25:59 CET for 2s

PORT      STATE         SERVICE      REASON
53/udp    open|filtered domain       no-response
67/udp    open|filtered dhcps        no-response
68/udp    closed        dhcpc        port-unreach ttl 64
69/udp    open|filtered tftp         no-response
123/udp   open|filtered ntp          no-response
135/udp   open|filtered msrpc        no-response
137/udp   open|filtered netbios-ns   no-response
138/udp   open|filtered netbios-dgm  no-response
139/udp   open|filtered netbios-ssn  no-response
161/udp   open|filtered snmp         no-response
162/udp   open|filtered snmptrap     no-response
445/udp   open|filtered microsoft-ds no-response
500/udp   open|filtered isakmp       no-response
514/udp   open|filtered syslog       no-response
520/udp   open|filtered route        no-response
631/udp   open|filtered ipp          no-response
1434/udp  open|filtered ms-sql-m     no-response
1900/udp  open|filtered upnp         no-response
4500/udp  open|filtered nat-t-ike    no-response
49152/udp open|filtered unknown      no-response
MAC Address: 02:42:AC:11:00:02 (Unknown)

Read data files from: /usr/local/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 1.65 seconds
        Raw packets sent: 54 (4.484KB) | Rcvd: 2 (84B)

The results of a top 20 UDP port scan show that all ports, except port 68, are shown as open|filtered. This makes sense, since the default INPUT policy is DROP. The parameter -g 67 was chosen to set the source port to 67, as per the ACCEPT rule in the pre-configured chain. The results show that port 68 is closed and an ICMP message of type Port Unreachable was sent back.

So, what would happen if users decide to block UDP port 68 by using the ufw cli? Let's try:

$ sudo ufw deny 68/udp
Rule added
Rule added (v6)

The command above added a deny rule for UDP port 68. Another scan is started:

$ sudo nmap -vvv -n -Pn -sU -g 67 -p 68 172.17.0.2
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.93SVN ( https://nmap.org ) at 2022-11-30 21:33 CET
Initiating ARP Ping Scan at 21:33
Scanning 172.17.0.2 [1 port]
Completed ARP Ping Scan at 21:33, 0.05s elapsed (1 total hosts)
Initiating UDP Scan at 21:33
Scanning 172.17.0.2 [1 port]
Completed UDP Scan at 21:33, 0.06s elapsed (1 total ports)
Nmap scan report for 172.17.0.2
Host is up, received arp-response (0.000025s latency).
Scanned at 2022-11-30 21:33:34 CET for 0s

PORT   STATE  SERVICE REASON
68/udp closed dhcpc   port-unreach ttl 64
MAC Address: 02:42:AC:11:00:02 (Unknown)

Read data files from: /usr/local/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.20 seconds
        Raw packets sent: 2 (56B) | Rcvd: 2 (84B)

The port still shows up as closed. The relevant iptables rules confirm why this happens:

Chain ufw-before-input (1 references)
target     prot opt source               destination         
[...]
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp spt:67 dpt:68
[...]
ufw-user-input  all  --  0.0.0.0/0            0.0.0.0/0           

Chain ufw-user-input (1 references)
target     prot opt source               destination         
DROP       udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:68

The ACCEPT rule is evaluated before the ufw-user-input chain with the DROP rule is called.

In order to block port 68/UDP, users would need to manually adjust the files /etc/ufw/before.rules or /etc/ufw/before6.rules.

This means that scanning UDP port 68 with source port of 67 is a good way to detect ufw. Please note that other firewalls might also allow port 68 by default, and this behaviour might not be unique to ufw.

The ufw-before-input chain also accepts certain ICMP types, for example ICMP type 8 Echo Request. ICMP type 13 Timestamp Request and ICMP type 17 Address Mask Request are not allowed and therefore dropped as per the default policy. Let's validate this with 3 consecutive scans using ICMP types 8, 13 and 17:

$ sudo nmap -vvv -n -sn -PE --send-ip 172.17.0.2
Starting Nmap 7.93SVN ( https://nmap.org ) at 2022-11-30 21:53 CET
Initiating Ping Scan at 21:53
Scanning 172.17.0.2 [1 port]
Completed Ping Scan at 21:53, 0.03s elapsed (1 total hosts)
Nmap scan report for 172.17.0.2
Host is up, received echo-reply ttl 64 (0.00015s latency).
MAC Address: 02:42:AC:11:00:02 (Unknown)
Read data files from: /usr/local/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.15 seconds
        Raw packets sent: 1 (28B) | Rcvd: 1 (28B)

$ sudo nmap -vvv -n -sn -PP --send-ip 172.17.0.2
Starting Nmap 7.93SVN ( https://nmap.org ) at 2022-11-30 21:53 CET
Initiating Ping Scan at 21:53
Scanning 172.17.0.2 [1 port]
Completed Ping Scan at 21:54, 2.02s elapsed (1 total hosts)
Nmap scan report for 172.17.0.2 [host down, received no-response]
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 2.06 seconds
        Raw packets sent: 2 (80B) | Rcvd: 0 (0B)

$ sudo nmap -vvv -n -sn -PM --send-ip 172.17.0.2
Starting Nmap 7.93SVN ( https://nmap.org ) at 2022-11-30 21:54 CET
Initiating Ping Scan at 21:54
Scanning 172.17.0.2 [1 port]
Completed Ping Scan at 21:54, 2.03s elapsed (1 total hosts)
Nmap scan report for 172.17.0.2 [host down, received no-response]
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 2.08 seconds
        Raw packets sent: 2 (64B) | Rcvd: 0 (0B)

The only response is shown in the first scan in which ICMP type 8 was used. In combination with the DHCP scan, this is another good candidate to detect ufw.

The IPv6 ruleset also containes a large number of ICMPv6 related rules, of which two allow the ICMPv6 types Echo Request and Neighbor Solicitation.

A Neighbor Solicitation request can be sent using the command line utility ndisc6:

$ ndisc6 fe80::42:acff:fe11:2 eth0
Soliciting fe80::42:acff:fe11:2 (fe80::42:acff:fe11:2) on eth0...
Target link-layer address: 02:42:AC:11:00:02
from fe80::42:acff:fe11:2

The command above shows that a response was received, which means that incoming ICMPv6 type 135 packets were accepted.

Echo Request (ICMPv6 type 128) packets could be sent using ping6:

$ ping6 -c 1 fe80::42:acff:fe11:2%eth0
PING fe80::42:acff:fe11:2%eth0(fe80::42:acff:fe11:2%eth0) 56 data bytes
64 bytes from fe80::42:acff:fe11:2%eth0: icmp_seq=1 ttl=64 time=0.172 ms

--- fe80::42:acff:fe11:2%eth0 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.172/0.172/0.172/0.000 ms

After submitting this request the host answered successfully.

These two checks above are not very meaningful, as these allow rules could be present in any firewall. Also we would observe the same behaviour if no firewalls would be present or if all packets would be accepted by a firewall.

Therefore, for accurate fingerprinting it is recommended to cross-check as many of the default allow and deny rules (explicit and implicit rules).

A rule that is more suitable for this purpose is the following, covering DHCPv6:

-A ufw6-before-input -s fe80::/10 -d fe80::/10 -p udp -m udp --sport 547 --dport 546 -j ACCEPT

As this rule only accepts link-local UDP datagrams that arrive on port 546 and originate from port 547, this is a good test case for nmap:

root@a35d59e6e8d7:~# nmap -vvv -n -Pn -sU -6 -e eth0 -g 547 -p 546 fe80::42:acff:fe11:2
Starting Nmap 7.80 ( https://nmap.org ) at 2022-11-30 21:56 UTC
Initiating ND Ping Scan at 21:56
Scanning fe80::42:acff:fe11:2 [1 port]
Completed ND Ping Scan at 15:56, 0.03s elapsed (1 total hosts)
Initiating UDP Scan at 21:56
Scanning fe80::42:acff:fe11:2 [1 port]
Completed UDP Scan at 21:56, 0.02s elapsed (1 total ports)
Nmap scan report for fe80::42:acff:fe11:2
Host is up, received nd-response (0.000069s latency).
Scanned at 2022-12-01 21:56:26 UTC for 0s

PORT    STATE  SERVICE       REASON
546/udp closed dhcpv6-client port-unreach ttl 64
MAC Address: 02:42:AC:11:00:02 (Unknown)

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.25 seconds
        Raw packets sent: 2 (120B) | Rcvd: 2 (168B)

The scan results above show that the port is closed (not filtered) because the rule matched.

Let's try the same scan, but without specifying the source port:

root@a35d59e6e8d7:~# nmap -vvv -n -Pn -sU -6 -e eth0 -p 546 fe80::42:acff:fe11:2
Starting Nmap 7.80 ( https://nmap.org ) at 2022-11-30 21:56 UTC
Initiating ND Ping Scan at 21:56
Scanning fe80::42:acff:fe11:2 [1 port]
Completed ND Ping Scan at 21:56, 0.03s elapsed (1 total hosts)
Initiating UDP Scan at 21:56
Scanning fe80::42:acff:fe11:2 [1 port]
Completed UDP Scan at 21:56, 0.02s elapsed (1 total ports)
Nmap scan report for fe80::42:acff:fe11:2
Host is up, received nd-response (0.000069s latency).
Scanned at 2022-12-01 21:56:26 UTC for 0s

PORT    STATE         SERVICE       REASON
546/udp open|filtered dhcpv6-client no-response
MAC Address: 02:42:AC:11:00:02 (Unknown)

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.65 seconds
        Raw packets sent: 2 (120B) | Rcvd: 2 (168B)

This time the port shows up as open|filtered because the allow rule does not match and the default policy dropped the datagram.

There is another interesting behaviour with the default ufw rules and default Debian or Ubuntu kernel settings. So far we have seen many rules that allow packets belonging to ESTABLISHED or RELATED connections. If new connections are created, the conntrack state is NEW - but what exactly does NEW mean?

Incoming TCP-SYN packets are the best example for attempts to create a new connection. For UDP usually the first datagram can be referred to as NEW. But that's not it. Let's take a look at the sysctl parameter nf_conntrack_tcp_loose and its documentation [6]:

nf_conntrack_tcp_loose - BOOLEAN
    0 - disabled
    not 0 - enabled (default)

    If it is set to zero, we disable picking up already established
    connections.

This variable defines how strict conntrack is handling connections. On default Debian or Ubuntu installations this variable is set to 1, which means that “picking up already established connections” is enabled.

In fact TCP packets carrying the ACK flag are also considered NEW, if this variable is set to 1. Consequently, if arbitrary ACKs are allowed, ACK-scans are allowed as well.

Let's try this by first adding an allow rule for port 22/TCP:

root@9ad217af69b6:~# ufw allow 22/tcp
Rule added
Rule added (v6)

Now with the new user-defined rule in place, let's run an ACK-scan:

root@a35d59e6e8d7:~# nmap -vvv -n -Pn -sA -p 22 172.17.0.2
Starting Nmap 7.80 ( https://nmap.org ) at 2022-11-30 21:58 UTC
Initiating ARP Ping Scan at 21:58
Scanning 172.17.0.2 [1 port]
Completed ARP Ping Scan at 21:58, 0.02s elapsed (1 total hosts)
Initiating ACK Scan at 21:58
Scanning 172.17.0.2 [1 port]
Completed ACK Scan at 21:58, 0.02s elapsed (1 total ports)
Nmap scan report for 172.17.0.2
Host is up, received arp-response (0.000044s latency).
Scanned at 2022-11-30 21:58:42 UTC for 0s

PORT   STATE      SERVICE REASON
22/tcp unfiltered ssh     reset ttl 64
MAC Address: 02:42:AC:11:00:02 (Unknown)

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.25 seconds
        Raw packets sent: 2 (68B) | Rcvd: 2 (68B)

The port shows up as unfiltered, because a TCP-RST was sent in response. Please note that it is not possible to identify ports that were not explicitely allowed, which means that an ACK-scan wouldn't bypass any intended deny rules. Nevertheless it's good to keep in mind that the rules added by ufw also allow ACK-scans by default.

In the previous sections we have seen that the ufw limit command allows ports and adds a rate-limiting mechanism that rejects packets if 6 packets were detected within the last 30 seconds. This can be easily detected, by sending 6 packets within a short timeframe. For the example above, the command ufw limit 22/tcp was executed before:

$ nc -v -w 0 192.168.0.2 22
Connection to 192.168.0.2 22 port [tcp/sunrpc] succeeded!
$ nc -v -w 0 192.168.0.2 22
Connection to 192.168.0.2 22 port [tcp/sunrpc] succeeded!
$ nc -v -w 0 192.168.0.2 22
Connection to 192.168.0.2 22 port [tcp/sunrpc] succeeded!
$ nc -v -w 0 192.168.0.2 22
Connection to 192.168.0.2 22 port [tcp/sunrpc] succeeded!
$ nc -v -w 0 192.168.0.2 22
Connection to 192.168.0.2 22 port [tcp/sunrpc] succeeded!
$ nc -v -w 0 192.168.0.2 22
nc: connect to 192.168.0.2 port 22 (tcp) failed: Connection refused

The 6th connection request was rejected by the remote system.

Security implications and summary

After providing this overview about default ufw rules and characteristics, what are the security implications?

First of all the default policy for incoming packets ensures that packets that were not explicitely allowed are dropped. Ufw defines some exclusions for RELATED and ESTABLISHED connections, selected ICMP/ICMPv6 packets and applications like DHCP/DHCPv6, mDNS and SSDP/UPnP.

If these services are running and are either misconfigured or affected by vulnerabilities, the default rules could allow attacks against these services.

In post-exploitation scenarios attackers could use unfiltered ports to bind malicious services on them. Whether this is successful depends on the granted privileges, as binding on ports < 1024 requires either root privileges or the cap_net_bind_service capability.

As the ufw default ruleset allows almost all types of egress traffic, it would be easy to establish C2 communication after a successful compromise.

In this blog post only the default rules were inspected. For additional hardening some rules could be added to the OUTPUT chain to further lock down the system, however the general ruleset offers a good baseline aready. Furthermore it should be noted that ufw is an frontend for iptables rather than a firewall. For managing finegrained and advanced rules, knowledge of iptables/netfilter is required.

References

[1]: https://home.regit.org/netfilter-en/secure-use-of-helpers/

[2]: https://github.com/torvalds/linux/commit/486dcf43da7815baa615822f3e46883ccca5400f

[3]: https://www.ietf.org/rfc/rfc5095.txt

[4]: https://www.ietf.org/rfc/rfc2461.txt

[5]: https://www.ietf.org/rfc/rfc2710.txt

[6]: https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt