206 lines
7.9 KiB
Python
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()
|