Backend / Networking / 01_osi_tcp_ip_model.md

OSI and TCP/IP models

Updated 4 interview angles 15 min read source
On this page10
  1. Layers, jobs and units
  2. Encapsulation, in bytes
  3. What your Python process sees
  4. When large responses hang: path MTU discovery
  5. Protocols that do not fit
  6. Where the boxes sit
  7. L4 vs L7, the question behind the question
  8. Debugging by layer
  9. Related
  10. Interview angle

OSI and TCP/IP models

A layer model answers one question about a protocol or a box: which headers can it read, and so what can it decide? OSI is ISO’s seven-layer reference model (ISO/IEC 7498-1); the Internet runs the TCP/IP suite, which RFC 1122 (1989) describes in four layers. The OSI protocols lost, but the numbering survived as vocabulary, so engineers say “L4” for TCP and “L7” for HTTP on a stack that has no layer 5 or 6.

Treat OSI as a teaching tool. Real systems cross layers constantly: TLS sits between TCP and HTTP, QUIC fuses transport and encryption, NAT rewrites L4 ports inside an L3 box, and an L7 load balancer terminates TCP. RFC 3439 (2002) titled a section “Layering Considered Harmful”. For one request traced through every layer, see What happens when you enter a URL.

Layers, jobs and units

Each layer wraps what the layer above hands it in a header of its own, and the wrapped unit - the protocol data unit (PDU) - has a name interviewers use:

OSI layer Job TCP/IP layer and PDU
7 Application the program’s protocol: HTTP, DNS, SMTP Application: message
6 Presentation encoding, compression, encryption Application
5 Session dialogue state, resumption Application
4 Transport process to process, by port Transport: segment (TCP), datagram (UDP)
3 Network host to host, across networks Internet: packet
2 Data link one hop on one link, by MAC address Link: frame
1 Physical bits as voltage, light or radio Link (RFC 1122 has no L1)

Textbooks often teach a five-layer hybrid that splits Physical out of Link. Layers 2, 3, 4 and 7 mean the same in every version, and they are the only numbers anyone uses in conversation.

Three mechanisms behind the table

  • Each header names the next parser. EtherType in the Ethernet header says what the payload is (0x0800 IPv4, 0x86DD IPv6, 0x0806 ARP), the IP protocol field does the same one layer up (6 TCP, 17 UDP, 1 ICMP), and the port picks the socket. Receiving is that chain of lookups.
  • Addresses have a scope. A MAC address is valid on one link and every router rewrites it; an IP address is end to end and changes only at a NAT; a port means something only inside one host. A packet that crosses ten routers travels in ten different frames.
  • The layers run in different places. L1-L2 in the NIC and its driver, L3-L4 in the kernel, everything above in your process - TLS included, since Python’s ssl module is OpenSSL running in-process. QUIC moves the transport itself into user space.

Encapsulation, in bytes

Going down the stack each layer prepends its header; going up each strips its own and passes the rest along:

text
HTTP  GET /users HTTP/1.1 ...                  message
        │ TLS: +5 header, +1 type byte, +16 tag
TLS   [5][ ciphertext of message + 1 ][16]     record
        │ TCP: +20 to 60, slices the byte stream
TCP   [20-60][ bytes of one or more records ]  segment
        │ IP: +20 (IPv4) or +40 (IPv6)
IP    [20][ segment ]                          packet
        │ Ethernet: +14 header, +4 checksum
Eth   [14][ packet, at most the MTU ][4]       frame

Records and segments do not line up. A TLS record holds up to 16 KB, so it spans about a dozen segments, and one segment can carry the tail of one record and the head of the next: TCP moves bytes, not messages.

Header sizes

Header Bytes Note
Preamble + SFD 8 on the wire only; no capture shows it
Ethernet II 14 +4 per 802.1Q VLAN tag
Ethernet FCS 4 CRC-32 trailer; bad frames are dropped silently
Inter-frame gap 12 idle line time between frames
IPv4 20-60 20 in practice; IP options are rare
IPv6 40 fixed; extension headers extra
TCP 20-60 32 with timestamps, which Linux sends by default
UDP 8 fixed
TLS 1.3 record 22 5 header + 1 type + 16 AEAD tag, per record
WireGuard 60 / 80 outer IPv4 / IPv6 + UDP 8 + its own 32

MTU, MSS and what is left

MTU (maximum transmission unit) is the largest IP packet a link carries: 1500 bytes on Ethernet, counting the IP header but not Ethernet’s 14 + 4. MSS (maximum segment size) is the largest TCP payload a host accepts, announced once, in its SYN. RFC 6691 sets the arithmetic: announce the MTU minus the fixed IP and TCP headers, then subtract each segment’s options.

