summaryrefslogtreecommitdiffstats
path: root/epan/dissectors/packet-ssyncp.c
blob: c5d1e09b1714f7bc5936933ca7eb85839e8e52a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
/* packet-ssyncp.c
 * Routines for dissecting mosh's State Synchronization Protocol
 * Copyright 2020 Google LLC
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

/*
 * State Synchronization Protocol is the protocol used by mosh:
 * <https://mosh.org/mosh-paper-draft.pdf>
 *
 * The protocol name is abbreviated as SSyncP to avoid conflict with the
 * "Scripting Service Protocol".
 *
 * The protocol is based on UDP, with a plaintext header followed by an
 * encrypted payload. For now we just support decrypting a single connection at
 * a time, using the MOSH_KEY dumped from the environment variables
 * (`cat /proc/$pid/environ | tr '\0' '\n' | grep MOSH_KEY` on Linux).
 * Note that to display the embedded protobuf properly, you'll have to add
 * src/protobufs/ from mosh's source code to the ProtoBuf search path.
 * For now we stop decoding after reaching the first level of protobufs; in
 * them, a second layer of protobufs is sometimes embedded (e.g. for
 * transmitting screen contents and such). Implementing that is left as an
 * exercise for the reader.
 */

#include <config.h>

#include <epan/packet.h>   /* Should be first Wireshark include (other than config.h) */
#include <epan/conversation.h>
#include <epan/wmem_scopes.h>
#include <epan/proto_data.h>
#include <epan/prefs.h>
#include <epan/expert.h>
#include <wsutil/report_message.h>
#include <wsutil/wsgcrypt.h>

void proto_reg_handoff_ssyncp(void);
void proto_register_ssyncp(void);

static dissector_handle_t ssyncp_handle;

static int proto_ssyncp = -1;
static int hf_ssyncp_direction = -1;
static int hf_ssyncp_seq = -1;
static int hf_ssyncp_encrypted = -1;
static int hf_ssyncp_seq_delta = -1;
static int hf_ssyncp_timestamp = -1;
static int hf_ssyncp_timestamp_reply = -1;
static int hf_ssyncp_frag_seq = -1;
static int hf_ssyncp_frag_final = -1;
static int hf_ssyncp_frag_idx = -1;
static int hf_ssyncp_rtt_to_server = -1;
static int hf_ssyncp_rtt_to_client = -1;

/* Initialize the subtree pointers */
static gint ett_ssyncp = -1;
static gint ett_ssyncp_decrypted = -1;

static expert_field ei_ssyncp_fragmented = EI_INIT;
static expert_field ei_ssyncp_bad_key = EI_INIT;

static const char *pref_ssyncp_key;
static char ssyncp_raw_aes_key[16];
static gboolean have_ssyncp_key;

static dissector_handle_t dissector_protobuf;

typedef struct _ssyncp_conv_info_t {
    /* last sequence numbers per direction */
    guint64 last_seq[2];
    /* for each direction, have we seen any traffic yet? */
    gboolean seen_packet[2];

    guint16 clock_offset[2];
    gboolean clock_seen[2];
} ssyncp_conv_info_t;

typedef struct _ssyncp_packet_info_t {
    gboolean first_packet;
    gint64 seq_delta;
    gboolean have_rtt_estimate;
    gint16 rtt_estimate;
} ssyncp_packet_info_t;

#define SSYNCP_IV_PAD 4
#define SSYNCP_SEQ_LEN 8
#define SSYNCP_DATAGRAM_HEADER_LEN (SSYNCP_SEQ_LEN + 2 + 2) /* 64-bit IV and two 16-bit timestamps */
#define SSYNCP_TRANSPORT_HEADER_LEN (8 + 2)
#define SSYNCP_AUTHTAG_LEN 16 /* 128-bit auth tag */

/*
 * We only match on 60001, which mosh uses for its first connection.
 * If there are more connections in the range 60002-61000, the user will have to
 * mark those as ssyncp traffic manually - we'd have too many false positives
 * otherwise.
 */
#define SSYNCP_UDP_PORT 60001

static int
dissect_ssyncp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree,
        void *data _U_)
{
    /* Check that we have at least a datagram plus an OCB auth tag. */
    if (tvb_reported_length(tvb) < SSYNCP_DATAGRAM_HEADER_LEN + SSYNCP_TRANSPORT_HEADER_LEN + SSYNCP_AUTHTAG_LEN)
        return 0;

    guint64 direction_and_seq = tvb_get_guint64(tvb, 0, ENC_BIG_ENDIAN);
    guint direction = direction_and_seq >> 63;
    guint64 seq = direction_and_seq & ~(1ULL << 63);

    /* Heuristic: The 63-bit sequence number starts from zero and increments
     * from there. Even if you send 1000 packets per second over 10 years, you
     * won't reach 2^35. So check that the sequence number is not outrageously
     * high.
     */
    if (seq > (1ULL << 35))
        return 0;

    /* On the first pass, track the previous sequence numbers per direction,
     * compute deltas between sequence numbers, and save those deltas.
     * On subsequent passes, use the computed deltas.
     */
    ssyncp_packet_info_t *ssyncp_pinfo;
    ssyncp_conv_info_t *ssyncp_info = NULL;
    if (pinfo->fd->visited) {
        ssyncp_pinfo = (ssyncp_packet_info_t *)p_get_proto_data(wmem_file_scope(), pinfo, proto_ssyncp, 0);
    } else {
        conversation_t *conversation = find_or_create_conversation(pinfo);
        ssyncp_info = (ssyncp_conv_info_t *)conversation_get_proto_data(conversation, proto_ssyncp);
        if (!ssyncp_info) {
            ssyncp_info = wmem_new(wmem_file_scope(), ssyncp_conv_info_t);
            conversation_add_proto_data(conversation, proto_ssyncp, ssyncp_info);
            ssyncp_info->seen_packet[0] = FALSE;
            ssyncp_info->seen_packet[1] = FALSE;
            ssyncp_info->clock_seen[0] = FALSE;
            ssyncp_info->clock_seen[1] = FALSE;
        }

        ssyncp_pinfo = wmem_new(wmem_file_scope(), ssyncp_packet_info_t);
        ssyncp_pinfo->first_packet = !ssyncp_info->seen_packet[direction];
        if (ssyncp_pinfo->first_packet) {
            ssyncp_info->seen_packet[direction] = TRUE;
        } else {
            ssyncp_pinfo->seq_delta = seq - ssyncp_info->last_seq[direction];
        }
        ssyncp_pinfo->have_rtt_estimate = FALSE;
        p_add_proto_data(wmem_file_scope(), pinfo, proto_ssyncp, 0, ssyncp_pinfo);

        ssyncp_info->last_seq[direction] = seq;
    }

    /*** COLUMN DATA ***/

    col_set_str(pinfo->cinfo, COL_PROTOCOL, "ssyncp");

    col_clear(pinfo->cinfo, COL_INFO);

    char *direction_str = direction ? "Server->Client" : "Client->Server";
    col_set_str(pinfo->cinfo, COL_INFO, direction_str);

    /*** PROTOCOL TREE ***/

    /* create display subtree for the protocol */
    proto_item *ti = proto_tree_add_item(tree, proto_ssyncp, tvb, 0, -1, ENC_NA);

    proto_tree *ssyncp_tree = proto_item_add_subtree(ti, ett_ssyncp);

    /* Add an item to the subtree, see section 1.5 of README.dissector for more
     * information. */
    proto_tree_add_item(ssyncp_tree, hf_ssyncp_direction, tvb,
            0, 1, ENC_BIG_ENDIAN);
    proto_tree_add_item(ssyncp_tree, hf_ssyncp_seq, tvb,
            0, 8, ENC_BIG_ENDIAN);
#ifdef GCRY_OCB_BLOCK_LEN
    proto_item *encrypted_item =
#endif
       proto_tree_add_item(ssyncp_tree, hf_ssyncp_encrypted,
            tvb, 8, -1, ENC_NA);

    if (!ssyncp_pinfo->first_packet) {
        proto_item *delta_item =
                proto_tree_add_int64(ssyncp_tree, hf_ssyncp_seq_delta, tvb, 0, 0,
                        ssyncp_pinfo->seq_delta);
        proto_item_set_generated(delta_item);
    }

    unsigned char *decrypted = NULL;
    guint decrypted_len = 0;

    /* avoid build failure on ancient libgcrypt without OCB support */
#ifdef GCRY_OCB_BLOCK_LEN
    if (have_ssyncp_key) {
        gcry_error_t gcry_err;

        /* try to decrypt the rest of the packet */
        gcry_cipher_hd_t gcry_hd;
        gcry_err = gcry_cipher_open(&gcry_hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_OCB, 0);
        if (gcry_err_code(gcry_err)) {
            /* this shouldn't happen (even if the packet is garbage) */
            report_failure("ssyncp: unable to initialize cipher???");
            return tvb_captured_length(tvb);
        }
        gcry_err = gcry_cipher_setkey(gcry_hd, ssyncp_raw_aes_key, sizeof(ssyncp_raw_aes_key));
        if (gcry_err_code(gcry_err)) {
            /* this shouldn't happen (even if the packet is garbage) */
            report_failure("ssyncp: unable to set key???");
            gcry_cipher_close(gcry_hd);
            return tvb_captured_length(tvb);
        }
        char nonce[SSYNCP_IV_PAD + SSYNCP_SEQ_LEN];
        memset(nonce, 0, SSYNCP_IV_PAD);
        tvb_memcpy(tvb, nonce + SSYNCP_IV_PAD, 0, SSYNCP_SEQ_LEN);
        gcry_err = gcry_cipher_setiv(gcry_hd, nonce, sizeof(nonce));
        if (gcry_err_code(gcry_err)) {
            /* this shouldn't happen (even if the packet is garbage) */
            report_failure("ssyncp: unable to set iv???");
            gcry_cipher_close(gcry_hd);
            return tvb_captured_length(tvb);
        }
        decrypted_len = tvb_captured_length(tvb) - SSYNCP_SEQ_LEN - SSYNCP_AUTHTAG_LEN;
        decrypted = (unsigned char *)tvb_memdup(pinfo->pool, tvb,
                    SSYNCP_SEQ_LEN, decrypted_len);
        gcry_cipher_final(gcry_hd);
        gcry_err = gcry_cipher_decrypt(gcry_hd, decrypted, decrypted_len, NULL, 0);
        if (gcry_err_code(gcry_err)) {
            /* this shouldn't happen (even if the packet is garbage) */
            report_failure("ssyncp: unable to decrypt???");
            gcry_cipher_close(gcry_hd);
            return tvb_captured_length(tvb);
        }
        gcry_err = gcry_cipher_checktag(gcry_hd,
            tvb_get_ptr(tvb, SSYNCP_SEQ_LEN+decrypted_len, SSYNCP_AUTHTAG_LEN),
            SSYNCP_AUTHTAG_LEN);
        if (gcry_err_code(gcry_err) && gcry_err_code(gcry_err) != GPG_ERR_CHECKSUM) {
            /* this shouldn't happen (even if the packet is garbage) */
            report_failure("ssyncp: unable to check auth tag???");
            gcry_cipher_close(gcry_hd);
            return tvb_captured_length(tvb);
        }
        if (gcry_err_code(gcry_err)) {
            /* if the tag is wrong, the key was wrong and the decrypted data is useless */
            decrypted = NULL;
            expert_add_info(pinfo, encrypted_item, &ei_ssyncp_bad_key);
        }
        gcry_cipher_close(gcry_hd);
    }
#endif

    if (decrypted) {
        tvbuff_t *decrypted_tvb = tvb_new_child_real_data(tvb, decrypted, decrypted_len, decrypted_len);
        add_new_data_source(pinfo, decrypted_tvb, "Decrypted data");

        if (!pinfo->fd->visited) {
            guint16 our_clock16 = ((guint64)pinfo->abs_ts.secs * 1000 + pinfo->abs_ts.nsecs / 1000000) & 0xffff;
            guint16 sender_ts = tvb_get_guint16(decrypted_tvb, 0, ENC_BIG_ENDIAN);
            guint16 reply_ts = tvb_get_guint16(decrypted_tvb, 2, ENC_BIG_ENDIAN);
            ssyncp_info->clock_offset[direction] = sender_ts - our_clock16;
            ssyncp_info->clock_seen[direction] = TRUE;
            if (reply_ts != 0xffff && ssyncp_info->clock_seen[1-direction]) {
                guint16 projected_send_time_our_clock = reply_ts - ssyncp_info->clock_offset[1-direction];
                ssyncp_pinfo->rtt_estimate = our_clock16 - projected_send_time_our_clock;
                ssyncp_pinfo->have_rtt_estimate = TRUE;
            }
        }

        proto_tree *dec_tree = proto_tree_add_subtree(ssyncp_tree, decrypted_tvb,
                0, -1, ett_ssyncp_decrypted, NULL, "Decrypted data");

        proto_tree_add_item(dec_tree, hf_ssyncp_timestamp, decrypted_tvb,
                0, 2, ENC_BIG_ENDIAN);
        proto_tree_add_item(dec_tree, hf_ssyncp_timestamp_reply, decrypted_tvb,
                2, 2, ENC_BIG_ENDIAN);

        if (ssyncp_pinfo->have_rtt_estimate) {
            int rtt_id = direction ? hf_ssyncp_rtt_to_server : hf_ssyncp_rtt_to_client;
            proto_item *rtt_item = proto_tree_add_int(dec_tree, rtt_id, decrypted_tvb, 2, 2, ssyncp_pinfo->rtt_estimate);
            proto_item_set_generated(rtt_item);
        }

        proto_tree_add_item(dec_tree, hf_ssyncp_frag_seq, decrypted_tvb,
                4, 8, ENC_BIG_ENDIAN);
        proto_tree_add_item(dec_tree, hf_ssyncp_frag_final, decrypted_tvb,
                12, 2, ENC_BIG_ENDIAN);
        proto_item *frag_idx_item = proto_tree_add_item(dec_tree,
                hf_ssyncp_frag_idx, decrypted_tvb, 12, 2, ENC_BIG_ENDIAN);

        /* TODO actually handle fragmentation; for now just bail out on fragmentation */
        if (tvb_get_guint16(decrypted_tvb, 12, ENC_BIG_ENDIAN) != 0x8000) {
            expert_add_info(pinfo, frag_idx_item, &ei_ssyncp_fragmented);
            return tvb_captured_length(tvb);
        }

        tvbuff_t *inflated_tvb = tvb_child_uncompress(decrypted_tvb, decrypted_tvb, 14, decrypted_len - 14);
        if (inflated_tvb == NULL)
            return tvb_captured_length(tvb);
        add_new_data_source(pinfo, inflated_tvb, "Inflated data");

        if (dissector_protobuf) {
            call_dissector_with_data(dissector_protobuf, inflated_tvb, pinfo,
                    dec_tree, "message,TransportBuffers.Instruction");
        }
    }

    return tvb_captured_length(tvb);
}

