Skip to main content

Failure Lab - Reproduce Each Network Failure Locally

TL;DR
  • You learn an error by causing it. Every failure here is reproduced on loopback, in your own container or against a documentation host, so nothing touches a real service.
  • The core lab is refused versus timeout. No listener answers a SYN with an RST at once. A firewall DROP answers with silence and the client retransmits. A firewall REJECT answers with a reset or an ICMP error.
  • Each lab has the same shape: goal, command, symptom, what to look at, and the undo command next to every command that changes the system.
  • macOS and Linux differ. The macOS labs below were run for real (dated). Anything needing root or Linux (iptables, tc netem, ip link, nstat) is marked illustrative.

Mental model​

ANALOGYEveryday

A fire drill. Nobody learns the evacuation route by reading the sign; they learn it by walking it once when nothing is burning. Each lab here is one drill: you trigger the exact alarm (a closed door, a silent phone, a hang-up) on a quiet day, so that on a bad day the symptom is familiar.

Where it breaks: a drill is staged and complete. A real failure arrives with noise from everything else on the network, and several of these causes look alike from the client side, so the lab teaches the signature, not the diagnosis.

ANALOGYFor engineers

A fault-injection test suite. Each lab is a unit test with a known input (a closed port, a dropped packet, an expired certificate) and an expected exception (ECONNREFUSED, ETIMEDOUT, a verify error). Run them once and you have a reference table to compare production errors against.

Where it breaks: unit tests assert a single outcome. These labs also have a time dimension (the timeout is the symptom), so you need a stopwatch, curl -w or a packet capture, not only an exit code.

How it works​

The setup, and what is safe​

Everything runs against 127.0.0.1, ports you pick (8000 to 9999 here), documentation hostnames, or a Linux container or VM you own. Three rules keep it safe.

  • Never run iptables, tc or pfctl rules on a production host. They apply to all matching traffic on the machine, including your SSH session.
  • Every system-changing command has its undo next to it. Run the undo before you close the terminal.
  • Use only hosts meant for failure. badssl.com publishes deliberately broken certificates, .invalid is a reserved top-level domain that never resolves (RFC 6761 section 6.4), and 192.0.2.1 is in a documentation range that is not routed (RFC 5737).

The tools are the ones in this module: ss, nc, lsof and ip for sockets, tcpdump and Wireshark for the wire, curl and openssl for the client, dig for DNS, and ping and mtr for reachability. What each error means is on Error Messages to Cause; the layer-by-layer method is on Debugging by Layer.

No listener-j DROP-j REJECT (ICMP)clientserverclientserverclientserverSYNRST,ACKSYN t=0SYN t=1 sSYN t=3 sSYNICMP unreachECONNREFUSED, at onceETIMEDOUT after retriesECONNREFUSED, at once
The same SYN, three server-side states. No listener answers instantly with RST. A DROP rule says nothing, so the client's kernel retransmits the SYN after 1 s and again 2 s later (cumulative t=3 s) with the gap doubling each time. A REJECT rule answers instantly with an ICMP error, which TCP reports as refused.

The lab index​

#FailureWhereSymptomUndo
1Connection refusedmacOS and Linux, ran on macOScurl: (7) in 0 msnothing to undo
2Connect timeout (DROP vs REJECT)Linux, illustrative; macOS pfctlcurl: (28) after the timeoutiptables -D, pfctl -d
3Read timeoutmacOS and Linux, ran on macOScurl: (28) with connect time near 0stop the server
4Connection resetmacOS and Linux, ran on macOScurl: (56), ECONNRESETnothing to undo
5Address in use and TIME_WAITmacOS and Linux, ran on macOSEADDRINUSEstop the server
6DNS NXDOMAIN, SERVFAIL, stallmacOS and Linux, ran on macOSstatus: NXDOMAIN, SERVFAIL, timeoutnothing to undo
7TLS cert mismatch and expiredmacOS and Linux, ran on macOScurl: (60)nothing to undo
8Loss and latencyLinux tc netem, macOS dnctl; illustrativeRTT and loss in pingtc qdisc del, dnctl flush
9PMTUD black holeLinux, illustrative; macOS local variant ransmall works, large hangs or errorsip link set mtu back
10Port exhaustionLinux, illustrativeEADDRNOTAVAILrestore the sysctl
11Slow receiver and zero windowmacOS and Linux, ran on macOSsend() blocks, win 0stop both ends

Hands-on​