text
   14    20     20    12        1448        4
┌─────┬──────┬──────┬────┬───────────────┬─────┐
│ Eth │  IP  │ TCP  │ TS │  TCP payload  │ FCS │
└─────┴──────┴──────┴────┴───────────────┴─────┘
      │◀─────────── MTU 1500 ───────────▶│
                    │◀──── MSS 1460 ────▶│

So IPv4 announces 1460 and, once both sides use the 12-byte timestamp option (TS), carries 1448 per segment; IPv6 announces 1440 and carries 1428. On the wire each frame also costs 38 bytes of preamble, header, FCS and gap, so efficiency is payload / (MTU + 38):

Stack Payload Efficiency
IPv4, no options 1460 94.9%
IPv4 + timestamps 1448 94.1%
IPv6 + timestamps 1428 92.8%
Inside WireGuard 1368 88.9%
AWS jumbo, MTU 9001 8949 99.0%

The WireGuard row is IPv4 with timestamps inside an 80-byte IPv6 outer header. 1448 / 1538 = 94.1% is the ~941 Mbit/s iperf3 shows on a clean gigabit link, not a slow NIC.

TLS barely registers: 22 bytes per 16 KB record is 0.13%. The costs that matter are per packet, which is why jumbo frames help, tunnels hurt, and QUIC, which seals every packet with its own 16-byte tag, pays per packet what TLS over TCP pays per record.

What your Python process sees

The kernel strips L2 to L4 and OpenSSL, inside the interpreter, strips TLS. What survives of the lower layers is metadata you have to ask for:

python
import socket
import ssl

HOST = "example.com"
raw = socket.create_connection((HOST, 443))
print("L3/L4", raw.getpeername())  # peer IP and port: all that is left of L2-L4
print("MSS  ", raw.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG))

ctx = ssl.create_default_context()
ctx.set_alpn_protocols(["http/1.1"])
tls = ctx.wrap_socket(raw, server_hostname=HOST)  # SNI + ALPN go in the ClientHello
print("TLS  ", tls.version(), tls.cipher()[0], tls.selected_alpn_protocol())

tls.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")
print("L7   ", tls.recv(17))  # plaintext: OpenSSL, in this process, removed TLS
text
L3/L4 ('172.66.147.243', 443)
MSS   1400
TLS   TLSv1.3 TLS_AES_256_GCM_SHA384 http/1.1
L7    b'HTTP/1.1 200 OK\r\n'

This side announced 1460 (1500-byte Ethernet), and a connection uses the smaller offer, so 1400 was the server’s. A value under 1460 means the server reserves room for extra headers - a tunnel, an encapsulating load balancer - instead of betting on path MTU discovery. Never hard-code 1460.

When large responses hang: path MTU discovery

TCP sends full-size segments with the IP DF (don’t fragment) bit set. When a link on the path has a smaller MTU - a VPN, a tunnel, an overlay - the router in front of it drops the packet and returns ICMP “fragmentation needed” (type 3 code 4; ICMPv6 type 2, Packet Too Big) carrying the smaller MTU. The sender caches it and resends in smaller segments. That is path MTU discovery (RFC 1191; RFC 8201 for IPv6, whose routers never fragment).

The black hole

It works only if the ICMP message gets back. Drop it - a NACL with no ICMP rule, a firewall told to “block ping” - and every full-size packet vanishes silently while the sender retransmits the same oversized segment. The signature: the TCP handshake and small responses work, large responses stall after the first bytes. Two commands prove it from the client side:

bash
ping -M do -s 1472 api.example.com   # DF set; 1472 + 8 ICMP + 20 IP = 1500
ping -M do -s 1372 api.example.com   # 1400 on the wire
tracepath -n api.example.com         # reports the pmtu per hop

If the 1500-byte probe vanishes without an error while the 1400-byte one is answered, something drops by size and says nothing; tracepath may even be fooled into reporting 1500. On macOS the probe is ping -D -s 1472; on Windows, ping -f -l 1472.

Fixes, in the order to try them

  1. Clamp the MSS where the tunnel starts, so every forwarded SYN asks for segments that fit: iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu. TCP only.
  2. Let the ICMP through: IPv4 type 3 code 4 and ICMPv6 type 2. Never deny all ICMPv6; IPv6 runs neighbour discovery over it.
  3. Give the tunnel the right MTU. WireGuard’s default 1420 is 1500 minus 80 (IPv6 40, UDP 8, WireGuard 32); AWS Site-to-Site VPN carries at most MTU 1446 / MSS 1406.
  4. Let hosts probe. net.ipv4.tcp_mtu_probing=1 (RFC 4821) shrinks segments after a black-hole timeout without any ICMP. Off by default.

