/* packet-resp.c * Routines for Redis Client/Server RESP (REdis Serialization Protocol) v2 as * documented by https://redis.io/topics/protocol * * Copyright 2022 Ryan Doyle doylenet dot net> * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "config.h" #include "packet-tcp.h" #include #include #define RESP_PORT 6379 #define CRLF_LENGTH 2 #define RESP_TOKEN_PREFIX_LENGTH 1 #define MAX_ARRAY_DEPTH_TO_RECURSE 30 #define BULK_STRING_MAX_DISPLAY 100 #define RESP_NULL_STRING (-1) #define RESP_NULL_ARRAY (-1) #define RESP_REQUEST(pinfo) ((pinfo)->match_uint == (pinfo)->destport) #define RESP_RESPONSE(pinfo) ((pinfo)->match_uint == (pinfo)->srcport) #define DESEGMENT_ENABLED(pinfo) ((pinfo)->can_desegment && resp_desegment) static dissector_handle_t resp_handle; static gboolean resp_desegment = TRUE; static int proto_resp = -1; static gint ett_resp = -1; static gint ett_resp_bulk_string = -1; static gint ett_resp_array = -1; static expert_field ei_resp_partial = EI_INIT; static expert_field ei_resp_malformed_length = EI_INIT; static expert_field ei_resp_array_recursion_too_deep = EI_INIT; static expert_field ei_resp_reassembled_in_next_frame = EI_INIT; static int hf_resp_string = -1; static int hf_resp_error = -1; static int hf_resp_bulk_string = -1; static int hf_resp_bulk_string_length = -1; static int hf_resp_bulk_string_value = -1; static int hf_resp_integer = -1; static int hf_resp_array = -1; static int hf_resp_array_length = -1; static int hf_resp_fragment = -1; static int dissect_resp_loop(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, gint offset, gint array_depth, gint64 expected_elements); static void resp_bulk_string_enhance_colinfo_ascii(packet_info *pinfo, gint array_depth, gint bulk_string_length, const guint8 *bulk_string_as_str); void proto_reg_handoff_resp(void); void proto_register_resp(void); static int dissect_resp_string(tvbuff_t *tvb, packet_info *pinfo _U_, proto_tree *tree, gint offset, gint string_lenth, gint array_depth) { guint8 *string_value; string_value = tvb_get_string_enc(wmem_packet_scope(), tvb, offset + RESP_TOKEN_PREFIX_LENGTH, string_lenth - RESP_TOKEN_PREFIX_LENGTH, ENC_ASCII); proto_tree_add_string(tree, hf_resp_string, tvb, offset, string_lenth + CRLF_LENGTH, string_value); /* Simple strings can be used as a response for commands */ if (RESP_RESPONSE(pinfo) && array_depth == 0) { col_append_sep_str(pinfo->cinfo, COL_INFO, " ", string_value); } return string_lenth + CRLF_LENGTH; } static int dissect_resp_error(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, gint offset, gint string_lenth) { guint8 *error_value; error_value = tvb_get_string_enc(wmem_packet_scope(), tvb, offset + RESP_TOKEN_PREFIX_LENGTH, string_lenth - RESP_TOKEN_PREFIX_LENGTH, ENC_ASCII); proto_tree_add_string(tree, hf_resp_error, tvb, offset, string_lenth + CRLF_LENGTH, error_value); col_append_fstr(pinfo->cinfo, COL_INFO, " Error: %s", error_value); return string_lenth + CRLF_LENGTH; } static int dissect_resp_bulk_string(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, gint offset, gint bulk_string_string_length, gint array_depth) { guint8 *bulk_string_length_as_str; gint bulk_string_length; gint bulk_string_captured_length; gint bulk_string_captured_length_with_crlf; proto_item *resp_string_item; proto_tree *resp_string_tree; bulk_string_length_as_str = tvb_get_string_enc(wmem_packet_scope(), tvb, offset + RESP_TOKEN_PREFIX_LENGTH, bulk_string_string_length - RESP_TOKEN_PREFIX_LENGTH, ENC_ASCII); bulk_string_length = (gint)g_ascii_strtoll(bulk_string_length_as_str, NULL, 10); /* Negative string lengths */ if (bulk_string_length < 0) { /* NULL string */ resp_string_item = proto_tree_add_item(tree, hf_resp_bulk_string, tvb, offset,bulk_string_string_length + CRLF_LENGTH, ENC_NA); resp_string_tree = proto_item_add_subtree(resp_string_item, ett_resp_bulk_string); proto_tree_add_int(resp_string_tree, hf_resp_bulk_string_length, tvb, offset, bulk_string_string_length + CRLF_LENGTH, bulk_string_length); if (bulk_string_length == RESP_NULL_STRING) { proto_item_append_text(resp_string_item, ": [NULL]"); } else { expert_add_info(pinfo, resp_string_item, &ei_resp_malformed_length); } return bulk_string_string_length + CRLF_LENGTH; } /* We have either a bulk string or an empty string */ gint remaining_bytes_for_bulkstring = tvb_captured_length_remaining(tvb, offset + bulk_string_string_length + CRLF_LENGTH); /* Do we have enough bytes in the tvb for what was reported in the string length? */ gint is_fragmented = remaining_bytes_for_bulkstring < bulk_string_length + CRLF_LENGTH; if (is_fragmented) { if (DESEGMENT_ENABLED(pinfo)) { /* Desegment at the start of the bulk string instead of part way through */ pinfo->desegment_offset = offset; /* We know how many bytes we will need */ pinfo->desegment_len = bulk_string_length + CRLF_LENGTH - remaining_bytes_for_bulkstring; return -1; } /* There's no CRLF, we didn't get all the bytes needed */ bulk_string_captured_length = remaining_bytes_for_bulkstring; bulk_string_captured_length_with_crlf = remaining_bytes_for_bulkstring; col_append_str(pinfo->cinfo, COL_INFO, " [partial]"); } else { bulk_string_captured_length = bulk_string_length; bulk_string_captured_length_with_crlf = bulk_string_length + CRLF_LENGTH; } /* Add protocol items */ resp_string_item = proto_tree_add_item(tree, hf_resp_bulk_string, tvb, offset, bulk_string_string_length + CRLF_LENGTH + bulk_string_captured_length_with_crlf,ENC_NA); if (is_fragmented) { expert_add_info(pinfo, resp_string_item, &ei_resp_partial); } resp_string_tree = proto_item_add_subtree(resp_string_item, ett_resp_bulk_string); proto_tree_add_int(resp_string_tree, hf_resp_bulk_string_length, tvb, offset, bulk_string_string_length + CRLF_LENGTH, bulk_string_length); offset += bulk_string_string_length + CRLF_LENGTH; if (bulk_string_captured_length > 0) { proto_tree_add_item(resp_string_tree, hf_resp_bulk_string_value, tvb, offset, bulk_string_captured_length, ENC_NA); } /* Enhance display */ guint8 *bulk_string_as_str = tvb_get_string_enc(wmem_packet_scope(), tvb, offset, bulk_string_captured_length, ENC_NA); if (g_str_is_ascii(bulk_string_as_str)) { proto_item_append_text(resp_string_item, ": %s", bulk_string_as_str); resp_bulk_string_enhance_colinfo_ascii(pinfo, array_depth, bulk_string_length, bulk_string_as_str); } else if(array_depth == 0) { /* Otherwise, just append that we captured bulk strings (and only do so if they aren't part of an array */ col_append_fstr(pinfo->cinfo, COL_INFO, " BulkString(%d)", bulk_string_length); } return bulk_string_string_length + CRLF_LENGTH + bulk_string_captured_length_with_crlf; } static void resp_bulk_string_enhance_colinfo_ascii(packet_info *pinfo, gint array_depth, gint bulk_string_length, const guint8 *bulk_string_as_str) { /* Request commands are arrays */ if (RESP_REQUEST(pinfo) && array_depth == 1) { if (bulk_string_length < BULK_STRING_MAX_DISPLAY) { col_append_sep_str(pinfo->cinfo, COL_INFO, " ", bulk_string_as_str); } else { col_append_fstr(pinfo->cinfo, COL_INFO, " BulkString(%d)", bulk_string_length); } return; } /* Print responses with zero depth */ if (RESP_RESPONSE(pinfo) && array_depth == 0 && bulk_string_length < BULK_STRING_MAX_DISPLAY) { col_append_sep_str(pinfo->cinfo, COL_INFO, " ", bulk_string_as_str); return; } /* Otherwise, just display that there is a bulk string in the response (for top-level) */ if (array_depth == 0) { col_append_fstr(pinfo->cinfo, COL_INFO, " BulkString(%d)", bulk_string_length); } } static int dissect_resp_integer(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, gint offset, gint bulk_string_string_length, gint array_depth) { guint8 *integer_as_string; gint64 integer; integer_as_string = tvb_get_string_enc(wmem_packet_scope(), tvb, offset + RESP_TOKEN_PREFIX_LENGTH, bulk_string_string_length - RESP_TOKEN_PREFIX_LENGTH, ENC_ASCII); integer = g_ascii_strtoll(integer_as_string, NULL, 10); proto_tree_add_int64(tree, hf_resp_integer, tvb, offset, bulk_string_string_length + CRLF_LENGTH, integer); /* Simple integers can be used as a response for commands */ if (RESP_RESPONSE(pinfo) && array_depth == 0) { col_append_fstr(pinfo->cinfo, COL_INFO, " %" PRId64, integer); } return bulk_string_string_length + CRLF_LENGTH; } static int dissect_resp_array(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, gint offset, gint string_length, gint array_depth) { guint8 *array_length_as_string = tvb_get_string_enc(wmem_packet_scope(), tvb, offset + RESP_TOKEN_PREFIX_LENGTH, string_length - RESP_TOKEN_PREFIX_LENGTH, ENC_ASCII); gint64 array_length = g_ascii_strtoll(array_length_as_string, NULL, 10); /* We'll fix up the length later when we know how long it actually was */ proto_item *array_item = proto_tree_add_item(tree, hf_resp_array, tvb, offset, string_length + CRLF_LENGTH, ENC_NA); proto_tree *array_tree = proto_item_add_subtree(array_item, ett_resp_array); proto_tree_add_int64(array_tree, hf_resp_array_length, tvb, offset, string_length + CRLF_LENGTH, array_length); /* Null array, no elements */ if (array_length <= 0) { switch (array_length) { case RESP_NULL_ARRAY: proto_item_append_text(array_item, ": NULL"); break; case 0: proto_item_append_text(array_item, ": Empty"); break; default: expert_add_info(pinfo, array_item, &ei_resp_malformed_length); break; } return string_length + CRLF_LENGTH; } proto_item_append_text(array_item, ": Length %" PRId64, array_length); /* Bail out if we're recursing too much */ if (array_depth > MAX_ARRAY_DEPTH_TO_RECURSE) { expert_add_info(pinfo, array_item, &ei_resp_array_recursion_too_deep); return string_length + CRLF_LENGTH; } /* Non-empty array, but we've ran out of bytes in the tvb */ if (!tvb_offset_exists(tvb, offset + string_length + CRLF_LENGTH) && array_length > 0) { if (DESEGMENT_ENABLED(pinfo)) { pinfo->desegment_offset = offset; pinfo->desegment_len = DESEGMENT_ONE_MORE_SEGMENT; return -1; } expert_add_info(pinfo, array_item, &ei_resp_partial); } /* Add to the info column for responses. Don't do this for requests which are typically commands. * These are extracted in the bulk string dissector */ if (RESP_RESPONSE(pinfo) && array_depth == 0) { col_append_fstr(pinfo->cinfo, COL_INFO, " Array(%" PRId64 ")" , array_length); } int dissected_offset = dissect_resp_loop(tvb, pinfo, array_tree, offset + string_length + CRLF_LENGTH,array_depth + 1, array_length); if (dissected_offset == -1) { /* Override any desegment lengths previously set as we want to start from the beginning of the array */ pinfo->desegment_offset = offset; pinfo->desegment_len = DESEGMENT_ONE_MORE_SEGMENT; /* We've partially decoded some of the array, but we've asked for all. It will still show in the proto tree so give an * indication as to why it's only partially there*/ expert_add_info(pinfo, array_item, &ei_resp_reassembled_in_next_frame); return -1; } proto_item_set_len(array_item, dissected_offset - offset); return dissected_offset - offset; } static int dissect_resp_message(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, gint offset, gint string_length, gint array_depth) { switch (tvb_get_guint8(tvb, offset)) { case '+': return dissect_resp_string(tvb, pinfo, tree, offset, string_length, array_depth); case '-': return dissect_resp_error(tvb, pinfo, tree, offset, string_length); case ':': return dissect_resp_integer(tvb, pinfo, tree, offset, string_length, array_depth); case '$': return dissect_resp_bulk_string(tvb, pinfo, tree, offset, string_length, array_depth); case '*': return dissect_resp_array(tvb, pinfo, tree, offset, string_length, array_depth); default: /* We have an erroneous \r\n if the string length is 0. */ if (string_length == 0) { return CRLF_LENGTH; } /* Otherwise, its: * - A command we don't support yet (RESPv3 commands that aren't implemented yet) * - Reassembly is disabled and data is between packet boundaries * - It's the first frame in a partial capture * */ col_append_str(pinfo->cinfo, COL_INFO, " [fragment]"); proto_tree_add_item(tree, hf_resp_fragment, tvb, offset, string_length + CRLF_LENGTH,ENC_NA); return string_length + CRLF_LENGTH; } } static int dissect_resp_loop(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, gint offset, gint array_depth, gint64 expected_elements) { gint error_or_offset; gint crlf_string_line_length; gint done_elements = 0; while (tvb_offset_exists(tvb, offset)) { /* If we ended up in here from a recursive call when traversing an array, don't drain the tvb to empty. * Only do the amount of elements that are expected in the array */ if (expected_elements >= 0 && done_elements == expected_elements) { return offset; } crlf_string_line_length = tvb_find_line_end(tvb, offset, -1, NULL, DESEGMENT_ENABLED(pinfo)); /* If desegment is disabled, tvb_find_line_end() will return a positive length, regardless if it finds a CRLF */ if (crlf_string_line_length == -1) { pinfo->desegment_offset = offset; pinfo->desegment_len = DESEGMENT_ONE_MORE_SEGMENT; return -1; } error_or_offset = dissect_resp_message(tvb, pinfo, tree, offset, crlf_string_line_length, array_depth); if (error_or_offset == -1) { return -1; } done_elements++; offset += error_or_offset; } return offset; } static int dissect_resp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_) { gint offset = 0; proto_item *root_resp_item; proto_tree *resp_tree; col_set_str(pinfo->cinfo, COL_PROTOCOL, "RESP"); col_clear(pinfo->cinfo, COL_INFO); col_append_str(pinfo->cinfo, COL_INFO, RESP_RESPONSE(pinfo) ? "Response:" : "Request:"); root_resp_item = proto_tree_add_item(tree, proto_resp, tvb, 0, -1, ENC_NA); resp_tree = proto_item_add_subtree(root_resp_item, ett_resp); int dissected_length = dissect_resp_loop(tvb, pinfo, resp_tree, offset, 0, -1); if (dissected_length == -1) { col_append_str(pinfo->cinfo, COL_INFO, " [continuation]"); return tvb_captured_length(tvb); } return dissected_length; } void proto_register_resp(void) { static hf_register_info hf[] = { { &hf_resp_string, { "String", "resp.string", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL } }, { &hf_resp_error, { "Error", "resp.error", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL } }, { &hf_resp_bulk_string, { "Bulk String", "resp.bulk_string", FT_NONE, BASE_NONE, NULL, 0x0, NULL, HFILL } }, { &hf_resp_bulk_string_value, { "Value", "resp.bulk_string.value", FT_BYTES, BASE_NONE, NULL, 0x0, NULL, HFILL } }, { &hf_resp_bulk_string_length, { "Length", "resp.bulk_string.length", FT_INT32, BASE_DEC, NULL, 0x0, NULL, HFILL } }, { &hf_resp_integer, { "Integer", "resp.integer", FT_INT64, BASE_DEC, NULL, 0x0, NULL, HFILL } }, { &hf_resp_array, { "Array", "resp.array", FT_NONE, BASE_NONE, NULL, 0x0, NULL, HFILL } }, { &hf_resp_array_length, { "Length", "resp.array.length", FT_INT64, BASE_DEC, NULL, 0x0, NULL, HFILL } }, { &hf_resp_fragment, { "Fragment", "resp.fragment", FT_NONE, BASE_NONE, NULL, 0x0, NULL, HFILL } }, }; static gint *ett[] = { &ett_resp, &ett_resp_bulk_string, &ett_resp_array, }; static ei_register_info ei[] = { { &ei_resp_partial, { "resp.partial", PI_UNDECODED, PI_NOTE, "Field is only partially decoded", EXPFILL }}, { &ei_resp_malformed_length, { "resp.malformed_length", PI_UNDECODED, PI_ERROR, "Malformed length specified", EXPFILL }}, { &ei_resp_reassembled_in_next_frame, {"resp.reassembled_in_next_frame", PI_UNDECODED, PI_NOTE, "Array is partially decoded. Re-assembled array is in the next frame", EXPFILL }}, { &ei_resp_array_recursion_too_deep, { "resp.array_recursion_too_deep", PI_UNDECODED, PI_NOTE, "Array is too deep to recurse any further. Subsequent elements attached to the protocol " "tree may not reflect their actual location in the array", EXPFILL }}, }; proto_resp = proto_register_protocol("REdis Serialization Protocol", "RESP", "resp"); module_t *resp_module = prefs_register_protocol(proto_resp, NULL); prefs_register_bool_preference(resp_module, "desegment_data", "Reassemble RESP data spanning multiple TCP segments", "Whether the RESP dissector should reassemble command and response lines" " spanning multiple TCP segments. To use this option, you must also enable " "\"Allow subdissectors to reassemble TCP streams\" in the TCP protocol settings.", &resp_desegment); expert_module_t *expert_pcp = expert_register_protocol(proto_resp); expert_register_field_array(expert_pcp, ei, array_length(ei)); resp_handle = register_dissector("resp", dissect_resp, proto_resp); proto_register_field_array(proto_resp, hf, array_length(hf)); proto_register_subtree_array(ett, array_length(ett)); } void proto_reg_handoff_resp(void) { dissector_add_uint_with_preference("tcp.port", RESP_PORT, resp_handle); }