/* Register the protocol with Wireshark.
 *
 * This format is required because a script is used to build the C function that
 * calls all the protocol registration.
 */
void
proto_register_ssyncp(void)
{
    static const true_false_string direction_name = {
        "Server->Client",
        "Client->Server"
    };

    /* Setup list of header fields  See Section 1.5 of README.dissector for
     * details. */
    static hf_register_info hf[] = {
        { &hf_ssyncp_direction,
          { "Direction", "ssyncp.direction",
            FT_BOOLEAN, 8, TFS(&direction_name), 0x80,
            "Direction of packet", HFILL }
        },
        { &hf_ssyncp_seq,
          { "Sequence number", "ssyncp.seq",
            FT_UINT64, BASE_HEX, NULL, 0x7fffffffffffffff,
            "Monotonically incrementing packet sequence number", HFILL }
        },
        { &hf_ssyncp_encrypted,
          { "Encrypted data", "ssyncp.enc_data",
            FT_BYTES, BASE_NONE, NULL, 0,
            "Encrypted RTT estimation fields and Transport Layer payload, encrypted with AES-128-OCB",
            HFILL }
        },
        { &hf_ssyncp_seq_delta,
          { "Sequence number delta", "ssyncp.seq_delta",
            FT_INT64, BASE_DEC, NULL, 0,
            "Delta from last sequence number; 1 is normal, 0 is duplicated packet, <0 is reordering, >1 is reordering or packet loss", HFILL }
        },
        { &hf_ssyncp_timestamp,
          { "Truncated timestamp", "ssyncp.timestamp",
            FT_UINT16, BASE_HEX, NULL, 0,
            "Low 16 bits of sender's time in milliseconds", HFILL }
        },
        { &hf_ssyncp_timestamp_reply,
          { "Last timestamp received", "ssyncp.timestamp_reply",
            FT_UINT16, BASE_HEX, NULL, 0,
            "Low 16 bits of timestamp of last received packet plus time since it was received (for RTT estimation)", HFILL }
        },
        { &hf_ssyncp_frag_seq,
          { "Fragment ID", "ssyncp.frag_seq",
            FT_UINT64, BASE_HEX, NULL, 0,
            "Transport-level sequence number, used for fragment reassembly", HFILL }
        },
        { &hf_ssyncp_frag_final,
          { "Final fragment", "ssyncp.frag_final",
            FT_BOOLEAN, 16, NULL, 0x8000,
            "Is this the last fragment?", HFILL }
        },
        { &hf_ssyncp_frag_idx,
          { "Fragment Index", "ssyncp.frag_idx",
            FT_UINT16, BASE_HEX, NULL, 0x7fff,
            "Index of this fragment in the list of fragments of the transport-level message", HFILL }
        },
        { &hf_ssyncp_rtt_to_server,
          { "RTT estimate to server (in ms)", "ssyncp.rtt_est_to_server",
            FT_INT16, BASE_DEC, NULL, 0,
            "Estimated round trip time from point of capture to server", HFILL }
        },
        { &hf_ssyncp_rtt_to_client,
          { "RTT estimate to client (in ms)", "ssyncp.rtt_est_to_client",
            FT_INT16, BASE_DEC, NULL, 0,
            "Estimated round trip time from point of capture to client", HFILL }
        }
    };

    /* Setup protocol subtree array */
    static gint *ett[] = {
        &ett_ssyncp,
        &ett_ssyncp_decrypted
    };

    /* Setup protocol expert items */
    static ei_register_info ei[] = {
        { &ei_ssyncp_fragmented,
          { "ssyncp.fragmented", PI_REASSEMBLE, PI_WARN,
            "SSYNCP-level fragmentation, dissector can't handle that", EXPFILL }
        },
        { &ei_ssyncp_bad_key,
          { "ssyncp.badkey", PI_DECRYPTION, PI_WARN,
            "Encrypted data could not be decrypted with the provided key", EXPFILL }
        }
    };

    /* Register the protocol name and description */
    proto_ssyncp = proto_register_protocol("State Synchronization Protocol", "SSyncP", "ssyncp");

    /* Register the dissector handle */
    ssyncp_handle = register_dissector("ssyncp", dissect_ssyncp, proto_ssyncp);

    /* Required function calls to register the header fields and subtrees */
    proto_register_field_array(proto_ssyncp, hf, array_length(hf));
    proto_register_subtree_array(ett, array_length(ett));

    expert_module_t *expert_ssyncp = expert_register_protocol(proto_ssyncp);
    expert_register_field_array(expert_ssyncp, ei, array_length(ei));

    module_t *ssyncp_module = prefs_register_protocol(proto_ssyncp, proto_reg_handoff_ssyncp);

    prefs_register_string_preference(ssyncp_module, "key",
        "ssyncp MOSH_KEY",
        "MOSH_KEY AES key (from mosh-{client,server} environment variable)",
        &pref_ssyncp_key);
}

