Securityfirewalliptablesnftablessecurity groupstateful

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.

ConceptualLinux
▶ InteractiveInterview question
Progress

The problem

A new service on port 8443 is unreachable from the office: 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

Linux

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.

A minimal stateful host firewall in nftables (Linux); the equivalent iptables rules are shown as comments
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 -n

Why 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

Linux

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.

What the client sees, and what it means
Client symptomWhat happened on the wireLook at
Connection timed out after ~2 minSYN dropped silently; no reply of any kindsecurity group / network firewall / host firewall DROP; routing black hole
Connection refused instantlySYN 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 instantlyfirewall REJECT sent RST or ICMP unreachablea firewall with reject rules; check its counters
Handshake ok, small requests ok, large transfer hangsICMP fragmentation-needed blockedMTU black hole
ping fails, service worksICMP echo dropped, TCP allowednothing — 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 conntrack table 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

Firewall rules, first match wins
Toggle rules, fire test packets, watch the ordered walk. Replies pass only because the firewall remembers the flow.
Connection tracking
Default policy
ConceptualLinux
Rules (evaluated top to bottom)
  1. 1allow established / relatedaccept
  2. 2allow tcp/443 from anyaccept
  3. 3allow tcp/22 from 10.0.0.0/8accept
  4. 4deny icmpdrop
  5. default policydrop
Send a test packet
Pick a packet. The firewall walks the list from the top and stops at the first rule that matches; nothing matching falls through to the default policy.
Equivalent nftables ruleset
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, curl hangs two minutes then times out: the security group or host firewall never had a rule for the port; tcpdump on 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.
  • ping fails, 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 full in dmesg while existing sessions are fine: the connection table hit nf_conntrack_max.
  • Rule added, still blocked: it was appended below a drop all; the rule’s packet counter reads zero.