wireshark/tools/eti2wireshark.py

1167 lines
44 KiB
Python
Executable File

#!/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 <mail@gms.tf>
# 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 <mail@gms.tf>
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* 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 <config.h>
#include <epan/packet.h> // Should be first Wireshark include (other than config.h)
{ph}
#include <epan/expert.h> // expert info
#include <inttypes.h>
#include <stdio.h> // 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())