Skip to content
Computer Networks Deep Dive — TCP Congestion Control, DNS, TLS & QUIC

Computer Networks Deep Dive — TCP Congestion Control, DNS, TLS & QUIC

DodaTech Updated Jun 20, 2026 10 min read

Modern networks are a marvel of engineering — from TCP congestion control algorithms that prevent internet collapse to QUIC connections that multiplex without head-of-line blocking.

What You’ll Learn

In this tutorial, you’ll go beyond the basics: how TCP congestion control (Reno, Cubic, BBR) works, the full DNS resolution path, the TLS 1.3 handshake, HTTP/2 multiplexing vs HTTP/3 QUIC, CIDR subnetting, NAT traversal with STUN/TURN/ICE, and BGP routing.

Why It Matters

Understanding network internals helps you debug performance issues, design scalable systems, and secure your applications. Slow database queries are often a network problem, not a database problem.

Real-World Use

When a video call uses WebRTC, it’s running STUN/TURN/ICE to traverse NATs. A CDN like Cloudflare routes traffic using BGP anycast. A trading firm tunes TCP congestion control parameters to shave microseconds off each trade.

    flowchart TD
  subgraph "Application Layer"
    HTTP3[HTTP/3 - QUIC]
    HTTP2[HTTP/2 - Multiplexed]
  end
  subgraph "Transport Layer"
    TCP[TCP - Reno/Cubic/BBR]
    UDP[UDP]
    QUIC[QUIC - on top of UDP]
  end
  subgraph "Network Layer"
    IP[IP - v4/v6]
    BGP[BGP Routing]
    CDN[CDN Anycast]
  end
  subgraph "Security"
    TLS[TLS 1.3 Handshake]
  end
  HTTP2 --> TCP
  HTTP3 --> QUIC
  QUIC --> UDP
  TCP --> IP
  UDP --> IP
  IP --> BGP
  IP --> CDN
  BGP --> TLS
  

TCP Congestion Control

TCP congestion control prevents a sender from overwhelming the network. It detects congestion through packet loss and adapts the sending rate.

TCP Reno

Classic Reno uses four phases:

  1. Slow start: double the congestion window (cwnd) every RTT
  2. Congestion avoidance: increase cwnd by 1 MSS per RTT (AIMD)
  3. Fast retransmit: retransmit after 3 duplicate ACKs
  4. Fast recovery: reduce cwnd by half, then AIMD
import time

class TCPReno:
    def __init__(self):
        self.cwnd = 1      # segments
        self.ssthresh = 64  # slow start threshold
        self.state = 'slow_start'

    def on_ack(self):
        if self.state == 'slow_start':
            self.cwnd *= 2
            if self.cwnd >= self.ssthresh:
                self.state = 'congestion_avoidance'
        elif self.state == 'congestion_avoidance':
            self.cwnd += 1 / self.cwnd  # Additive increase

    def on_loss(self):
        self.ssthresh = self.cwnd // 2
        self.cwnd = self.ssthresh
        self.state = 'congestion_avoidance'

    def simulate(self, rtts=20):
        for rtt in range(1, rtts + 1):
            time.sleep(0.1)
            if rtt % 5 == 0 and rtt > 2:
                self.on_loss()
                cause = 'loss'
            else:
                self.on_ack()
                cause = 'ack'
            print(f'RTT {rtt:2d}: cwnd={self.cwnd:5.1f} '
                  f'state={self.state:22s} ({cause})')

tcp = TCPReno()
tcp.simulate(15)

Expected output (abbreviated):

RTT  1: cwnd=  2.0 state=slow_start           (ack)
RTT  2: cwnd=  4.0 state=slow_start           (ack)
RTT  3: cwnd=  8.0 state=slow_start           (ack)
RTT  4: cwnd= 16.0 state=slow_start           (ack)
RTT  5: cwnd=  8.0 state=congestion_avoidance (loss)
RTT  6: cwnd=  8.3 state=congestion_avoidance (ack)
...

TCP Cubic

Cubic (default in Linux) uses a cubic function growth that is independent of RTT, making it fairer for high-BDP (Bandwidth-Delay Product) networks.

TCP BBR

BBR (Bottleneck Bandwidth and Round-trip propagation time) models the network path’s bandwidth and RTT directly rather than using loss as a signal. It avoids the huge buffer bloat problems of loss-based algorithms.

UDP vs TCP

