TCP/IP: Find the Broken Layer

“The network is down” is not a diagnosis. A connection crosses name lookup, routing, a destination port, TCP, usually TLS, and finally an application protocol. Test them in that order and the vague outage becomes one failed step.

🎙️ Published & recorded: ·

01Layers turn one request into packets

Your application writes bytes to a socket. TCP splits the byte stream into segments, tracks order, and retransmits missing data. IP puts those segments into packets and moves each packet toward a destination address. Ethernet or Wi-Fi carries frames across the current link. Routers care about IP; they do not understand your JSON error.

HTTP request bytes
  ↓ application
TLS records              # when HTTPS is used
  ↓
TCP segments             # ordered, reliable byte stream
  ↓
IP packets               # source and destination addresses
  ↓
Ethernet / Wi-Fi frames  # one local hop at a time
The model is a debugging toolIf DNS cannot produce an address, TCP has not started. If the TCP connection is refused, HTTP has not started. If TLS rejects a certificate, changing a REST route cannot help. Stop using “API problem” for failures that happen before any API request exists.

02DNS names; IP addresses route

DNS translates a hostname into one or more IP addresses. IPv4 looks like four decimal numbers; IPv6 is hexadecimal and much larger. After resolution, IP routing chooses a path toward the address. A hostname can return different addresses by region, network, or time, so record the address the failing client actually received.

# Resolve without making a TCP connection
nslookup api.example.com
# or
dig api.example.com A +short
dig api.example.com AAAA +short

# Inspect the route, not the application
tracert api.example.com       # Windows
traceroute api.example.com    # Linux/macOS

Do not type a production IP into an HTTPS URL and call that a clean DNS test. TLS certificate selection and HTTP virtual hosts need the hostname. Use curl --resolve to pin an address while preserving the real host name.

03A port selects the listening process

An IP address gets traffic to a machine or network interface. A port gets it to a service. A listening socket is bound to a local address and port. A connected TCP socket is identified by source address, source port, destination address, and destination port, so thousands of clients can talk to the same server port at once.

client  192.0.2.44:53182  →  203.0.113.10:443  server
        temporary port                   HTTPS listener

# See listeners
ss -lntp                       # Linux
netstat -ano | findstr LISTEN  # Windows
lsof -nP -iTCP -sTCP:LISTEN    # macOS/Linux
EADDRINUSE, fixed step by stepError: listen EADDRINUSE: address already in use 0.0.0.0:3000. One: inspect port three thousand with ss, lsof, or netstat -ano. Two: map the PID to a process. Three: stop the stale instance gracefully, or configure a different port. Four: restart and verify exactly one expected listener. Killing every process named Node hides supervision bugs and can stop unrelated apps.

04The TCP handshake proves both directions

Before application bytes flow, the client sends SYN, the server replies SYN-ACK, and the client answers ACK. This negotiates initial sequence numbers and confirms a path in both directions. After that, TCP presents an ordered byte stream. It has no messages: two writes can arrive as one read, and one write can require several reads.

client                                  server
  | -------- SYN, seq=x --------------> |
  | <----- SYN-ACK, seq=y, ack=x+1 ---- |
  | -------- ACK, ack=y+1 ------------> |
  | ===== application byte stream ===== |
  | -------- FIN ---------------------> |  # orderly close

TCP reliability is not infinite patience. Retransmissions happen below your app, but the application still needs connect, read, and overall deadlines. A successful handshake proves only that something accepted TCP on that address and port. It does not prove TLS, authentication, or the requested HTTP route works.

05Connection refused is not a timeout

Refused is a fast answer: the destination host or an active reject rule said nothing is accepting that connection. Timeout is silence: packets were dropped, routed nowhere useful, or replies could not return. Treating both as “server down” wastes the information the network already gave you.

Error: connect ECONNREFUSED 127.0.0.1:5432
Error: connect ETIMEDOUT 203.0.113.10:443

# Test only TCP reachability to the destination port
Test-NetConnection api.example.com -Port 443  # PowerShell
nc -vz api.example.com 443                    # Linux/macOS
curl -v --connect-timeout 5 https://api.example.com/
ECONNREFUSED, fixed step by stepOne: verify the host and port in the error, especially accidental 127.0.0.1. Two: on the server, confirm the service is running. Three: inspect the listener's bound address and port. Four: test locally on the server, then from the client. If local works and remote is refused, inspect bind address, container publishing, and active reject rules.
ETIMEDOUT, fixed step by stepOne: resolve the hostname and record every IPv4 and IPv6 result. Two: run a port-specific test, not just ping. Three: inspect client egress, cloud security rules, host firewall, route, VPN, and NAT return path. Four: capture traffic at the server: no SYN means the path dropped it; SYN with no reply means the server path or firewall is wrong. Randomly restarting the app is not a timeout diagnosis.

06localhost is private; 0.0.0.0 is a bind instruction

127.0.0.1 and ::1 are loopback addresses. Traffic never leaves that network namespace. A server bound to loopback accepts only local clients. Binding to 0.0.0.0 means listen on all IPv4 interfaces; it is not a destination that clients should use. In a container, localhost means that container, not the host and not another container.

