aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--epan/dissectors/packet-wireguard.c279
1 files changed, 271 insertions, 8 deletions
diff --git a/epan/dissectors/packet-wireguard.c b/epan/dissectors/packet-wireguard.c
index 0fcd37b053..143fd257bc 100644
--- a/epan/dissectors/packet-wireguard.c
+++ b/epan/dissectors/packet-wireguard.c
@@ -18,6 +18,7 @@
#include <epan/packet.h>
#include <epan/expert.h>
#include <epan/prefs.h>
+#include <epan/proto_data.h>
void proto_reg_handoff_wg(void);
void proto_register_wg(void);
@@ -37,6 +38,9 @@ static int hf_wg_nonce = -1;
static int hf_wg_encrypted_cookie = -1;
static int hf_wg_counter = -1;
static int hf_wg_encrypted_packet = -1;
+static int hf_wg_stream = -1;
+static int hf_wg_response_in = -1;
+static int hf_wg_response_to = -1;
static gint ett_wg = -1;
@@ -62,6 +66,151 @@ static const value_string wg_type_names[] = {
{ 0x00, NULL }
};
+/*
+ * Information required to process and link messages as required on the first
+ * sequential pass. After that it can be erased.
+ */
+typedef struct {
+ address initiator_address;
+ address responder_address;
+ guint16 initiator_port;
+ guint16 responder_port;
+} wg_initial_info_t;
+
+/*
+ * A "session" between two peer is identified by a "sender" id as independently
+ * chosen by each side. In case both peer IDs collide, the source IP and UDP
+ * port number could be used to distinguish sessions. As IDs can be recycled
+ * over time, lookups should use the most recent initiation (or response).
+ *
+ * XXX record timestamps (time since last message, for validating timers).
+ */
+typedef struct {
+ guint32 stream; /* Session identifier (akin to udp.stream). */
+ guint32 initiator_frame;
+ guint32 response_frame; /* Responder or Cookie Reply message. */
+ wg_initial_info_t initial; /* Valid only on the first pass. */
+} wg_session_t;
+
+/* Per-packet state. */
+typedef struct {
+ wg_session_t *session;
+} wg_packet_info_t;
+
+/* Map from Sender/Receiver IDs to a list of session information. */
+static wmem_map_t *sessions;
+static guint32 wg_session_count;
+
+
+static void
+wg_sessions_insert(guint32 id, wg_session_t *session)
+{
+ wmem_list_t *list = (wmem_list_t *)wmem_map_lookup(sessions, GUINT_TO_POINTER(id));
+ if (!list) {
+ list = wmem_list_new(wmem_file_scope());
+ wmem_map_insert(sessions, GUINT_TO_POINTER(id), list);
+ }
+ wmem_list_append(list, session);
+}
+
+static wg_session_t *
+wg_session_new(void)
+{
+ wg_session_t *session = wmem_new0(wmem_file_scope(), wg_session_t);
+ session->stream = wg_session_count++;
+ return session;
+}
+
+/* Updates the peer address based on the source address. */
+static void
+wg_session_update_address(wg_session_t *session, packet_info *pinfo, gboolean sender_is_initiator)
+{
+ DISSECTOR_ASSERT(!PINFO_FD_VISITED(pinfo));
+
+ if (sender_is_initiator) {
+ copy_address_wmem(wmem_file_scope(), &session->initial.initiator_address, &pinfo->src);
+ session->initial.initiator_port = (guint16)pinfo->srcport;
+ } else {
+ copy_address_wmem(wmem_file_scope(), &session->initial.responder_address, &pinfo->src);
+ session->initial.responder_port = (guint16)pinfo->srcport;
+ }
+}
+
+/* Finds an initiation message based on the given Receiver ID that was not
+ * previously associated with a responder message. Returns the session if a
+ * matching initation message can be found or NULL otherwise.
+ */
+static wg_session_t *
+wg_sessions_lookup_initiation(packet_info *pinfo, guint32 receiver_id)
+{
+ DISSECTOR_ASSERT(!PINFO_FD_VISITED(pinfo));
+
+ /* Look for the initiation message matching this Receiver ID. */
+ wmem_list_t *list = (wmem_list_t *)wmem_map_lookup(sessions, GUINT_TO_POINTER(receiver_id));
+ if (!list) {
+ return NULL;
+ }
+
+ /* Walk backwards to find the most recent message first. All packets are
+ * guaranteed to arrive before this frame because this is the first pass. */
+ for (wmem_list_frame_t *item = wmem_list_tail(list); item; item = wmem_list_frame_prev(item)) {
+ wg_session_t *session = (wg_session_t *)wmem_list_frame_data(item);
+ if (session->initial.initiator_port != pinfo->destport ||
+ !addresses_equal(&session->initial.initiator_address, &pinfo->dst)) {
+ /* Responder messages are expected to be sent to the initiator. */
+ continue;
+ }
+ if (session->response_frame && session->response_frame != pinfo->num) {
+ /* This session was linked elsewhere. */
+ continue;
+ }
+
+ /* This assumes no malicious messages and no contrived sequences:
+ * Any initiator or responder message is not duplicated nor are these
+ * mutated. If this must be detected, the caller could decrypt or check
+ * mac1 to distinguish valid messages.
+ */
+ return session;
+ }
+
+ return NULL;
+}
+
+/* Finds a session with a completed handshake that matches the Receiver ID. */
+static wg_session_t *
+wg_sessions_lookup(packet_info *pinfo, guint32 receiver_id, gboolean *receiver_is_initiator)
+{
+ DISSECTOR_ASSERT(!PINFO_FD_VISITED(pinfo));
+
+ wmem_list_t *list = (wmem_list_t *)wmem_map_lookup(sessions, GUINT_TO_POINTER(receiver_id));
+ if (!list) {
+ return NULL;
+ }
+
+ /* Walk backwards to find the most recent message first. */
+ for (wmem_list_frame_t *item = wmem_list_tail(list); item; item = wmem_list_frame_prev(item)) {
+ wg_session_t *session = (wg_session_t *)wmem_list_frame_data(item);
+ if (!session->response_frame) {
+ /* Ignore sessions that are not fully established. */
+ continue;
+ }
+ if (session->initial.initiator_port == pinfo->destport &&
+ addresses_equal(&session->initial.initiator_address, &pinfo->dst)) {
+ *receiver_is_initiator = TRUE;
+ } else if (session->initial.responder_port == pinfo->destport &&
+ addresses_equal(&session->initial.responder_address, &pinfo->dst)) {
+ *receiver_is_initiator = FALSE;
+ } else {
+ /* Both peers do not match the destination, ignore. */
+ continue;
+ }
+ return session;
+ }
+
+ return NULL;
+}
+
+
static void
wg_dissect_pubkey(proto_tree *tree, tvbuff_t *tvb, int offset, gboolean is_ephemeral)
{
@@ -75,9 +224,10 @@ wg_dissect_pubkey(proto_tree *tree, tvbuff_t *tvb, int offset, gboolean is_ephem
}
static int
-wg_dissect_handshake_initiation(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree)
+wg_dissect_handshake_initiation(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
{
guint32 sender_id;
+ proto_item *ti;
proto_tree_add_item_ret_uint(wg_tree, hf_wg_sender, tvb, 4, 4, ENC_LITTLE_ENDIAN, &sender_id);
col_append_fstr(pinfo->cinfo, COL_INFO, ", sender=0x%08X", sender_id);
@@ -87,13 +237,33 @@ wg_dissect_handshake_initiation(tvbuff_t *tvb, packet_info *pinfo, proto_tree *w
proto_tree_add_item(wg_tree, hf_wg_mac1, tvb, 116, 16, ENC_NA);
proto_tree_add_item(wg_tree, hf_wg_mac2, tvb, 132, 16, ENC_NA);
+ if (!PINFO_FD_VISITED(pinfo)) {
+ /* XXX should an initiation message with the same contents (except MAC2) be
+ * considered part of the same "session"? */
+ wg_session_t *session = wg_session_new();
+ session->initiator_frame = pinfo->num;
+ wg_session_update_address(session, pinfo, TRUE);
+ wg_sessions_insert(sender_id, session);
+ wg_pinfo->session = session;
+ }
+ wg_session_t *session = wg_pinfo->session;
+ if (session) {
+ ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
+ PROTO_ITEM_SET_GENERATED(ti);
+ }
+ if (session && session->response_frame) {
+ ti = proto_tree_add_uint(wg_tree, hf_wg_response_in, tvb, 0, 0, session->response_frame);
+ PROTO_ITEM_SET_GENERATED(ti);
+ }
+
return 148;
}
static int
-wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree)
+wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
{
guint32 sender_id, receiver_id;
+ proto_item *ti;
proto_tree_add_item_ret_uint(wg_tree, hf_wg_sender, tvb, 4, 4, ENC_LITTLE_ENDIAN, &sender_id);
col_append_fstr(pinfo->cinfo, COL_INFO, ", sender=0x%08X", sender_id);
@@ -104,24 +274,67 @@ wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_
proto_tree_add_item(wg_tree, hf_wg_mac1, tvb, 60, 16, ENC_NA);
proto_tree_add_item(wg_tree, hf_wg_mac2, tvb, 76, 16, ENC_NA);
+ wg_session_t *session;
+ if (!PINFO_FD_VISITED(pinfo)) {
+ session = wg_sessions_lookup_initiation(pinfo, receiver_id);
+ /* XXX should probably check whether decryption succeeds before linking
+ * and somehow mark that this response is related but not correct. */
+ if (session) {
+ session->response_frame = pinfo->num;
+ wg_session_update_address(session, pinfo, FALSE);
+ wg_sessions_insert(sender_id, session);
+ wg_pinfo->session = session;
+ }
+ } else {
+ session = wg_pinfo->session;
+ }
+ if (session) {
+ ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
+ PROTO_ITEM_SET_GENERATED(ti);
+ ti = proto_tree_add_uint(wg_tree, hf_wg_response_to, tvb, 0, 0, session->initiator_frame);
+ PROTO_ITEM_SET_GENERATED(ti);
+ }
+
return 92;
}
static int
-wg_dissect_handshake_cookie(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree)
+wg_dissect_handshake_cookie(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
{
guint32 receiver_id;
+ proto_item *ti;
proto_tree_add_item_ret_uint(wg_tree, hf_wg_receiver, tvb, 4, 4, ENC_LITTLE_ENDIAN, &receiver_id);
col_append_fstr(pinfo->cinfo, COL_INFO, ", receiver=0x%08X", receiver_id);
proto_tree_add_item(wg_tree, hf_wg_nonce, tvb, 8, 24, ENC_NA);
proto_tree_add_item(wg_tree, hf_wg_encrypted_cookie, tvb, 32, 16 + AUTH_TAG_LENGTH, ENC_NA);
+ wg_session_t *session;
+ if (!PINFO_FD_VISITED(pinfo)) {
+ /* Check for Cookie Reply from Responder to Initiator. */
+ session = wg_sessions_lookup_initiation(pinfo, receiver_id);
+ if (session) {
+ session->response_frame = pinfo->num;
+ wg_session_update_address(session, pinfo, FALSE);
+ wg_pinfo->session = session;
+ }
+ /* XXX check for cookie reply from Initiator to Responder */
+ } else {
+ session = wg_pinfo->session;
+ }
+ if (session) {
+ ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
+ PROTO_ITEM_SET_GENERATED(ti);
+ /* XXX check for cookie reply from Initiator to Responder */
+ ti = proto_tree_add_uint(wg_tree, hf_wg_response_to, tvb, 0, 0, session->initiator_frame);
+ PROTO_ITEM_SET_GENERATED(ti);
+ }
+
return 64;
}
static int
-wg_dissect_data(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree)
+wg_dissect_data(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo)
{
guint32 receiver_id;
guint64 counter;
@@ -146,6 +359,22 @@ wg_dissect_data(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree)
expert_add_info(pinfo, ti, &ei_wg_keepalive);
}
+ wg_session_t *session;
+ if (!PINFO_FD_VISITED(pinfo)) {
+ gboolean receiver_is_initiator;
+ session = wg_sessions_lookup(pinfo, receiver_id, &receiver_is_initiator);
+ if (session) {
+ wg_session_update_address(session, pinfo, !receiver_is_initiator);
+ wg_pinfo->session = session;
+ }
+ } else {
+ session = wg_pinfo->session;
+ }
+ if (session) {
+ ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
+ PROTO_ITEM_SET_GENERATED(ti);
+ }
+
return 16 + packet_length;
}
@@ -156,6 +385,7 @@ dissect_wg(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
proto_tree *wg_tree;
guint32 message_type;
const char *message_type_str;
+ wg_packet_info_t *wg_pinfo;
/* Heuristics check: check for reserved bits (zeros) and message type. */
if (tvb_reported_length(tvb) < 4 || tvb_get_ntoh24(tvb, 1) != 0)
@@ -180,20 +410,33 @@ dissect_wg(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
proto_tree_add_item(wg_tree, hf_wg_type, tvb, 0, 1, ENC_NA);
proto_tree_add_item(wg_tree, hf_wg_reserved, tvb, 1, 3, ENC_NA);
+ if (!PINFO_FD_VISITED(pinfo)) {
+ wg_pinfo = wmem_new0(wmem_file_scope(), wg_packet_info_t);
+ p_add_proto_data(wmem_file_scope(), pinfo, proto_wg, 0, wg_pinfo);
+ } else {
+ wg_pinfo = (wg_packet_info_t *)p_get_proto_data(wmem_file_scope(), pinfo, proto_wg, 0);
+ }
+
switch ((wg_message_type)message_type) {
case WG_TYPE_HANDSHAKE_INITIATION:
- return wg_dissect_handshake_initiation(tvb, pinfo, wg_tree);
+ return wg_dissect_handshake_initiation(tvb, pinfo, wg_tree, wg_pinfo);
case WG_TYPE_HANDSHAKE_RESPONSE:
- return wg_dissect_handshake_response(tvb, pinfo, wg_tree);
+ return wg_dissect_handshake_response(tvb, pinfo, wg_tree, wg_pinfo);
case WG_TYPE_COOKIE_REPLY:
- return wg_dissect_handshake_cookie(tvb, pinfo, wg_tree);
+ return wg_dissect_handshake_cookie(tvb, pinfo, wg_tree, wg_pinfo);
case WG_TYPE_TRANSPORT_DATA:
- return wg_dissect_data(tvb, pinfo, wg_tree);
+ return wg_dissect_data(tvb, pinfo, wg_tree, wg_pinfo);
}
DISSECTOR_ASSERT_NOT_REACHED();
}
+static void
+wg_init(void)
+{
+ wg_session_count = 0;
+}
+
void
proto_register_wg(void)
{
@@ -278,6 +521,23 @@ proto_register_wg(void)
FT_NONE, BASE_NONE, NULL, 0x0,
NULL, HFILL }
},
+
+ /* Association tracking. */
+ { &hf_wg_stream,
+ { "Stream index", "wg.stream",
+ FT_UINT32, BASE_DEC, NULL, 0x0,
+ "Identifies a session in this capture file", HFILL }
+ },
+ { &hf_wg_response_in,
+ { "Response in Frame", "wg.response_in",
+ FT_FRAMENUM, BASE_NONE, FRAMENUM_TYPE(FT_FRAMENUM_RESPONSE), 0x0,
+ "The response to this initiation message is in this frame", HFILL }
+ },
+ { &hf_wg_response_to,
+ { "Response to Frame", "wg.response_to",
+ FT_FRAMENUM, BASE_NONE, FRAMENUM_TYPE(FT_FRAMENUM_REQUEST), 0x0,
+ "This is a response to the initiation message in this frame", HFILL }
+ },
};
static gint *ett[] = {
@@ -304,6 +564,9 @@ proto_register_wg(void)
expert_register_field_array(expert_wg, ei, array_length(ei));
register_dissector("wg", dissect_wg, proto_wg);
+
+ register_init_routine(wg_init);
+ sessions = wmem_map_new_autoreset(wmem_epan_scope(), wmem_file_scope(), g_direct_hash, g_direct_equal);
}
void