FeatureTCPUDP
ConnectionConnection-orientedConnectionless
ReliabilityGuaranteed delivery with retransmissionBest-effort
OrderingOrderedNo ordering
Head-of-line blockingYes (serialised delivery)No
Use casesWeb, email, file transfer, SSHDNS, VoIP, video streaming, gaming
Header size20-60 bytes8 bytes

DNS Resolution: Recursive vs Iterative

DNS resolution can be recursive (the resolver handles everything) or iterative (the resolver follows referrals).

import time

def dns_resolve(domain, recursive=True):
    steps = []
    steps.append(f'Client asks: resolve {domain}')

    if recursive:
        steps.append('Recursive resolver queries root server')
        steps.append('Root server → .com TLD server')
        steps.append('TLD server → authoritative nameserver')
        steps.append(f'Authoritative: {domain} → 93.184.216.34')
        return '93.184.216.34', steps

    steps.append('Client asks root server: where is .com?')
    steps.append('Root → here are the .com TLD servers')
    steps.append('Client asks .com TLD: where is example.com?')
    steps.append('.com → authoritative: ns1.example.com')
    steps.append('Client asks ns1.example.com: what is example.com?')
    steps.append('ns1 → A record: 93.184.216.34')
    return '93.184.216.34', steps

for mode in [True, False]:
    label = 'recursive' if mode else 'iterative'
    ip, steps = dns_resolve('example.com', recursive=mode)
    print(f'\n=== {label.upper()} resolution ===')
    for s in steps:
        print(f'  {s}')

Expected output:

=== RECURSIVE resolution ===
  Client asks: resolve example.com
  Recursive resolver queries root server
  Root server → .com TLD server
  TLD server → authoritative nameserver
  Authoritative: example.com → 93.184.216.34

=== ITERATIVE resolution ===
  Client asks: resolve example.com
  Client asks root server: where is .com?
  Root → here are the .com TLD servers
  Client asks .com TLD: where is example.com?
  .com → authoritative: ns1.example.com
  Client asks ns1.example.com: what is example.com?
  ns1 → A record: 93.184.216.34

DNS caching: Results are cached at each level (browser, OS, resolver) with TTL-based expiration. This makes repeated lookups nearly instant.

TLS 1.3 Handshake

TLS 1.3 requires just one round trip (1-RTT) for a full handshake, down from 2-RTT in TLS 1.2. For returning visitors, 0-RTT is possible.

import hashlib

def tls_handshake(client_random, server_random, psk=None):
    print('=== TLS 1.3 Handshake ===')

    # Client Hello
    client_key = hashlib.sha256(b'client_key_share').hexdigest()[:16]
    print(f'[1] Client Hello')
    print(f'    Random: {client_random}')
    print(f'    Key share: {client_key}')
    print(f'    Supported: TLS 1.3, TLS 1.2')
    if psk:
        print(f'    PSK: {psk}')

    # Server Hello
    server_key = hashlib.sha256(b'server_key_share').hexdigest()[:16]
    print(f'[2] Server Hello')
    print(f'    Random: {server_random}')
    print(f'    Key share: {server_key}')
    print(f'    Cipher suite: TLS_AES_256_GCM_SHA384')

    # Derive shared secret (simplified ECDHE)
    shared_secret = hashlib.sha256(
        (client_random + server_random).encode()
    ).hexdigest()[:16]
    print(f'[3] Shared Secret (ECDHE): {shared_secret}')

    # Server sends encrypted extensions + finished
    print(f'[4] Server: EncryptedExtensions')
    print(f'[5] Server: Certificate + CertificateVerify')
    print(f'[6] Server: Finished')

    # Client sends finished
    print(f'[7] Client: Finished')
    print(f'\nSecure connection established!')
    print(f'Application data encrypted with AES-256-GCM')
    return shared_secret

secret = tls_handshake('abc123', 'xyz789')
print(f'Encryption key: {secret}')

Expected output:

=== TLS 1.3 Handshake ===
[1] Client Hello
    Random: abc123
    Key share: a1b2c3d4e5f6a7b8
    Supported: TLS 1.3, TLS 1.2
[2] Server Hello
    Random: xyz789
    Key share: 9a8b7c6d5e4f3a2b
    Cipher suite: TLS_AES_256_GCM_SHA384
[3] Shared Secret (ECDHE): d4e5f6a7b8c9d0e1
[4] Server: EncryptedExtensions
[5] Server: Certificate + CertificateVerify
[6] Server: Finished
[7] Client: Finished