# Reachable only inside the same host or container
server.listen(3000, "127.0.0.1")

# Reachable on the container's interfaces; publish separately
server.listen(3000, "0.0.0.0")
docker run -p 8080:3000 my-app

# Verify the actual bind
ss -lntp | grep :3000
LISTEN 0 511 127.0.0.1:3000 ...
Container works inside, refuses outsideOne: enter the container and curl http://127.0.0.1:3000. Two: inspect ss -lnt; if it shows 127.0.0.1:3000, change the app bind to 0.0.0.0. Three: verify the container publishes the port, for example host eight thousand eighty to container three thousand. Four: test the host port. Five: only then inspect host or cloud firewalls. Exposing the port cannot repair an app bound to container loopback.

07NAT rewrites private conversations

Private IPv4 ranges are reusable inside networks and are not routed across the public internet. A NAT gateway rewrites private source addresses and ports to a public address for outbound connections, then maps replies back. Unsolicited inbound traffic has no mapping unless you add port forwarding or a load balancer.

10.0.0.24:53182  →  NAT  →  198.51.100.7:62014  → internet
private host                  public mapping

Private IPv4:
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16

# Public-looking address from outside
curl https://ifconfig.example  # use a trusted endpoint you control

NAT is not a security policy. Stateful mappings happen to block most unsolicited inbound traffic, but authorization belongs in a firewall and the application. Also remember carrier-grade NAT: two routers may translate the connection, making home port forwarding impossible without provider support, IPv6, a tunnel, or a relay.

08Firewalls decide by direction, tuple, and state

A firewall rule usually considers protocol, source, destination, port, direction, interface, and connection state. Cloud networks add security groups and network ACLs before the host firewall. Kubernetes adds Services and NetworkPolicies. One green rule in one dashboard proves very little if another layer drops the same packet.

# Minimum question, stated precisely
Allow TCP from 198.51.100.0/24
         to 10.0.2.15 destination port 443
         with return traffic for established connections

# Linux host checks vary by system
sudo nft list ruleset
sudo ufw status verbose

# Windows
Get-NetFirewallProfile
Get-NetFirewallRule -Enabled True
Do not “temporarily disable every firewall”That destroys evidence and can expose the machine. Add a narrow, logged rule for the exact source, destination, protocol, and port; retest; then remove or formalize it. If packet counters remain zero, the traffic never reached that rule. Work outward to route tables, load balancers, and cloud controls.

09DNS, TCP, TLS, then HTTP

An HTTPS request is a stack of dependencies. DNS returns an IP. TCP connects to a port. TLS authenticates the hostname and creates encryption. HTTP sends a method, path, headers, and body. Test in that order. My strong opinion: ping is a weak availability test. Many healthy servers block ICMP, while a machine that answers ping may have no working application.

# 1. DNS
nslookup api.example.com

# 2. TCP port
Test-NetConnection api.example.com -Port 443

# 3 and 4. TLS plus HTTP, with verbose evidence
curl -v --connect-timeout 5 https://api.example.com/health

# Bypass DNS while preserving hostname for TLS and HTTP
curl -v --resolve api.example.com:443:203.0.113.10 \
  https://api.example.com/health

# Listener ownership
ss -lntp | grep :443
netstat -ano | findstr :443
lsof -nP -iTCP:443 -sTCP:LISTEN
Read where curl stoppedCould not resolve host is DNS. Failed to connect is TCP. certificate verify failed is TLS identity or trust. An HTTP 404 proves DNS, TCP, and usually TLS worked; now inspect the Host header and path. Never use -k as the production fix for a certificate failure.

10TCP/IP debugging cheat sheet

Run the test from the machine or container that actually fails. A successful request from your laptop proves your laptop's path, not the production worker's path.

1. Record exact HOST:PORT and failing client location
2. Resolve A and AAAA; note the returned addresses
3. Test TCP to that exact port with a short deadline
4. On server: verify process + bound address + listener
5. Check container/service port publication
6. Check route, NAT, cloud rules, and host firewall
7. Test TLS with the real hostname
8. Send the smallest HTTP request; read status and headers

ECONNREFUSED → host answered; no listener or active reject
ETIMEDOUT    → dropped path, wrong route, or missing return path
EADDRINUSE   → another socket already owns address and port
404          → network stack worked; inspect host/path/routing
TLS error    → TCP worked; inspect name, chain, date, trust

localhost    → this network namespace only
0.0.0.0      → listen on all IPv4 interfaces; not a client address
ping works   → only ICMP replied
ping fails   → server may still be healthy

Best evidence: timestamp, client, resolved IP, HOST:PORT,
listener output, curl -v trace, and firewall packet counters.

The rule worth keeping is simple: name the failed layer before changing anything. Resolve the name, reach the address, connect to the port, verify TLS, then speak HTTP. “Networking” becomes manageable when each test asks one question and the next action follows from the exact answer.

Tell me what missed

A correction is more useful than a compliment. This goes straight to the person who writes SwiftGrasp.

Was this page useful?
0/1000

Please do not include passwords, private keys, or personal information.