Dated output below was captured on macOS (Darwin 27.0.0, curl, nc, dig 9.10.6, Python 3.14) on 2026-10-14. Output is abridged. Blocks marked "illustrative" were not run on this machine because they need root or Linux; their output is typical, not captured.

Lab 1: connection refused​

Goal: see what an RST in reply to a SYN looks like to the client. Nothing listens on port 9999.

curl -sS --connect-timeout 3 http://127.0.0.1:9999/; echo rc=$?
curl: (7) Failed to connect to 127.0.0.1 port 9999 after 0 ms: Couldn't connect to server
rc=7

after 0 ms is the signature: the kernel replied immediately. With curl -v the line reads connect to 127.0.0.1 port 9999 ... failed: Connection refused. To watch the wire (needs root, illustrative), capture loopback and run the curl again:

sudo tcpdump -i lo0 -nn 'tcp port 9999' # Linux: -i lo
IP 127.0.0.1.50088 > 127.0.0.1.9999: Flags [S], seq 1829183840, win 65535
IP 127.0.0.1.9999 > 127.0.0.1.50088: Flags [R.], seq 0, ack 1829183841, win 0

A SYN, then [R.] (RST plus ACK) with no handshake in between. That is RFC 9293 section 3.5.2 reset generation. Now start a listener to see the success case, and find it:

python3 -m http.server 8765 --bind 127.0.0.1 &
curl -sS -o /dev/null -w 'code=%{http_code} connect=%{time_connect}\n' http://127.0.0.1:8765/
lsof -nP -iTCP:8765 -sTCP:LISTEN # Linux: ss -ltnp 'sport = :8765'
code=200 connect=0.000552
COMMAND PID USER FD TYPE NODE NAME
Python 14767 abhishekr 3u IPv4 TCP 127.0.0.1:8765 (LISTEN)

Undo: kill %1 (or the PID that lsof printed). Pick a port nobody else uses: on this machine port 8000 was already taken by another listener, which is itself the next lab.

Lab 2: connect timeout, with a firewall that drops or rejects​

Goal: produce a timeout and a refusal against the same port, so the difference is something you have felt. This needs a Linux box, VM or container you own, and root. Illustrative; do not run it on a host you cannot afford to lock yourself out of.

python3 -m http.server 8000 --bind 127.0.0.1 &
sudo iptables -A INPUT -i lo -p tcp --dport 8000 -j DROP
curl -sS --connect-timeout 5 http://127.0.0.1:8000/; echo rc=$?
curl: (28) Failed to connect to 127.0.0.1 port 8000 after 5001 ms: Timeout was reached
rc=28

A listener exists and the port is open, yet the connect hangs until curl's own --connect-timeout. Without that flag, Linux gives up on its own after the SYN retries, governed by net.ipv4.tcp_syn_retries (default 6, 127 s on the classic schedule and about 131 s on recent kernels per the kernel ip-sysctl reference); see Retransmission and timers. In tcpdump -i lo -nn 'tcp port 8000' you see the same SYN repeated with the gap doubling (illustrative):

00:00:00.000 127.0.0.1.41422 > 127.0.0.1.8000: Flags [S]
00:00:01.003 127.0.0.1.41422 > 127.0.0.1.8000: Flags [S]
00:00:03.011 127.0.0.1.41422 > 127.0.0.1.8000: Flags [S]

Now swap the rule so the same port is refused instantly, two ways. Remove the DROP first:

sudo iptables -D INPUT -i lo -p tcp --dport 8000 -j DROP
sudo iptables -A INPUT -i lo -p tcp --dport 8000 -j REJECT --reject-with tcp-reset
curl -sS --connect-timeout 5 http://127.0.0.1:8000/ # refused, 0 ms
sudo iptables -D INPUT -i lo -p tcp --dport 8000 -j REJECT --reject-with tcp-reset
sudo iptables -A INPUT -i lo -p tcp --dport 8000 -j REJECT --reject-with icmp-port-unreachable
curl -sS --connect-timeout 5 http://127.0.0.1:8000/ # refused, 0 ms (TCP maps the ICMP error to ECONNREFUSED)
sudo iptables -D INPUT -i lo -p tcp --dport 8000 -j REJECT --reject-with icmp-port-unreachable
sudo iptables -S INPUT # confirm no leftover rule

Undo is the -D line that mirrors each -A; iptables -S INPUT must show none of your rules. The --reject-with options are documented in iptables-extensions(8).

macOS substitute. macOS has no iptables; the equivalent is pf. This replaces the active pf ruleset, so only do it on a personal Mac and undo it immediately (illustrative, needs root, not run here):

