# # Wireshark tests # By Gerald Combs # # Ported from a set of Bash scripts which were copyright 2005 Ulf Lamping # # SPDX-License-Identifier: GPL-2.0-or-later # '''Text2pcap tests''' import re import subprocess from subprocesstest import get_capture_info, grep_output import json import pytest testin_txt = 'testin.txt' testout_pcap = 'testout.pcap' testout_pcapng = 'testout.pcapng' file_type_to_descr = { 'pcap': 'Wireshark/tcpdump/... - pcap', 'pcapng': 'Wireshark/... - pcapng', } file_type_to_testout = { 'pcap': testout_pcap, 'pcapng': testout_pcapng, } encap_to_link_type_long = { 'Ethernet': 1, 'Raw IP': 14, 'Linux cooked-mode capture v1': 113, 'IEEE 802.11 plus radiotap radio header': 127, 'DVB-CI (Common Interface)': 235, } encap_to_link_type = { 'ether': 1, 'rawip': 14, 'linux-sll': 113, 'ieee-802-11-radiotap': 127, 'dvbci': 235, } def check_capinfos_info(cmd_capinfos, cap_file): cap_info = { 'filetype': None, 'encapsulation': None, 'packets': None, 'datasize': None, 'timeend': None, 'interfaces': None, 'userappl': [], } str_pats = { 'filetype': 'File type', 'encapsulation': 'File encapsulation', 'timeend': 'Latest packet time', } int_pats = { 'packets': 'Number of packets', 'datasize': 'Data size', 'interfaces': 'Number of interfaces in file', } str_list_pats = { 'userappl': 'Capture application', } capinfos_out = get_capture_info(cmd_capinfos, ('-MtEcdeIF',), cap_file) for ci_line in capinfos_out.splitlines(): for sp_key in str_pats: str_pat = r'{}:\s+([\S ]+)'.format(str_pats[sp_key]) str_res = re.search(str_pat, ci_line) if str_res is not None: cap_info[sp_key] = str_res.group(1) for ip_key in int_pats: int_pat = r'{}:\s+(\d+)'.format(int_pats[ip_key]) int_res = re.search(int_pat, ci_line) if int_res is not None: cap_info[ip_key] = int(int_res.group(1)) for sp_key, sp_value in str_list_pats.items(): str_pat = r'{}:\s+([\S ]+)'.format(sp_value) str_res = re.search(str_pat, ci_line) if str_res is not None: cap_info[sp_key].append(str_res.group(1)) return cap_info def get_capinfos_cmp_info(cii): cmp_keys = ('encapsulation', 'packets', 'datasize') return { k: v for k, v in cii.items() if k in cmp_keys } def compare_capinfos_info(cii1, cii2, filename1, filename2): cii_cmp_i1 = get_capinfos_cmp_info(cii1) cii_cmp_i2 = get_capinfos_cmp_info(cii2) assert cii_cmp_i1 == cii_cmp_i2 @pytest.fixture def check_text2pcap(cmd_tshark, cmd_text2pcap, cmd_capinfos, capture_file, result_file, base_env): def check_text2pcap_real(cap_filename, file_type, expected_packets=None, expected_datasize=None): # Perform the following actions # - Get information for the input pcap file with capinfos # - Generate an ASCII hexdump with TShark # - Convert the ASCII hexdump back to pcap using text2pcap # - Get information for the output pcap file with capinfos # - Check that file type, encapsulation type, number of packets and data size # in the output file are the same as in the input file cap_file = capture_file(cap_filename) pre_cap_info = check_capinfos_info(cmd_capinfos, cap_file) # Due to limitations of "tshark -x", the output might contain more than one # data source which is subsequently interpreted as additional frame data. # See https://gitlab.com/wireshark/wireshark/-/issues/14639 if expected_packets is not None: assert pre_cap_info['packets'] != expected_packets pre_cap_info['packets'] = expected_packets if expected_datasize is not None: assert pre_cap_info['datasize'] != expected_datasize pre_cap_info['datasize'] = expected_datasize assert pre_cap_info['encapsulation'] in encap_to_link_type assert file_type in file_type_to_testout, 'Invalid file type' testin_file = result_file(testin_txt) tshark_cmd = '"{cmd}" -r "{cf}" -t ad --hexdump frames --hexdump time > "{of}"'.format( cmd = cmd_tshark, cf = cap_file, of = testin_file, ) subprocess.check_call(tshark_cmd, shell=True, env=base_env) testout_fname = file_type_to_testout[file_type] testout_file = result_file(testout_fname) # The first word is the file type (the rest might be compression info) filetype_flag = pre_cap_info['filetype'].split()[0] # We want the -a flag, because the tshark -x format is a hex+ASCII # format where the ASCII can be confused for hex bytes without it. text2pcap_cmd = '"{cmd}" -a -F {filetype} -l {linktype} -t "ISO" "{in_f}" "{out_f}"'.format( cmd = cmd_text2pcap, filetype = filetype_flag, linktype = encap_to_link_type[pre_cap_info['encapsulation']], in_f = testin_file, out_f = testout_file, ) proc = subprocess.run(text2pcap_cmd, shell=True, check=True, capture_output=True, encoding='utf-8', env=base_env) assert grep_output(proc.stderr, 'potential packet'), "text2pcap didn't complete" assert not grep_output(proc.stderr, 'Inconsistent offset'), 'text2pcap detected inconsistent offset' post_cap_info = check_capinfos_info(cmd_capinfos, testout_file) compare_capinfos_info(pre_cap_info, post_cap_info, cap_file, testout_fname) return check_text2pcap_real class TestText2pcapPcap: def test_text2pcap_empty_pcap(self, check_text2pcap): '''Test text2pcap with empty.pcap.''' check_text2pcap('empty.pcap', 'pcap') def test_text2pcap_dhcp_pcap(self, check_text2pcap): '''Test text2pcap with dhcp.pcap.''' check_text2pcap('dhcp.pcap', 'pcap') def test_text2pcap_dhcp_nanosecond_pcap(self, check_text2pcap): '''Test text2pcap with dhcp-nanosecond.pcap.''' check_text2pcap('dhcp-nanosecond.pcap', 'pcap') def test_text2pcap_segmented_fpm_pcap(self, check_text2pcap): '''Test text2pcap with segmented_fpm.pcap.''' check_text2pcap('segmented_fpm.pcap', 'pcap') def test_text2pcap_c1222_std_example8_pcap(self, check_text2pcap): '''Test text2pcap with c1222_std_example8.pcap.''' check_text2pcap('c1222_std_example8.pcap', 'pcap') def test_text2pcap_dns_port_pcap(self, check_text2pcap): '''Test text2pcap with dns_port.pcap.''' check_text2pcap('dns_port.pcap', 'pcap') def test_text2pcap_dvb_ci_uv1_0000_pcap(self, check_text2pcap): '''Test text2pcap with dvb-ci_UV1_0000.pcap.''' check_text2pcap('dvb-ci_UV1_0000.pcap', 'pcap') def test_text2pcap_ikev1_certs_pcap(self, check_text2pcap): '''Test text2pcap with ikev1-certs.pcap.''' check_text2pcap('ikev1-certs.pcap', 'pcap') def test_text2pcap_rsa_p_lt_q_pcap(self, check_text2pcap): '''Test text2pcap with rsa-p-lt-q.pcap.''' check_text2pcap('rsa-p-lt-q.pcap', 'pcap') def test_text2pcap_rsasnakeoil2_pcap(self, check_text2pcap): '''Test text2pcap with rsasnakeoil2.pcap.''' check_text2pcap('rsasnakeoil2.pcap', 'pcap') def test_text2pcap_sample_control4_2012_03_24_pcap(self, check_text2pcap): '''Test text2pcap with sample_control4_2012-03-24.pcap.''' # Tests handling additional data source (decrypted ZigBee packets) # Either tshark must not output the additional data source, # or text2pcap must ignore it. check_text2pcap('sample_control4_2012-03-24.pcap', 'pcap') def test_text2pcap_snakeoil_dtls_pcap(self, check_text2pcap): '''Test text2pcap with snakeoil-dtls.pcap.''' check_text2pcap('snakeoil-dtls.pcap', 'pcap') def test_text2pcap_wpa_eap_tls_pcap_gz(self, check_text2pcap): '''Test text2pcap with wpa-eap-tls.pcap.gz.''' # Tests handling additional data source (reassemblies) # Either tshark must not output the additional data source, # or text2pcap must ignore it. check_text2pcap('wpa-eap-tls.pcap.gz', 'pcap') def test_text2pcap_wpa_induction_pcap(self, check_text2pcap): '''Test text2pcap with wpa-Induction.pcap.gz.''' check_text2pcap('wpa-Induction.pcap.gz', 'pcap') class TestText2pcapPcapng: def test_text2pcap_dhcp_pcapng(self, check_text2pcap): '''Test text2pcap with dhcp.pcapng.''' check_text2pcap('dhcp.pcapng', 'pcapng') def test_text2pcap_dhcp_nanosecond_pcapng(self, check_text2pcap): '''Test text2pcap with dhcp-nanosecond.pcapng.''' check_text2pcap('dhcp-nanosecond.pcapng', 'pcapng') def test_text2pcap_dhe1_pcapng_gz(self, check_text2pcap): '''Test text2pcap with dhe1.pcapng.gz.''' check_text2pcap('dhe1.pcapng.gz', 'pcapng') def test_text2pcap_dmgr_pcapng(self, check_text2pcap): '''Test text2pcap with dmgr.pcapng.''' check_text2pcap('dmgr.pcapng', 'pcapng') def test_text2pcap_dns_icmp_pcapng_gz(self, check_text2pcap): '''Test text2pcap with dns+icmp.pcapng.gz.''' # This file needs (and thus tests) the -a flag to identify when the # start of the ASCII dump looks like hex. check_text2pcap('dns+icmp.pcapng.gz', 'pcapng') def test_text2pcap_packet_h2_14_headers_pcapng(self, check_text2pcap): '''Test text2pcap with packet-h2-14_headers.pcapng.''' check_text2pcap('packet-h2-14_headers.pcapng', 'pcapng') def test_text2pcap_sip_pcapng(self, check_text2pcap): '''Test text2pcap with sip.pcapng.''' check_text2pcap('sip.pcapng', 'pcapng') def test_text2pcap_icmp_ascii_pcapng(self, check_text2pcap): '''Test text2pcap with a test that needs special ASCII handling.''' # This file has multiple spaces and spaces at hex dump line start, # and was not handled by a simpler version of the ASCII rollback. check_text2pcap('icmp_ascii.pcapng', 'pcapng') @pytest.fixture def check_rawip(run_text2pcap_capinfos_tshark): def check_rawip_real(pdata, packets, datasize): assert {'encapsulation': 'rawip4', 'packets': packets, \ 'datasize': datasize, 'expert': ''} == \ run_text2pcap_capinfos_tshark(pdata, ("-l228",)) return check_rawip_real class TestText2pcapParsing: def test_text2pcap_eol_hash(self, cmd_text2pcap, cmd_capinfos, capture_file, result_file, base_env): '''Test text2pcap hash sign at the end-of-line.''' txt_fname = 'text2pcap_hash_eol.txt' testout_file = result_file(testout_pcap) proc = subprocess.run((cmd_text2pcap, '-F', 'pcapng', '-t', '%Y-%m-%d %H:%M:%S.', capture_file(txt_fname), testout_file, ), check=True, capture_output=True, encoding='utf-8', env=base_env) assert not grep_output(proc.stderr, 'Inconsistent offset'), 'text2pcap failed to parse the hash sign at the end of the line' assert grep_output(proc.stderr, r'Directive \[ test_directive'), 'text2pcap failed to parse #TEXT2PCAP test_directive' pre_cmp_info = {'encapsulation': 'ether', 'packets': 1, 'datasize': 96, 'timeend': '2015-10-01 21:16:24.317453000'} post_cmp_info = check_capinfos_info(cmd_capinfos, testout_file) compare_capinfos_info(pre_cmp_info, post_cmp_info, txt_fname, testout_pcap) def test_text2pcap_doc_no_line_limit(self, check_rawip): ''' Verify: There is no limit on the width or number of bytes per line and Bytes/hex numbers can be uppercase or lowercase. ''' pdata = "0000 45 00 00 21 00 01 00 00 40 11\n" \ "000A 7C C9 7F 00 00 01" \ " 7f 00 00 01 ff 98 00 13 00 0d b5 48 66 69 72 73\n" \ "0020 74\n" check_rawip(pdata, 1, 33) def test_text2pcap_doc_ignore_text(self, check_rawip): ''' Verify: the text dump at the end of the line is ignored. Any hex numbers in this text are also ignored. Any lines of text between the bytestring lines is ignored. Any line where the first non-whitespace character is '#' will be ignored as a comment. ''' pdata = "0000 45 00 00 21 00 01 00 00 40 11 7c c9 7f 00 00 01 bad\n" \ "0010 7f 00 00 01 ff 98 00 13 00 0d b5 48 66 69 72 73 - 42\n" \ "0020 74\n" \ "0021\n" \ "That 0021 should probably be ignored as it this: 00 20\n" \ "0000 45 00 00 22 00 01 00 00 40 11 7c c8 7f 00 00 01\n" \ "0010 7f 00 00 01 ff 99 00 13 00 0e bc e9 73 65 63 6f ...\n" \ " \t# 0020 12 34 56<-- comment, ignore this!\n" \ "0020 6e 64\n" \ "12 34 56 78 90 # ignore this due to missing offset!\n" check_rawip(pdata, 2, 67) def test_text2pcap_doc_leading_text_ignored(self, check_rawip): ''' Verify: Any test before the offset is ignored, including email forwarding characters '>'. An offset is a hex number longer than two characters. An offset of zero is indicative of starting a new packet. ''' pdata = "> >> 000 45 00 00 21 00 01 00 00 40 11 7c c9 7f 00 00 01\n" \ "> >> 010 7f 00 00 01 ff 98 00 13 00 0d b5 48 66 69 72 73\n" \ "> >> 020 74\n" \ "> >> 000 45 00 00 22 00 01 00 00 40 11 7c c8 7f 00 00 01\n" \ "> >> 010 7f 00 00 01 ff 99 00 13 00 0e bc e9 73 65 63 6f\n" \ "> >> 020 6e 64\n" check_rawip(pdata, 2, 67) def test_text2pcap_doc_require_offset(self, check_rawip): '''Any line which has only bytes without a leading offset is ignored.''' pdata = "45 00 00 21 00 01 00 00 40 11 7c c9 7f 00 00 01\n" \ "7f 00 00 01 ff 98 00 13 00 0d b5 48 66 69 72 73\n" check_rawip(pdata, 0, 0) def test_text2pcap_eol_missing(self, check_rawip): '''Verify that the last LF can be missing.''' pdata = "0000 45 00 00 21 00 01 00 00 40 11 7c c9 7f 00 00 01\n" \ "0010 7f 00 00 01 ff 98 00 13 00 0d b5 48 66 69 72 73\n" \ "0020 74" check_rawip(pdata, 1, 33) def test_text2pcap_two_byte_words(self, check_rawip): '''Verify: Byte groups of 2 to 4 bytes can be used.''' pdata = "000 4500 0021 0001 0000 4011 7cc9 7f00 0001\n" \ "010 7f00 0001 ff98 0013 000d b548 6669 7273\n" \ "020 74\n" \ "000 4500 0022 000100 004011 7cc87f00 0001\n" \ "010 7f00 0001 ff990013 000e bce9 7365636f\n" \ "020 6e64\n" check_rawip(pdata, 2, 67) def test_text2pcap_two_byte_words_od(self, check_rawip): '''Verify: od/hexdump style multiple byte groups where the last group is zero padded and an extra line indicates the true length can be used.''' pdata = "000 4500 0021 0001 0000 4011 7cc9 7f00 0001\n" \ "010 7f00 0001 ff98 0013 000d b548 6669 7273\n" \ "020 7400\n" \ "021\n" check_rawip(pdata, 1, 33) def test_text2pcap_two_byte_words_le(self, run_text2pcap_capinfos_tshark): '''Verify: Little-endian 2-byte groups can be used.''' pdata = "000 0045 2100 0100 0000 1140 c97c 007f 0100\n" \ "010 007f 0100 98ff 1300 0d00 48b5 6966 7372\n" \ "020 0074\n" \ "021\n" assert {'encapsulation': 'rawip4', 'packets': 1, \ 'datasize': 33, 'expert': ''} == \ run_text2pcap_capinfos_tshark(pdata, ("-Erawip4", "--little-endian")) class TestText2pcapRegex: def test_text2pcap_regex(self, run_text2pcap_capinfos_tshark): '''Verify basic functionality of text2pcap in regex mode.''' capinfo = run_text2pcap_capinfos_tshark( "> 0:00:00.265620 a130368b000000080060\n" \ "< 0:00:00.295459 a2010800000000000000000800000000\n", ( "-r", r'^(?[<>])\s(?