void
proto_reg_handoff_ssyncp(void)
{
    static gboolean initialized = FALSE;

    if (!initialized) {
        dissector_add_uint("udp.port", SSYNCP_UDP_PORT, ssyncp_handle);

        dissector_protobuf = find_dissector("protobuf");
        if (dissector_protobuf == NULL) {
            report_failure("unable to find protobuf dissector");
        }

        initialized = TRUE;
    }

    have_ssyncp_key = FALSE;
    if (strlen(pref_ssyncp_key) != 0) {
        if (strlen(pref_ssyncp_key) != 22) {
            report_failure("ssyncp: invalid key, must be 22 characters long");
            return;
        }
        char base64_key[25];
        memcpy(base64_key, pref_ssyncp_key, 22);
        memcpy(base64_key+22, "==\0", 3);
        gsize out_len;
        if (g_base64_decode_inplace(base64_key, &out_len) == NULL || out_len != sizeof(ssyncp_raw_aes_key)) {
            report_failure("ssyncp: invalid key, base64 decoding (with \"==\" appended) failed");
            return;
        }
        memcpy(ssyncp_raw_aes_key, base64_key, sizeof(ssyncp_raw_aes_key));
        have_ssyncp_key = TRUE;
    }
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 4
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=4 tabstop=8 expandtab:
 * :indentSize=4:tabSize=8:noTabs=true:
 */