#!/usr/bin/env python3 """Decode a hex-dump capture of LiteNetLib 1.x traffic. Input format: alternating timestamp lines and lowercase hex payload lines (one UDP payload per line), which is what the in-game capture produces. ./tools/decode_lnl.py traffic.txt """ import re, sys, struct, collections PROP = {0:'Unreliable',1:'Channeled',2:'Ack',3:'Ping',4:'Pong',5:'ConnectRequest', 6:'ConnectAccept',7:'Disconnect',8:'UnconnectedMessage',9:'MtuCheck',10:'MtuOk', 11:'Broadcast',12:'Merged',13:'ShutdownOk',14:'PeerNotFound',15:'InvalidProtocol', 16:'NatMessage',17:'Empty'} BATCH_MAGIC = 0x1160 def read_packed_uint(d, p): n = d[p] & 0x07 if n == 5: return struct.unpack_from('> 3, p + n def decode_ngo(payload): """Decode an NGO batch riding inside a Channeled/Unreliable LiteNetLib payload.""" if len(payload) < 16: return " (too short for a batch header)" magic, count, size = struct.unpack_from('= len(payload): break mtype, p = read_packed_uint(payload, p) msize, p = read_packed_uint(payload, p) out.append(f" [{mtype}] {msize} bytes: {payload[p:p+min(msize,32)].hex()}") p += msize return "\n".join(out) def main(path): lines = open(path).read().splitlines() events, ts = [], None for l in lines: l = l.strip() if re.match(r'^\d{4}-\d\d-\d\d', l): ts = l elif re.fullmatch(r'[0-9a-f]+', l) and len(l) >= 2: events.append((ts, bytes.fromhex(l))) hist = collections.Counter() for ts, b in events: prop = b[0] & 0x1f name = PROP.get(prop, f'?{prop}') hist[name] += 1 t = ts.split()[1][:12] if ts else '' detail = '' if name == 'ConnectRequest': pid = struct.unpack_from('H', addr, 2)[0] ip = '.'.join(str(x) for x in addr[4:8]) detail = (f"protocolId={pid} ct={ct} target={ip}:{port} " f"connectData={b[18+asz:].hex()}") elif name == 'ConnectAccept': # [1..8] connectionTime, [9] connectionNumber, [10] isReused, [11..14] peerId detail = (f"ct={struct.unpack_from(' 3 and hist['ConnectAccept']: print(" NOTE: repeated ConnectRequests answered by ConnectAccepts. If the connectionTime" "\n is identical across them it is ONE attempt being resent - the client is" "\n not acting on the accept. Capture with tcpdump to see if it arrives.") if __name__ == '__main__': main(sys.argv[1] if len(sys.argv) > 1 else 'traffic.txt')