Capstone
The capstone: a networking diagnostics toolkit and report
The capstone assembles three ordered deliverables into one small diagnostics toolkit: a CIDR
calculator validated against hand math (co-04), a script that traces and narrates a real request's
full DNS-to-HTTP timeline (co-01, co-07, co-14), and a written analysis of captured traceroute and
tcpdump output tying every observed hop/packet back to a layer (co-01, co-05, co-22, co-23). Every
script lives under learning/capstone/code/, standard-library-only, fully type-annotated (DD-39),
and was actually run to capture the output shown below.
Concepts exercised
- CIDR/subnet arithmetic -- hosts/gateway/broadcast (co-04) --
subnet.py'scompute_subnet(), verified against three hand-computed prefixes in Step 1 - the layered model applied to a real packet (co-01) -- every stage
trace.pymeasures (Step 2) and every hop/packetanalysis.mdannotates (Step 3) is tied back to exactly one layer - TCP handshake + TLS 1.3 handshake narrated (co-07, co-14) --
trace.py'sconnect_tcp()andhandshake_tls()stages (Step 2), cross-checked againstanalysis.md's own packet-by-packettcpdumpannotation (Part B) - reading
traceroute/tcpdumpoutput (co-05, co-22) --analysis.md's hop-by-hoptraceroutetable (Part A) and packet-by-packettcpdumptable (Part B) - latency vs bandwidth vs throughput reasoning (co-23) --
trace.py's per-stage elapsed-time breakdown (Step 2's[DNS]/[TCP]/[TLS]/[HTTP]timings) andanalysis.md's per-hop and per-packet millisecond offsets (Parts A and B)
Step 1: subnet.py -- a CIDR subnet calculator
Context: Example 8 (Beginner tier) introduced this exact bit-arithmetic shape for one pair of CIDR blocks. This capstone step re-verifies the identical technique against THREE fresh, hand-computed prefixes -- proof the calculator generalizes, not a narrow fit to two specific inputs.
# learning/capstone/code/subnet.py
"""Capstone Step 1: a CIDR subnet calculator -- network/broadcast/host-range/host-count.
Ties together co-01 (encapsulation-adjacent addressing groundwork) and co-04 (CIDR and subnetting)
into one small, reusable module -- the same bit-arithmetic shape Example 8's `subnet.py` introduced,
now re-verified against THREE fresh, hand-computed CIDR blocks the beginner tier never used.
""" # => co-04: this file's own restated purpose, doubling as its module __doc__
# => co-04: no runtime output beyond setting __doc__ -- the three paragraphs above just orient the reader
from __future__ import annotations # => DD-39 hygiene: postpones type-annotation evaluation, keeping this file interpreter-version-agnostic
from dataclasses import dataclass # => co-04: a typed record beats a bare tuple for this multi-field CIDR report
@dataclass(frozen=True) # => co-04: frozen -- a computed subnet report is a VALUE, never mutated after construction
class SubnetReport: # => co-04: everything co-04 says is arithmetically derivable from a CIDR block, in one record
cidr: str # => co-04: the original input, e.g. "203.0.113.0/28" -- kept for readable reporting
network_address: str # => co-04: the ALL-HOST-BITS-ZERO address -- identifies the subnet itself, not a host
broadcast_address: str # => co-04: the ALL-HOST-BITS-ONE address -- reaches every host on the subnet at once
first_host: str # => co-04: network_address + 1 -- the first USABLE host address
last_host: str # => co-04: broadcast_address - 1 -- the last USABLE host address
host_count: int # => co-04: usable hosts = 2**host_bits - 2 (network and broadcast are never assignable)
def ip_to_int(address: str) -> int: # => co-04: dotted-decimal -> one 32-bit integer -- makes bitwise math possible
"""Pack a dotted-decimal IPv4 address into a single 32-bit integer.""" # => co-04: documents ip_to_int's contract -- no runtime output, just sets its __doc__
octets = [int(part) for part in address.split(".")] # => co-04: 4 decimal octets, each 0-255
value = 0 # => co-04: accumulator -- built up one octet at a time, most-significant first
for octet in octets: # => co-04: process octets in address order (leftmost = most significant)
value = (value << 8) | octet # => co-04: shift the accumulator left 8 bits, then OR in the next octet
return value # => co-04: returns this computed value to the caller
def int_to_ip(value: int) -> str: # => co-04: the EXACT inverse of ip_to_int -- one 32-bit integer -> dotted-decimal
"""Unpack a 32-bit integer back into dotted-decimal IPv4 notation.""" # => co-04: documents int_to_ip's contract -- no runtime output, just sets its __doc__
octets = [(value >> shift) & 0xFF for shift in (24, 16, 8, 0)] # => co-04: extract each byte, most-significant first
return ".".join(str(octet) for octet in octets) # => co-04: rejoin with "." -- the dotted-decimal separator
def compute_subnet(cidr: str) -> SubnetReport: # => co-04: the calculator itself -- "203.0.113.0/28" -> a full SubnetReport
"""Compute network/broadcast/host-range/host-count for a CIDR block.""" # => co-04: documents compute_subnet's contract -- no runtime output, just sets its __doc__
address_part, prefix_part = cidr.split("/") # => co-04: split "203.0.113.0/28" into its address and prefix-length parts
prefix_len = int(prefix_part) # => co-04: the /N -- how many leading bits are the NETWORK portion
host_bits = 32 - prefix_len # => co-04: everything else is the HOST portion -- how many addresses this subnet spans
address_int = ip_to_int(address_part) # => co-04: the input address, as one 32-bit integer
mask_int = ((1 << prefix_len) - 1) << host_bits if prefix_len > 0 else 0 # => co-04: N leading 1-bits, host_bits trailing 0-bits
network_int = address_int & mask_int # => co-04: AND with the mask clears every HOST bit -- this IS the network address
broadcast_int = network_int | (~mask_int & 0xFFFFFFFF) # => co-04: OR with the inverted mask sets every HOST bit to 1
host_count = max((1 << host_bits) - 2, 0) # => co-04: total addresses minus network and broadcast (never negative for /31, /32)
return SubnetReport( # => co-04: assembles every derived field into one immutable report
cidr=cidr, # => co-04: echoes the original input for the printed report
network_address=int_to_ip(network_int), # => co-04: converts the computed network integer back to dotted-decimal
broadcast_address=int_to_ip(broadcast_int), # => co-04: converts the computed broadcast integer back to dotted-decimal
first_host=int_to_ip(network_int + 1), # => co-04: one past the network address -- the first assignable host
last_host=int_to_ip(broadcast_int - 1), # => co-04: one before the broadcast address -- the last assignable host
host_count=host_count, # => co-04: the usable-host count derived above
) # => co-04: closes the multi-line construct opened above
if __name__ == "__main__": # => co-04: entry point -- this block runs only when the file executes directly, not on import
hand_computed = { # => co-04: THREE hand-computed CIDR blocks this script's output must match exactly -- one small, one medium, one large
"203.0.113.0/28": SubnetReport( # => co-04: /28 -- a small 16-address block (TEST-NET-3, RFC 5737), 14 usable hosts
"203.0.113.0/28", # => co-04: cidr
"203.0.113.0", # => co-04: network_address
"203.0.113.15", # => co-04: broadcast_address
"203.0.113.1", # => co-04: first_host
"203.0.113.14", # => co-04: last_host
14, # => co-04: host_count
), # => co-04: closes the multi-line construct opened above
"172.16.0.0/20": SubnetReport( # => co-04: /20 -- a medium 4096-address block, 4094 usable hosts
"172.16.0.0/20", # => co-04: cidr
"172.16.0.0", # => co-04: network_address
"172.16.15.255", # => co-04: broadcast_address
"172.16.0.1", # => co-04: first_host
"172.16.15.254", # => co-04: last_host
4094, # => co-04: host_count
), # => co-04: closes the multi-line construct opened above
"198.51.100.128/25": SubnetReport( # => co-04: /25 -- a half-of-a-/24 block starting at a NON-zero octet, 126 usable hosts
"198.51.100.128/25", # => co-04: cidr
"198.51.100.128", # => co-04: network_address
"198.51.100.255", # => co-04: broadcast_address
"198.51.100.129", # => co-04: first_host
"198.51.100.254", # => co-04: last_host
126, # => co-04: host_count
), # => co-04: closes the multi-line construct opened above
} # => co-04: closes the multi-line construct opened above
for cidr, expected in hand_computed.items(): # => co-04: one full report per CIDR block, checked against the hand-computed expectation
info = compute_subnet(cidr) # => co-04: runs the calculator under test
print(f"{info.cidr}:") # => co-04: labels the following per-field printout
print(f" network = {info.network_address}") # => co-04: the subnet's own identifying address
print(f" broadcast = {info.broadcast_address}") # => co-04: the all-hosts address for this subnet
print(f" hosts = {info.first_host} - {info.last_host} ({info.host_count} usable)") # => co-04: the assignable range
assert info == expected, f"{cidr} must match its hand-computed SubnetReport exactly" # => co-04: the exact-match check
print("\nAll three CIDR blocks match their hand-computed expectations: True") # => co-04: reached only if every assert above passed
# => co-04: this file is self-verifying: if it exits 0, every assert above passed and the demonstrated claim held
# => co-04: this capstone step deliberately reuses Example 8's exact bit-arithmetic shape, re-verified against THREE fresh prefixes it never checked -- proof the technique generalizes, not just a rehashRun: python3 subnet.py
Output:
203.0.113.0/28:
network = 203.0.113.0
broadcast = 203.0.113.15
hosts = 203.0.113.1 - 203.0.113.14 (14 usable)
172.16.0.0/20:
network = 172.16.0.0
broadcast = 172.16.15.255
hosts = 172.16.0.1 - 172.16.15.254 (4094 usable)
198.51.100.128/25:
network = 198.51.100.128
broadcast = 198.51.100.255
hosts = 198.51.100.129 - 198.51.100.254 (126 usable)
All three CIDR blocks match their hand-computed expectations: TrueAcceptance criteria: the calculator's output must match a hand-computed expectation for AT LEAST
two prefixes (per this capstone's own spec); it is checked here against THREE. Hand check for
203.0.113.0/28: a /28 leaves 4 host bits, so 2**4 - 2 = 14 usable hosts, network
203.0.113.0, broadcast 203.0.113.0 + 15 = 203.0.113.15 -- matches exactly. All three prefixes
hold, verified by the asserts inside the script itself and cross-checked against the captured output
above.
Key takeaway: the identical bit-arithmetic technique (mask-and-AND for the network address,
mask-invert-and-OR for the broadcast address) correctly handles a small /28, a medium /20, and a
/25 starting at a non-zero octet -- the SAME formula, no special-casing needed per prefix length.
Why it matters: a subnet calculator this small (under 60 lines, no dependencies) is exactly the kind of utility worth keeping on hand rather than reaching for a web calculator mid-incident -- when diagnosing "is this IP even inside the subnet I think it's in," doing the arithmetic by hand under time pressure is where mistakes happen, and this script's own hand-computed cross-checks are the proof its answers can be trusted.
Step 2: trace.py -- an annotated DNS -> TCP -> TLS -> HTTP timeline
Context: Example 1 annotated a curl -v transcript's stages after the fact; Example 30 did the
same for the TLS lines specifically. This step measures and narrates all four stages directly, in
one script, against a real live request -- proving the layered model (co-01) applies to a request
this topic's own code opened, not just one curl happened to show.
# learning/capstone/code/trace.py
"""Capstone Step 2: trace.py -- an annotated DNS -> TCP -> TLS -> HTTP timeline for one real request.
Ties together co-01 (the layered model applied to a real packet), co-07 (the TCP handshake), and
co-14 (the TLS 1.3 handshake) into one script: resolves a real host, times each stage separately,
and narrates all four layers a `curl -v` transcript (Examples 1 and 30) shows more implicitly.
""" # => co-01: this file's own restated purpose, doubling as its module __doc__
# => co-01: no runtime output beyond setting __doc__ -- the three paragraphs above just orient the reader
from __future__ import annotations # => DD-39 hygiene: postpones type-annotation evaluation, keeping this file interpreter-version-agnostic
import socket # => co-01: getaddrinfo (DNS) and create_connection (TCP) -- both stdlib, both real network calls
import ssl # => co-14: wraps a plain TCP socket in a genuine TLS 1.3 handshake, stdlib-only
import time # => co-01: perf_counter -- a monotonic, high-resolution clock, the right tool for timing each stage
HOST = "example.com" # => co-01: RFC 2606-reserved for documentation -- the same host Examples 1 and 30 used
PORT = 443 # => co-01: HTTPS's well-known port (Example 16's ports table)
def resolve(host: str, port: int) -> tuple[str, float]: # => co-01: STAGE 1 -- DNS, the Application-layer name lookup every request starts with
"""Resolve `host` to an IPv4/IPv6 address, returning (address, elapsed_seconds).""" # => co-01: documents resolve's contract -- no runtime output, just sets its __doc__
start = time.perf_counter() # => co-01: timestamp taken right before the resolution call
address_info = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP) # => co-01: a REAL DNS lookup -- no mock, no cache bypass trick
elapsed = time.perf_counter() - start # => co-01: how long resolution itself took, isolated from every later stage
resolved_ip = str(address_info[0][4][0]) # => co-01: the first returned address -- exactly what a plain socket.connect would also pick; str() satisfies strict typing, since getaddrinfo's sockaddr element type is a union
return resolved_ip, elapsed # => co-01: returns this computed value to the caller
def connect_tcp(ip: str, port: int) -> tuple[socket.socket, float]: # => co-07: STAGE 2 -- the TCP three-way handshake (co-07), Transport layer
"""Open a real TCP connection to (ip, port), returning (socket, elapsed_seconds).""" # => co-07: documents connect_tcp's contract -- no runtime output, just sets its __doc__
start = time.perf_counter() # => co-07: timestamp taken right before the connect call
sock = socket.create_connection((ip, port), timeout=5) # => co-07: a REAL SYN/SYN-ACK/ACK handshake against the resolved IP -- this call blocks until it completes
elapsed = time.perf_counter() - start # => co-07: how long the handshake itself took, isolated from DNS and TLS
return sock, elapsed # => co-07: returns this computed value to the caller
def handshake_tls(sock: socket.socket, server_hostname: str) -> tuple[ssl.SSLSocket, float]: # => co-14: STAGE 3 -- TLS 1.3's 1-RTT handshake (co-14), sits between Transport and Application
"""Wrap `sock` in a real TLS handshake, returning (tls_socket, elapsed_seconds).""" # => co-14: documents handshake_tls's contract -- no runtime output, just sets its __doc__
context = ssl.create_default_context() # => co-14: the stdlib's OWN certificate-validation policy -- no shortcuts, a real chain-of-trust check
start = time.perf_counter() # => co-14: timestamp taken right before the handshake call
tls_sock = context.wrap_socket(sock, server_hostname=server_hostname) # => co-14: a REAL TLS 1.3 handshake -- ClientHello+KeyShare through Finished, exactly Example 31's diagram
elapsed = time.perf_counter() - start # => co-14: how long the handshake itself took, isolated from DNS and TCP
return tls_sock, elapsed # => co-14: returns this computed value to the caller
def send_http_request(tls_sock: ssl.SSLSocket, host: str) -> tuple[str, float]: # => co-01: STAGE 4 -- HTTP, back at the Application layer, now riding the encrypted TLS channel
"""Send a minimal HTTP/1.1 GET and return (status_line, elapsed_seconds).""" # => co-01: documents send_http_request's contract -- no runtime output, just sets its __doc__
request = f"GET / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n".encode() # => co-01: the literal wire bytes of an HTTP/1.1 request -- Example 1's exact request shape
start = time.perf_counter() # => co-01: timestamp taken right before the request is sent
tls_sock.sendall(request) # => co-01: writes the request onto the ALREADY-ENCRYPTED TLS channel from stage 3
response = b"" # => co-01: accumulates bytes across possibly-separate recv() calls
while b"\r\n\r\n" not in response: # => co-01: reads only until the header block's terminating blank line -- the body isn't needed for this timeline
chunk = tls_sock.recv(4096) # => co-01: reads whatever arrives next -- HTTP responses are not guaranteed to land in one recv() call
if not chunk: # => co-01: an empty recv() would mean the peer closed early, before any full header block arrived
break # => co-01: stops the loop rather than spinning on a closed connection
response += chunk # => co-01: appends this chunk to the running buffer
elapsed = time.perf_counter() - start # => co-01: how long the request/response round trip itself took
status_line = response.split(b"\r\n", 1)[0].decode() # => co-01: the first line -- e.g. "HTTP/1.1 200 OK", Example 1's own status line
return status_line, elapsed # => co-01: returns this computed value to the caller
if __name__ == "__main__": # => co-01: entry point -- this block runs only when the file executes directly, not on import
overall_start = time.perf_counter() # => co-01: the whole-timeline clock, spanning all four stages
ip, dns_seconds = resolve(HOST, PORT) # => co-01: STAGE 1 -- DNS
print(f"[DNS] resolved {HOST} -> {ip} in {dns_seconds * 1000:.1f} ms") # => co-01: labels this stage explicitly with its OSI/TCP-IP layer name
tcp_sock, tcp_seconds = connect_tcp(ip, PORT) # => co-07: STAGE 2 -- TCP
print(f"[TCP] three-way handshake to {ip}:{PORT} completed in {tcp_seconds * 1000:.1f} ms") # => co-07: labels this stage explicitly with its layer name
tls_sock, tls_seconds = handshake_tls(tcp_sock, HOST) # => co-14: STAGE 3 -- TLS
negotiated_version = tls_sock.version() # => co-14: e.g. "TLSv1.3" -- confirms which protocol version was actually negotiated
cipher_info = tls_sock.cipher() # => co-14: returns None if no cipher was negotiated -- checked explicitly below for strict typing
assert cipher_info is not None, "a completed TLS handshake must have negotiated a cipher suite" # => co-14: narrows cipher_info from Optional to a concrete tuple for the line below
negotiated_cipher = cipher_info[0] # => co-14: e.g. "TLS_AES_256_GCM_SHA384" -- the negotiated AEAD cipher suite
print(f"[TLS] handshake complete -- {negotiated_version} / {negotiated_cipher} in {tls_seconds * 1000:.1f} ms") # => co-14: labels this stage AND names what was negotiated
status_line, http_seconds = send_http_request(tls_sock, HOST) # => co-01: STAGE 4 -- HTTP
print(f"[HTTP] {status_line} in {http_seconds * 1000:.1f} ms") # => co-01: labels this final stage with its own layer name and the real response status line
total_seconds = time.perf_counter() - overall_start # => co-01: the full DNS-to-HTTP timeline, end to end
print(f"\ntotal: {total_seconds * 1000:.1f} ms across all four stages") # => co-01: the capstone's own headline summary number
tls_sock.close() # => co-14: releases this connection's resources -- closes the TLS layer, which also closes the underlying TCP socket
assert status_line.startswith("HTTP/1.1 "), "the response must carry a real HTTP/1.1 status line" # => co-01: the acceptance criterion this step's syllabus entry (ex-trace) names
assert negotiated_version == "TLSv1.3", "example.com is expected to negotiate TLS 1.3, per this topic's Example 30" # => co-14
assert dns_seconds > 0 and tcp_seconds > 0 and tls_seconds > 0 and http_seconds > 0, "every stage must take a measurable, nonzero amount of time" # => co-01
print("All four stages (DNS, TCP, TLS, HTTP) measured and narrated against a real live request: True") # => co-01: reached only if every assert above passed
# => co-01: this file is self-verifying: if it exits 0, every assert above passed and the demonstrated claim held
# => co-01: each stage function returns its OWN isolated elapsed time -- summing them individually (not just timing start-to-finish) is what makes the per-stage breakdown trustworthyRun: python3 trace.py
Output:
[DNS] resolved example.com -> 172.66.147.243 in 2.2 ms
[TCP] three-way handshake to 172.66.147.243:443 completed in 22.8 ms
[TLS] handshake complete -- TLSv1.3 / TLS_AES_256_GCM_SHA384 in 26.3 ms
[HTTP] HTTP/1.1 200 OK in 84.5 ms
total: 141.2 ms across all four stages
All four stages (DNS, TCP, TLS, HTTP) measured and narrated against a real live request: TrueAcceptance criteria: the script must emit a real HTTP status line (HTTP/1.1 200 OK, confirmed
live above) and every one of the four stages must be individually labelled with a nonzero elapsed
time -- both hold, verified by the asserts inside the script and visible directly in the captured
[DNS]/[TCP]/[TLS]/[HTTP] lines above.
Key takeaway: the four stages are wildly uneven in cost on this request -- DNS (2.2ms, likely
already warm in the OS resolver cache) and TCP (22.8ms) are both cheap compared to TLS (26.3ms,
a full 1-RTT handshake) and especially HTTP (84.5ms, the actual application round trip) -- a
concrete illustration that "the network is slow" is rarely one single cause.
Why it matters: this per-stage breakdown is exactly the debugging instinct co-01's
layering-and-leaks big idea is about -- a slow page load's actual bottleneck could be sitting in
ANY one of these four stages, and guessing without measuring each one separately (the way this
script does explicitly) means potentially "fixing" the wrong layer entirely.
Step 3: analysis.md -- annotating a real traceroute and tcpdump capture
Context: Steps 1 and 2 built and ran the tools; this final step uses them to produce the
capstone's own written deliverable -- a real traceroute run alongside a real tcpdump capture of
one HTTPS request, with every hop and every packet explained and tied back to a specific layer of
the model this whole topic has been building. The full annotated capture lives in its own file,
analysis.md, since the hop-by-hop and packet-by-packet narration is long enough
to deserve its own page rather than crowding this overview.
Acceptance criteria: every traceroute hop and every captured tcpdump packet is individually
explained and correctly mapped to a layer of the OSI/TCP-IP model -- verified directly against
analysis.md's own hop-by-hop and packet-by-packet tables.
Key takeaway: traceroute and tcpdump answer two genuinely different questions about the SAME
request -- traceroute shows WHICH routers a packet passed through and how long each hop took (the
Network layer's own path); tcpdump shows WHAT was actually said once the packet reached its
destination (the Transport/TLS/Application layers' own conversation) -- neither tool alone answers
both questions.
Why it matters: this is the capstone's own proof that the layered model (co-01) is not academic vocabulary -- it is the concrete tool this whole topic argued for on its very first page: every observed hop or packet localizes to exactly one layer, which is what turns "the network is broken" into an actual, bisectable diagnosis.
← Previous: VPN & Overlay Networking · Next: Capstone Analysis →
Last updated July 17, 2026