diff --git a/docbook/release-notes.adoc b/docbook/release-notes.adoc index 9fdf69da86..8943e8ecb0 100644 --- a/docbook/release-notes.adoc +++ b/docbook/release-notes.adoc @@ -137,6 +137,7 @@ Host IP Configuration Protocol (HICP) Mesh Connex (MCX) Microsoft Cluster Remote Control Protocol (RCP) Realtek +REdis Serialization Protocol v2 (RESP) Secure File Transfer Protocol (sftp) Secure Host IP Configuration Protocol (SHICP) USB Attached SCSI (UASP) diff --git a/epan/dissectors/CMakeLists.txt b/epan/dissectors/CMakeLists.txt index bcfbb2c43d..3e576e49fe 100644 --- a/epan/dissectors/CMakeLists.txt +++ b/epan/dissectors/CMakeLists.txt @@ -1687,6 +1687,7 @@ set(DISSECTOR_SRC ${CMAKE_CURRENT_SOURCE_DIR}/packet-redbackli.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-reload-framing.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-reload.c + ${CMAKE_CURRENT_SOURCE_DIR}/packet-resp.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-retix-bpdu.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-rfc2190.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-rfid-felica.c diff --git a/epan/dissectors/packet-resp.c b/epan/dissectors/packet-resp.c new file mode 100644 index 0000000000..0b80767525 --- /dev/null +++ b/epan/dissectors/packet-resp.c @@ -0,0 +1,441 @@ +/* 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)); + + + proto_register_field_array(proto_resp, hf, array_length(hf)); + proto_register_subtree_array(ett, array_length(ett)); +} + + +void proto_reg_handoff_resp(void) { + resp_handle = create_dissector_handle(dissect_resp, proto_resp); + + dissector_add_uint_with_preference("tcp.port", RESP_PORT, resp_handle); +}