// The capture table's rules that no Java run of the oracle shows // (Lauscher.datenVorbereiten, classifyApplicationLayerProtocol); the rest is // compared with Java's own tables in the *Java.test.ts files (javaRun.ts). import { describe, expect, it } from 'vitest'; import { captureCommentText, captureRows, icmpMessage, TCP_FLAG, type CapturedFrame, type CaptureRow, type EthernetFrame, type IPv4Packet, } from '../src/index.js'; const WORDS = { protocol: 'Protocol', searchForMac: 'Search for MAC', macIs: 'MAC is' }; const plain = (value: number) => String(value); function captured(packet: IPv4Packet, number = 1): CapturedFrame { const frame: EthernetFrame = { identity: number, sourceMacAddress: '02:00:00:00:00:01', destinationMacAddress: '02:00:00:00:00:02', payload: { kind: 'ipv4', packet } }; return { number, timeMilliseconds: 10 * number, frame }; } function udp(text: string, sourcePort: number, destinationPort: number): IPv4Packet { return { identity: 7, senderIPAddress: '10.0.0.1', receiverIPAddress: '10.0.0.2', timeToLive: 64, protocol: 'udp', payload: { kind: 'udp', datagram: { sourcePort, destinationPort, payload: new TextEncoder().encode(text) } }, }; } function tcp(text: string, sourcePort: number, destinationPort: number, flags: number = TCP_FLAG.acknowledgement): IPv4Packet { return { identity: 8, senderIPAddress: '10.0.0.1', receiverIPAddress: '10.0.0.2', timeToLive: 64, protocol: 'tcp', payload: { kind: 'tcp', segment: { sourcePort, destinationPort, sequenceNumber: 1000, acknowledgementNumber: 2000, flags, payload: new TextEncoder().encode(text) }, }, }; } /** The protocol of the data row, or undefined without one. */ function applicationProtocol(packet: IPv4Packet): string | undefined { return captureRows([captured(packet)], { withAddresses: true }).find((row) => row.layer === 'application')?.protocol; } const comments = (rows: CaptureRow[]) => rows.map((row) => captureCommentText(row.comment, WORDS, plain)); describe('capture table', () => { it("names the data's protocol as Java does, by markers, ports and words", () => { expect(applicationProtocol(tcp('x//0x40//y', 6346, 6346))).toBe('GNT'); expect(applicationProtocol(tcp('+ok 2 messages', 110, 50000))).toBe('POP3'); expect(applicationProtocol(tcp('retr 1', 50000, 110))).toBe('POP3'); expect(applicationProtocol(tcp('noop', 50000, 110))).toBe('POP3'); expect(applicationProtocol(tcp('hello', 50000, 110))).toBe(''); expect(applicationProtocol(tcp('250 OK', 25, 50000))).toBe('SMTP'); expect(applicationProtocol(tcp('anything', 50000, 25))).toBe('SMTP'); expect(applicationProtocol(udp('ID=1 QR=0 RCODE=0', 50000, 53))).toBe('DNS'); expect(applicationProtocol(udp('DHCPDISCOVER', 68, 67))).toBe('DHCP'); expect(applicationProtocol(udp('table', 521, 520))).toBe('RIP'); expect(applicationProtocol(udp('table', 520, 520))).toBe(''); expect(applicationProtocol(tcp('GET / HTTP/1.1', 50000, 80))).toBe('HTTP'); expect(applicationProtocol(tcp('GET /', 50000, 80))).toBe(''); expect(applicationProtocol(tcp('', 80, 50000))).toBe('HTTP'); // GNT comes first, then POP3, SMTP, DNS, DHCP, RIP, HTTP. expect(applicationProtocol(udp('DHCP ID=1 QR=0 RCODE=0', 68, 67))).toBe('DNS'); }); it('shows data only when Java trims them to something, and without ends in the layered view', () => { expect(applicationProtocol(udp(' \t\n', 9, 9))).toBeUndefined(); expect(applicationProtocol(tcp('', 50000, 80))).toBeUndefined(); const withAddresses = captureRows([captured(udp(' x ', 5000, 9))], { withAddresses: true }); const layered = captureRows([captured(udp(' x ', 5000, 9))], { withAddresses: false }); expect(withAddresses.map((row) => [row.source, row.destination])).toEqual([ ['02:00:00:00:00:01', '02:00:00:00:00:02'], ['10.0.0.1', '10.0.0.2'], ['10.0.0.1:5000', '10.0.0.2:9'], ['10.0.0.1:5000', '10.0.0.2:9'], ]); expect(layered.map((row) => [row.source, row.destination]).slice(2)).toEqual([ ['5000', '9'], [null, null], ]); expect(comments(layered)).toEqual(['0x800', 'Protocol: 17, TTL: 64', '', ' x ']); }); it('marks a SYN, else a FIN, and an acknowledgement only with its flag', () => { const segmentComment = (flags: number) => comments(captureRows([captured(tcp('', 1, 2, flags))], { withAddresses: true }))[2]; expect(segmentComment(TCP_FLAG.synchronize)).toBe('SYN, SEQ: 1000'); expect(segmentComment(TCP_FLAG.synchronize | TCP_FLAG.finish | TCP_FLAG.acknowledgement)).toBe('SYN, SEQ: 1000, ACK: 2000'); expect(segmentComment(TCP_FLAG.finish | TCP_FLAG.acknowledgement)).toBe('FIN, SEQ: 1000, ACK: 2000'); expect(segmentComment(TCP_FLAG.push | TCP_FLAG.reset)).toBe('SEQ: 1000'); expect(captureCommentText({ kind: 'tcp', flag: undefined, sequence: 1234567, acknowledgement: 1000 }, WORDS, (value) => value.toLocaleString('de-DE'))).toBe( 'SEQ: 1.234.567, ACK: 1.000', ); }); it('writes an ICMP error with the packet it carries, also an ICMP one inside', () => { const echo: IPv4Packet = { identity: 3, senderIPAddress: '10.0.0.1', receiverIPAddress: '10.0.0.9', timeToLive: 1, protocol: 'icmp', payload: { kind: 'icmp', message: icmpMessage('echoRequest', { identifier: 4, sequenceNumber: 5 }) }, }; const error: IPv4Packet = { identity: 6, senderIPAddress: '10.0.0.254', receiverIPAddress: '10.0.0.1', timeToLive: 64, protocol: 'icmp', payload: { kind: 'icmp', message: icmpMessage('timeExceeded', { embeddedOriginalPacket: echo }) }, }; const rows = captureRows([captured(error)], { withAddresses: true }); expect(rows.map((row) => [row.protocol, row.layer])).toEqual([ ['', 'networkAccess'], ['ICMP', 'internet'], ]); expect(comments(rows)[1]).toBe( 'ICMP Time Exceeded (poof), TTL: 64, Identifier: 0, Seq.-No.: 0, Payload: ' + '[id=3; ttl=1; protocol=1; sender=10.0.0.1; target=10.0.0.9; icmpType=8; icmpCode=0; identifier=4; seqNr=5]', ); const unreachable = { ...error, payload: { kind: 'icmp' as const, message: icmpMessage('destinationNetworkUnreachable', { embeddedOriginalPacket: udp('x', 1, 2) }) } }; expect(comments(captureRows([captured(unreachable)], { withAddresses: true }))[1]).toBe( 'ICMP Network Unreachable, TTL: 64, Identifier: 0, Seq.-No.: 0, Payload: [id=7, ttl=64, protocol=17, dest=10.0.0.2, src=10.0.0.1]', ); }); it('keeps the frame number and time on each of its rows', () => { const rows = captureRows([captured(udp('a', 1, 2), 4), captured(udp('', 1, 2), 9)], { withAddresses: true }); expect(rows.map((row) => [row.number, row.timeMilliseconds])).toEqual([ [4, 40], [4, 40], [4, 40], [4, 40], [9, 90], [9, 90], [9, 90], ]); }); });