Two cloud twists. Inside one AWS VPC instances use 9001-byte jumbo frames, while traffic through an internet gateway or a VPN is capped at 1500 - so two jumbo hosts across a VPN hit exactly this (VPN). And an ECMP router or L4 balancer hashes a flow’s 5-tuple to pick a server, but the ICMP error has no ports of its own and can land on a server that never saw the connection; Cloudflare’s fix was to broadcast it to every server.

Protocols that do not fit

Protocol Filed under Why it does not fit
TLS L6, or “application” on TCP, under HTTP; runs inside your process
QUIC L4 a transport inside UDP, with TLS 1.3 built in
ARP L2 Ethernet payload (0x0806) mapping IPv4 to MAC
NDP L3 IPv6’s ARP: ICMPv6 messages doing a link-layer job
ICMP L3 carried in IP (protocol 1), yet “an integral part of IP”
BGP L7 a TCP application (port 179) that steers L3 routing
OSPF L3 a routing protocol directly on IP (protocol 89)
DHCP L7 UDP 67/68 traffic that hands out the L3 address
IPsec L3 ESP is IP protocol 50; its IKE key exchange is UDP 500/4500
VXLAN, WireGuard tunnel whole frames or packets inside UDP: the stack recurses

OSI files encryption under L6, so TLS gets called “L6”; TCP/IP has no slot and calls it application. It talks to both neighbours: SNI names the host and ALPN picks the L7 protocol (h2, http/1.1) during the handshake, before any HTTP byte exists (TLS, HTTPS, Certificates).

Is QUIC layer 4 or layer 7?

Both answers are defensible, which is the point of the question. By function QUIC is a transport: streams, ordering, loss recovery, congestion control (RFC 9000). By position it is application code, a user-space library sending UDP datagrams. And it absorbs TLS 1.3: the handshake rides in QUIC’s own CRYPTO frames, with no TLS record layer (RFC 9001).

The keys then hide what TCP keeps in its header - ACKs, windows, stream offsets travel encrypted, and the packet number is masked. HTTP/3 is HTTP over QUIC (HTTP versions: 1.1, 2 and 3). What an on-path box loses:

TCP + TLS QUIC
Seq, ACK, flags, window readable encrypted
Connection start and end SYN, FIN, RST only handshake packets look different
Forge RST, clamp MSS possible impossible: headers are authenticated
Flow identity 5-tuple connection ID, survives an address change
Still visible ports, TCP header, handshake UDP ports, connection IDs, a few bits

With nobody on the path able to clamp it, QUIC sizes its own packets: it needs a path that carries 1200-byte UDP payloads and probes for more. A client moving from Wi-Fi to cellular keeps its connection ID but changes its 5-tuple, breaking L4 balancers that hash addresses; the QUIC-LB draft, not an RFC as of 2026-10, puts a server ID in the connection ID for that reason. Where UDP 443 is blocked, browsers fall back to HTTP/2 over TCP.

Where the boxes sit

Box Layer Reads, decides, rewrites
Switch L2 MAC addresses; an “L3 switch” also routes
Router L3 destination IP; new L2 header per hop, TTL minus 1
NAT gateway L3 + L4 rewrites IPs and ports, fixes checksums, keeps state
Security group L3-L4 5-tuple plus connection state; drops silently
L4 balancer L4 picks a backend per connection
L7 balancer L7 ends TCP and TLS; picks a backend per request
WAF, API gateway L7 method, path, headers, body
VPN gateway L3 tunnel wraps whole packets in an encrypted outer packet

Two rows break the picture. NAT is filed under L3, but the common kind rewrites ports, and even address-only NAT must fix the TCP checksum, which covers the IP addresses through a pseudo-header (NAT (Network Address Translation)). An L7 balancer is two TCP endpoints glued by an HTTP parser: two connections, two MSS negotiations, two sets of timeouts (HTTPS Handshake, mTLS, Keepalive, and the Reverse-Proxy Edge).

L4 vs L7, the question behind the question

“Is it L4 or L7?” asks what the box can see, and so what it can do:

L4 balancer L7 balancer
Balances connections requests, or HTTP/2 streams
Reads 5-tuple; the TLS SNI at most method, path, host, headers, cookies
TCP forwarded, or relayed byte for byte terminated; a new connection to the backend
TLS passed through, or ended with no HTTP terminated, so it can read HTTP
Client IP kept, or sent by PROXY protocol gone; rebuilt from X-Forwarded-For
gRPC all RPCs on a connection hit one backend RPCs spread across backends
Examples AWS NLB, IPVS, kube-proxy, HAProxy mode tcp AWS ALB, nginx, Envoy, HAProxy mode http

