HardStacked
Share:

Attack surface · ufw

ufw block ping: "deny incoming" and the server still answers (plus the IPv6 trap)

Published · HardStacked · 12 min

You enabled ufw. ufw status verbose says Default: deny (incoming). You ping the server from another machine and it answers, every time. Someone asked it on askubuntu (question 1504520, 2024-02-23, 15,072 views on 2026-10-21, no accepted answer) and put the goal in one sentence:

"For security, I'd like to hide my server from ping requests."

You search ufw deny ping, then ufw block ping, and ufw has no option for that. sudo ufw deny proto icmp from any returns ERROR: Unsupported protocol 'icmp', and this is where people paste a sed found on the internet.

That sed works on IPv4. Your server keeps answering ping over IPv6, and nobody tells you what else the sed touched. We measured all of it on 2026-10-21. The commands are below.

01. The verdict in three lines

ufw does not let the ping through by mistake. It accepts it on purpose, in /etc/ufw/before.rules line 37, a file that sits in front of the rules you type. No ufw command displays that line and none changes it. The fix is two commented lines, one for IPv4 and one for IPv6, then sudo ufw reload.

The answers online forget one of the two lines or comment out far too many. On our bench, commenting every ICMPv6 input line made SSH unreachable over IPv6, with port 22 allowed.

Three lights lie here: Default: deny (incoming) while the ping goes through, a ufw status that is identical whether the ping is accepted or not, and a deny proto icmp that doesn't exist.

02. The bench: a client and a server on one machine

Everything was measured on 2026-10-21 on a Raspberry Pi Zero 2 W under Debian 13 trixie, ufw 0.36.2-9, kernel 6.18 (Raspberry Pi build). One machine only. The "client" sits in a network namespace, linked to the server by a veth pair, with lab addresses 10.215.0.1 (server) and 10.215.0.2 (client), fd00:215::1 and fd00:215::2 in IPv6. The client's traffic crosses the server's input firewall like a real client's, and you need no second machine to reproduce it.

These commands are provided as is, without warranty. Test on a non critical machine or a snapshot first.

bench setup
$ sudo ip netns add client
$ sudo ip link add veth-host type veth peer name veth-client
$ sudo ip link set veth-client netns client
$ sudo ip addr add 10.215.0.1/24 dev veth-host
$ sudo ip -6 addr add fd00:215::1/64 dev veth-host nodad
$ sudo ip link set veth-host up
$ sudo ip -n client addr add 10.215.0.2/24 dev veth-client
$ sudo ip -n client -6 addr add fd00:215::2/64 dev veth-client nodad
$ sudo ip -n client link set veth-client up

The control is TCP port 8080, where nothing listens. With ufw off, the connection is refused (exit=1): the path works. With ufw on we expect silence (exit=124): ufw drops the packet. That is the proof that ufw filters while the ping goes through.

03. ufw off, then ufw on

State A, ufw disabled:

state A: ufw disabled
$ sudo ufw disable
Firewall stopped and disabled on system startup
$ sudo ip netns exec client ping -c 3 -W 2 10.215.0.1 | tail -2
3 packets transmitted, 3 received, 0% packet loss, time 2050ms
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/10.215.0.1/8080"; echo "exit=$?"
bash: connect: Connection refused exit=1

State B, deny incoming, only 22/tcp allowed (we cut the other lines of the status):

state B: deny incoming, 22/tcp allowed
$ sudo ufw default deny incoming
$ sudo ufw allow 22/tcp
$ sudo ufw --force enable
$ sudo ufw status verbose
Default: deny (incoming), allow (outgoing), disabled (routed)

Same tests:

state B: same tests
$ sudo ip netns exec client ping -c 3 -W 2 10.215.0.1 | tail -2
3 packets transmitted, 3 received, 0% packet loss, time 2053ms
$ sudo ip netns exec client ping -6 -c 3 -W 2 fd00:215::1 | tail -2
3 packets transmitted, 3 received, 0% packet loss, time 2026ms
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/10.215.0.1/8080"; echo "exit=$?"
exit=124

Ping 3/3 in both families, TCP control mute. ufw filters, the ping is not filtered. The command that should have solved it:

state B: deny proto icmp
$ sudo ufw deny proto icmp from any
ERROR: Unsupported protocol 'icmp'

Where is the ping accepted? The chain order says it:

chain order, INPUT
$ sudo iptables -S INPUT
-P INPUT DROP -A INPUT -j ufw-before-logging-input -A INPUT -j ufw-before-input -A INPUT -j ufw-after-input

ufw-before-input is hit first, and an ACCEPT ends the walk. So where are the rules you typed? At the very end of that same chain:

where your rules live
$ sudo iptables -S ufw-before-input | tail -1
-A ufw-before-input -j ufw-user-input
$ sudo iptables -S ufw-user-input
-N ufw-user-input -A ufw-user-input -p tcp -m tcp --dport 22 -j ACCEPT