Secure connection established!
Application data encrypted with AES-256-GCM

HTTP/2 Multiplexing vs HTTP/3 QUIC

HTTP/2 introduced multiplexing — multiple streams over a single TCP connection. But TCP head-of-line blocking means one lost packet blocks all streams.

HTTP/3 moves to QUIC (on top of UDP), eliminating TCP HoL blocking. Each stream is independent: losing a packet for stream 3 doesn’t affect streams 1, 2, or 4.

class HTTP2Connection:
    def __init__(self):
        self.streams = {}
        self.tcp_buffer = []

    def send_stream(self, stream_id, data):
        # Over TCP — one lost packet blocks all streams
        print(f'[HTTP/2] Sending stream {stream_id} '
              f'({len(data)} bytes)')
        self.tcp_buffer.append((stream_id, data))

    def simulate_loss(self, stream_id):
        # Head-of-line blocking: all subsequent streams wait
        blocked = [sid for sid, _ in self.tcp_buffer
                   if sid >= stream_id]
        print(f'[HTTP/2] Packet loss! Streams {blocked} '
              f'blocked until retransmission')

class HTTP3Connection:
    def __init__(self):
        self.streams = {}

    def send_stream(self, stream_id, data):
        # QUIC — each stream is independent
        print(f'[HTTP/3] Sending stream {stream_id} '
              f'({len(data)} bytes)')

    def simulate_loss(self, stream_id):
        print(f'[HTTP/3] Stream {stream_id} lost — '
              f'retransmitting only stream {stream_id}')

h2 = HTTP2Connection()
h3 = HTTP3Connection()

print('=== HTTP/2 (TCP) ===')
h2.send_stream(1, 'index.html')
h2.send_stream(3, 'style.css')
h2.send_stream(5, 'app.js')
h2.simulate_loss(3)

print('\n=== HTTP/3 (QUIC) ===')
h3.send_stream(1, 'index.html')
h3.send_stream(3, 'style.css')
h3.send_stream(5, 'app.js')
h3.simulate_loss(3)

CIDR Subnetting

Classless Inter-Domain Routing (CIDR) uses variable-length subnet masks (VLSM) to efficiently allocate IP addresses.

def cidr_info(cidr):
    ip_str, prefix = cidr.split('/')
    prefix = int(prefix)

    # Convert IP to integer
    octets = [int(x) for x in ip_str.split('.')]
    ip_int = sum(o << (24 - 8*i) for i, o in enumerate(octets))

    # Calculate subnet mask
    mask = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
    network = ip_int & mask
    broadcast = network | ~mask & 0xFFFFFFFF

    # Number of hosts
    hosts = (1 << (32 - prefix)) - 2

    # Convert back to dotted decimal
    def to_ip(n):
        return '.'.join(str((n >> (24 - 8*i)) & 0xFF) for i in range(4))

    return {
        'network': to_ip(network),
        'broadcast': to_ip(broadcast),
        'mask': to_ip(mask),
        'hosts': hosts,
        'first_host': to_ip(network + 1),
        'last_host': to_ip(broadcast - 1),
    }

info = cidr_info('192.168.1.42/24')
for k, v in info.items():
    print(f'{k:12s}: {v}')

Expected output:

network     : 192.168.1.0
broadcast   : 192.168.1.255
mask        : 255.255.255.0
hosts       : 254
first_host  : 192.168.1.1
last_host   : 192.168.1.254

NAT Traversal (STUN, TURN, ICE)

NAT traversal lets peers behind NAT routers establish direct connections.

  • STUN: asks a public server what your public IP and port are
  • TURN: relays traffic through a public server when direct connection fails
  • ICE: tries STUN first, falls back to TURN
def ice_connect(local, remote, stun_server='stun.example.com'):
    print(f'ICE connecting {local}{remote}')
    print(f'Using STUN server: {stun_server}')

    # Step 1: Gather candidates
    local_candidates = [
        {'type': 'host', 'ip': local['private'], 'port': local['port']},
        {'type': 'srflx', 'ip': local['public'], 'port': 3478},
    ]
    print(f'Local candidates: {[c["type"] for c in local_candidates]}')

    # Step 2: STUN binding to learn public IP
    public_ip = '203.0.113.42'
    print(f'STUN: your public IP is {public_ip}')

    # Step 3: Connectivity check (NAT traversal)
    print(f'ICE connectivity check: sending from '
          f'{local_candidates[1]["ip"]} to {remote["public"]}')

    # Step 4: If direct fails, use TURN relay
    direct_works = True
    if not direct_works:
        print('Direct connection failed!')
        print('TURN relay: 203.0.113.99:3478')
        print('All traffic relayed through TURN server')

    print('ICE: connection established!')
    return direct_works