echo 'block drop in quick on lo0 proto tcp from any to any port 8765' | sudo pfctl -f - -e
curl -sS --connect-timeout 5 http://127.0.0.1:8765/ # times out
sudo pfctl -d && sudo pfctl -f /etc/pf.conf # UNDO: disable, reload default rules

block drop is the DROP analogue; block return is the REJECT analogue (RST for TCP, ICMP unreachable for UDP). See pfctl(8) and pf.conf(5).

Lab 3: read timeout​

Goal: a connection that opens fine and then goes silent. This is a different failure from lab 2, and curl's timing fields show it. The server below accepts and then sleeps.

cat > slow.py <<'EOF'
import socket, time
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 9100)); s.listen(5)
while True:
c, _ = s.accept(); time.sleep(60)
EOF
python3 slow.py &
curl -sS --connect-timeout 2 --max-time 3 -w 'connect=%{time_connect} total=%{time_total}\n' http://127.0.0.1:9100/
curl: (28) Operation timed out after 3008 milliseconds with 0 bytes received
connect=0.000576 total=3.008930

Same exit code 28 as lab 2, but the message differs (Operation timed out ... with 0 bytes received, not Failed to connect ... Timeout was reached) and time_connect is near zero: the handshake finished, the peer never answered. The distinction is explained in Connect timeout versus read timeout. Undo: kill %1.

Lab 4: connection reset​

Goal: get ECONNRESET. The server below reads one request, then closes with SO_LINGER set to zero, which makes close() send an RST instead of a FIN.

cat > rst.py <<'EOF'
import socket, struct
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 9200)); s.listen(5)
c, _ = s.accept()
c.recv(1024)
c.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
c.close()
EOF
python3 rst.py &
curl -sS http://127.0.0.1:9200/; echo rc=$?
curl: (56) Recv failure: Connection reset by peer
rc=56

A Python client sees the same thing as an exception: ConnectionResetError: [Errno 54] Connection reset by peer (errno 54 on macOS, 104 on Linux). The other classic route needs no code: run nc -l 9000 in one terminal, connect with nc 127.0.0.1 9000 in another, type a line, then kill the listener with the client still attached; the next write from the client gets a reset. (BSD nc as on macOS takes -l 9000; some Linux nc builds need -l -p 9000, see nc(1).) In a capture the end of the exchange is a bare [R] on an established connection, unlike lab 1 where the RST answers a SYN. Reset semantics are on Reset: the peer said abort.

Lab 5: address in use and TIME_WAIT​

Goal: see EADDRINUSE two ways. First, two servers on one port:

python3 -m http.server 8765 --bind 127.0.0.1 &
python3 -m http.server 8765 --bind 127.0.0.1
OSError: [Errno 48] Address already in use

(errno 48 on macOS, 98 on Linux.) Second, the case where nothing is running but the restart still fails: a server that closes first leaves a socket in TIME_WAIT on its own port, and a plain bind() is refused until it expires. This script closes a connection from the server side and then rebinds, with and without SO_REUSEADDR:

cat > tw.py <<'EOF'
import socket, sys
reuse = sys.argv[1] == "reuse"
def listen():
s = socket.socket()
if reuse: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 9400)); s.listen(1); return s
s = listen()
cl = socket.create_connection(("127.0.0.1", 9400))
c, _ = s.accept()
c.close(); cl.recv(1); cl.close()
s.close()
try: listen(); print("rebind ok")
except OSError as e: print("rebind failed:", e)
EOF
python3 tw.py plain; netstat -an -p tcp | grep '\.9400 .*TIME_WAIT'
python3 tw.py reuse
rebind failed: [Errno 48] Address already in use
tcp4 0 0 127.0.0.1.9400 127.0.0.1.50137 TIME_WAIT
rebind ok

The second run passed even though a TIME_WAIT entry existed, because SO_REUSEADDR lets a listener bind a port that has only TIME_WAIT leftovers. Linux: ss -tan state time-wait '( sport = :9400 )'. Why the state exists, and why the fix is not to shorten it, is on Port exhaustion and TIME_WAIT.

Lab 6: DNS failures​

Goal: three different DNS outcomes. .invalid never resolves, so it gives a clean NXDOMAIN:

dig +noall +comments nosuchname.invalid | sed -n 2,3p
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 1198
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

