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:
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:
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:
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:
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:
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:
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:
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