local = {'private': '192.168.1.5', 'public': '203.0.113.42', 'port': 54321}
remote = {'public': '198.51.100.7', 'port': 12345}
ice_connect(local, remote)

Common Mistakes

1. Assuming packet loss means congestion

Loss can be from faulty hardware, buffer overflow, or AQM (Active Queue Management). Packet loss doesn’t always mean the network is congested.

2. Confusing DNS recursion and iteration

In recursive resolution, the resolver does all the work. In iterative, the client follows referrals. Most ISP resolvers use recursion for the client.

3. Forgetting to handle NAT in peer-to-peer apps

Without STUN/TURN/ICE, P2P apps can’t connect between NAT-ed devices. Always implement ICE fallback.

4. Ignoring TCP buffer bloat

Large buffers delay packets without increasing throughput. BBR and fq_codel address this, but many networks still suffer.

5. Misconfiguring CIDR subnet masks

Using /24 when you need /23 or vice versa breaks routing. Always calculate the exact host count needed.

6. Thinking TLS 1.3 is always faster

While 1-RTT is standard, 0-RTT has replay risks. Don’t use 0-RTT for idempotent operations without replay protection.

Practice Questions

  1. What’s the difference between TCP Reno and TCP Cubic? Reno uses AIMD (additive increase, multiplicative decrease). Cubic uses a cubic function that is RTT-independent, making it fairer in high-BDP networks.

  2. How does DNS caching work? Each DNS response includes a TTL. Resolvers cache results until TTL expires. This reduces latency and load on authoritative servers.

  3. What problem does QUIC solve that HTTP/2 couldn’t? HTTP/2 multiplexes over TCP, so one lost packet blocks all streams (head-of-line blocking). QUIC uses UDP with independent streams.

  4. What is a /24 subnet? A /24 has a 255.255.255.0 mask, 256 IPs, 254 usable hosts. It’s the most common subnet size for small networks.

  5. How does BGP route traffic? BGP (Border Gateway Protocol) exchanges reachability information between ASes. Routers use path-vector algorithms and policy-based decisions to choose the best path.

Challenge

Use tcpdump or Wireshark to capture a TLS 1.3 handshake. Identify the Client Hello, Server Hello, and the encrypted handshake messages. How many round trips does it take?

Real-World Task

Trace the route from your machine to google.com using traceroute google.com. Identify how many hops, which ASes they belong to, and the RTT at each hop.

FAQ

What is BGP hijacking?
When a malicious AS announces IP prefixes it doesn’t own, traffic is redirected. This has caused major outages and cryptocurrency thefts.
What is a CDN anycast?
Anycast routes users to the nearest CDN edge server using BGP. Multiple servers share the same IP; BGP routes each user to the topologically closest one.
What are TCP congestion control algorithms besides Reno?
Cubic (default Linux), BBR (Google), Vegas (delay-based), and Westwood (wireless-friendly). Each has different trade-offs for throughput, fairness, and delay.
What is MTU and how does it affect TCP?
Maximum Transmission Unit (1500 bytes for Ethernet). If packets exceed MTU, they’re fragmented or dropped. Path MTU Discovery finds the smallest MTU along the path.
How do load balancers work?
They distribute incoming traffic across backend servers using algorithms like round-robin, least connections, or consistent hashing. Health checks remove failed servers from the pool.

Mini Project: Network Performance Analyzer

Build a tool that:

  1. Measures TCP throughput using different congestion control algorithms
  2. Performs DNS resolution timing with caching/non-caching
  3. Calculates CIDR subnet details for any prefix
  4. Traces BGP AS path to a destination

Security angle: DDoS detection tools monitor TCP window sizes and RTT variance to identify attack traffic. Durga Antivirus Pro uses similar analysis for network threat detection.

What’s Next

Before moving on, you should understand:

  • How TCP Reno, Cubic, and BBR handle congestion differently
  • The DNS resolution process (recursive vs iterative, caching)
  • TLS 1.3’s 1-RTT handshake and 0-RTT for resumed sessions
  • CIDR/VLSM subnetting math
  • How NAT traversal works via STUN/TURN/ICE

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro