Firewall and routing
iptables/nftables, ufw, the routing table, and why ping/traceroute stay silent even though the service is alive.
The service is running, the port is listening, but you can't reach it from outside. Usually one of two layers is to blame: routing (the packet has nowhere to go) or the firewall (the packet got there, but it was dropped). In this lesson we'll go through both layers and learn to tell from client behavior which one it was.
Routing: where the packet goes
When a host sends a packet, it looks at the routing table and decides which interface and gateway to send it through:
ip route
# default via 192.168.1.1 dev eth0
# 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.42
This reads as:
192.168.1.0/24 dev eth0: "addresses in this subnet are neighbors, send directly through eth0",default via 192.168.1.1: "everything else goes to the gateway192.168.1.1".
default is the default route (default gateway). No default gateway, no internet, even
with a correct IP: the packet has nowhere to go outside.
Find out which route a specific address will take:
ip route get 8.8.8.8
# 8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.42
Add a route by hand (for example, to a separate subnet through a different gateway):
ip route add 10.10.0.0/16 via 192.168.1.254
In production, static routes are usually not written with a command (it only lives until
reboot), but in the network config (netplan / NetworkManager / /etc/network/interfaces).
Routes with hands-on practice are covered in detail in the lesson "Host networking: ip, ss, routes" (the lab "Route to a neighboring network"). From here on, this is about the firewall.
Firewall on Linux: netfilter
Packet filtering in the Linux kernel is done by netfilter. It's managed through iptables
(the classic) or nftables (the successor). Simpler wrappers are often layered on top: ufw on
Debian and Ubuntu, firewalld on RHEL, Rocky, Alma and Fedora.
Key iptables concepts:
- tables:
filter(allow/deny),nat(address translation, see the NAT lesson), and others, - chains in the
filtertable:INPUT(incoming to us),OUTPUT(outgoing from us),FORWARD(transit, when we act as a router), - rules: a condition plus an action (
-j ACCEPT,-j DROP,-j REJECT), - default policy: what to do if no rule matched.
iptables -L -n -v # show filter rules with counters
iptables -t nat -L -n # rules of the nat table
Example: let SSH through and close everything else incoming.
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j DROP
iptables rules live until reboot. To make them survive a reboot, they're saved
(iptables-save / netfilter-persistent), or you use ufw/nftables, which do this themselves.
iptables today is a frontend to nftables
On Debian 10+, Ubuntu 20.10+ and RHEL 8+, the iptables command no longer writes rules into the
old mechanism. This is iptables-nft: the familiar syntax, but the rules go into nftables. The
two "parallel options" from old articles are in fact long since one engine with two interfaces.
The practical consequence bites during diagnostics. Rules created directly through nft won't
show up in iptables -L output: it only shows its own tables. The host looks like it has "no rules", but
traffic is still being cut.
nft list ruleset # the full netfilter picture
iptables -L -n -v # only what iptables put there
In nftables each table has its own chains, and several chains can hang on the same incoming
traffic. A packet passes through all of them in turn. accept in one chain isn't final: the next
chain can still drop it. But drop is final immediately. So an allow rule in iptables won't
help if another nftables table cuts the same packet. Look for the deny rule in
nft list ruleset and allow the traffic where it's being cut.
Check which frontend is set, and switch it:
iptables --version # prints (nf_tables) or (legacy)
sudo update-alternatives --config iptables
The list of alternatives will have iptables-nft and iptables-legacy. Switch deliberately:
rules from one mechanism aren't visible in the other, and having both enabled at once gives
effects that are hard to track down.
iptables-translate helps rewrite an existing rule into nftables language:
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
nft 'add rule ip filter INPUT tcp dport 22 counter accept'
What the same logic looks like entirely in nftables, with the example "SSH and HTTP allowed, everything else incoming closed":
table inet example {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept
tcp dport { 22, 80 } accept
}
}
Here policy drop is the default policy, ct state established,related accept lets through
responses to connections the host opened itself, and iif "lo" allows local traffic. Adding a
port to such a chain: nft add rule inet example input tcp dport 8443 accept.
ufw: a simple facade
Managing iptables directly is verbose. ufw (Uncomplicated FireWall) covers typical tasks on a
single host:
ufw allow 22/tcp # let SSH through
ufw allow 80,443/tcp # HTTP/HTTPS
ufw default deny incoming
ufw enable
ufw status verbose # what's open right now
Under the hood ufw still generates netfilter rules. For a single server, it's usually enough.
On RHEL, Rocky, Alma and Fedora, a different wrapper is installed by default, firewalld. It
works with zones instead of a flat list of rules:
firewall-cmd --state
firewall-cmd --list-all # what's open in the active zone
firewall-cmd --add-port=8080/tcp --permanent
firewall-cmd --reload
Without --permanent the rule lives until reboot, and without --reload a permanent rule won't
apply to the current configuration. You need both halves.
Cloud firewall: a separate layer
In the cloud (AWS, Google Cloud, Azure) there's a second firewall outside the OS: security groups / network ACLs. The packet passes through it first, then the firewall inside the OS.
So the classic "the port is open in ufw, the service is listening on 0.0.0.0, but it's still
unreachable from outside" most often means a closed port in a cloud security group. Check
both layers.
Why ping doesn't respond even though the service is alive
ping uses the ICMP protocol (echo request/reply), not TCP. This is separate traffic, and
it's very often cut:
- the firewall drops ICMP by security policy,
- the cloud security group doesn't let ICMP through,
- the host itself is configured not to respond to echo.
Meanwhile TCP ports (80, 443, 22) can be open, and the service works fine. Conclusion: "ping
doesn't go through" does not equal "the server is down". Check the service itself through
nc -zv host port or curl, not with a ping.
Why traceroute shows * * *
traceroute relies on a TTL trick: it sends packets with an increasing TTL and catches an ICMP
"time exceeded" from each hop. Asterisks * * * mean that hop didn't send that ICMP. The reasons
are the same:
- the router is configured not to respond to an expired TTL (deliberately),
- the firewall cuts ICMP / UDP probes from traceroute,
- rate limiting on ICMP at an intermediate node.
* * * in the middle of the path is considered normal and doesn't mean a break: the final host
can still be responding. It's only concerning if the stars go all the way to the end.
DROP vs REJECT: reading the symptom
How the firewall drops a packet shows in client behavior:
- DROP: the packet is silently discarded. The client waits and gets a timeout. If a connection hangs and then times out, that's DROP's signature (or the packet never arrived at all).
- REJECT: the firewall replies "closed". By default for TCP this is an ICMP
port-unreachable; to get an actual TCP RST back, you need
--reject-with tcp-reset. In both cases the client immediately gets Connection refused.
Here's how each variant looks in nc (part of the netcat-openbsd package):
Connection to 10.0.0.5 22 port [tcp/ssh] succeeded!
nc: connect to 10.0.0.5 port 3306 (tcp) timed out: Operation in progress
nc: connect to 10.0.0.5 port 5432 (tcp) failed: Connection refused
First line: the port is open. Second: no response, this is DROP or a loss along the way. Third:
reached the host, but the port is closed. There are two causes for a refusal: nobody's listening
on the port, or REJECT is cutting it. You can only tell them apart on the server: ss -ltn shows
whether a process is listening on the port, and nft list ruleset shows whether there's a reject
rule.
Common mistakes
- Looking only at
iptables -Land concluding the firewall is empty. Rules placed throughnftaren't visible there. Start withnft list ruleset. - Adding
ACCEPTiniptablesand expecting the port to open, while another nftables table is cutting the packet. Allow traffic where it's being denied. - Fixing a problem with
nft flush rulesetorufw disable. The port will open, but so will everything else the firewall was closing. Open only the port you need. - Concluding "the server is down" because
pingdoesn't respond. Check the service's port throughnc -zvorcurl. - Confusing timeout and refused. Timeout means there's no response at all, refused means the host responded with a denial.
- Opening a port in the OS and forgetting about the cloud security group (or the other way around).
Diagnostic order for "unreachable from outside"
ip route: is there a default gateway? Is the route to the target correct?ss -tulnpon the server: is the service actually listening, and on0.0.0.0, not127.0.0.1?nft list ruleset,iptables -L -n,ufw statusorfirewall-cmd --list-all: does the local firewall cut the port? Look specifically atnft list ruleset, because rules placed throughnftaren't visible iniptables -L.- Cloud security group: is the port open in the external firewall?
nc -zvfrom outside: timeout (DROP/routing) or refused (no service/REJECT)?
What to remember
ip route shows the routing table; no default gateway, no way out.
Filtering is managed by netfilter; it's managed through iptables/nftables, with ufw and
firewalld working as wrappers on top. Chains INPUT/OUTPUT/FORWARD, actions ACCEPT/DROP/REJECT.
On modern distros iptables is a frontend to nftables. The full picture comes from
nft list ruleset, iptables -L shows only its own part. accept in one chain doesn't cancel
drop in another.
In the cloud there's a second firewall (security groups) outside the OS. Check both layers.
ping uses ICMP, which is often cut: a silent ping doesn't mean the service is down. Check the
port through nc/curl.
traceroute's * * * means a hop isn't responding to ICMP, usually on purpose; stars all the way
to the end point to a problem, in the middle of the path it's normal.
DROP gives a timeout, REJECT gives connection refused. The symptom tells you what's happening.
What you'll need for the tasks
docker exec <container> <command>: all lab commands run inside the lab environment's containers, nosudoneeded;nft list rulesetandiptables -L INPUT -n -v: what's configured in the firewall and how these two outputs differ;nft add rule <family> <table> <chain> <condition> accept: add an allow rule to an existing chain;ss -ltn: which ports the server is listening on;nc -zv -w 3 <address> <port>,wget -T 3 -q -O - <url>andping -c 2 -W 2 <address>: checks from the client;- sections "iptables today is a frontend to nftables" and "DROP vs REJECT: reading the symptom".
Interview-style questions
Interview questions on this topic: 3
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson networking-basics-06-firewall-routing
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress