xref: /openbmc/openbmc-tools/dbus-pcap/dbus-pcap (revision 96ebc4e7)
1#!/usr/bin/python3
2
3# SPDX-License-Identifier: Apache-2.0
4# Copyright 2019 IBM Corp.
5
6import json
7import struct
8import sys
9from argparse import ArgumentParser
10from collections import namedtuple
11from enum import Enum
12from itertools import islice
13
14from scapy.all import rdpcap
15
16RawMessage = namedtuple("RawMessage", "endian, header, data")
17FixedHeader = namedtuple(
18    "FixedHeader", "endian, type, flags, version, length, cookie"
19)
20CookedHeader = namedtuple("CookedHeader", "fixed, fields")
21CookedMessage = namedtuple("CookedMessage", "header, body")
22TypeProperty = namedtuple("TypeProperty", "field, type, nature")
23TypeContainer = namedtuple("TypeContainer", "type, members")
24Field = namedtuple("Field", "type, data")
25
26
27class MessageEndian(Enum):
28    LITTLE = ord("l")
29    BIG = ord("B")
30
31
32StructEndianLookup = {
33    MessageEndian.LITTLE.value: "<",
34    MessageEndian.BIG.value: ">",
35}
36
37
38class MessageType(Enum):
39    INVALID = 0
40    METHOD_CALL = 1
41    METHOD_RETURN = 2
42    ERROR = 3
43    SIGNAL = 4
44
45
46class MessageFlags(Enum):
47    NO_REPLY_EXPECTED = 0x01
48    NO_AUTO_START = 0x02
49    ALLOW_INTERACTIVE_AUTHORIZATION = 0x04
50
51
52class MessageFieldType(Enum):
53    INVALID = 0
54    PATH = 1
55    INTERFACE = 2
56    MEMBER = 3
57    ERROR_NAME = 4
58    REPLY_SERIAL = 5
59    DESTINATION = 6
60    SENDER = 7
61    SIGNATURE = 8
62    UNIX_FDS = 9
63
64
65class DBusType(Enum):
66    INVALID = 0
67    BYTE = ord("y")
68    BOOLEAN = ord("b")
69    INT16 = ord("n")
70    UINT16 = ord("q")
71    INT32 = ord("i")
72    UINT32 = ord("u")
73    INT64 = ord("x")
74    UINT64 = ord("t")
75    DOUBLE = ord("d")
76    STRING = ord("s")
77    OBJECT_PATH = ord("o")
78    SIGNATURE = ord("g")
79    ARRAY = ord("a")
80    STRUCT = ord("(")
81    VARIANT = ord("v")
82    DICT_ENTRY = ord("{")
83    UNIX_FD = ord("h")
84
85
86DBusContainerTerminatorLookup = {
87    chr(DBusType.STRUCT.value): ")",
88    chr(DBusType.DICT_ENTRY.value): "}",
89}
90
91
92class DBusTypeCategory(Enum):
93    FIXED = {
94        DBusType.BYTE.value,
95        DBusType.BOOLEAN.value,
96        DBusType.INT16.value,
97        DBusType.UINT16.value,
98        DBusType.INT32.value,
99        DBusType.UINT32.value,
100        DBusType.INT64.value,
101        DBusType.UINT64.value,
102        DBusType.DOUBLE.value,
103        DBusType.UNIX_FD.value,
104    }
105    STRING = {
106        DBusType.STRING.value,
107        DBusType.OBJECT_PATH.value,
108        DBusType.SIGNATURE.value,
109    }
110    CONTAINER = {
111        DBusType.ARRAY.value,
112        DBusType.STRUCT.value,
113        DBusType.VARIANT.value,
114        DBusType.DICT_ENTRY.value,
115    }
116    RESERVED = {
117        DBusType.INVALID.value,
118    }
119
120
121TypePropertyLookup = {
122    DBusType.BYTE.value: TypeProperty(DBusType.BYTE, "B", 1),
123    # DBus booleans are 32 bit, with only the LSB used. Extract as 'I'.
124    DBusType.BOOLEAN.value: TypeProperty(DBusType.BOOLEAN, "I", 4),
125    DBusType.INT16.value: TypeProperty(DBusType.INT16, "h", 2),
126    DBusType.UINT16.value: TypeProperty(DBusType.UINT16, "H", 2),
127    DBusType.INT32.value: TypeProperty(DBusType.INT32, "i", 4),
128    DBusType.UINT32.value: TypeProperty(DBusType.UINT32, "I", 4),
129    DBusType.INT64.value: TypeProperty(DBusType.INT64, "q", 8),
130    DBusType.UINT64.value: TypeProperty(DBusType.UINT64, "Q", 8),
131    DBusType.DOUBLE.value: TypeProperty(DBusType.DOUBLE, "d", 8),
132    DBusType.STRING.value: TypeProperty(DBusType.STRING, "s", DBusType.UINT32),
133    DBusType.OBJECT_PATH.value: TypeProperty(
134        DBusType.OBJECT_PATH, "s", DBusType.UINT32
135    ),
136    DBusType.SIGNATURE.value: TypeProperty(
137        DBusType.SIGNATURE, "s", DBusType.BYTE
138    ),
139    DBusType.ARRAY.value: TypeProperty(DBusType.ARRAY, None, DBusType.UINT32),
140    DBusType.STRUCT.value: TypeProperty(DBusType.STRUCT, None, 8),
141    DBusType.VARIANT.value: TypeProperty(DBusType.VARIANT, None, 1),
142    DBusType.DICT_ENTRY.value: TypeProperty(DBusType.DICT_ENTRY, None, 8),
143    DBusType.UNIX_FD.value: TypeProperty(DBusType.UINT32, None, 8),
144}
145
146
147def parse_signature(sigstream):
148    sig = ord(next(sigstream))
149    assert sig not in DBusTypeCategory.RESERVED.value
150    if sig in DBusTypeCategory.FIXED.value:
151        ty = TypePropertyLookup[sig].field, None
152    elif sig in DBusTypeCategory.STRING.value:
153        ty = TypePropertyLookup[sig].field, None
154    elif sig in DBusTypeCategory.CONTAINER.value:
155        if sig == DBusType.ARRAY.value:
156            ty = DBusType.ARRAY, parse_signature(sigstream)
157        elif sig == DBusType.STRUCT.value or sig == DBusType.DICT_ENTRY.value:
158            collected = list()
159            ty = parse_signature(sigstream)
160            while ty is not StopIteration:
161                collected.append(ty)
162                ty = parse_signature(sigstream)
163            ty = DBusType.STRUCT, collected
164        elif sig == DBusType.VARIANT.value:
165            ty = TypePropertyLookup[sig].field, None
166        else:
167            assert False
168    else:
169        assert chr(sig) in DBusContainerTerminatorLookup.values()
170        return StopIteration
171
172    return TypeContainer._make(ty)
173
174
175class AlignedStream(object):
176    def __init__(self, buf, offset=0):
177        self.stash = (buf, offset)
178        self.stream = iter(buf)
179        self.offset = offset
180
181    def align(self, tc):
182        assert tc.type.value in TypePropertyLookup
183        prop = TypePropertyLookup[tc.type.value]
184        if prop.field.value in DBusTypeCategory.STRING.value:
185            prop = TypePropertyLookup[prop.nature.value]
186        if prop.nature == DBusType.UINT32:
187            prop = TypePropertyLookup[prop.nature.value]
188        advance = (
189            prop.nature - (self.offset & (prop.nature - 1))
190        ) % prop.nature
191        _ = bytes(islice(self.stream, advance))
192        self.offset += len(_)
193
194    def take(self, size):
195        val = islice(self.stream, size)
196        self.offset += size
197        return val
198
199    def autotake(self, tc):
200        assert tc.type.value in DBusTypeCategory.FIXED.value
201        assert tc.type.value in TypePropertyLookup
202        self.align(tc)
203        prop = TypePropertyLookup[tc.type.value]
204        return self.take(prop.nature)
205
206    def drain(self):
207        remaining = bytes(self.stream)
208        offset = self.offset
209        self.offset += len(remaining)
210        if self.offset - self.stash[1] != len(self.stash[0]):
211            print(
212                "(self.offset - self.stash[1]): %d, len(self.stash[0]): %d"
213                % (self.offset - self.stash[1], len(self.stash[0])),
214                file=sys.stderr,
215            )
216            raise MalformedPacketError
217        return remaining, offset
218
219    def dump(self):
220        print(
221            "AlignedStream: absolute offset: {}".format(self.offset),
222            file=sys.stderr,
223        )
224        print(
225            "AlignedStream: relative offset: {}".format(
226                self.offset - self.stash[1]
227            ),
228            file=sys.stderr,
229        )
230        print(
231            "AlignedStream: remaining buffer:\n{}".format(self.drain()[0]),
232            file=sys.stderr,
233        )
234        print(
235            "AlignedStream: provided buffer:\n{}".format(self.stash[0]),
236            file=sys.stderr,
237        )
238
239    def dump_assert(self, condition):
240        if condition:
241            return
242        self.dump()
243        assert condition
244
245
246def parse_fixed(endian, stream, tc):
247    assert tc.type.value in TypePropertyLookup
248    prop = TypePropertyLookup[tc.type.value]
249    val = bytes(stream.autotake(tc))
250    try:
251        val = struct.unpack("{}{}".format(endian, prop.type), val)[0]
252        return bool(val) if prop.type == DBusType.BOOLEAN else val
253    except struct.error as e:
254        print(e, file=sys.stderr)
255        print("parse_fixed: Error unpacking {}".format(val), file=sys.stderr)
256        print(
257            (
258                "parse_fixed: Attempting to unpack type {} "
259                + "with properties {}"
260            ).format(tc.type, prop),
261            file=sys.stderr,
262        )
263        stream.dump_assert(False)
264
265
266def parse_string(endian, stream, tc):
267    assert tc.type.value in TypePropertyLookup
268    prop = TypePropertyLookup[tc.type.value]
269    size = parse_fixed(endian, stream, TypeContainer(prop.nature, None))
270    # Empty DBus strings have no NUL-terminator
271    if size == 0:
272        return ""
273    # stream.dump_assert(size > 0)
274    val = bytes(stream.take(size + 1))
275    try:
276        stream.dump_assert(len(val) == size + 1)
277        try:
278            return struct.unpack("{}{}".format(size, prop.type), val[:size])[
279                0
280            ].decode()
281        except struct.error as e:
282            stream.dump()
283            raise AssertionError(e)
284    except AssertionError as e:
285        print(
286            "parse_string: Error unpacking string of length {} from {}".format(
287                size, val
288            ),
289            file=sys.stderr,
290        )
291        raise e
292
293
294def parse_type(endian, stream, tc):
295    if tc.type.value in DBusTypeCategory.FIXED.value:
296        val = parse_fixed(endian, stream, tc)
297    elif tc.type.value in DBusTypeCategory.STRING.value:
298        val = parse_string(endian, stream, tc)
299    elif tc.type.value in DBusTypeCategory.CONTAINER.value:
300        val = parse_container(endian, stream, tc)
301    else:
302        stream.dump_assert(False)
303
304    return val
305
306
307def parse_array(endian, stream, tc):
308    arr = list()
309    length = parse_fixed(endian, stream, TypeContainer(DBusType.UINT32, None))
310    stream.align(tc)
311    offset = stream.offset
312    while (stream.offset - offset) < length:
313        elem = parse_type(endian, stream, tc)
314        arr.append(elem)
315        if (stream.offset - offset) < length:
316            stream.align(tc)
317    return arr
318
319
320def parse_struct(endian, stream, tcs):
321    arr = list()
322    stream.align(TypeContainer(DBusType.STRUCT, None))
323    for tc in tcs:
324        arr.append(parse_type(endian, stream, tc))
325    return arr
326
327
328def parse_variant(endian, stream):
329    sig = parse_string(endian, stream, TypeContainer(DBusType.SIGNATURE, None))
330    tc = parse_signature(iter(sig))
331    return parse_type(endian, stream, tc)
332
333
334def parse_container(endian, stream, tc):
335    if tc.type == DBusType.ARRAY:
336        return parse_array(endian, stream, tc.members)
337    elif tc.type in (DBusType.STRUCT, DBusType.DICT_ENTRY):
338        return parse_struct(endian, stream, tc.members)
339    elif tc.type == DBusType.VARIANT:
340        return parse_variant(endian, stream)
341    else:
342        stream.dump_assert(False)
343
344
345def parse_fields(endian, stream):
346    sig = parse_signature(iter("a(yv)"))
347    fields = parse_container(endian, stream, sig)
348    # The header ends after its alignment padding to an 8-boundary.
349    # https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-messages
350    stream.align(TypeContainer(DBusType.STRUCT, None))
351    return list(map(lambda v: Field(MessageFieldType(v[0]), v[1]), fields))
352
353
354class MalformedPacketError(Exception):
355    pass
356
357
358def parse_header(raw, ignore_error):
359    assert raw.endian in StructEndianLookup.keys()
360    endian = StructEndianLookup[raw.endian]
361    fixed = FixedHeader._make(
362        struct.unpack("{}BBBBLL".format(endian), raw.header)
363    )
364    astream = AlignedStream(raw.data, len(raw.header))
365    fields = parse_fields(endian, astream)
366    data, offset = astream.drain()
367    if (not ignore_error) and fixed.length > len(data):
368        raise MalformedPacketError
369    return CookedHeader(fixed, fields), AlignedStream(data, offset)
370
371
372def parse_body(header, stream):
373    assert header.fixed.endian in StructEndianLookup
374    endian = StructEndianLookup[header.fixed.endian]
375    body = list()
376    for field in header.fields:
377        if field.type == MessageFieldType.SIGNATURE:
378            sigstream = iter(field.data)
379            try:
380                while True:
381                    tc = parse_signature(sigstream)
382                    val = parse_type(endian, stream, tc)
383                    body.append(val)
384            except StopIteration:
385                pass
386            break
387    return body
388
389
390def parse_message(raw):
391    try:
392        header, data = parse_header(raw, False)
393        try:
394            body = parse_body(header, data)
395            return CookedMessage(header, body)
396        except AssertionError as e:
397            print(header, file=sys.stderr)
398            raise e
399    except AssertionError as e:
400        print(raw, file=sys.stderr)
401        raise e
402
403
404def parse_packet(packet):
405    data = bytes(packet)
406    raw = RawMessage(data[0], data[:12], data[12:])
407    try:
408        msg = parse_message(raw)
409    except MalformedPacketError:
410        print("Got malformed packet: {}".format(raw), file=sys.stderr)
411        # For a message that is so large that its payload data could not be
412        # parsed, just parse its header, then set its data field to empty.
413        header, data = parse_header(raw, True)
414        msg = CookedMessage(header, [])
415    return msg
416
417
418CallEnvelope = namedtuple("CallEnvelope", "cookie, origin")
419
420
421def parse_session(session, matchers, track_calls):
422    calls = set()
423    for packet in session:
424        try:
425            cooked = parse_packet(packet)
426            if not matchers:
427                yield packet.time, cooked
428            elif any(all(r(cooked) for r in m) for m in matchers):
429                if cooked.header.fixed.type == MessageType.METHOD_CALL.value:
430                    s = [
431                        f
432                        for f in cooked.header.fields
433                        if f.type == MessageFieldType.SENDER
434                    ][0]
435                    calls.add(CallEnvelope(cooked.header.fixed.cookie, s.data))
436                yield packet.time, cooked
437            elif track_calls:
438                responseTypes = {
439                    MessageType.METHOD_RETURN.value,
440                    MessageType.ERROR.value,
441                }
442                if cooked.header.fixed.type not in responseTypes:
443                    continue
444                rs = [
445                    f
446                    for f in cooked.header.fields
447                    if f.type == MessageFieldType.REPLY_SERIAL
448                ][0]
449                d = [
450                    f
451                    for f in cooked.header.fields
452                    if f.type == MessageFieldType.DESTINATION
453                ][0]
454                ce = CallEnvelope(rs.data, d.data)
455                if ce in calls:
456                    calls.remove(ce)
457                    yield packet.time, cooked
458        except MalformedPacketError:
459            pass
460
461
462def gen_match_type(rule):
463    mt = MessageType.__members__[rule.value.upper()]
464    return lambda p: p.header.fixed.type == mt.value
465
466
467def gen_match_sender(rule):
468    mf = Field(MessageFieldType.SENDER, rule.value)
469    return lambda p: any(f == mf for f in p.header.fields)
470
471
472def gen_match_interface(rule):
473    mf = Field(MessageFieldType.INTERFACE, rule.value)
474    return lambda p: any(f == mf for f in p.header.fields)
475
476
477def gen_match_member(rule):
478    mf = Field(MessageFieldType.MEMBER, rule.value)
479    return lambda p: any(f == mf for f in p.header.fields)
480
481
482def gen_match_path(rule):
483    mf = Field(MessageFieldType.PATH, rule.value)
484    return lambda p: any(f == mf for f in p.header.fields)
485
486
487def gen_match_destination(rule):
488    mf = Field(MessageFieldType.DESTINATION, rule.value)
489    return lambda p: any(f == mf for f in p.header.fields)
490
491
492ValidMatchKeys = {
493    "type",
494    "sender",
495    "interface",
496    "member",
497    "path",
498    "destination",
499}
500MatchRule = namedtuple("MatchExpression", "key, value")
501
502
503# https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
504def parse_match_rules(exprs):
505    matchers = list()
506    for mexpr in exprs:
507        rules = list()
508        for rexpr in mexpr.split(","):
509            rule = MatchRule._make(
510                map(lambda s: str.strip(s, "'"), rexpr.split("="))
511            )
512            assert rule.key in ValidMatchKeys, f"Invalid expression: {rule}"
513            rules.append(globals()["gen_match_{}".format(rule.key)](rule))
514        matchers.append(rules)
515    return matchers
516
517
518def packetconv(obj):
519    if isinstance(obj, Enum):
520        return obj.value
521    raise TypeError
522
523
524def main():
525    parser = ArgumentParser()
526    parser.add_argument(
527        "--json",
528        action="store_true",
529        help="Emit a JSON representation of the messages",
530    )
531    parser.add_argument(
532        "--no-track-calls",
533        action="store_true",
534        default=False,
535        help="Make a call response pass filters",
536    )
537    parser.add_argument("file", help="The pcap file")
538    parser.add_argument(
539        "expressions", nargs="*", help="DBus message match expressions"
540    )
541    args = parser.parse_args()
542    stream = rdpcap(args.file)
543    matchers = parse_match_rules(args.expressions)
544    try:
545        if args.json:
546            for _, msg in parse_session(
547                stream, matchers, not args.no_track_calls
548            ):
549                print("{}".format(json.dumps(msg, default=packetconv)))
550        else:
551            for time, msg in parse_session(
552                stream, matchers, not args.no_track_calls
553            ):
554                print("{}: {}".format(time, msg))
555                print()
556    except BrokenPipeError:
557        pass
558
559
560if __name__ == "__main__":
561    main()
562