SERVFAIL with a safe, real example: dnssec-failed.org is a zone with a deliberately broken DNSSEC signature, so a validating resolver refuses it. Asking again with +cd (checking disabled) shows the data is there and only validation failed:

dig +noall +comments dnssec-failed.org | sed -n 2,3p
dig +noall +comments +cd dnssec-failed.org | sed -n 2,3p
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 57022
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51125
;; flags: qr rd ra cd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

(This depends on your resolver validating DNSSEC; one that does not returns NOERROR to both.) Last, the stall: send a query to an address that is in a documentation range and will never answer.

dig +time=2 +tries=1 @192.0.2.1 example.com
;; connection timed out; no servers could be reached

That took the full 2 s. A system resolver with a dead nameserver in /etc/resolv.conf stalls for its own timeout (5 s per attempt in glibc's default), then tries the next server; do not edit /etc/resolv.conf on a real machine to prove it, use dig @192.0.2.1 as above. See dig and Why dig works but the app fails.

Lab 7: TLS certificate failures​

Goal: two different verify errors, from hosts that exist for this purpose (badssl.com).

curl -sS -o /dev/null https://expired.badssl.com/
curl -sS -o /dev/null https://wrong.host.badssl.com/
curl -sS -o /dev/null https://self-signed.badssl.com/
curl: (60) SSL certificate problem: certificate has expired
curl: (60) SSL: no alternative certificate subject name matches target host name 'wrong.host.badssl.com'
curl: (60) SSL certificate problem: self signed certificate

All three exit with code 60, so match on the message. The curl trailer text ("More details here") was dropped. To see the certificate itself with openssl:

openssl s_client -connect expired.badssl.com:443 -servername expired.badssl.com </dev/null 2>/dev/null | openssl x509 -noout -subject -enddate

The notAfter date in the past is the expiry. Do not "fix" it with curl -k; that turns verification off and so removes the very check you are studying. See curl and openssl and TLS and certificate errors.

Lab 8: packet loss and latency​

Goal: make a fast link slow and lossy on purpose. Linux, root, illustrative. tc qdisc ... netem is documented in tc-netem(8). Apply it only to loopback or an interface on a box you own:

sudo tc qdisc add dev lo root netem delay 100ms 20ms loss 5%
ping -c 10 127.0.0.1 # RTT about 200 ms: delay applies each way on lo
curl -s -o /dev/null -w 'connect=%{time_connect} total=%{time_total}\n' http://127.0.0.1:8000/
sudo tc qdisc del dev lo root # UNDO
tc qdisc show dev lo # confirm: no netem line
10 packets transmitted, 9 received, 10% packet loss
rtt min/avg/max/mdev = 160.2/199.8/241.7/26.4 ms

(Illustrative; 10 probes only roughly sample a 5% loss, so a given run can show 0%, 10% or 20%.) Because loopback carries both directions of the conversation, a 100 ms delay shows up as about 200 ms RTT; apply it to a real interface of a second container to model one side. delay 100ms 20ms is mean 100 ms with 20 ms jitter. For bandwidth limits use the tbf or netem rate options in the same man page.

macOS substitute. Dummynet. This replaces the active pf ruleset, so undo it right after (illustrative, needs root):

sudo dnctl pipe 1 config delay 100 plr 0.05 # delay is in ms; plr is a 0 to 1 fraction
echo 'dummynet in quick on lo0 proto tcp from any to any port 8765 pipe 1' | sudo pfctl -f - -e
curl -s -o /dev/null -w 'total=%{time_total}\n' http://127.0.0.1:8765/
sudo dnctl -q flush && sudo pfctl -d && sudo pfctl -f /etc/pf.conf # UNDO

See dnctl(8) and pfctl(8). Apple's Network Link Conditioner (from the Additional Tools for Xcode download) does the same with a GUI profile, and is simpler for app testing. In a container, run tc inside it with docker run --cap-add NET_ADMIN; the container's network namespace is then yours alone (Docker run reference, runtime privilege and capabilities).

Lab 9: a PMTUD black hole​

Goal: small packets work and large ones do not. Mechanism: MTU, MSS and fragmentation. Linux, root, illustrative, on two network namespaces you create:

sudo ip netns add a; sudo ip netns add b
sudo ip link add va type veth peer name vb
sudo ip link set va netns a; sudo ip link set vb netns b
sudo ip -n a addr add 10.9.0.1/24 dev va; sudo ip -n b addr add 10.9.0.2/24 dev vb
sudo ip -n a link set va up; sudo ip -n b link set vb up
sudo ip -n a link set va mtu 1400 # shrink one side
sudo ip netns exec a ping -c 2 -M do -s 1372 10.9.0.2 # 1372 + 28 = 1400: fits
sudo ip netns exec a ping -c 2 -M do -s 1472 10.9.0.2 # 1500 total: too big
sudo ip netns del a; sudo ip netns del b # UNDO (removes the veth pair too)
ping: local error: message too long, mtu=1400

Here the failing interface is the sender's own, so ping is told at once. A real black hole is when the small link is in the middle and its ICMP "fragmentation needed" message is filtered, so the sender never hears and just retransmits. To model that, put a third namespace in the middle with forwarding on and an iptables -A FORWARD -p icmp -j DROP; the large ping then hangs silently instead of failing. The -M do flag sets DF on Linux. On macOS the flag is -D, and this local variant did run:

ping -D -c 1 -s 1472 127.0.0.1 # fits: lo0 MTU is 16384
ping -D -c 1 -s 20000 127.0.0.1 # does not
ping: sendto: Message too long

Lab 10: port exhaustion​

Goal: run out of ephemeral ports. Mechanism and fixes: Port exhaustion and TIME_WAIT. Linux, root, illustrative. Shrink the range so it is reachable in seconds, open connections to one destination, and read the error:

sysctl net.ipv4.ip_local_port_range # note the current value
sudo sysctl -w net.ipv4.ip_local_port_range="40000 40050" # 51 ports
python3 -m http.server 8000 --bind 127.0.0.1 &
for i in $(seq 1 80); do curl -s -o /dev/null http://127.0.0.1:8000/ || echo "fail $i"; done
sudo sysctl -w net.ipv4.ip_local_port_range="32768 60999" # UNDO: put back the value you noted

After roughly 51 connections in TIME_WAIT toward the same destination, further connects fail (curl reports Cannot assign requested address, EADDRNOTAVAIL). The default range varies by distribution and kernel; use the value you noted, not the one printed here. macOS uses net.inet.ip.portrange.first and .last (49152 to 65535 on this machine) and sudo sysctl -w the same way.

Lab 11: slow receiver and zero window​

Goal: a receiver that never reads makes the sender stall. The sink accepts and sleeps; the flooder sends until send() would block.

cat > sink.py <<'EOF'
import socket, time
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
s.bind(("127.0.0.1", 9300)); s.listen(1)
c, _ = s.accept(); time.sleep(30)
EOF
cat > flood.py <<'EOF'
import socket, time
s = socket.create_connection(("127.0.0.1", 9300))
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)
s.setblocking(False)
sent = 0
try:
while True: sent += s.send(b"x" * 16384)
except BlockingIOError:
print("send() would block after", sent, "bytes")
time.sleep(3)
EOF
python3 sink.py & python3 flood.py &
sleep 2; netstat -an -p tcp | grep -E '9300|Recv-Q'
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 65536 0 127.0.0.1.9300 127.0.0.1.50135 ESTABLISHED
tcp4 0 0 127.0.0.1.50135 127.0.0.1.9300 ESTABLISHED
send() would block after 65536 bytes