Your allow 22/tcp lives in ufw-user-input, and that chain is only reached from the last line of ufw-before-input. Whatever ufw-before-input accepts earlier never meets your rules. And earlier in that chain:

ICMP in ufw-before-input
$ sudo iptables -S ufw-before-input | grep icmp
-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

Type 3 is destination-unreachable, 11 time-exceeded, 12 parameter-problem, 8 echo-request. Four types accepted, not only the ping. Section 05 comes back to this. The source of the rules:

source of the rules
$ sudo grep -n "echo-request" /etc/ufw/before.rules /etc/ufw/before6.rules
/etc/ufw/before.rules:37:-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT /etc/ufw/before.rules:43:-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT /etc/ufw/before6.rules:49:-A ufw6-before-input -p icmpv6 --icmpv6-type echo-request -j ACCEPT

Line 37 is the IPv4 ping, line 49 of before6.rules the IPv6 one, line 43 the FORWARD twin (we cut two more IPv6 lines). These files come from ufw itself: we compared Debian's 0.36.2-9 with upstream on launchpad, and the -A lines are identical. Same fix on Ubuntu then, but not measured there.

04. The fix, with proof

Two lines, no more. sed -i.bak keeps a copy of each file:

the fix: two lines
$ sudo sed -i.bak '/ufw-before-input -p icmp --icmp-type echo-request/s/^/#/' /etc/ufw/before.rules
$ sudo sed -i.bak '/ufw6-before-input -p icmpv6 --icmpv6-type echo-request/s/^/#/' /etc/ufw/before6.rules
$ sudo grep -n "input.*echo-request" /etc/ufw/before.rules /etc/ufw/before6.rules
/etc/ufw/before.rules:37:#-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT /etc/ufw/before6.rules:49:#-A ufw6-before-input -p icmpv6 --icmpv6-type echo-request -j ACCEPT
$ sudo ufw reload
Firewall reloaded

No reboot, ufw reload is enough. Same tests:

after the fix: same tests
$ sudo ip netns exec client ping -c 3 -W 2 10.215.0.1 | tail -2
3 packets transmitted, 0 received, 100% packet loss, time 2041ms
$ sudo ip netns exec client ping -6 -c 3 -W 2 fd00:215::1 | tail -2
3 packets transmitted, 0 received, 100% packet loss, time 2040ms
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/10.215.0.1/8080"; echo "exit=$?"
exit=124

Ping 0/3 in both families, TCP control still mute. ufw status verbose after the reload is identical word for word to the one from state B (we compared the ten lines). It says nothing about ICMP, so it can't confirm your work.

To undo, put the copies back and reload:

undo
$ sudo mv /etc/ufw/before.rules.bak /etc/ufw/before.rules
$ sudo mv /etc/ufw/before6.rules.bak /etc/ufw/before6.rules
$ sudo ufw reload
Firewall reloaded
$ sudo ip netns exec client ping -c 3 -W 2 10.215.0.1 | tail -2
3 packets transmitted, 3 received, 0% packet loss, time 2037ms

IPv4 ping is back at 3/3 (IPv6 too in the full capture). If your machine routes traffic for others (gateway, VPN), line 43 of before.rules is the same rule in FORWARD. We did not test it and did not touch it.

05. What the two answers on askubuntu miss

The first answer (score 1, 2024-02-23) writes: "By default, ufw blocks all ICMP requests except pings." The listing in section 03 says otherwise: four ICMP types are accepted on input. Its sed pattern, s/^\s*-A\ ufw-before-input\ -p\ icmp\ --icmp-type.*ACCEPT\s*/#\ \0/g, matches all four and comments them, then does the same in ufw-before-forward. We read that pattern, we did not run it. The second answer (score 0, 2025-03-26) comments only echo-request, writes "maybe reboot" and "I tried it, and it worked for me". It did work, in IPv4. A reboot is not needed.

Neither answer mentions IPv6, and both touch only before.rules. We did the same:

what both answers do: before.rules only
$ sudo sed -i.bak '/ufw-before-input -p icmp --icmp-type echo-request/s/^/#/' /etc/ufw/before.rules
$ sudo ufw reload
Firewall reloaded
$ sudo ip netns exec client ping -c 3 -W 2 10.215.0.1 | tail -2
3 packets transmitted, 0 received, 100% packet loss, time 2051ms
$ sudo ip netns exec client ping -6 -c 3 -W 2 fd00:215::1 | tail -2
3 packets transmitted, 3 received, 0% packet loss, time 2036ms

IPv4 0/3, IPv6 3/3. The server still answers ping on IPv6, and whoever applied this doesn't know.

Now the opposite mistake: apply the first answer's idea to IPv6, that is comment every ICMPv6 input line. First put before.rules back, then the witness: original rules, SSH reachable over IPv6. We flush the client's neighbor cache before each measure, the paragraph after explains why:

witness: original rules, SSH over IPv6
$ sudo mv /etc/ufw/before.rules.bak /etc/ufw/before.rules
$ sudo ufw reload
Firewall reloaded
$ sudo ip -n client -6 neigh flush all
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/fd00:215::1/22"; echo "exit=$?"
exit=0

