Runbook: Network incident - service unreachable triage
1 · Prerequisites
Confirm every item is in place before any state change.
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Confirm the symptom by reproducing from a known-good source
- · Capture the exact error: connection refused, timeout, no route, etc.
- · Identify whether one host or many are affected
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Test DNS resolution with dig
- 2Test routing with ip route get, then the port with nc -vz, before trusting ping
- 3Test the layer 3 path with traceroute -T
- 4Test the path MTU with tracepath and ping -M do when large transfers hang
- 5Test layer 2 with arping, but only where ip route get shows the target is on-link
- 6Test the target service with ss on the target host, reading the local-address column
- 7Prove or clear the target host firewall with tcpdump and rule counters
- 8Capture all output to an incident log file
- 9Apply the fix for the identified layer
- 10Verify the original reproducer succeeds
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓The original reproducer now succeeds
- ✓curl -I https://<hostname> returns 200 (or expected status)
- ✓ss -tn state established shows the connection
- ✓No new errors in the journal
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Restore previous network configuration from backup
- ↶Restart affected services
- ↶Revert any firewall rules added during debugging
- ↶Remove any temporary /etc/hosts entries
6 · Escalation
When the runbook isn't enough, contact:
- · If the failure involves multiple hosts, escalate to senior engineering immediately
- · If the fix requires changing a production firewall or routing, escalate before changing
- · If root cause is not identified within 30 minutes, escalate
- · If the same failure has occurred twice in a week, escalate for permanent fix
This runbook triages a “service unreachable” report. Use it when a user, monitor, or service reports it cannot reach a specific service on a known host. The goal is to identify the failing layer and restore service within 30 minutes for most incidents.
When to use this runbook
Use this runbook when:
- A user reports “X is down” or “I cannot reach Y”.
- A monitor reports a connectivity alert.
- An integration test or health check fails.
- A new host or service is not reachable from expected clients.
Inputs
Gather before starting:
- Service name: what the user is trying to reach (ssh, database, web app, internal API).
- Target host: hostname or IP.
- Target port: the specific port the service listens on.
- Source: where the user is connecting from (their host, an internal network, etc.).
- When it started: timestamp or approximate time.
Procedure
Step 1: Confirm the symptom
Reproduce the failure from a known-good source (your workstation or a debug host):
nc -vz target.example.com 80
curl -I https://target.example.comNote the exact error: “Connection refused”, “Connection timed out”, “No route to host”, or another message. The error maps to a layer.
Step 2: Test DNS
dig target.example.com
dig @8.8.8.8 target.example.comIf DNS fails, jump to the DNS failure runbook. If DNS works but the IP is unexpected, check the resolver cache and the authoritative zone.
Step 3: Test routing, then reachability
Ask the local kernel what it will do with the packet before
you send anything onto the wire. ip route get is answered by
the FIB on this host, so no firewall anywhere can filter it and
its answer is never ambiguous:
ip route get <target-ip>Read the source address and the outgoing interface, not just
the presence of a route. A route via the wrong interface or
with an unexpected src is a real layer 3 finding.
Then probe the port you actually need:
nc -vz <target-ip> <port>Only then, optionally, ICMP:
ping -c 3 target.example.com
ping -c 3 <target-ip>If ping fails by hostname but works by IP, the problem is DNS (already addressed in step 2).
Step 4: Test the layer 3 path
traceroute -T -p <port> target.example.com
mtr -T -P <port> target.example.comIf traceroute stops with * * * early, routing or filtering
is breaking. If the path looks complete, the issue is at
the target.
Step 4a: Test the path MTU
Run this whenever the symptom is not a clean failure: the connection opens, the TLS handshake completes, small requests succeed, and then a larger response hangs or stalls part-way. That shape is path MTU, and no other step in this runbook will find it.
TARGET=target.example.com
tracepath -n "$TARGET"
ping -M do -s 1472 -c3 "$TARGET" # 1472 + 28 = a 1500-byte packet
ping -M do -s 1372 -c3 "$TARGET" # 1400 MTU, typical of a tunnelIf the 1472 probe reports Message too long or simply times
out while a plain ping succeeds, the path MTU is below 1500.
A tunnel (VPN, VXLAN, GRE) somewhere on the path is the usual
cause, together with a firewall dropping the ICMP
fragmentation needed messages that would otherwise let PMTUD
discover it. Escalate rather than clamping MTU on the host: the
fix belongs on the device that owns the tunnel.
Step 5: Test layer 2 — only for an on-link target
Skip this step unless the kernel says the target is on your own
segment. arping is a link-local probe: it puts an ARP request on
the wire and waits for the owner of that address to answer. For
anything reachable via a gateway it will always fail, and that
failure is the expected result, not a finding. Running it against
a remote target and concluding “layer 2 is broken” sends the
incident down a path that has no end.
Ask the kernel which case you are in, and read the answer you already captured in step 3:
ip route get <target-ip>- No
viain the output (... dev eth0 src 10.0.0.10): the target is on-link. ARP is meaningful — probe the target. - A
viain the output (... via 10.0.0.1 dev eth0 src ...): the target is off-subnet. Probe the gateway instead. It is the only layer 2 neighbour on the path that you own.
Then check the neighbour cache before generating traffic — the kernel has usually already answered this question:
ip neigh show <ip>
arping -I <iface> -c 3 <ip>REACHABLE means layer 2 is fine and this step is done. FAILED
means the kernel already tried and got nothing. STALE is normal
and is not a finding.
An arping that fails for an address the kernel says is
on-link narrows to four causes, and three of them are not
“the network is broken”:
- The host is genuinely down, or its NIC is gone.
net.ipv4.conf.*.arp_ignoreorarp_filteron the target is suppressing the reply. Common on multi-homed hosts, and it affects only ARP — the service itself may be perfectly reachable.- Switch port security or dynamic ARP inspection is dropping it.
- You passed the wrong interface to
-I. Compare it against thedevthatip route getprinted, and do not assumeeth0.
Step 6: Test the target service
On the target host, or via SSH:
ss -tlnp '( sport = :<port> )'
systemctl status <service>
journalctl -u <service> -n 50Read the local-address column of the LISTEN line, not just the fact that a line came back.
If nothing is listening at all, the temptation is to restart the service. Collect the evidence first — a restart destroys most of it, and “restart as the first action” is the top pitfall in the methodology lesson this runbook depends on.
# Why did it stop? Capture this BEFORE any restart.
systemctl status <service> --no-pager -l
journalctl -u <service> --since "1 hour ago" --no-pager | tail -100
# Has it been flapping? NRestarts is the whole story in one line.
systemctl show <service> -p NRestarts,ExecMainStatus,Result
# Did the packet even arrive?
sudo tcpdump -i any -nn "tcp port <port> and tcp[tcpflags] & tcp-syn != 0"ExecMainStatus and Result tell you whether it exited cleanly,
was OOM-killed, or failed to start. NRestarts tells you whether
this is the first time. A service that has restarted 40 times has
a different problem from one that stopped once.
Write down a hypothesis for why it stopped. Then, and only then:
sudo systemctl restart <service>A restart that brings the service back is a mitigation, not a diagnosis. It restores the SLA and it buys you time to read the journal you just captured. If you close the incident here, the same page arrives at 03:00 with the evidence gone.
Step 6a: Prove whether the target’s firewall is dropping it
If the listener is bound correctly and remote clients still
time out, find out whether their SYN reaches the host at all.
tcpdump taps below netfilter, so it sees packets the firewall
is about to drop:
PORT=5432
sudo timeout 30 tcpdump -i any -nn "tcp port $PORT and tcp[tcpflags] & tcp-syn != 0"| What you see | What it means |
|---|---|
| SYN in, nothing out | The local firewall is dropping it |
| SYN in, RST out | Nothing listening on that address, or a REJECT rule |
| SYN in, SYN-ACK out | This host is fine; look at the return path or the client |
| No SYN at all | Upstream: routing, security group, or a filter on the path |
Then read the ruleset and its counters - a rule with a rising counter is the rule eating your traffic:
PORT=5432
sudo nft list ruleset | grep -n "$PORT"
sudo nft list chain inet filter input
# iptables hosts:
sudo iptables -L INPUT -v -n --line-numbersStep 7: Capture evidence
Throughout the procedure, capture:
# All command outputs to a single file
exec > /tmp/incident-$(date +%Y%m%d-%H%M%S).log 2>&1
echo "Incident: $SERVICE unreachable"
echo "Time: $(date)"
echo "Source: $(hostname)"
echo "Target: $TARGET:$PORT"
echo "---"
# ... run all commands ...The log file is the input to the post-incident review.
Step 8: Restore service
Common fixes, by failure layer:
- DNS: flush caches (
resolvectl flush-caches), check authoritative zone. - Routing:
ip route getandip route addfor missing routes. - Firewall: check iptables/nftables rules; check the cloud security group.
- Service: restart the service. If it fails to start, check logs.
- Hardware: replace cable, NIC, or escalate to ops.
Step 9: Verify and document
After the fix:
nc -vz target.example.com <port>
curl -I https://target.example.comConfirm the original reproducer now succeeds.
Write a one-paragraph incident summary: what was the symptom, what was the cause, what fixed it, what should change to prevent recurrence.
Common patterns
| Symptom | Likely cause | Fix |
|---|---|---|
| Connection refused | Nothing listening on that address (often a loopback bind), or a REJECT rule | Read the local-address column of the LISTEN line; fix the bind, not the network |
| Connection timed out | Firewall dropping silently, or host down | Step 6a: tcpdump plus rule counters |
| Name resolution failed | DNS misconfiguration | Fix DNS, flush cache |
No route to host (EHOSTUNREACH) | ARP/ND failed for an on-link destination, or a REJECT with icmp-host-unreachable. Rarely routing | ip route get then ip neigh show; FAILED is layer 2 |
Network is unreachable (ENETUNREACH) | Genuinely no matching route | Check ip route, gateway |
| TLS handshake failure | Certificate or cipher | Check cert expiry, SAN, and TLS version |
| Connects, then large transfers hang | Path MTU below 1500 with PMTUD blocked | Step 4a: tracepath, ping -M do -s 1472 |
| Slow but working | Congestion or interface errors | Check interface error/drop counters |
Escalation
Escalate to senior engineering when:
- The failure involves multiple hosts.
- The fix requires a change to a production firewall or routing configuration.
- The root cause is not identified within 30 minutes.
- The same failure has occurred twice in a week (recurring incident).
Bring: the symptom, the evidence (logs from the incident capture), the changes tried, the hypothesis about root cause.