The gRPC row is the trap: every call shares one long-lived HTTP/2 connection, an L4 balancer places that connection once, and one backend takes all of the client’s calls, even after you scale out. Use an HTTP/2-aware L7 balancer or client-side balancing (gRPC's HTTP/2 Basis). L4 suits databases, raw TCP and TLS passthrough.

An L4 proxy can still peek one layer up. The hostname crosses in cleartext in the ClientHello, so nginx ssl_preread, HAProxy req.ssl_sni and Envoy’s TLS inspector route by SNI while relaying encrypted bytes. Encrypted Client Hello (RFC 9849) hides the real name from boxes without the ECH keys.

What the backend loses behind an L7 balancer

Ending TCP and TLS costs the backend the client IP (its TCP peer is the balancer) and the scheme (the request arrives as plain HTTP). An L7 hop restores them as X-Forwarded-For and X-Forwarded-Proto, or Forwarded (RFC 7239).

An L4 hop cannot add headers to a stream it does not parse, so it prepends a PROXY protocol line before the client’s first byte: PROXY TCP4 203.0.113.7 10.0.1.20 51234 443\r\n (AWS NLB sends version 2, the same data in binary).

Those headers are text the client can also write, and each trusted proxy appends the address it saw: read X-Forwarded-For from the right, skipping exactly your trusted hops. Behind one ALB, 6.6.6.6, 203.0.113.7 means the client is 203.0.113.7, and 6.6.6.6 is whatever the client sent. The count holds only if nothing reaches the backend except through those hops.

Uvicorn trusts the headers only from --forwarded-allow-ips (default 127.0.0.1 and ::1); * on a backend reachable another way hands the client IP to the client. More in Proxies: Forward vs Reverse.

Debugging by layer

Layer First tool What it catches
L1-L2 ip -s link, ethtool -S eth0 link down, CRC errors, drops
L2 ip neigh FAILED neighbour: wrong subnet or dead gateway
L3 ip route get <ip>, mtr, tracepath no route, loss at a hop, path MTU
Cloud L3-L4 VPC flow logs REJECT: a security group or NACL dropped it
L4 ss -tanp, nc -vz host 443 refused or timed out; who owns the port
L4 tcpdump 'tcp[tcpflags] & tcp-rst != 0' which side sent the reset
L4 state dmesg nf_conntrack: table full, dropping packet
TLS openssl s_client -connect h:443 -servername h chain, SNI, ALPN, version
L7 curl -v, curl -w timings status, headers, which phase is slow

Bisect rather than walk the stack: if nc -vz connects, L1 to L4 work and the fault is TLS or above. Refused means a RST came back, so the host is reachable and nothing listens (or a firewall rejects); timed out means silence, so something dropped the SYN. Full toolbox: Network Troubleshooting Tools.

Interview angle 4

  • “A load balancer terminates TLS and routes by path. Which layer is it, and what does the backend lose?” - L7: it can read the path only after ending TCP and TLS. The backend’s TCP peer is now the balancer and the request arrives as plain HTTP, so it loses the client IP and the scheme.
    • Follow-up: “How does it get them back, and when must it refuse them?” - X-Forwarded-For and X-Forwarded-Proto (or Forwarded) from an L7 hop, PROXY protocol from an L4 one. Trust them only when the TCP peer is your own proxy, read from the right skipping exactly your trusted hops, and make the backend unreachable except through them.
  • “Small API calls work over the VPN, but large responses hang. Which layer?” - L3, path MTU. The tunnel lowers the MTU, full-size segments carry DF, and a firewall drops the ICMP “fragmentation needed” (type 3 code 4; ICMPv6 type 2) that should say so, so the server resends the same oversized segment until the connection dies.
    • Follow-up: “Which two commands prove it, and what fixes it?” - ping -M do -s 1472 vanishes while -s 1372 answers, and tracepath shows the pmtu drop, or is fooled into 1500 when ICMP is filtered. Clamp the MSS at the tunnel, then let type 3 code 4 through.
  • “Is QUIC layer 4 or layer 7?” - a transport by function, application code by position (a user-space library over UDP), with TLS 1.3 inside. “Both, and here is why” is the answer they want.
    • Follow-up: “What can an L4 middlebox no longer see or do?” - sequence numbers, ACKs, flags, where connections start and end; it cannot forge resets or clamp the MSS. It sees UDP ports and connection IDs, and a migrating client changes its 5-tuple under any balancer hashing on it.
  • “L4 or L7 load balancer for a gRPC service?” - L7 or client-side. Every call shares one long-lived HTTP/2 connection and L4 places connections, so one backend gets all of a client’s calls.
    • Follow-up: “When is L4 the right answer?” - non-HTTP protocols, TLS passthrough when only the backend may hold the keys, and very high connection rates; it keeps the client IP without parsing anything.