Firewalls
A firewall is an ordered rule list evaluated against each packet’s addresses, ports and protocol — stateful ones remember which connections you started so replies get in — and a silent drop looks completely different on the wire from nothing listening.
The problem
curl hangs for two minutes and then says Connection timed out. On the server, ss -ltn shows something listening on 8443 and curl localhost:8443 works. Nothing is broken; something is deciding, packet by packet, that yours may not pass. Where is that decision made, and why does the symptom look like this?Traffic → rules → verdict
A packet filter reads the headers it has been told to care about — source and destination IP (IP: Best-Effort Delivery Between Machines), transport protocol, source and destination port (Ports: Addressing a Process, Not a Machine), TCP flags, sometimes the interface it arrived on — and walks an ordered list of rules. The first rule whose match fields all apply decides the verdict: accept, drop (discard silently), or reject (discard and send an ICMP error or a TCP RST back). If no rule matches, the chain’s policy applies, and the only defensible policy on an inbound chain is drop — default-deny, with explicit holes for what must be reachable.
Ordering is the whole game. allow tcp/22 from 10.0.0.0/8 above drop all opens SSH to the internal network; the same two rules swapped block it, and the allow rule is never reached. Firewalls that count matches (iptables -L -v -n shows packet counters per rule) make this debuggable: a rule with zero hits below a rule with many is the classic misordering.
Filters exist at several places on the path and a packet must pass all of them: the cloud network (security groups, network ACLs), a dedicated network firewall at a site boundary, and the host firewall in the destination kernel. A working rule in one place and a missing rule in another is the usual "but I opened the port" outcome.
- Match fields: src/dst address or prefix, protocol (
tcp/udp/icmp), src/dst port, TCP flags, input/output interface, connection state. - Verdicts: accept; drop (silent — the sender sees a timeout); reject (loud — the sender sees "refused" or "unreachable").
- First match wins; the chain policy decides the rest. Default-deny inbound, allow-list what must be reachable.
Stateless vs stateful
A stateless filter judges each packet alone. To allow an outbound HTTPS connection it needs a rule for the outgoing packets (dst port 443) and a rule for the returning ones (src port 443, and for TCP the ACK flag set, to at least stop unsolicited SYNs). Every allowed conversation needs its reply direction spelled out, and UDP replies cannot be distinguished from unsolicited packets at all. Cloud network ACLs work like this, which is why they need an inbound rule for ephemeral ports 1024–65535 to let return traffic through.
A stateful filter keeps a connection table: when the first packet of a flow is accepted (the SYN outbound), an entry for its 5-tuple is created, and later packets that match an existing entry — in either direction — are accepted by a single rule (ct state established,related accept) without walking the rest of the list. Linux calls this conntrack; every security group in AWS, GCP and Azure is stateful in exactly this sense; so is the firewall in every home router, which is also its NAT table (NAT: Many Private Hosts Behind One Public Address). related covers helper flows the kernel understands, such as an ICMP error about an existing connection or FTP’s data channel.
State makes rules short and safe — "allow what I started, and only 22 and 443 inbound" — and introduces a resource: the table has a maximum size (nf_conntrack_max) and entries live until the connection ends or an idle timeout expires (Linux default 5 days for established TCP, 30 s for unreplied UDP). A full table drops new connections with nf_conntrack: table full, dropping packet in the kernel log while everything already connected keeps working.
table inet filter {
chain input {
type filter hook input priority 0; policy drop; # default-deny
ct state established,related accept # -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ct state invalid drop
iif lo accept # -A INPUT -i lo -j ACCEPT
ip protocol icmp accept # keep ICMP: ping, and PMTUD "fragmentation needed"
ip6 nexthdr icmpv6 accept # IPv6 does not work without ICMPv6 (neighbor discovery)
tcp dport 22 ip saddr 10.0.0.0/8 accept # -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
tcp dport { 80, 443 } accept
# everything else: policy drop → the sender sees a timeout, not a refusal
}
}
# counters show which rule matched: nft list ruleset -a / iptables -L INPUT -v -nWhy ping fails while the service works
ping: What an Echo Actually Proves uses ICMP echo, not TCP. A rule set that allows tcp/443 and drops everything else drops ICMP echo requests, so ping api.example.com reports 100 % loss while curl https://api.example.com works perfectly. The reverse is also common: ICMP allowed, the port closed — ping succeeds and the service is unreachable. Ping answers "is the host up and is ICMP allowed", never "is the service up" (the "ping fails, service works" interview question).
Blocking all ICMP is a mistake with a delayed cost. ICMP type 3 code 4, "fragmentation needed", is how a router tells a sender its packet is too large for the next link; path MTU discovery depends on it. Drop it and connections that send small packets (the handshake, small requests) work while large transfers stall forever — the MTU black-hole pattern (the mtu-blackhole challenge), in which a curl of a small file works and a large one hangs. Allow ICMP, or at least type 3 and, for IPv6, all of ICMPv6, which the protocol cannot function without.
The debugging signature: timeout vs refused
When a client sends a SYN and a firewall drops it, nothing comes back. The client’s TCP retransmits the SYN with exponential backoff — on Linux tcp_syn_retries = 6, so at 1, 3, 7, 15, 31, 63 s — and gives up after ~127 s with ETIMEDOUT: "Connection timed out". When the SYN reaches the host and nothing is listening on that port, the kernel answers immediately with a RST, and the client fails in one RTT with ECONNREFUSED: "Connection refused". A firewall configured to reject rather than drop also produces an immediate failure — ECONNREFUSED if it sends a TCP RST or an ICMP port-unreachable, "No route to host" (EHOSTUNREACH) for the other ICMP unreachable codes.
So the symptom names the culprit before you log in anywhere. A slow timeout means the packets are being discarded somewhere on the path (a firewall, a security group, a black-holed route). An instant refusal means the packets arrived at a host that had no listener — wrong port, the process not started, or bound to 127.0.0.1 instead of 0.0.0.0. nc -vz host 8443 and curl -m 5 are enough to tell them apart; tcpdump -ni eth0 port 8443 on the server shows whether the SYN ever got there, which separates a network filter from a host one.
| Client symptom | What happened on the wire | Look at |
|---|---|---|
Connection timed out after ~2 min | SYN dropped silently; no reply of any kind | security group / network firewall / host firewall DROP; routing black hole |
Connection refused instantly | SYN reached the host; kernel sent RST (no listener) | is the process running? which port? bound to which address? |
Connection refused or No route to host instantly | firewall REJECT sent RST or ICMP unreachable | a firewall with reject rules; check its counters |
| Handshake ok, small requests ok, large transfer hangs | ICMP fragmentation-needed blocked | MTU black hole |
ping fails, service works | ICMP echo dropped, TCP allowed | nothing — but stop using ping as a health check |
Key points
- A firewall evaluates an ordered rule list against addresses, protocol, ports and flags; first match wins; the chain policy handles the rest, and it should be drop.
- Stateless filters need both directions spelled out; stateful ones keep a connection table so replies to accepted flows pass with one rule.
- Filters stack: cloud security groups, network ACLs, dedicated firewalls and the host kernel’s filter must all allow the packet.
- Ping tests ICMP, not your service; blocking all ICMP also kills path MTU discovery and, on IPv6, the protocol itself.
- Drop → slow timeout (SYN retransmitted for ~2 min on Linux). Nothing listening → instant RST, "connection refused". Reject → instant refusal or "no route to host".
- The connection table is a resource: a full
conntracktable drops new connections while existing ones keep working.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why default-deny?
Because an allow-list of what must be reachable is finite and reviewable; a deny-list of what must not is neither. Every service that starts listening on a new port is exposed by a default-allow policy the moment it starts.
▸Why drop rather than reject?
Drop tells an attacker nothing — a scan cannot distinguish a filtered port from a dead host — and wastes their time in retransmits. Reject is friendlier to legitimate clients, which fail fast. Internal firewalls often reject; edge firewalls usually drop.
▸Why do stateful firewalls exist if stateless ones can do the job?
Because the stateless rule for return traffic is either wrong (allows unsolicited packets to ephemeral ports) or unwritable (UDP). Remembering that the connection was initiated from inside is the only accurate way to say "let replies in".
Firewall rules
- 1allow established / relatedaccept
- 2allow tcp/443 from anyaccept
- 3allow tcp/22 from 10.0.0.0/8accept
- 4deny icmpdrop
- ∅default policydrop
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
tcp dport 443 accept
ip saddr 10.0.0.0/8 tcp dport 22 accept
ip protocol icmp drop
}
}Linux: nftables (or iptables) on the input hook; cloud security groups and Windows Defender Firewall are stateful by default too. drop vs reject is a policy choice: reject is kinder to your own clients, drop tells strangers less.
How it fails
What the failure looks like from inside real software.
- New service unreachable,
curlhangs two minutes then times out: the security group or host firewall never had a rule for the port;tcpdumpon the host shows no SYN arriving. - Works from the bastion, times out from the office: a network ACL permits the bastion subnet only; the host firewall was correctly opened and is not the problem.
pingfails, monitoring pages the on-call, the service is fine: ICMP echo is dropped; the health check should have been a TCP connect or an HTTP probe.- Small API calls succeed, file uploads hang at the same byte count every time: all ICMP blocked, PMTUD broken; the MTU black hole classic.
- After a traffic spike, new connections fail with
nf_conntrack: table fullindmesgwhile existing sessions are fine: the connection table hitnf_conntrack_max. - Rule added, still blocked: it was appended below a
drop all; the rule’s packet counter reads zero.