The receiver's Recv-Q is full at 65536 bytes (its receive buffer) and nobody is reading it, so it advertises a window of 0. On loopback the kernel can also buffer on the send side, so the exact byte count where send() blocks depends on the OS and buffer sizes; read the Recv-Q column, not the number printed by the script. In a capture you would see win 0 from the receiver and then the sender's zero-window probes (illustrative, needs root):

127.0.0.1.9300 > 127.0.0.1.50135: Flags [.], ack 65537, win 0

The mechanism is Flow control and the receive window. On Linux read the same state with ss -tn 'sport = :9300'. Undo: stop both scripts (kill %1 %2).

Gotchas​

GOTCHAS4
A firewall that "blocks" a port makes it refuse the connection
Every curl exit code 28 is the same failure
Leave the tc or pf rule in place and the machine is just slow now
curl -k is a way to read the TLS error

Interview questions​

INTERVIEWHow would you reproduce a firewall drop versus a reject locally?mid
Show answer

Run a listener on a port, then add an iptables -A INPUT -p tcp --dport PORT -j DROP rule and curl it with --connect-timeout. The client hangs and tcpdump shows the SYN retransmitted with doubling gaps. Delete the rule with -D and add -j REJECT --reject-with tcp-reset and the same curl is refused in 0 ms. Doing it on loopback or in a namespace keeps it off real traffic.