Before touching anything, look at two of the lines this will hit. In IPv6, a machine finds the hardware address of its neighbor with ICMPv6 messages, neighbor solicitation and advertisement, accepted here:

neighbor lines in before6.rules
$ sudo grep -n "neighbor" /etc/ufw/before6.rules
52:-A ufw6-before-input -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT 53:-A ufw6-before-input -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT

Then the transposed version:

every ICMPv6 input line commented
$ sudo sed -i.bak -E 's/^-A ufw6-before-input -p icmpv6 .*-j ACCEPT/#&/' /etc/ufw/before6.rules
$ sudo grep -c "^#-A ufw6-before-input -p icmpv6" /etc/ufw/before6.rules
25
$ sudo ufw reload
Firewall reloaded
$ sudo ip -n client -6 neigh flush all
$ sudo ip netns exec client ping -6 -c 3 -W 2 fd00:215::1 | tail -2
3 packets transmitted, 0 received, 100% packet loss, time 2026ms
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/fd00:215::1/22"; echo "exit=$?"
exit=124

25 lines commented, ping 0/3, and SSH over IPv6 gives exit=124 with port 22 allowed. We commented all 25 at once and did not isolate which lines matter. Lines 52 and 53 shown above are among them.

That is our reading of the failure: without those messages the client can't find the server's hardware address, and nothing gets delivered, SSH included. We did not isolate it line by line. The neigh flush all empties the client's cache. Without it, SSH keeps working while the cached entry lives and you conclude nothing broke. Put the original file back and SSH returns:

original before6.rules back
$ sudo mv /etc/ufw/before6.rules.bak /etc/ufw/before6.rules
$ sudo ufw reload
Firewall reloaded
$ sudo ip -n client -6 neigh flush all
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/fd00:215::1/22"; echo "exit=$?"
exit=0

The first answer also comments destination-unreachable. RFC 1191 describes what it is for: a router that can't forward a packet with the DF bit set "will discard them and return ICMP Destination Unreachable messages with a code meaning 'fragmentation needed and DF set'", and on receipt "the source host reduces its assumed PMTU for the path." So does that sed break path MTU discovery? Probably not for your own connections, and the reason sits higher in the same file. Line 25 of before.rules, before any ICMP line, is -A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT. The iptables manual (iptables-extensions, iptables 1.8.11) defines RELATED as a packet "associated with an existing connection, such as an FTP data transfer or an ICMP error." A "fragmentation needed" message about one of your connections is such an error, so line 25 lets it in before the commented lines are ever read. That is our reading of the manual, we did not measure it. What the first answer certainly does is comment four rules to block one, with no gain we could find for the other three.

06. Does hiding the ping hide the server?

No. nmap's manual, section on host discovery (nmap.org/book/man-host-discovery.html, read on 2026-10-21): -Pn "skips the host discovery stage altogether", and "causes Nmap to attempt the requested scanning functions against every target IP address specified." Whoever scans with -Pn doesn't care that you don't answer ping.

Our bench shows it. Apply the fix from section 04 again, the state you want to end in, and test SSH this time:

the fix again, SSH this time
$ sudo sed -i.bak '/ufw-before-input -p icmp --icmp-type echo-request/s/^/#/' /etc/ufw/before.rules
$ sudo sed -i.bak '/ufw6-before-input -p icmpv6 --icmpv6-type echo-request/s/^/#/' /etc/ufw/before6.rules
$ sudo ufw reload
Firewall reloaded
$ sudo ip netns exec client ping -c 3 -W 2 10.215.0.1 | tail -2
3 packets transmitted, 0 received, 100% packet loss, time 2041ms
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/10.215.0.1/22"; echo "exit=$?"
exit=0
$ sudo ip netns exec client ping -6 -c 3 -W 2 fd00:215::1 | tail -2
3 packets transmitted, 0 received, 100% packet loss, time 2035ms
$ sudo ip netns exec client timeout 5 bash -c "</dev/tcp/fd00:215::1/22"; echo "exit=$?"
exit=0

Ping 0/3 in IPv4 and in IPv6, and SSH reachable on both with exit=0. A blocked ping reduces the noise from naive sweeps, that is all. What protects the server is the closed ports and the deny incoming policy, which worked before we touched anything. On what the status screen really reads, see the firewall that reports green while blocking nothing.

The lab client comes off with one command. Deleting the namespace also deletes both ends of the veth pair, and the fix stays in place:

remove the lab client
$ sudo ip netns del client
$ ip link show veth-host
Device "veth-host" does not exist.

07. Measured, read, not measured

Measured on 2026-10-21: Debian 13 trixie, ufw 0.36.2-9, kernel 6.18 Raspberry Pi, IPv4 and IPv6, client in a namespace on the same machine. Read only: the upstream ufw files, the first answer's sed pattern, RFC 1191, the iptables manual (RELATED). Not measured: Ubuntu, the FORWARD line, the PMTUD failure.

Share: