Sign in
← Networking: basics
15 min · medium · Lesson 6 of 13

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:

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:

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:

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:

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

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

Diagnostic order for "unreachable from outside"

  1. ip route: is there a default gateway? Is the route to the target correct?
  2. ss -tulnp on the server: is the service actually listening, and on 0.0.0.0, not 127.0.0.1?
  3. nft list ruleset, iptables -L -n, ufw status or firewall-cmd --list-all: does the local firewall cut the port? Look specifically at nft list ruleset, because rules placed through nft aren't visible in iptables -L.
  4. Cloud security group: is the port open in the external firewall?
  5. nc -zv from 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

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson networking-basics-06-firewall-routing