Follow-ups:

  • Why do clouds drop rather than reject? A silent drop reveals nothing to a scanner and costs the host no reply traffic.
  • What does REJECT --reject-with icmp-port-unreachable look like to a TCP client? Also ECONNREFUSED, since TCP maps the ICMP error to a refusal.
INTERVIEWHow do you simulate packet loss and latency on a Linux host?mid
Show answer

Use tc with the netem qdisc: tc qdisc add dev eth0 root netem delay 100ms 20ms loss 5% adds 100 ms of delay with 20 ms jitter and 5% random loss, and tc qdisc del dev eth0 root removes it. Test with ping and curl -w. Do it on a container or namespace interface you own so it affects only the system under test.

Follow-ups:

  • What is the tradeoff? It shapes outgoing traffic at one interface only, so for symmetric effects you need both ends, and loopback doubles the delay.
  • How would you limit bandwidth? tbf or the rate option, in the same man page.
INTERVIEWHow would you test your service's behaviour under a slow or lossy network?senior
Show answer

Inject faults at a layer the service cannot cheat around. At the kernel, tc netem (delay, jitter, loss, reorder, duplication, rate) on the test host or the container's namespace. At the proxy, a tool such as Toxiproxy sits between service and dependency and adds latency, drops or slow close per connection, which is easy to script in CI. Then assert on the behaviour you care about: timeouts fire at the configured value, retries back off and are bounded, and the service sheds load instead of queueing forever.

Follow-ups:

  • Why not only unit tests with mocks? A mock cannot produce a half-open connection or a zero window; the kernel is the source of those symptoms.
  • What about jitter? Real paths vary, so a fixed delay hides timeouts that only trip on the tail; set a jitter value and watch p99.
INTERVIEWHow would you reproduce ECONNRESET?mid
Show answer

Make the server close with an abortive close. In Python, set SO_LINGER to (1, 0) on the accepted socket and call close(); the kernel sends an RST instead of a FIN, and the client's next read fails with ECONNRESET. Without code, kill a nc -l listener while a client is still connected and write again. In a capture it is a bare RST on an established connection.

Follow-ups:

  • How is that different from refused? Refused is an RST in answer to a SYN, before any connection exists.
INTERVIEWA connect hangs for about two minutes then fails. What did you just reproduce?mid
Show answer

A connect timeout with no application timeout set: the kernel retransmits the SYN with doubling gaps and gives up after tcp_syn_retries attempts, about 127 seconds on Linux with the default of 6 on the classic schedule (about 131 seconds on recent kernels). Cause on the path: a drop rule, a dead host or a bad route. Always set an explicit connect timeout in the client so it fails in seconds.

Follow-ups:

  • How do you tell a drop from a dead host? Capture on the server: if the SYN arrives and nothing is sent back, it is a drop or the process; if it never arrives, it is the path.

Fun facts​

FACTSTrivia worth knowing
  • The .invalid top-level domain is reserved so that it never resolves, which makes it a safe NXDOMAIN source (RFC 6761).
  • 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24 are set aside for documentation and are not supposed to be routed, so a packet sent to one is a reliable silent black hole (RFC 5737).
  • netem was written by Stephen Hemminger for Linux, and its man page, tc-netem(8), also covers reordering, duplication, corruption and rate limiting.
  • On this macOS run, a rebind that failed with Address already in use succeeded as soon as SO_REUSEADDR was set, with the TIME_WAIT entry still present.

Cheat sheet​

FailureMake it withYou seeUndo
Refusedcurl to a port with no listenercurl: (7), 0 ms; SYN then RSTnone
Connect timeoutiptables -j DROP (macOS: pfctl block drop)curl: (28), SYN retransmitsiptables -D, pfctl -d
Read timeoutserver that accepts and sleepscurl: (28), connect near 0kill the server
ResetSO_LINGER (1, 0) then closecurl: (56), ECONNRESETnone
Address in usetwo servers on one port; restart in TIME_WAITEADDRINUSEkill the server
NXDOMAINdig nosuchname.invalidstatus: NXDOMAINnone
SERVFAILdig dnssec-failed.orgstatus: SERVFAILnone
TLScurl https://expired.badssl.com/curl: (60)none
Loss and latencytc qdisc add dev lo root netem ... (macOS: dnctl pipe)ping RTT and losstc qdisc del, dnctl -q flush
MTU black holeip link set va mtu 1400 with ping -M domessage too long, or a silent hangdelete the namespaces
Zero windowreceiver that never readswin 0, send() blockskill both ends