tcp-capture/capture.py
2026-10-07 06:29:58 -06:00

206 lines
7.9 KiB
Python

"""Capture one TCP connection between two Mininet hosts and print every segment.
Given; do not change it. Run inside the course container, from this folder:
python3 capture.py # h1 sends 3000 bytes to h2 over TCP, h2 answers
python3 capture.py slow # h2 reads slowly: watch the window close
python3 capture.py udp # the same 3000 bytes over UDP, for comparison
python3 capture.py show FILE # print a saved capture again
python3 capture.py cli # the network with a mininet> prompt
Add --relative to any of them to print sequence and acknowledgment numbers
counted from the start of the connection, the way Wireshark shows them.
The network: h1 (10.0.0.1) and h2 (10.0.0.2) on one switch, s1, which delays
every packet 10 ms, so a round trip takes 20 ms.
tcpdump runs on h1 and saves every packet to captures/<mode>.pcap, which you
can also open in Wireshark on your own computer.
Two TCP options are turned off on both hosts so the numbers in the capture
are the ones the lecture uses: timestamps (which would take 12 bytes of every
segment) and window scaling (which would multiply the window field). The
maximum segment size is set to 1000 bytes so the arithmetic is easy.
References: tcpdump https://man7.org/linux/man-pages/man8/tcpdump.8.html,
tshark https://www.wireshark.org/docs/man-pages/tshark.html,
Python sockets https://docs.python.org/3/library/socket.html
"""
import os
import socket
import subprocess
import sys
import time
PORT = 5000
MSS = 1000
SEND_BYTES = 3000 # three full segments
SLOW_BYTES = 10000 # more than h2's small receive buffer holds
SLOW_BUFFER = 4096 # h2's receive buffer in slow mode (Linux doubles it)
SLOW_WAIT = 2.0 # seconds h2 waits before it starts reading
FLAGS = [(0x02, "SYN"), (0x10, "ACK"), (0x08, "PSH"), (0x01, "FIN"), (0x04, "RST")]
HERE = os.path.dirname(os.path.abspath(__file__))
def server(mode):
if mode == "udp":
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", PORT))
got = 0
while got < SEND_BYTES:
data, peer = s.recvfrom(65535)
got += len(data)
s.sendto(f"got {got} bytes\n".encode(), peer)
return
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, MSS)
if mode == "slow":
# set before listen() so the window h2 advertises in its SYN-ACK is already small
listener.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, SLOW_BUFFER)
listener.bind(("0.0.0.0", PORT))
listener.listen(1)
conn, _ = listener.accept()
if mode == "slow":
time.sleep(SLOW_WAIT)
got = 0
while True:
data = conn.recv(65535)
if not data: # h1 closed its sending direction (its FIN)
break
got += len(data)
conn.sendall(f"got {got} bytes\n".encode())
conn.close()
def client(mode):
total = SLOW_BYTES if mode == "slow" else SEND_BYTES
if mode == "udp":
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for _ in range(total // MSS):
s.sendto(b"x" * MSS, ("10.0.0.2", PORT))
s.settimeout(3)
print("h2 answered:", s.recv(100).decode().strip())
return
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, MSS)
s.connect(("10.0.0.2", PORT))
s.sendall(b"x" * total)
s.shutdown(socket.SHUT_WR) # "I am done sending", but h1 still reads the answer
answer = b""
while True:
data = s.recv(100)
if not data:
break
answer += data
s.close()
print("h2 answered:", answer.decode().strip())
def flag_names(value):
bits = int(value, 16)
return ",".join(name for bit, name in FLAGS if bits & bit) or "-"
def show(path, relative):
"""Print one line per packet: who sent it, flags, numbers, payload, window."""
fields = ["frame.number", "frame.time_relative", "ip.src", "udp.length",
"tcp.flags", "tcp.seq", "tcp.ack", "tcp.len", "tcp.window_size_value",
"_ws.expert.message"]
cmd = ["tshark", "-r", path, "-T", "fields", "-E", "separator=|", "-E", "occurrence=f",
"-o", f"tcp.relative_sequence_numbers:{'TRUE' if relative else 'FALSE'}"]
for f in fields:
cmd += ["-e", f]
out = subprocess.run(cmd, capture_output=True, text=True).stdout
names = {"10.0.0.1": "h1 -> h2", "10.0.0.2": "h2 -> h1"}
print(f"{'#':>3} {'time (ms)':>9} {'direction':<9} {'flags':<12} {'seq':>10} {'ack':>10} "
f"{'len':>5} {'win':>6} note")
for line in out.splitlines():
num, t, src, udplen, flags, seq, ack, length, win, note = (line.split("|") + [""] * 10)[:10]
ms = f"{float(t) * 1000:9.1f}"
who = names.get(src, src)
if udplen:
print(f"{num:>3} {ms} {who:<9} {'UDP':<12} {'':>10} {'':>10} {int(udplen) - 8:>5} {'':>6}")
continue
if not flags:
continue
ack_shown = ack if int(flags, 16) & 0x10 else ""
print(f"{num:>3} {ms} {who:<9} {flag_names(flags):<12} {seq:>10} {ack_shown:>10} "
f"{length:>5} {win:>6} {note.strip()}")
def build():
from mininet.net import Mininet
from mininet.nodelib import LinuxBridge
from mininet.topo import Topo
class TwoHosts(Topo):
def build(self):
h1 = self.addHost("h1", ip="10.0.0.1/24")
h2 = self.addHost("h2", ip="10.0.0.2/24")
s1 = self.addSwitch("s1")
self.addLink(h1, s1)
self.addLink(h2, s1)
net = Mininet(topo=TwoHosts(), switch=LinuxBridge, controller=None, autoSetMacs=True)
net.start()
s1 = net.get("s1")
for port in ("s1-eth1", "s1-eth2"):
s1.cmd(f"tc qdisc replace dev {port} root netem delay 10ms")
for h in net.hosts:
intf = f"{h.name}-eth0"
h.cmd(f"ethtool -K {intf} tso off gso off gro off lro off 2>/dev/null")
h.cmd("sysctl -qw net.ipv4.tcp_timestamps=0 net.ipv4.tcp_window_scaling=0 net.ipv4.tcp_sack=0")
h1, h2 = net.get("h1", "h2")
for _ in range(5):
if " 0 received" not in h1.cmd("ping -c 1 -W 1 10.0.0.2"):
break
return net, h1, h2
def run(mode, relative):
net, h1, h2 = build()
try:
os.makedirs(os.path.join(HERE, "captures"), exist_ok=True)
path = os.path.join(HERE, "captures", f"{mode}.pcap")
dump = h1.popen(["tcpdump", "-i", "h1-eth0", "-U", "-n", "-w", path, "tcp or udp"],
stderr=subprocess.DEVNULL)
time.sleep(1) # let tcpdump start listening
srv = h2.popen([sys.executable, os.path.abspath(__file__), "_server", mode])
time.sleep(0.5) # let the server start listening
print(h1.cmd(f"{sys.executable} {os.path.abspath(__file__)} _client {mode}").strip())
srv.wait(timeout=30)
time.sleep(0.5) # the last ACK reaches tcpdump
dump.terminate()
dump.wait()
print(f"saved {os.path.relpath(path, os.getcwd())}\n")
show(path, relative)
finally:
net.stop()
def main():
args = [a for a in sys.argv[1:] if a != "--relative"]
relative = "--relative" in sys.argv
mode = args[0] if args else "tcp"
if mode == "_server":
server(args[1])
elif mode == "_client":
client(args[1])
elif mode == "show":
show(args[1], relative)
elif mode == "cli":
from mininet.cli import CLI
net, _, _ = build()
try:
CLI(net)
finally:
net.stop()
elif mode in ("tcp", "slow", "udp"):
from mininet.log import setLogLevel
setLogLevel("info")
run(mode, relative)
else:
sys.exit(__doc__)
if __name__ == "__main__":
main()