2 * Routines for WireGuard dissection
3 * Copyright 2018, Peter Wu <peter@lekensteyn.nl>
5 * Wireshark - Network traffic analyzer
6 * By Gerald Combs <gerald@wireshark.org>
7 * Copyright 1998 Gerald Combs
9 * SPDX-License-Identifier: GPL-2.0-or-later
13 * Protocol details: https://www.wireguard.com/protocol/
18 #include <epan/packet.h>
19 #include <epan/expert.h>
20 #include <epan/prefs.h>
21 #include <epan/proto_data.h>
23 #include <wsutil/wsgcrypt.h>
24 #include <wsutil/curve25519.h>
26 #if GCRYPT_VERSION_NUMBER >= 0x010800 /* 1.8.0 */
27 /* Decryption requires Curve25519, ChaCha20-Poly1305 (1.7) and Blake2s (1.8). */
28 #define WG_DECRYPTION_SUPPORTED
31 void proto_reg_handoff_wg(void);
32 void proto_register_wg(void);
34 static int proto_wg = -1;
35 static int hf_wg_type = -1;
36 static int hf_wg_reserved = -1;
37 static int hf_wg_sender = -1;
38 static int hf_wg_ephemeral = -1;
39 static int hf_wg_encrypted_static = -1;
40 static int hf_wg_encrypted_timestamp = -1;
41 static int hf_wg_mac1 = -1;
42 static int hf_wg_mac2 = -1;
43 static int hf_wg_receiver = -1;
44 static int hf_wg_encrypted_empty = -1;
45 static int hf_wg_nonce = -1;
46 static int hf_wg_encrypted_cookie = -1;
47 static int hf_wg_counter = -1;
48 static int hf_wg_encrypted_packet = -1;
49 static int hf_wg_stream = -1;
50 static int hf_wg_response_in = -1;
51 static int hf_wg_response_to = -1;
52 static int hf_wg_receiver_pubkey = -1;
53 static int hf_wg_receiver_pubkey_known_privkey = -1;
55 static gint ett_wg = -1;
56 static gint ett_key_info = -1;
58 static expert_field ei_wg_bad_packet_length = EI_INIT;
59 static expert_field ei_wg_keepalive = EI_INIT;
62 // Length of AEAD authentication tag
63 #define AUTH_TAG_LENGTH 16
66 WG_TYPE_HANDSHAKE_INITIATION = 1,
67 WG_TYPE_HANDSHAKE_RESPONSE = 2,
68 WG_TYPE_COOKIE_REPLY = 3,
69 WG_TYPE_TRANSPORT_DATA = 4
72 static const value_string wg_type_names[] = {
73 { 0x01, "Handshake Initiation" },
74 { 0x02, "Handshake Response" },
75 { 0x03, "Cookie Reply" },
76 { 0x04, "Transport Data" },
80 #ifdef WG_DECRYPTION_SUPPORTED
81 /* Decryption types. {{{ */
83 * Most operations operate on 32 byte units (keys and hash output).
87 guchar data[WG_KEY_LEN];
91 * Static key with the MAC1 key pre-computed and an optional private key.
93 typedef struct wg_skey {
96 wg_qqword priv_key; /* Optional, set to all zeroes if missing. */
100 * Set of (long-term) static keys (for guessing the peer based on MAC1).
101 * Maps the public key to the "wg_skey_t" structure.
102 * Keys are populated from the UAT and key log file.
104 static GHashTable *wg_static_keys;
106 /* UAT adapter for populating wg_static_keys. */
107 enum { WG_KEY_UAT_PUBLIC, WG_KEY_UAT_PRIVATE };
108 static const value_string wg_key_uat_type_vals[] = {
109 { WG_KEY_UAT_PUBLIC, "Public" },
110 { WG_KEY_UAT_PRIVATE, "Private" },
115 guint key_type; /* See "wg_key_uat_type_vals". */
117 } wg_key_uat_record_t;
119 static wg_key_uat_record_t *wg_key_records;
120 static guint num_wg_key_records;
121 /* Decryption types. }}} */
122 #endif /* WG_DECRYPTION_SUPPORTED */
125 * Information required to process and link messages as required on the first
126 * sequential pass. After that it can be erased.
129 address initiator_address;
130 address responder_address;
131 guint16 initiator_port;
132 guint16 responder_port;
136 * A "session" between two peer is identified by a "sender" id as independently
137 * chosen by each side. In case both peer IDs collide, the source IP and UDP
138 * port number could be used to distinguish sessions. As IDs can be recycled
139 * over time, lookups should use the most recent initiation (or response).
141 * XXX record timestamps (time since last message, for validating timers).
144 guint32 stream; /* Session identifier (akin to udp.stream). */
145 guint32 initiator_frame;
146 guint32 response_frame; /* Responder or Cookie Reply message. */
147 wg_initial_info_t initial; /* Valid only on the first pass. */
150 /* Per-packet state. */
152 wg_session_t *session;
155 /* Map from Sender/Receiver IDs to a list of session information. */
156 static wmem_map_t *sessions;
157 static guint32 wg_session_count;
160 #ifdef WG_DECRYPTION_SUPPORTED
161 /* Key conversion routines. {{{ */
162 /* Import external random data as private key. */
164 set_private_key(wg_qqword *privkey, const wg_qqword *inkey)
166 // The 254th bit of a Curve25519 secret will always be set in calculations,
167 // use this property to recognize whether a private key is set.
169 privkey->data[31] |= 64;
172 /* Whether a private key is initialized (see set_private_key). */
173 static inline gboolean
174 has_private_key(const wg_qqword *secret)
176 return !!(secret->data[31] & 64);
180 * Compute the Curve25519 public key from a private key.
183 priv_to_pub(wg_qqword *pub, const wg_qqword *priv)
185 int r = crypto_scalarmult_curve25519_base(pub->data, priv->data);
186 /* The computation should always be possible. */
187 DISSECTOR_ASSERT(r == 0);
191 * Returns the string representation (base64) of a public key.
192 * The returned value is allocated with wmem_packet_scope.
195 pubkey_to_string(const wg_qqword *pubkey)
197 gchar *str = g_base64_encode(pubkey->data, WG_KEY_LEN);
198 gchar *ret = wmem_strdup(wmem_packet_scope(), str);
204 decode_base64_key(wg_qqword *out, const char *str)
209 if (strlen(str) + 1 != sizeof(tmp)) {
212 memcpy(tmp, str, sizeof(tmp));
213 g_base64_decode_inplace(tmp, &out_len);
214 if (out_len != WG_KEY_LEN) {
217 memcpy(out->data, tmp, WG_KEY_LEN);
220 /* Key conversion routines. }}} */
223 wg_pubkey_equal(gconstpointer v1, gconstpointer v2)
225 const wg_qqword *pubkey1 = (const wg_qqword *)v1;
226 const wg_qqword *pubkey2 = (const wg_qqword *)v2;
227 return !memcmp(pubkey1->data, pubkey2->data, WG_KEY_LEN);
231 /* Protocol-specific crypto routines. {{{ */
233 * Computes MAC1. Caller must ensure that GCRY_MD_BLAKE2S_256 is available.
236 wg_mac1_key(const wg_qqword *static_public, wg_qqword *mac_key_out)
239 if (gcry_md_open(&hd, GCRY_MD_BLAKE2S_256, 0) == 0) {
240 const char wg_label_mac1[] = "mac1----";
241 gcry_md_write(hd, wg_label_mac1, strlen(wg_label_mac1));
242 gcry_md_write(hd, static_public->data, sizeof(wg_qqword));
243 memcpy(mac_key_out->data, gcry_md_read(hd, 0), sizeof(wg_qqword));
247 // caller should have checked this.
248 DISSECTOR_ASSERT_NOT_REACHED();
252 * Verify that MAC(mac_key, data) matches "mac_output".
255 wg_mac_verify(const wg_qqword *mac_key,
256 const guchar *data, guint data_len, const guint8 mac_output[16])
260 if (gcry_md_open(&hd, GCRY_MD_BLAKE2S_128, 0) == 0) {
262 // not documented by Libgcrypt, but required for keyed blake2s
263 r = gcry_md_setkey(hd, mac_key->data, WG_KEY_LEN);
264 DISSECTOR_ASSERT(r == 0);
265 gcry_md_write(hd, data, data_len);
266 ok = memcmp(mac_output, gcry_md_read(hd, 0), 16) == 0;
269 // caller should have checked this.
270 DISSECTOR_ASSERT_NOT_REACHED();
274 /* Protocol-specific crypto routines. }}} */
277 * Add a static public or private key to "wg_static_keys".
280 wg_add_static_key(const wg_qqword *tmp_key, gboolean is_private)
282 wg_skey_t *key = g_new0(wg_skey_t, 1);
284 set_private_key(&key->priv_key, tmp_key);
285 priv_to_pub(&key->pub_key, tmp_key);
287 key->pub_key = *tmp_key;
290 // If a previous pubkey exists, skip adding the new key. Do add the
291 // secret if it has become known in meantime.
292 wg_skey_t *oldkey = (wg_skey_t *)g_hash_table_lookup(wg_static_keys, &key->pub_key);
294 if (!has_private_key(&oldkey->priv_key) && is_private) {
295 oldkey->priv_key = key->priv_key;
301 // New key, precompute the MAC1 label.
302 wg_mac1_key(&key->pub_key, &key->mac1_key);
304 g_hash_table_insert(wg_static_keys, &key->pub_key, key);
307 /* UAT and key configuration. {{{ */
309 wg_key_uat_record_update_cb(void *r, char **error)
311 wg_key_uat_record_t *rec = (wg_key_uat_record_t *)r;
314 /* Check for valid base64-encoding. */
315 if (!decode_base64_key(&key, rec->key)) {
316 *error = g_strdup("Invalid key");
324 wg_key_uat_apply(void)
326 if (!wg_static_keys) {
327 // The first field of "wg_skey_t" is the pubkey (and the table key),
328 // its initial four bytes should be good enough as key hash.
329 wg_static_keys = g_hash_table_new_full(g_int_hash, wg_pubkey_equal, NULL, g_free);
331 g_hash_table_remove_all(wg_static_keys);
334 /* Convert base64-encoded strings to wg_skey_t and derive pubkey. */
335 for (guint i = 0; i < num_wg_key_records; i++) {
336 wg_key_uat_record_t *rec = &wg_key_records[i];
337 wg_qqword tmp_key; /* Either public or private, not sure yet. */
339 /* Populate public (and private) keys. */
340 gboolean decoded = decode_base64_key(&tmp_key, rec->key);
341 DISSECTOR_ASSERT(decoded);
342 wg_add_static_key(&tmp_key, rec->key_type == WG_KEY_UAT_PRIVATE);
347 wg_key_uat_reset(void)
349 /* Erase keys when the UAT is unloaded. */
350 g_hash_table_destroy(wg_static_keys);
351 wg_static_keys = NULL;
354 UAT_VS_DEF(wg_key_uat, key_type, wg_key_uat_record_t, guint, WG_KEY_UAT_PUBLIC, "Public")
355 UAT_CSTRING_CB_DEF(wg_key_uat, key, wg_key_uat_record_t)
356 /* UAT and key configuration. }}} */
357 #endif /* WG_DECRYPTION_SUPPORTED */
361 wg_sessions_insert(guint32 id, wg_session_t *session)
363 wmem_list_t *list = (wmem_list_t *)wmem_map_lookup(sessions, GUINT_TO_POINTER(id));
365 list = wmem_list_new(wmem_file_scope());
366 wmem_map_insert(sessions, GUINT_TO_POINTER(id), list);
368 wmem_list_append(list, session);
371 static wg_session_t *
374 wg_session_t *session = wmem_new0(wmem_file_scope(), wg_session_t);
375 session->stream = wg_session_count++;
379 /* Updates the peer address based on the source address. */
381 wg_session_update_address(wg_session_t *session, packet_info *pinfo, gboolean sender_is_initiator)
383 DISSECTOR_ASSERT(!PINFO_FD_VISITED(pinfo));
385 if (sender_is_initiator) {
386 copy_address_wmem(wmem_file_scope(), &session->initial.initiator_address, &pinfo->src);
387 session->initial.initiator_port = (guint16)pinfo->srcport;
389 copy_address_wmem(wmem_file_scope(), &session->initial.responder_address, &pinfo->src);
390 session->initial.responder_port = (guint16)pinfo->srcport;
394 /* Finds an initiation message based on the given Receiver ID that was not
395 * previously associated with a responder message. Returns the session if a
396 * matching initation message can be found or NULL otherwise.
398 static wg_session_t *
399 wg_sessions_lookup_initiation(packet_info *pinfo, guint32 receiver_id)
401 DISSECTOR_ASSERT(!PINFO_FD_VISITED(pinfo));
403 /* Look for the initiation message matching this Receiver ID. */
404 wmem_list_t *list = (wmem_list_t *)wmem_map_lookup(sessions, GUINT_TO_POINTER(receiver_id));
409 /* Walk backwards to find the most recent message first. All packets are
410 * guaranteed to arrive before this frame because this is the first pass. */
411 for (wmem_list_frame_t *item = wmem_list_tail(list); item; item = wmem_list_frame_prev(item)) {
412 wg_session_t *session = (wg_session_t *)wmem_list_frame_data(item);
413 if (session->initial.initiator_port != pinfo->destport ||
414 !addresses_equal(&session->initial.initiator_address, &pinfo->dst)) {
415 /* Responder messages are expected to be sent to the initiator. */
418 if (session->response_frame && session->response_frame != pinfo->num) {
419 /* This session was linked elsewhere. */
423 /* This assumes no malicious messages and no contrived sequences:
424 * Any initiator or responder message is not duplicated nor are these
425 * mutated. If this must be detected, the caller could decrypt or check
426 * mac1 to distinguish valid messages.
434 /* Finds a session with a completed handshake that matches the Receiver ID. */
435 static wg_session_t *
436 wg_sessions_lookup(packet_info *pinfo, guint32 receiver_id, gboolean *receiver_is_initiator)
438 DISSECTOR_ASSERT(!PINFO_FD_VISITED(pinfo));
440 wmem_list_t *list = (wmem_list_t *)wmem_map_lookup(sessions, GUINT_TO_POINTER(receiver_id));
445 /* Walk backwards to find the most recent message first. */
446 for (wmem_list_frame_t *item = wmem_list_tail(list); item; item = wmem_list_frame_prev(item)) {
447 wg_session_t *session = (wg_session_t *)wmem_list_frame_data(item);
448 if (!session->response_frame) {
449 /* Ignore sessions that are not fully established. */
452 if (session->initial.initiator_port == pinfo->destport &&
453 addresses_equal(&session->initial.initiator_address, &pinfo->dst)) {
454 *receiver_is_initiator = TRUE;
455 } else if (session->initial.responder_port == pinfo->destport &&
456 addresses_equal(&session->initial.responder_address, &pinfo->dst)) {
457 *receiver_is_initiator = FALSE;
459 /* Both peers do not match the destination, ignore. */
468 #ifdef WG_DECRYPTION_SUPPORTED
470 * Finds the static public key for the receiver of this message based on the
472 * TODO on PINFO_FD_VISITED, reuse previously discovered keys from session?
474 static const wg_skey_t *
475 wg_mac1_key_probe(tvbuff_t *tvb, gboolean is_initiation)
477 const int mac1_offset = is_initiation ? 116 : 60;
479 // Shortcut: skip MAC1 validation if no pubkeys are configured.
480 if (g_hash_table_size(wg_static_keys) == 0) {
484 const guint8 *mac1_msgdata = tvb_get_ptr(tvb, 0, mac1_offset);
485 const guint8 *mac1_output = tvb_get_ptr(tvb, mac1_offset, 16);
486 // Find public key that matches the 16-byte MAC1 field.
489 g_hash_table_iter_init(&iter, wg_static_keys);
490 while (g_hash_table_iter_next(&iter, NULL, &value)) {
491 const wg_skey_t *skey = (wg_skey_t *)value;
492 if (wg_mac_verify(&skey->mac1_key, mac1_msgdata, (guint)mac1_offset, mac1_output)) {
499 #endif /* WG_DECRYPTION_SUPPORTED */
503 wg_dissect_pubkey(proto_tree *tree, tvbuff_t *tvb, int offset, gboolean is_ephemeral)
505 const guint8 *pubkey = tvb_get_ptr(tvb, offset, 32);
506 gchar *str = g_base64_encode(pubkey, 32);
507 gchar *key_str = wmem_strdup(wmem_packet_scope(), str);
510 int hf_id = is_ephemeral ? hf_wg_ephemeral : -1; // TODO extend for static keys
511 proto_tree_add_string(tree, hf_id, tvb, offset, 32, key_str);
514 #ifdef WG_DECRYPTION_SUPPORTED
516 wg_dissect_mac1_pubkey(proto_tree *tree, tvbuff_t *tvb, const wg_skey_t *skey)
524 ti = proto_tree_add_string(tree, hf_wg_receiver_pubkey, tvb, 0, 0, pubkey_to_string(&skey->pub_key));
525 PROTO_ITEM_SET_GENERATED(ti);
526 proto_tree *key_tree = proto_item_add_subtree(ti, ett_key_info);
527 ti = proto_tree_add_boolean(key_tree, hf_wg_receiver_pubkey_known_privkey, tvb, 0, 0, !!has_private_key(&skey->priv_key));
528 PROTO_ITEM_SET_GENERATED(ti);
530 #endif /* WG_DECRYPTION_SUPPORTED */
533 wg_dissect_handshake_initiation(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
538 #ifdef WG_DECRYPTION_SUPPORTED
539 const wg_skey_t *skey_r = wg_mac1_key_probe(tvb, TRUE);
540 #endif /* WG_DECRYPTION_SUPPORTED */
542 proto_tree_add_item_ret_uint(wg_tree, hf_wg_sender, tvb, 4, 4, ENC_LITTLE_ENDIAN, &sender_id);
543 col_append_fstr(pinfo->cinfo, COL_INFO, ", sender=0x%08X", sender_id);
544 wg_dissect_pubkey(wg_tree, tvb, 8, TRUE);
545 proto_tree_add_item(wg_tree, hf_wg_encrypted_static, tvb, 40, 32 + AUTH_TAG_LENGTH, ENC_NA);
546 proto_tree_add_item(wg_tree, hf_wg_encrypted_timestamp, tvb, 88, 12 + AUTH_TAG_LENGTH, ENC_NA);
547 proto_tree_add_item(wg_tree, hf_wg_mac1, tvb, 116, 16, ENC_NA);
548 #ifdef WG_DECRYPTION_SUPPORTED
549 wg_dissect_mac1_pubkey(wg_tree, tvb, skey_r);
550 #endif /* WG_DECRYPTION_SUPPORTED */
551 proto_tree_add_item(wg_tree, hf_wg_mac2, tvb, 132, 16, ENC_NA);
553 if (!PINFO_FD_VISITED(pinfo)) {
554 /* XXX should an initiation message with the same contents (except MAC2) be
555 * considered part of the same "session"? */
556 wg_session_t *session = wg_session_new();
557 session->initiator_frame = pinfo->num;
558 wg_session_update_address(session, pinfo, TRUE);
559 wg_sessions_insert(sender_id, session);
560 wg_pinfo->session = session;
562 wg_session_t *session = wg_pinfo->session;
564 ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
565 PROTO_ITEM_SET_GENERATED(ti);
567 if (session && session->response_frame) {
568 ti = proto_tree_add_uint(wg_tree, hf_wg_response_in, tvb, 0, 0, session->response_frame);
569 PROTO_ITEM_SET_GENERATED(ti);
576 wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
578 guint32 sender_id, receiver_id;
581 #ifdef WG_DECRYPTION_SUPPORTED
582 const wg_skey_t *skey_i = wg_mac1_key_probe(tvb, FALSE);
583 #endif /* WG_DECRYPTION_SUPPORTED */
585 proto_tree_add_item_ret_uint(wg_tree, hf_wg_sender, tvb, 4, 4, ENC_LITTLE_ENDIAN, &sender_id);
586 col_append_fstr(pinfo->cinfo, COL_INFO, ", sender=0x%08X", sender_id);
587 proto_tree_add_item_ret_uint(wg_tree, hf_wg_receiver, tvb, 8, 4, ENC_LITTLE_ENDIAN, &receiver_id);
588 col_append_fstr(pinfo->cinfo, COL_INFO, ", receiver=0x%08X", receiver_id);
589 wg_dissect_pubkey(wg_tree, tvb, 12, TRUE);
590 proto_tree_add_item(wg_tree, hf_wg_encrypted_empty, tvb, 44, 16, ENC_NA);
591 proto_tree_add_item(wg_tree, hf_wg_mac1, tvb, 60, 16, ENC_NA);
592 #ifdef WG_DECRYPTION_SUPPORTED
593 wg_dissect_mac1_pubkey(wg_tree, tvb, skey_i);
594 #endif /* WG_DECRYPTION_SUPPORTED */
595 proto_tree_add_item(wg_tree, hf_wg_mac2, tvb, 76, 16, ENC_NA);
597 wg_session_t *session;
598 if (!PINFO_FD_VISITED(pinfo)) {
599 session = wg_sessions_lookup_initiation(pinfo, receiver_id);
600 /* XXX should probably check whether decryption succeeds before linking
601 * and somehow mark that this response is related but not correct. */
603 session->response_frame = pinfo->num;
604 wg_session_update_address(session, pinfo, FALSE);
605 wg_sessions_insert(sender_id, session);
606 wg_pinfo->session = session;
609 session = wg_pinfo->session;
612 ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
613 PROTO_ITEM_SET_GENERATED(ti);
614 ti = proto_tree_add_uint(wg_tree, hf_wg_response_to, tvb, 0, 0, session->initiator_frame);
615 PROTO_ITEM_SET_GENERATED(ti);
622 wg_dissect_handshake_cookie(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
627 proto_tree_add_item_ret_uint(wg_tree, hf_wg_receiver, tvb, 4, 4, ENC_LITTLE_ENDIAN, &receiver_id);
628 col_append_fstr(pinfo->cinfo, COL_INFO, ", receiver=0x%08X", receiver_id);
629 proto_tree_add_item(wg_tree, hf_wg_nonce, tvb, 8, 24, ENC_NA);
630 proto_tree_add_item(wg_tree, hf_wg_encrypted_cookie, tvb, 32, 16 + AUTH_TAG_LENGTH, ENC_NA);
632 wg_session_t *session;
633 if (!PINFO_FD_VISITED(pinfo)) {
634 /* Check for Cookie Reply from Responder to Initiator. */
635 session = wg_sessions_lookup_initiation(pinfo, receiver_id);
637 session->response_frame = pinfo->num;
638 wg_session_update_address(session, pinfo, FALSE);
639 wg_pinfo->session = session;
641 /* XXX check for cookie reply from Initiator to Responder */
643 session = wg_pinfo->session;
646 ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
647 PROTO_ITEM_SET_GENERATED(ti);
648 /* XXX check for cookie reply from Initiator to Responder */
649 ti = proto_tree_add_uint(wg_tree, hf_wg_response_to, tvb, 0, 0, session->initiator_frame);
650 PROTO_ITEM_SET_GENERATED(ti);
657 wg_dissect_data(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
663 proto_tree_add_item_ret_uint(wg_tree, hf_wg_receiver, tvb, 4, 4, ENC_LITTLE_ENDIAN, &receiver_id);
664 col_append_fstr(pinfo->cinfo, COL_INFO, ", receiver=0x%08X", receiver_id);
665 proto_tree_add_item_ret_uint64(wg_tree, hf_wg_counter, tvb, 8, 8, ENC_LITTLE_ENDIAN, &counter);
666 col_append_fstr(pinfo->cinfo, COL_INFO, ", counter=%" G_GUINT64_FORMAT, counter);
668 gint packet_length = tvb_captured_length_remaining(tvb, 16);
669 if (packet_length < AUTH_TAG_LENGTH) {
670 proto_tree_add_expert(wg_tree, pinfo, &ei_wg_bad_packet_length, tvb, 16, packet_length);
671 return 16 + packet_length;
672 } else if (packet_length != AUTH_TAG_LENGTH) {
673 /* Keepalive messages are already marked, no need to append data length. */
674 col_append_fstr(pinfo->cinfo, COL_INFO, ", datalen=%d", packet_length - AUTH_TAG_LENGTH);
676 ti = proto_tree_add_item(wg_tree, hf_wg_encrypted_packet, tvb, 16, packet_length, ENC_NA);
678 if (packet_length == AUTH_TAG_LENGTH) {
679 expert_add_info(pinfo, ti, &ei_wg_keepalive);
682 wg_session_t *session;
683 if (!PINFO_FD_VISITED(pinfo)) {
684 gboolean receiver_is_initiator;
685 session = wg_sessions_lookup(pinfo, receiver_id, &receiver_is_initiator);
687 wg_session_update_address(session, pinfo, !receiver_is_initiator);
688 wg_pinfo->session = session;
691 session = wg_pinfo->session;
694 ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
695 PROTO_ITEM_SET_GENERATED(ti);
698 return 16 + packet_length;
702 dissect_wg(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
706 guint32 message_type;
707 const char *message_type_str;
708 wg_packet_info_t *wg_pinfo;
710 /* Heuristics check: check for reserved bits (zeros) and message type. */
711 if (tvb_reported_length(tvb) < 4 || tvb_get_ntoh24(tvb, 1) != 0)
714 message_type = tvb_get_guint8(tvb, 0);
715 message_type_str = try_val_to_str(message_type, wg_type_names);
716 if (!message_type_str)
719 /* Special case: zero-length data message is a Keepalive message. */
720 if (message_type == WG_TYPE_TRANSPORT_DATA && tvb_reported_length(tvb) == 32) {
721 message_type_str = "Keepalive";
724 col_set_str(pinfo->cinfo, COL_PROTOCOL, "WireGuard");
725 col_set_str(pinfo->cinfo, COL_INFO, message_type_str);
727 ti = proto_tree_add_item(tree, proto_wg, tvb, 0, -1, ENC_NA);
728 wg_tree = proto_item_add_subtree(ti, ett_wg);
730 proto_tree_add_item(wg_tree, hf_wg_type, tvb, 0, 1, ENC_NA);
731 proto_tree_add_item(wg_tree, hf_wg_reserved, tvb, 1, 3, ENC_NA);
733 if (!PINFO_FD_VISITED(pinfo)) {
734 wg_pinfo = wmem_new0(wmem_file_scope(), wg_packet_info_t);
735 p_add_proto_data(wmem_file_scope(), pinfo, proto_wg, 0, wg_pinfo);
737 wg_pinfo = (wg_packet_info_t *)p_get_proto_data(wmem_file_scope(), pinfo, proto_wg, 0);
740 switch ((wg_message_type)message_type) {
741 case WG_TYPE_HANDSHAKE_INITIATION:
742 return wg_dissect_handshake_initiation(tvb, pinfo, wg_tree, wg_pinfo);
743 case WG_TYPE_HANDSHAKE_RESPONSE:
744 return wg_dissect_handshake_response(tvb, pinfo, wg_tree, wg_pinfo);
745 case WG_TYPE_COOKIE_REPLY:
746 return wg_dissect_handshake_cookie(tvb, pinfo, wg_tree, wg_pinfo);
747 case WG_TYPE_TRANSPORT_DATA:
748 return wg_dissect_data(tvb, pinfo, wg_tree, wg_pinfo);
751 DISSECTOR_ASSERT_NOT_REACHED();
757 wg_session_count = 0;
761 proto_register_wg(void)
763 #ifdef WG_DECRYPTION_SUPPORTED
765 #endif /* WG_DECRYPTION_SUPPORTED */
766 expert_module_t *expert_wg;
768 static hf_register_info hf[] = {
769 /* Initiation message */
772 FT_UINT8, BASE_DEC, VALS(wg_type_names), 0x0,
776 { "Reserved", "wg.reserved",
777 FT_NONE, BASE_NONE, NULL, 0x0,
781 { "Sender", "wg.sender",
782 FT_UINT32, BASE_HEX, NULL, 0x0,
783 "Identifier as chosen by the sender", HFILL }
786 { "Ephemeral", "wg.ephemeral",
787 FT_STRING, BASE_NONE, NULL, 0x0,
788 "Ephemeral public key of sender", HFILL }
790 { &hf_wg_encrypted_static,
791 { "Encrypted Static", "wg.encrypted_static",
792 FT_NONE, BASE_NONE, NULL, 0x0,
793 "Encrypted long-term static public key of sender", HFILL }
795 { &hf_wg_encrypted_timestamp,
796 { "Encrypted Timestamp", "wg.encrypted_timestamp",
797 FT_NONE, BASE_NONE, NULL, 0x0,
802 FT_BYTES, BASE_NONE, NULL, 0x0,
807 FT_BYTES, BASE_NONE, NULL, 0x0,
811 /* Response message */
813 { "Receiver", "wg.receiver",
814 FT_UINT32, BASE_HEX, NULL, 0x0,
815 "Identifier as chosen by receiver", HFILL }
817 { &hf_wg_encrypted_empty,
818 { "Encrypted Empty", "wg.encrypted_empty",
819 FT_NONE, BASE_NONE, NULL, 0x0,
820 "Authenticated encryption of an empty string", HFILL }
825 { "Nonce", "wg.nonce",
826 FT_BYTES, BASE_NONE, NULL, 0x0,
829 { &hf_wg_encrypted_cookie,
830 { "Encrypted Cookie", "wg.encrypted_cookie",
831 FT_BYTES, BASE_NONE, NULL, 0x0,
834 /* TODO decrypted cookie field. */
838 { "Counter", "wg.counter",
839 FT_UINT64, BASE_DEC, NULL, 0x0,
842 { &hf_wg_encrypted_packet,
843 { "Encrypted Packet", "wg.encrypted_packet",
844 FT_NONE, BASE_NONE, NULL, 0x0,
848 /* Association tracking. */
850 { "Stream index", "wg.stream",
851 FT_UINT32, BASE_DEC, NULL, 0x0,
852 "Identifies a session in this capture file", HFILL }
854 { &hf_wg_response_in,
855 { "Response in Frame", "wg.response_in",
856 FT_FRAMENUM, BASE_NONE, FRAMENUM_TYPE(FT_FRAMENUM_RESPONSE), 0x0,
857 "The response to this initiation message is in this frame", HFILL }
859 { &hf_wg_response_to,
860 { "Response to Frame", "wg.response_to",
861 FT_FRAMENUM, BASE_NONE, FRAMENUM_TYPE(FT_FRAMENUM_REQUEST), 0x0,
862 "This is a response to the initiation message in this frame", HFILL }
865 /* Additional fields. */
866 { &hf_wg_receiver_pubkey,
867 { "Receiver Static Public Key", "wg.receiver_pubkey",
868 FT_STRING, BASE_NONE, NULL, 0x0,
869 "Public key of the receiver (matched based on MAC1)", HFILL }
871 { &hf_wg_receiver_pubkey_known_privkey,
872 { "Has Private Key", "wg.receiver_pubkey.known_privkey",
873 FT_BOOLEAN, BASE_NONE, NULL, 0x0,
874 "Whether the corresponding private key is known (configured via prefs)", HFILL }
878 static gint *ett[] = {
883 static ei_register_info ei[] = {
884 { &ei_wg_bad_packet_length,
885 { "wg.bad_packet_length", PI_MALFORMED, PI_ERROR,
886 "Packet length is too small", EXPFILL }
889 { "wg.keepalive", PI_SEQUENCE, PI_CHAT,
890 "This is a Keepalive message", EXPFILL }
894 #ifdef WG_DECRYPTION_SUPPORTED
895 /* UAT for header fields */
896 static uat_field_t wg_key_uat_fields[] = {
897 UAT_FLD_VS(wg_key_uat, key_type, "Key type", wg_key_uat_type_vals, "Public or Private"),
898 UAT_FLD_CSTRING(wg_key_uat, key, "Key", "Base64-encoded key"),
901 #endif /* WG_DECRYPTION_SUPPORTED */
903 proto_wg = proto_register_protocol("WireGuard Protocol", "WireGuard", "wg");
905 proto_register_field_array(proto_wg, hf, array_length(hf));
906 proto_register_subtree_array(ett, array_length(ett));
908 expert_wg = expert_register_protocol(proto_wg);
909 expert_register_field_array(expert_wg, ei, array_length(ei));
911 register_dissector("wg", dissect_wg, proto_wg);
913 #ifdef WG_DECRYPTION_SUPPORTED
914 wg_module = prefs_register_protocol(proto_wg, NULL);
916 uat_t *wg_keys_uat = uat_new("WireGuard static keys",
917 sizeof(wg_key_uat_record_t),
918 "wg_keys", /* filename */
919 TRUE, /* from_profile */
920 &wg_key_records, /* data_ptr */
921 &num_wg_key_records, /* numitems_ptr */
922 UAT_AFFECTS_DISSECTION, /* affects dissection of packets, but not set of named fields */
923 NULL, /* Help section (currently a wiki page) */
925 wg_key_uat_record_update_cb, /* update_cb */
927 wg_key_uat_apply, /* post_update_cb */
928 wg_key_uat_reset, /* reset_cb */
931 prefs_register_uat_preference(wg_module, "keys",
932 "WireGuard static keys",
933 "A table of long-term static keys to enable WireGuard peer identification or partial decryption",
935 #endif /* WG_DECRYPTION_SUPPORTED */
937 register_init_routine(wg_init);
938 sessions = wmem_map_new_autoreset(wmem_epan_scope(), wmem_file_scope(), g_direct_hash, g_direct_equal);
942 proto_reg_handoff_wg(void)
944 heur_dissector_add("udp", dissect_wg, "WireGuard", "wg", proto_wg, HEURISTIC_ENABLE);
948 * Editor modelines - https://www.wireshark.org/tools/modelines.html
953 * indent-tabs-mode: nil
956 * vi: set shiftwidth=4 tabstop=8 expandtab:
957 * :indentSize=4:tabSize=8:noTabs=true: