#!/usr/bin/env python3 # Generate Wireshark Dissectors for eletronic trading/market data # protocols such as ETI/EOBI. # # Targets Wireshark 3.5 or later. # # SPDX-FileCopyrightText: © 2021 Georg Sauthoff # SPDX-License-Identifier: GPL-2.0-or-later import argparse import itertools import re import sys import xml.etree.ElementTree as ET # inlined from upstream's etimodel.py import itertools def get_max_sizes(st, dt): h = {} for name, e in dt.items(): v = e.get('size', '0') h[name] = int(v) for name, e in itertools.chain((i for i in st.items() if i[1].get('type') != 'Message'), (i for i in st.items() if i[1].get('type') == 'Message')): s = 0 for m in e: x = h.get(m.get('type'), 0) s += x * int(m.get('cardinality')) h[name] = s return h def get_min_sizes(st, dt): h = {} for name, e in dt.items(): v = e.get('size', '0') if e.get('variableSize') is None: h[name] = int(v) else: h[name] = 0 for name, e in itertools.chain((i for i in st.items() if i[1].get('type') != 'Message'), (i for i in st.items() if i[1].get('type') == 'Message')): s = 0 for m in e: x = h.get(m.get('type'), 0) s += x * int(m.get('minCardinality', '1')) h[name] = s return h # end # inlined from upstream's etimodel.py def get_used_types(st): xs = set(y.get('type') for _, x in st.items() for y in x) return xs def get_data_types(d): r = d.getroot() x = r.find('DataTypes') h = {} for e in x: h[e.get('name')] = e return h def get_structs(d): r = d.getroot() x = r.find('Structures') h = {} for e in x: h[e.get('name')] = e return h def get_templates(st): ts = [] for k, v in st.items(): if v.get('type') == 'Message': ts.append((int(v.get('numericID')), k)) ts.sort() return ts def gen_header(proto, desc, o=sys.stdout): if proto.startswith('eti') or proto.startswith('xti'): ph = '#include "packet-tcp.h" // tcp_dissect_pdus()' else: ph = '#include "packet-udp.h" // udp_dissect_pdus()' print(f'''// auto-generated by Georg Sauthoff's eti2wireshark.py /* packet-eti.c * Routines for {proto.upper()} dissection * Copyright 2021, Georg Sauthoff * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ /* * The {desc} ({proto.upper()}) is an electronic trading protocol * that is used by a few exchanges (Eurex, Xetra, ...). * * It's a Length-Tag based protocol consisting of mostly fix sized * request/response messages. * * Links: * https://en.wikipedia.org/wiki/List_of_electronic_trading_protocols#Europe * https://github.com/gsauthof/python-eti#protocol-descriptions * https://github.com/gsauthof/python-eti#protocol-introduction * */ #include #include // Should be first Wireshark include (other than config.h) {ph} #include // expert info #include #include // snprintf() /* Prototypes */ /* (Required to prevent [-Wmissing-prototypes] warnings */ void proto_reg_handoff_{proto}(void); void proto_register_{proto}(void); ''', file=o) def name2ident(name): ll = True xs = [] for i, c in enumerate(name): if c.isupper(): if i > 0 and ll: xs.append('_') xs.append(c.lower()) ll = False else: xs.append(c) ll = True return ''.join(xs) def gen_enums(dt, ts, o=sys.stdout): print('static const value_string template_id_vals[] = { // TemplateID', file=o) min_tid, max_tid = ts[0][0], ts[-1][0] xs = [None] * (max_tid - min_tid + 1) for tid, name in ts: xs[tid-min_tid] = name for i, name in enumerate(xs): if name is None: print(f' {{ {min_tid + i}, "Unknown" }},', file=o) else: print(f' {{ {min_tid + i}, "{name}" }},', file=o) print(''' { 0, NULL } }; static value_string_ext template_id_vals_ext = VALUE_STRING_EXT_INIT(template_id_vals);''', file=o) name2access = { 'TemplateID': '&template_id_vals_ext' } dedup = {} for name, e in dt.items(): vs = [ (x.get('value'), x.get('name')) for x in e.findall('ValidValue') ] if not vs: continue if e.get('rootType') == 'String' and e.get('size') != '1': continue ident = name2ident(name) nv = e.get('noValue') ws = [ v[0] for v in vs ] if nv not in ws: if nv.startswith('0x0') and e.get('rootType') == 'String': nv = '\0' vs.append( (nv, 'NO_VALUE') ) if e.get('type') == 'int': vs.sort(key = lambda x : int(x[0], 0)) else: vs.sort(key = lambda x : ord(x[0])) s = '-'.join(f'{v[0]}:{v[1]}' for v in vs) x = dedup.get(s) if x is None: dedup[s] = name else: name2access[name] = name2access[x] print(f'// {name} aliased by {x}', file=o) continue print(f'static const value_string {ident}_vals[] = {{ // {name}', file=o) for i, v in enumerate(vs): if e.get('rootType') == 'String': k = f"'{v[0]}'" if ord(v[0]) != 0 else '0' print(f''' {{ {k}, "{v[1]}" }},''', file=o) else: print(f' {{ {v[0]}, "{v[1]}" }},', file=o) print(''' { 0, NULL } };''', file=o) if len(vs) > 7: print(f'static value_string_ext {ident}_vals_ext = VALUE_STRING_EXT_INIT({ident}_vals);', file=o) name2access[name] = f'&{ident}_vals_ext' else: name2access[name] = f'VALS({ident}_vals)' return name2access def get_fields(st, dt): seen = {} for name, e in st.items(): for m in e: t = dt.get(m.get('type')) if is_padding(t): continue if not (is_int(t) or is_fixed_string(t) or is_var_string(t)): continue name = m.get('name') if name in seen: if seen[name] != t: raise RuntimeError(f'Mismatching type for: {name}') else: seen[name] = t vs = list(seen.items()) vs.sort() return vs def gen_field_handles(st, dt, proto, o=sys.stdout): print(f'''static expert_field ei_{proto}_counter_overflow = EI_INIT; static expert_field ei_{proto}_invalid_template = EI_INIT; static expert_field ei_{proto}_invalid_length = EI_INIT;''', file=o) if not proto.startswith('eobi'): print(f'static expert_field ei_{proto}_unaligned = EI_INIT;', file=o) print(f'''static expert_field ei_{proto}_missing = EI_INIT; static expert_field ei_{proto}_overused = EI_INIT; ''', file=o) vs = get_fields(st, dt) s = ', '.join('-1' for i in range(len(vs))) print(f'static int hf_{proto}[] = {{ {s} }};', file=o) print(f'''static int hf_{proto}_dscp_exec_summary = -1; static int hf_{proto}_dscp_improved = -1; static int hf_{proto}_dscp_widened = -1;''', file=o) print('enum Field_Handle_Index {', file=o) for i, (name, _) in enumerate(vs): c = ' ' if i == 0 else ',' print(f' {c} {name.upper()}_FH_IDX', file=o) print('};', file=o) def type2ft(t): if is_timestamp_ns(t): return 'FT_ABSOLUTE_TIME' if is_dscp(t): return 'FT_UINT8' if is_int(t): if t.get('rootType') == 'String': return 'FT_CHAR' u = 'U' if is_unsigned(t) else '' if t.get('size') is None: raise RuntimeError(f'None size: {t.get("name")}') size = int(t.get('size')) * 8 return f'FT_{u}INT{size}' if is_fixed_string(t) or is_var_string(t): # NB: technically, ETI fixed-strings are blank-padded, # unless they are marked NO_VALUE, in that case # the first byte is zero, followed by unspecified content. # Also, some fixed-strings are zero-terminated, where again # the bytes following the terminator are unspecified. return 'FT_STRINGZTRUNC' raise RuntimeError('unexpected type') def type2enc(t): if is_timestamp_ns(t): return 'ABSOLUTE_TIME_UTC' if is_dscp(t): return 'BASE_HEX' if is_int(t): if t.get('rootType') == 'String': # NB: basically only used when enum and value is unknown return 'BASE_HEX' else: return 'BASE_DEC' if is_fixed_string(t) or is_var_string(t): # previously 'STR_ASCII', which was removed upstream # cf. 19dcb725b61e384f665ad4b955f3b78f63e626d9 return 'BASE_NONE' raise RuntimeError('unexpected type') def gen_field_info(st, dt, n2enum, proto='eti', o=sys.stdout): print(' static hf_register_info hf[] ={', file=o) vs = get_fields(st, dt) for i, (name, t) in enumerate(vs): c = ' ' if i == 0 else ',' ft = type2ft(t) enc = type2enc(t) if is_enum(t) and not is_dscp(t): vals = n2enum[t.get('name')] if vals.startswith('&'): extra_enc = '| BASE_EXT_STRING' else: extra_enc = '' else: vals = 'NULL' extra_enc = '' print(f''' {c} {{ &hf_{proto}[{name.upper()}_FH_IDX], {{ "{name}", "{proto}.{name.lower()}", {ft}, {enc}{extra_enc}, {vals}, 0x0, NULL, HFILL }} }}''', file=o) print(f''' , {{ &hf_{proto}_dscp_exec_summary, {{ "DSCP_ExecSummary", "{proto}.dscp_execsummary", FT_BOOLEAN, 8, NULL, 0x10, NULL, HFILL }} }} , {{ &hf_{proto}_dscp_improved, {{ "DSCP_Improved", "{proto}.dscp_improved", FT_BOOLEAN, 8, NULL, 0x20, NULL, HFILL }} }} , {{ &hf_{proto}_dscp_widened, {{ "DSCP_Widened", "{proto}.dscp_widened", FT_BOOLEAN, 8, NULL, 0x40, NULL, HFILL }} }}''', file=o) print(' };', file=o) def gen_subtree_handles(st, proto='eti', o=sys.stdout): ns = [ name for name, e in st.items() if e.get('type') != 'Message' ] ns.sort() s = ', '.join('-1' for i in range(len(ns) + 1)) h = dict( (n, i) for i, n in enumerate(ns, 1) ) print(f'static gint ett_{proto}[] = {{ {s} }};', file=o) print(f'static gint ett_{proto}_dscp = -1;', file=o) return h def gen_subtree_array(st, proto='eti', o=sys.stdout): n = sum(1 for name, e in st.items() if e.get('type') != 'Message') n += 1 s = ', '.join(f'&ett_{proto}[{i}]' for i in range(n)) print(f' static gint * const ett[] = {{ {s}, &ett_{proto}_dscp }};', file=o) def gen_fields_table(st, dt, sh, o=sys.stdout): name2off = {} off = 0 names = [] for name, e in st.items(): if e.get('type') == 'Message': continue if name.endswith('Comp'): s = name[:-4] name2off[name] = off off += len(s) + 1 names.append(s) s = '\\0'.join(names) print(f' static const char struct_names[] = "{s}";', file=o) xs = [ x for x in st.items() if x[1].get('type') != 'Message' ] xs += [ x for x in st.items() if x[1].get('type') == 'Message' ] print(' static const struct ETI_Field fields[] = {', file=o) i = 0 fields2idx = {} for name, e in xs: fields2idx[name] = i print(f' // {name}@{i}', file=o) counters = {} cnt = 0 for m in e: t = dt.get(m.get('type')) c = ' ' if i == 0 else ',' typ = '' size = int(t.get('size')) if t is not None else 0 rep = '' fh = f'{m.get("name").upper()}_FH_IDX' sub = '' if is_padding(t): print(f' {c} {{ ETI_PADDING, 0, {size}, 0, 0 }}', file=o) elif is_fixed_point(t): if size != 8: raise RuntimeError('only supporting 8 byte fixed point') fraction = int(t.get('precision')) if fraction > 16: raise RuntimeError('unusual high precisio in fixed point') print(f' {c} {{ ETI_FIXED_POINT, {fraction}, {size}, {fh}, 0 }}', file=o) elif is_timestamp_ns(t): if size != 8: raise RuntimeError('only supporting timestamps') print(f' {c} {{ ETI_TIMESTAMP_NS, 0, {size}, {fh}, 0 }}', file=o) elif is_dscp(t): print(f' {c} {{ ETI_DSCP, 0, {size}, {fh}, 0 }}', file=o) elif is_int(t): u = 'U' if is_unsigned(t) else '' if t.get('rootType') == 'String': typ = 'ETI_CHAR' else: typ = f'ETI_{u}INT' if is_enum(t): typ += '_ENUM' if t.get('type') == 'Counter': counters[m.get('name')] = cnt suf = f' // <- counter@{cnt}' if cnt > 7: raise RuntimeError(f'too many counters in message: {name}') rep = cnt cnt += 1 if typ != 'ETI_UINT': raise RuntimeError('only unsigned counters supported') if size > 2: raise RuntimeError('only smaller counters supported') typ = 'ETI_COUNTER' ett_idx = t.get('maxValue') else: rep = 0 suf = '' ett_idx = 0 print(f' {c} {{ {typ}, {rep}, {size}, {fh}, {ett_idx} }}{suf}', file=o) elif is_fixed_string(t): print(f' {c} {{ ETI_STRING, 0, {size}, {fh}, 0 }}', file=o) elif is_var_string(t): k = m.get('counter') x = counters[k] print(f' {c} {{ ETI_VAR_STRING, {x}, {size}, {fh}, 0 }}', file=o) else: a = m.get('type') fields_idx = fields2idx[a] k = m.get('counter') if k: counter_off = counters[k] typ = 'ETI_VAR_STRUCT' else: counter_off = 0 typ = 'ETI_STRUCT' names_off = name2off[m.get('type')] ett_idx = sh[a] print(f' {c} {{ {typ}, {counter_off}, {names_off}, {fields_idx}, {ett_idx} }} // {m.get("name")}', file=o) i += 1 print(' , { ETI_EOF, 0, 0, 0, 0 }', file=o) i += 1 print(' };', file=o) return fields2idx def gen_template_table(min_templateid, n, ts, fields2idx, o=sys.stdout): xs = [ '-1' ] * n for tid, name in ts: xs[tid - min_templateid] = f'{fields2idx[name]} /* {name} */' s = '\n , '.join(xs) print(f' static const int16_t tid2fidx[] = {{\n {s}\n }};', file=o) def gen_sizes_table(min_templateid, n, st, dt, ts, proto, o=sys.stdout): is_eobi = proto.startswith('eobi') xs = [ '0' if is_eobi else '{ 0, 0}' ] * n min_s = get_min_sizes(st, dt) max_s = get_max_sizes(st, dt) if is_eobi: for tid, name in ts: xs[tid - min_templateid] = f'{max_s[name]} /* {name} */' else: for tid, name in ts: xs[tid - min_templateid] = f'{{ {min_s[name]}, {max_s[name]} }} /* {name} */' s = '\n , '.join(xs) if is_eobi: print(f' static const uint32_t tid2size[] = {{\n {s}\n }};', file=o) else: print(f' static const uint32_t tid2size[{n}][2] = {{\n {s}\n }};', file=o) # yes, usage attribute of single fields depends on the context # otherwise, we could just put the information into the fields table # Example: EOBI.PacketHeader.MessageHeader.MsgSeqNum is unused whereas # it's required in the EOBI ExecutionSummary and other messages def gen_usage_table(min_templateid, n, ts, ams, o=sys.stdout): def map_usage(m): x = m.get('usage') if x == 'mandatory': return 0 elif x == 'optional': return 1 elif x == 'unused': return 2 else: raise RuntimeError(f'unknown usage value: {x}') h = {} i = 0 print(' static const unsigned char usages[] = {', file=o) for am in ams: name = am.get("name") tid = int(am.get('numericID')) print(f' // {name}', file=o) h[tid] = i for e in am: if e.tag == 'Group': print(f' //// {e.get("type")}', file=o) for m in e: if m.get('hidden') == 'true' or pad_re.match(m.get('name')): continue k = ' ' if i == 0 else ',' print(f' {k} {map_usage(m)} // {m.get("name")}#{i}', file=o) i += 1 print(' ///', file=o) else: if e.get('hidden') == 'true' or pad_re.match(e.get('name')): continue k = ' ' if i == 0 else ',' print(f' {k} {map_usage(e)} // {e.get("name")}#{i}', file=o) i += 1 # NB: the last element is a filler to simplify the out-of-bounds check # (cf. the uidx DISSECTOR_ASSER_CMPUINIT() before the switch statement) # when the ETI_EOF of the message whose usage information comes last # is reached print(f' , 0 // filler', file=o) print(' };', file=o) xs = [ '-1' ] * n t2n = dict(ts) for tid, uidx in h.items(): name = t2n[tid] xs[tid - min_templateid] = f'{uidx} /* {name} */' s = '\n , '.join(xs) print(f' static const int16_t tid2uidx[] = {{\n {s}\n }};', file=o) def gen_dscp_table(proto, o=sys.stdout): print(f''' static int * const dscp_bits[] = {{ &hf_{proto}_dscp_exec_summary, &hf_{proto}_dscp_improved, &hf_{proto}_dscp_widened, NULL }};''', file=o) def mk_int_case(size, signed, proto): signed_str = 'i' if signed else '' unsigned_str = '' if signed else 'u' fmt_str = 'i' if signed else 'u' if size == 2: size_str = 's' elif size == 4: size_str = 'l' elif size == 8: size_str = '64' type_str = f'g{unsigned_str}int{size * 8}' no_value_str = f'INT{size * 8}_MIN' if signed else f'UINT{size * 8}_MAX' pt_size = '64' if size == 8 else '' if signed: hex_str = '0x80' + '00' * (size - 1) else: hex_str = '0x' + 'ff' * size if size == 1: fn = f'tvb_get_g{unsigned_str}int8' else: fn = f'tvb_get_letoh{signed_str}{size_str}' s = f'''case {size}: {{ {type_str} x = {fn}(tvb, off); if (x == {no_value_str}) {{ proto_item *e = proto_tree_add_{unsigned_str}int{pt_size}_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE ({hex_str})"); if (!usages[uidx]) expert_add_info_format(pinfo, e, &ei_{proto}_missing, "required value is missing"); }} else {{ proto_item *e = proto_tree_add_{unsigned_str}int{pt_size}_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%" PRI{fmt_str}{size * 8}, x); if (usages[uidx] == 2) expert_add_info_format(pinfo, e, &ei_{proto}_overused, "unused value is set"); }} }} break;''' return s def gen_dissect_structs(o=sys.stdout): print(''' enum ETI_Type { ETI_EOF, ETI_PADDING, ETI_UINT, ETI_INT, ETI_UINT_ENUM, ETI_INT_ENUM, ETI_COUNTER, ETI_FIXED_POINT, ETI_TIMESTAMP_NS, ETI_CHAR, ETI_STRING, ETI_VAR_STRING, ETI_STRUCT, ETI_VAR_STRUCT, ETI_DSCP }; struct ETI_Field { uint8_t type; uint8_t counter_off; // offset into counter array // if ETI_COUNTER => storage // if ETI_VAR_STRING or ETI_VAR_STRUCT => load // to get length or repeat count // if ETI_FIXED_POINT: #fractional digits uint16_t size; // or offset into struct_names if ETI_STRUCT/ETI_VAR_STRUCT uint16_t field_handle_idx; // or index into fields array if ETI_STRUCT/ETI_VAR_STRUT uint16_t ett_idx; // index into ett array if ETI_STRUCT/ETI_VAR_STRUCT // or max value if ETI_COUNTER }; ''', file=o) def gen_dissect_fn(st, dt, ts, sh, ams, proto, o=sys.stdout): if proto.startswith('eti') or proto.startswith('xti'): bl_fn = 'tvb_get_letohl' template_off = 4 else: bl_fn = 'tvb_get_letohs' template_off = 2 print(f'''/* This method dissects fully reassembled messages */ static int dissect_{proto}_message(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_) {{ col_set_str(pinfo->cinfo, COL_PROTOCOL, "{proto.upper()}"); col_clear(pinfo->cinfo, COL_INFO); guint16 templateid = tvb_get_letohs(tvb, {template_off}); const char *template_str = val_to_str_ext(templateid, &template_id_vals_ext, "Unknown {proto.upper()} template: 0x%04x"); col_add_fstr(pinfo->cinfo, COL_INFO, "%s", template_str); /* create display subtree for the protocol */ proto_item *ti = proto_tree_add_item(tree, proto_{proto}, tvb, 0, -1, ENC_NA); guint32 bodylen= {bl_fn}(tvb, 0); proto_item_append_text(ti, ", %s (%" PRIu16 "), BodyLen: %u", template_str, templateid, bodylen); proto_tree *root = proto_item_add_subtree(ti, ett_{proto}[0]); ''', file=o) min_templateid = ts[0][0] max_templateid = ts[-1][0] n = max_templateid - min_templateid + 1 fields2idx = gen_fields_table(st, dt, sh, o) gen_template_table(min_templateid, n, ts, fields2idx, o) gen_sizes_table(min_templateid, n, st, dt, ts, proto, o) gen_usage_table(min_templateid, n, ts, ams, o) gen_dscp_table(proto, o) print(f''' if (templateid < {min_templateid} || templateid > {max_templateid}) {{ proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_template, tvb, {template_off}, 4, "Template ID out of range: %" PRIu16, templateid); return tvb_captured_length(tvb); }} int fidx = tid2fidx[templateid - {min_templateid}]; if (fidx == -1) {{ proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_template, tvb, {template_off}, 4, "Unallocated Template ID: %" PRIu16, templateid); return tvb_captured_length(tvb); }}''', file=o) if proto.startswith('eobi'): print(f''' if (bodylen != tid2size[templateid - {min_templateid}]) {{ proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_length, tvb, 0, {template_off}, "Unexpected BodyLen value of %" PRIu32 ", expected: %" PRIu32, bodylen, tid2size[templateid - {min_templateid}]); }}''', file=o) else: print(f''' if (bodylen < tid2size[templateid - {min_templateid}][0] || bodylen > tid2size[templateid - {min_templateid}][1]) {{ if (tid2size[templateid - {min_templateid}][0] != tid2size[templateid - {min_templateid}][1]) proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_length, tvb, 0, {template_off}, "Unexpected BodyLen value of %" PRIu32 ", expected: %" PRIu32 "..%" PRIu32, bodylen, tid2size[templateid - {min_templateid}][0], tid2size[templateid - {min_templateid}][1]); else proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_length, tvb, 0, {template_off}, "Unexpected BodyLen value of %" PRIu32 ", expected: %" PRIu32, bodylen, tid2size[templateid - {min_templateid}][0]); }} if (bodylen % 8) proto_tree_add_expert_format(root, pinfo, &ei_{proto}_unaligned, tvb, 0, {template_off}, "BodyLen value of %" PRIu32 " is not divisible by 8", bodylen); ''', file=o) print(f''' int uidx = tid2uidx[templateid - {min_templateid}]; DISSECTOR_ASSERT_CMPINT(uidx, >=, 0); DISSECTOR_ASSERT_CMPUINT(((size_t)uidx), <, (sizeof usages / sizeof usages[0])); ''', file=o) print(f''' int old_fidx = 0; int old_uidx = 0; unsigned top = 1; unsigned counter[8] = {{0}}; unsigned off = 0; unsigned struct_off = 0; unsigned repeats = 0; proto_tree *t = root; while (top) {{ DISSECTOR_ASSERT_CMPINT(fidx, >=, 0); DISSECTOR_ASSERT_CMPUINT(((size_t)fidx), <, (sizeof fields / sizeof fields[0])); DISSECTOR_ASSERT_CMPINT(uidx, >=, 0); DISSECTOR_ASSERT_CMPUINT(((size_t)uidx), <, (sizeof usages / sizeof usages[0])); switch (fields[fidx].type) {{ case ETI_EOF: DISSECTOR_ASSERT_CMPUINT(top, >=, 1); DISSECTOR_ASSERT_CMPUINT(top, <=, 2); if (t != root) proto_item_set_len(t, off - struct_off); if (repeats) {{ --repeats; fidx = fields[old_fidx].field_handle_idx; uidx = old_uidx; t = proto_tree_add_subtree(root, tvb, off, -1, ett_{proto}[fields[old_fidx].ett_idx], NULL, &struct_names[fields[old_fidx].size]); struct_off = off; }} else {{ fidx = old_fidx + 1; t = root; --top; }} break; case ETI_VAR_STRUCT: case ETI_STRUCT: DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <, sizeof counter / sizeof counter[0]); repeats = fields[fidx].type == ETI_VAR_STRUCT ? counter[fields[fidx].counter_off] : 1; if (repeats) {{ --repeats; t = proto_tree_add_subtree(root, tvb, off, -1, ett_{proto}[fields[fidx].ett_idx], NULL, &struct_names[fields[fidx].size]); struct_off = off; old_fidx = fidx; old_uidx = uidx; fidx = fields[fidx].field_handle_idx; DISSECTOR_ASSERT_CMPUINT(top, ==, 1); ++top; }} else {{ ++fidx; }} break; case ETI_PADDING: off += fields[fidx].size; ++fidx; break; case ETI_CHAR: proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_ASCII); off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_STRING: {{ guint8 c = tvb_get_guint8(tvb, off); if (c) proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_ASCII); else {{ proto_item *e = proto_tree_add_string(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, "NO_VALUE ('0x00...')"); if (!usages[uidx]) expert_add_info_format(pinfo, e, &ei_{proto}_missing, "required value is missing"); }} }} off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_VAR_STRING: DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <, sizeof counter / sizeof counter[0]); proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, counter[fields[fidx].counter_off], ENC_ASCII); off += counter[fields[fidx].counter_off]; ++fidx; ++uidx; break; case ETI_COUNTER: DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <, sizeof counter / sizeof counter[0]); DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, <=, 2); {{ switch (fields[fidx].size) {{ case 1: {{ guint8 x = tvb_get_guint8(tvb, off); if (x == UINT8_MAX) {{ proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE (0xff)"); counter[fields[fidx].counter_off] = 0; }} else {{ proto_item *e = proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%" PRIu8, x); if (x > fields[fidx].ett_idx) {{ counter[fields[fidx].counter_off] = fields[fidx].ett_idx; expert_add_info_format(pinfo, e, &ei_{proto}_counter_overflow, "Counter overflow: %" PRIu8 " > %" PRIu16, x, fields[fidx].ett_idx); }} else {{ counter[fields[fidx].counter_off] = x; }} }} }} break; case 2: {{ guint16 x = tvb_get_letohs(tvb, off); if (x == UINT16_MAX) {{ proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE (0xffff)"); counter[fields[fidx].counter_off] = 0; }} else {{ proto_item *e = proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%" PRIu16, x); if (x > fields[fidx].ett_idx) {{ counter[fields[fidx].counter_off] = fields[fidx].ett_idx; expert_add_info_format(pinfo, e, &ei_{proto}_counter_overflow, "Counter overflow: %" PRIu16 " > %" PRIu16, x, fields[fidx].ett_idx); }} else {{ counter[fields[fidx].counter_off] = x; }} }} }} break; }} }} off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_UINT: switch (fields[fidx].size) {{ {mk_int_case(1, False, proto)} {mk_int_case(2, False, proto)} {mk_int_case(4, False, proto)} {mk_int_case(8, False, proto)} }} off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_INT: switch (fields[fidx].size) {{ {mk_int_case(1, True, proto)} {mk_int_case(2, True, proto)} {mk_int_case(4, True, proto)} {mk_int_case(8, True, proto)} }} off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_UINT_ENUM: case ETI_INT_ENUM: proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_LITTLE_ENDIAN); off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_FIXED_POINT: DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, ==, 8); DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, >, 0); DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <=, 16); {{ gint64 x = tvb_get_letohi64(tvb, off); if (x == INT64_MIN) {{ proto_item *e = proto_tree_add_int64_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE (0x8000000000000000)"); if (!usages[uidx]) expert_add_info_format(pinfo, e, &ei_{proto}_missing, "required value is missing"); }} else {{ unsigned slack = fields[fidx].counter_off + 1; if (x < 0) slack += 1; char s[21]; int n = snprintf(s, sizeof s, "%0*" PRIi64, slack, x); DISSECTOR_ASSERT_CMPUINT(n, >, 0); unsigned k = n - fields[fidx].counter_off; proto_tree_add_int64_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%.*s.%s", k, s, s + k); }} }} off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_TIMESTAMP_NS: DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, ==, 8); proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_LITTLE_ENDIAN | ENC_TIME_NSECS); off += fields[fidx].size; ++fidx; ++uidx; break; case ETI_DSCP: DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, ==, 1); proto_tree_add_bitmask(t, tvb, off, hf_{proto}[fields[fidx].field_handle_idx], ett_{proto}_dscp, dscp_bits, ENC_LITTLE_ENDIAN); off += fields[fidx].size; ++fidx; ++uidx; break; }} }} ''', file=o) print(''' return tvb_captured_length(tvb); } ''', file=o) print(f'''/* determine PDU length of protocol {proto.upper()} */ static guint get_{proto}_message_len(packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_) {{ return (guint){bl_fn}(tvb, offset); }} ''', file=o) if proto.startswith('eobi'): print(f'''static int dissect_{proto}(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data) {{ return udp_dissect_pdus(tvb, pinfo, tree, 4, NULL, get_{proto}_message_len, dissect_{proto}_message, data); }} ''', file=o) else: print(f'''static int dissect_{proto}(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data) {{ tcp_dissect_pdus(tvb, pinfo, tree, TRUE, 4 /* bytes to read for bodylen */, get_{proto}_message_len, dissect_{proto}_message, data); return tvb_captured_length(tvb); }} ''', file=o) def gen_register_fn(st, dt, n2enum, proto, desc, o=sys.stdout): print(f'''void proto_register_{proto}(void) {{''', file=o) gen_field_info(st, dt, n2enum, proto, o) print(f''' static ei_register_info ei[] = {{ {{ &ei_{proto}_counter_overflow, {{ "{proto}.counter_overflow", PI_PROTOCOL, PI_WARN, "Counter Overflow", EXPFILL }} }}, {{ &ei_{proto}_invalid_template, {{ "{proto}.invalid_template", PI_PROTOCOL, PI_ERROR, "Invalid Template ID", EXPFILL }} }}, {{ &ei_{proto}_invalid_length, {{ "{proto}.invalid_length", PI_PROTOCOL, PI_ERROR, "Invalid Body Length", EXPFILL }} }},''', file=o) if not proto.startswith('eobi'): print(f''' {{ &ei_{proto}_unaligned, {{ "{proto}.unaligned", PI_PROTOCOL, PI_ERROR, "A Body Length not divisible by 8 leads to unaligned followup messages", EXPFILL }} }},''', file=o) print(f''' {{ &ei_{proto}_missing, {{ "{proto}.missing", PI_PROTOCOL, PI_WARN, "A required value is missing", EXPFILL }} }}, {{ &ei_{proto}_overused, {{ "{proto}.overused", PI_PROTOCOL, PI_WARN, "An unused value is set", EXPFILL }} }} }};''', file=o) print(f''' proto_{proto} = proto_register_protocol("{desc}", "{proto.upper()}", "{proto}");''', file=o) print(f''' expert_module_t *expert_{proto} = expert_register_protocol(proto_{proto}); expert_register_field_array(expert_{proto}, ei, array_length(ei));''', file=o) print(f' proto_register_field_array(proto_{proto}, hf, array_length(hf));', file=o) gen_subtree_array(st, proto, o) print(' proto_register_subtree_array(ett, array_length(ett));', file=o) if proto.startswith('eobi'): print(f' proto_disable_by_default(proto_{proto});', file=o) print('}\n', file=o) def gen_handoff_fn(proto, o=sys.stdout): print(f'''void proto_reg_handoff_{proto}(void) {{ dissector_handle_t {proto}_handle = create_dissector_handle(dissect_{proto}, proto_{proto}); // cf. N7 Network Access Guide, e.g. // https://www.xetra.com/xetra-en/technology/t7/system-documentation/release10-0/Release-10.0-2692700?frag=2692724 // https://www.xetra.com/resource/blob/2762078/388b727972b5122945eedf0e63c36920/data/N7-Network-Access-Guide-v2.0.59.pdf ''', file=o) if proto.startswith('eti'): print(f''' // NB: can only be called once for a port/handle pair ... // dissector_add_uint_with_preference("tcp.port", 19006 /* LF PROD */, eti_handle); dissector_add_uint("tcp.port", 19006 /* LF PROD */, {proto}_handle); dissector_add_uint("tcp.port", 19043 /* PS PROD */, {proto}_handle); dissector_add_uint("tcp.port", 19506 /* LF SIMU */, {proto}_handle); dissector_add_uint("tcp.port", 19543 /* PS SIMU */, {proto}_handle);''', file=o) elif proto.startswith('xti'): print(f''' // NB: unfortunately, Cash-ETI shares the same ports as Derivatives-ETI ... // We thus can't really add a well-know port for XTI. // Use Wireshark's `Decode As...` or tshark's `-d tcp.port=19043,xti` feature // to switch from ETI to XTI dissection. dissector_add_uint_with_preference("tcp.port", 19042 /* dummy */, {proto}_handle);''', file=o) else: print(f''' static const int ports[] = {{ 59000, // Snapshot EUREX US-allowed PROD 59001, // Incremental EUREX US-allowed PROD 59032, // Snapshot EUREX US-restricted PROD 59033, // Incremental EUREX US-restricted PROD 59500, // Snapshot EUREX US-allowed SIMU 59501, // Incremental EUREX US-allowed SIMU 59532, // Snapshot EUREX US-restricted SIMU 59533, // Incremental EUREX US-restricted SIMU 57000, // Snapshot FX US-allowed PROD 57001, // Incremental FX US-allowed PROD 57032, // Snapshot FX US-restricted PROD 57033, // Incremental FX US-restricted PROD 57500, // Snapshot FX US-allowed SIMU 57501, // Incremental FX US-allowed SIMU 57532, // Snapshot FX US-restricted SIMU 57533, // Incremental FX US-restricted SIMU 59000, // Snapshot Xetra PROD 59001, // Incremental Xetra PROD 59500, // Snapshot Xetra SIMU 59501, // Incremental Xetra SIMU 56000, // Snapshot Boerse Frankfurt PROD 56001, // Incremental Boerse Frankfurt PROD 56500, // Snapshot Boerse Frankfurt SIMU 56501 // Incremental Boerse Frankfurt SIMU }}; for (unsigned i = 0; i < sizeof ports / sizeof ports[0]; ++i) dissector_add_uint("udp.port", ports[i], {proto}_handle);''', file=o) print('}', file=o) def is_int(t): if t is not None: r = t.get('rootType') return r in ('int', 'floatDecimal') or (r == 'String' and t.get('size') == '1') return False def is_enum(t): if t is not None: r = t.get('rootType') if r == 'int' or (r == 'String' and t.get('size') == '1'): return t.find('ValidValue') is not None return False def is_fixed_point(t): return t is not None and t.get('rootType') == 'floatDecimal' def is_timestamp_ns(t): return t is not None and t.get('type') == 'UTCTimestamp' def is_dscp(t): return t is not None and t.get('name') == 'DSCP' pad_re = re.compile('Pad[1-9]') def is_padding(t): if t is not None: return t.get('rootType') == 'String' and pad_re.match(t.get('name')) return False def is_fixed_string(t): if t is not None: return t.get('rootType') in ('String', 'data') and not t.get('variableSize') return False def is_var_string(t): if t is not None: return t.get('rootType') in ('String', 'data') and t.get('variableSize') is not None return False def is_unsigned(t): v = t.get('minValue') return v is not None and not v.startswith('-') def is_counter(t): return t.get('type') == 'Counter' def type_to_fmt(t): if is_padding(t): return f'{t.get("size")}x' elif is_int(t): n = int(t.get('size')) if n == 1: return 'B' else: if n == 2: c = 'h' elif n == 4: c = 'i' elif n == 8: c = 'q' else: raise ValueError(f'unknown int size {n}') if is_unsigned(t): c = c.upper() return c elif is_fixed_string(t): return f'{t.get("size")}s' else: return '?' def pp_int_type(t): if not is_int(t): return None s = 'i' if is_unsigned(t): s = 'u' n = int(t.get('size')) s += str(n) return s def is_elementary(t): return t is not None and t.get('counter') is None def group_members(e, dt): xs = [] ms = [] for m in e: t = dt.get(m.get('type')) if is_elementary(t): ms.append(m) else: if ms: xs.append(ms) ms = [] xs.append([m]) if ms: xs.append(ms) return xs def parse_args(): p = argparse.ArgumentParser(description='Generate Wireshark Dissector for ETI/EOBI style protocol specifictions') p.add_argument('filename', help='protocol description XML file') p.add_argument('--proto', default='eti', help='short protocol name (default: %(default)s)') p.add_argument('--desc', '-d', default='Enhanced Trading Interface', help='protocol description (default: %(default)s)') p.add_argument('--output', '-o', default='-', help='output filename (default: stdout)') args = p.parse_args() return args def main(): args = parse_args() filename = args.filename d = ET.parse(filename) o = sys.stdout if args.output == '-' else open(args.output, 'w') proto = args.proto version = (d.getroot().get('version'), d.getroot().get('subVersion')) desc = f'{args.desc} {version[0]}' dt = get_data_types(d) st = get_structs(d) used = get_used_types(st) for k in list(dt.keys()): if k not in used: del dt[k] ts = get_templates(st) ams = d.getroot().find('ApplicationMessages') gen_header(proto, desc, o) print(f'static int proto_{proto} = -1;', file=o) gen_field_handles(st, dt, proto, o) n2enum = gen_enums(dt, ts, o) gen_dissect_structs(o) sh = gen_subtree_handles(st, proto, o) gen_dissect_fn(st, dt, ts, sh, ams, proto, o) gen_register_fn(st, dt, n2enum, proto, desc, o) gen_handoff_fn(proto, o) if __name__ == '__main__': sys.exit(main())