/* packet-sane.c * Routines for SANE dissection * Copyright 2024, James Ring * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ /** * A dissector for the SANE protocol (https://sane-project.gitlab.io/standard/net.html). * * This dissector works only for the control protocol (typically between a * client on some ephemeral port and a server on port 6566). The data transfer * of scanned images is done on a separate connection. Future versions of this * dissector might provide dissected image information. * * SANE is a protocol layered on top of TCP. The main dissect_sane function * relies on tcp_dissect_pdus to reassemble large messages. The protocol has no * meta-information (e.g. length of packet, type of message), the only way you * can know the format of a response is to see the corresponding request. The * only way you can know the length of a PDU is to read and understand various * fields in the request/response. * * For these reasons, the get_sane_pdu_len function has to pretty much do all * the work of the dissector itself. There is probably a more elegant way to do * this without the duplication that exists between get_sane_pdu_len and * dissect_sane_pdu. */ /* TODO * - Setup proper request / response tracking, with * - references to related packets, * - response time indications. */ #include "config.h" #include #include #include #include #include "packet-tcp.h" #define SANE_WORD_LENGTH 4 #define SANE_MODULE_NAME "sane" #define SANE_PORT "6566" static range_t *sane_server_ports; static dissector_handle_t sane_handle; static int hf_sane_opcode; static int hf_sane_version; static int hf_sane_version_major; static int hf_sane_version_minor; static int hf_sane_version_build; static int hf_sane_username; static int hf_sane_password; static int hf_sane_string; static int hf_sane_string_length; static int hf_sane_array_length; static int hf_sane_device_descriptor; static int hf_sane_device_name; static int hf_sane_device_vendor; static int hf_sane_device_model; static int hf_sane_device_type; static int hf_sane_resource_name; static int hf_sane_device_handle; static int hf_sane_option_descriptor; static int hf_sane_option_index; static int hf_sane_option_control_action; static int hf_sane_option_value_type; static int hf_sane_option_length; static int hf_sane_option_count; static int hf_sane_option_name; static int hf_sane_option_value; static int hf_sane_option_string_value; static int hf_sane_option_numeric_value; static int hf_sane_option_boolean_value; static int hf_sane_option_title; static int hf_sane_option_description; static int hf_sane_option_unit; static int hf_sane_option_size; static int hf_sane_option_capabilities; static int hf_sane_option_constraints; static int hf_sane_option_constraint_type; static int hf_sane_option_possible_string_value; static int hf_sane_option_possible_word_value; static int hf_sane_option_range_min; static int hf_sane_option_range_max; static int hf_sane_option_range_quant; static int hf_sane_status; static int hf_sane_data_port; static int hf_sane_byte_order; static int hf_sane_pointer_value; static int hf_sane_frame_format; static int hf_sane_scan_line_count; static int hf_sane_scan_pixel_depth; static int hf_sane_scan_pixels_per_line; static int hf_sane_scan_bytes_per_line; static int hf_sane_scan_is_last_frame; static int hf_sane_dummy_value; #define SANE_CAP_NONE 0x00000000 #define SANE_CAP_SOFT_SELECT 0x00000001 #define SANE_CAP_HARD_SELECT 0x00000002 #define SANE_CAP_SOFT_DETECT 0x00000004 #define SANE_CAP_EMULATED 0x00000008 #define SANE_CAP_AUTOMATIC 0x00000010 #define SANE_CAP_INACTIVE 0x00000020 #define SANE_CAP_ADVANCED 0x00000040 static int hf_sane_option_capability_soft_select; static int hf_sane_option_capability_hard_select; static int hf_sane_option_capability_soft_detect; static int hf_sane_option_capability_emulated; static int hf_sane_option_capability_automatic; static int hf_sane_option_capability_inactive; static int hf_sane_option_capability_advanced; #define SANE_INFO_INEXACT 0x00000001 #define SANE_INFO_RELOAD_OPTIONS 0x00000002 #define SANE_INFO_RELOAD_PARAMS 0x00000004 static int hf_sane_control_option_info; static int hf_sane_control_option_inexact; static int hf_sane_control_option_reload_options; static int hf_sane_control_option_reload_params; static int* const sane_cap_bits[] = { &hf_sane_option_capability_soft_select, &hf_sane_option_capability_hard_select, &hf_sane_option_capability_soft_detect, &hf_sane_option_capability_emulated, &hf_sane_option_capability_automatic, &hf_sane_option_capability_inactive, &hf_sane_option_capability_advanced, NULL, }; static int* const sane_control_option_info_bits[] = { &hf_sane_control_option_inexact, &hf_sane_control_option_reload_options, &hf_sane_control_option_reload_params, NULL, }; static int proto_sane; static int ett_sane; static int ett_sane_version; static int ett_sane_string; static int ett_sane_option; static int ett_sane_option_value; static int ett_sane_option_capabilities; static int ett_sane_option_constraints; static int ett_sane_control_option_info; static int ett_sane_device_descriptor; typedef enum { SANE_NET_UNKNOWN = -1, SANE_NET_INIT = 0, SANE_NET_GET_DEVICES = 1, SANE_NET_OPEN = 2, SANE_NET_CLOSE = 3, SANE_NET_GET_OPTION_DESCRIPTORS = 4, SANE_NET_CONTROL_OPTION = 5, SANE_NET_GET_PARAMETERS = 6, SANE_NET_START = 7, SANE_NET_CANCEL = 8, SANE_NET_AUTHORIZE = 9, SANE_NET_EXIT = 10, } sane_rpc_code; static const value_string opcode_vals[] = { {SANE_NET_INIT, "SANE_NET_INIT"}, {SANE_NET_GET_DEVICES, "SANE_NET_GET_DEVICES"}, {SANE_NET_OPEN, "SANE_NET_OPEN"}, {SANE_NET_CLOSE, "SANE_NET_CLOSE"}, {SANE_NET_GET_OPTION_DESCRIPTORS, "SANE_NET_GET_OPTION_DESCRIPTORS"}, {SANE_NET_CONTROL_OPTION, "SANE_NET_CONTROL_OPTION"}, {SANE_NET_GET_PARAMETERS, "SANE_NET_GET_PARAMETERS"}, {SANE_NET_START, "SANE_NET_START"}, {SANE_NET_CANCEL, "SANE_NET_CANCEL"}, {SANE_NET_AUTHORIZE, "SANE_NET_AUTHORIZE"}, {SANE_NET_EXIT, "SANE_NET_EXIT"}, {0, NULL}, }; typedef enum { SANE_NO_CONSTRAINT = 0, SANE_CONSTRAINT_RANGE = 1, SANE_CONSTRAINT_WORD_LIST = 2, SANE_CONSTRAINT_STRING_LIST = 3, } sane_constraint_type; static const value_string sane_constraint_type_names[] = { {SANE_NO_CONSTRAINT, "SANE_NO_CONSTRAINT"}, {SANE_CONSTRAINT_RANGE, "SANE_CONSTRAINT_RANGE"}, {SANE_CONSTRAINT_WORD_LIST, "SANE_CONSTRAINT_WORD_LIST"}, {SANE_CONSTRAINT_STRING_LIST, "SANE_CONSTRAINT_STRING_LIST"}, {0, NULL}, }; typedef enum { SANE_TYPE_BOOL = 0, SANE_TYPE_INT = 1, SANE_TYPE_FIXED = 2, SANE_TYPE_STRING = 3, SANE_TYPE_BUTTON = 4, SANE_TYPE_GROUP = 5, } sane_value_type; static const value_string sane_value_types[] = { {SANE_TYPE_BOOL, "SANE_TYPE_BOOL"}, {SANE_TYPE_INT, "SANE_TYPE_INT"}, {SANE_TYPE_FIXED, "SANE_TYPE_FIXED"}, {SANE_TYPE_STRING, "SANE_TYPE_STRING"}, {SANE_TYPE_BUTTON, "SANE_TYPE_BUTTON"}, {SANE_TYPE_GROUP, "SANE_TYPE_GROUP"}, {0, NULL}, }; static const value_string control_types[] = { {0, "SANE_ACTION_GET_VALUE"}, {1, "SANE_ACTION_SET_VALUE"}, {2, "SANE_ACTION_SET_AUTO"}, {0, NULL}, }; typedef enum { SANE_UNIT_NONE = 0, SANE_UNIT_PIXEL = 1, SANE_UNIT_BIT = 2, SANE_UNIT_MM = 3, SANE_UNIT_DPI = 4, SANE_UNIT_PERCENT = 5, SANE_UNIT_MICROSECOND = 6, } sane_option_unit; static const value_string sane_option_units[] = { {SANE_UNIT_NONE, "SANE_UNIT_NONE"}, {SANE_UNIT_PIXEL, "SANE_UNIT_PIXEL"}, {SANE_UNIT_BIT, "SANE_UNIT_BIT"}, {SANE_UNIT_MM, "SANE_UNIT_MM"}, {SANE_UNIT_DPI, "SANE_UNIT_DPI"}, {SANE_UNIT_PERCENT, "SANE_UNIT_PERCENT"}, {SANE_UNIT_MICROSECOND, "SANE_UNIT_MICROSECOND"}, {0, NULL}, }; static const value_string sane_option_unit_suffixes[] = { {1, "px"}, {2, "bits"}, {3, "mm"}, {4, "dpi"}, {5, "%"}, {6, "ms"}, {0, NULL}, }; typedef enum { SANE_STATUS_UNKNOWN = -1, SANE_STATUS_OK = 0, } sane_status; static const value_string status_values[] = { {0, "SANE_STATUS_GOOD"}, {1, "SANE_STATUS_UNSUPPORTED"}, {2, "SANE_STATUS_CANCELLED"}, {3, "SANE_STATUS_DEVICE_BUSY"}, {4, "SANE_STATUS_INVAL"}, {5, "SANE_STATUS_EOF"}, {6, "SANE_STATUS_JAMMED"}, {7, "SANE_STATUS_NO_DOCS"}, {8, "SANE_STATUS_COVER_OPEN"}, {9, "SANE_STATUS_IO_ERROR"}, {10, "SANE_STATUS_NO_MEM"}, {11, "SANE_STATUS_ACCESS_DENIED"}, {0, NULL}, }; static const value_string sane_frame_format_names[] = { {0, "SANE_FRAME_GRAY"}, {1, "SANE_FRAME_RGB"}, {2, "SANE_FRAME_RED"}, {3, "SANE_FRAME_GREEN"}, {4, "SANE_FRAME_BLUE"}, {0, NULL}, }; typedef struct { bool is_request; sane_rpc_code opcode; uint32_t packet_num; } sane_pdu; /* Keep track of current request status during first pass. N.B. opcode is stored in per-frame data and read during subsequent passes. Could if necessary be expanded to include frame numbers and timestamps for more complete request/response tracking. */ typedef struct { bool seen_request; sane_pdu last_request; bool auth; } sane_session; typedef struct { tvbuff_t *tvb; int offset; int bytes_read; } tvb_sane_reader; static int tvb_read_sane_word(tvb_sane_reader *r, uint32_t *dest) { if (tvb_captured_length_remaining(r->tvb, r->offset) < SANE_WORD_LENGTH) { return 0; } if (dest) { *dest = tvb_get_ntohl(r->tvb, r->offset); } r->offset += SANE_WORD_LENGTH; r->bytes_read += SANE_WORD_LENGTH; return SANE_WORD_LENGTH; } #define WORD_OR_RETURN(r, var) \ do { if (tvb_read_sane_word((r), (var)) == 0) { return 0; } } while(0) static int tvb_read_sane_string(tvb_sane_reader *r, wmem_allocator_t *alloc, char **dest) { int str_len; WORD_OR_RETURN(r, &str_len); if (tvb_captured_length_remaining(r->tvb, r->offset) < str_len) { return 0; } if (dest) { *dest = tvb_get_string_enc(alloc, r->tvb, r->offset, str_len, ENC_ASCII | ENC_NA); } r->offset += str_len; r->bytes_read += str_len; return SANE_WORD_LENGTH + str_len; } #define STRING_OR_RETURN(r) \ do { if (tvb_read_sane_string((r), NULL, NULL) == 0) { return 0; } } while(0) static int tvb_skip_bytes(tvb_sane_reader *r, int len) { if (tvb_captured_length_remaining(r->tvb, r->offset) < len) { return 0; } r->offset += len; r->bytes_read += len; return len; } /** * Returns the expected response type for the (presumed) response in `pinfo`. * This usually returns the opcode of the last request seen in the conversation, * except for special handling of the authorization flow, for example: * * Client: SANE_NET_OPEN request (1) * Server: SANE_NET_OPEN response, authentication resource set (2) * Client: SANE_NET_AUTHORIZE request, username+password sent (3) * Server: SANE_NET_AUTHORIZE response sent, success (4) * Server: SANE_NET_OPEN response immediately sent (5) * * In this case, if the expected response type of PDU 5 is SANE_NET_OPEN, * because the server is responding to the request sent in PDU 2. */ static sane_rpc_code get_sane_expected_response_type(sane_session *sess, packet_info *pinfo) { /* Look up any previous result. N.B. as called for length *and* dissecting, there may already be a value stored on first pass! */ if (PINFO_FD_VISITED(pinfo) || p_get_proto_data(wmem_file_scope(), pinfo, proto_sane, 0)) { return (sane_rpc_code)GPOINTER_TO_UINT(p_get_proto_data(wmem_file_scope(), pinfo, proto_sane, 0)); } /* First pass. Will be response to last_request if set, or AUTH request if flag set. */ sane_rpc_code code = SANE_NET_UNKNOWN; if (sess->seen_request) { if (sess->auth) { code = SANE_NET_AUTHORIZE; sess->auth = false; } else { code = sess->last_request.opcode; } } /* Remember this code for later queries. */ p_add_proto_data(wmem_file_scope(), pinfo, proto_sane, 0, GUINT_TO_POINTER(code)); return code; } static proto_item * dissect_sane_word(tvb_sane_reader *r, proto_tree *tree, int hfindex, int *word) { proto_item *item = proto_tree_add_item(tree, hfindex, r->tvb, r->offset, SANE_WORD_LENGTH, ENC_BIG_ENDIAN); // safe to ignore the return value here, we're guaranteed to have enough bytes to // read a word. tvb_read_sane_word(r, word); return item; } /** * Dissects and returns a SANE-encoded string from `r`. * * Also creates a proto_item representing the string. The `format` string should * contain a string format specifier (i.e. "%s"), which will be replaced with the * consumed string in the proto_item's text. */ static char * dissect_sane_string(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree, int hfindex, const char *format) { int offset = r->offset; char *str = ""; int len = tvb_read_sane_string(r, pinfo->pool, &str); proto_item *str_item = proto_tree_add_item(tree, hf_sane_string, r->tvb, offset, len, ENC_NA); proto_tree *str_tree = proto_item_add_subtree(str_item, ett_sane_string); proto_item_set_text(str_item, format, str); proto_tree_add_item(str_tree, hf_sane_string_length, r->tvb, offset, SANE_WORD_LENGTH, ENC_BIG_ENDIAN); proto_tree_add_item(str_tree, hfindex, r->tvb, offset + SANE_WORD_LENGTH, len - SANE_WORD_LENGTH, ENC_NA); return str; } static void dissect_sane_net_init_request(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { int version = 0; int offset = r->offset; proto_item *version_item = dissect_sane_word(r, tree, hf_sane_version, &version); proto_item *version_tree = proto_item_add_subtree(version_item, ett_sane_version); proto_item_append_text(version_item, " (major: %d, minor: %d, build: %d)", version >> 24, (version >> 16) & 0xff, version & 0xffff); proto_tree_add_item(version_tree, hf_sane_version_major, r->tvb, offset, 1, ENC_NA); proto_tree_add_item(version_tree, hf_sane_version_minor, r->tvb, offset + 1, 1, ENC_NA); proto_tree_add_item(version_tree, hf_sane_version_build, r->tvb, offset + 2, 2, ENC_BIG_ENDIAN); dissect_sane_string(r, pinfo, tree, hf_sane_username, "Username: %s"); } static void dissect_sane_net_open_request(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { dissect_sane_string(r, pinfo, tree, hf_sane_device_name, "Device name: %s"); } static void dissect_control_option_value(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { int value_type = 0; dissect_sane_word(r, tree, hf_sane_option_value_type, &value_type); proto_item *value_item = proto_tree_add_item(tree, hf_sane_option_value, r->tvb, r->offset, -1, ENC_NA); proto_tree *value_tree = proto_item_add_subtree(value_item, ett_sane_option_value); int array_length = 0; proto_item *length_item = dissect_sane_word(r, value_tree, hf_sane_option_length, &array_length); if (value_type == SANE_TYPE_STRING) { dissect_sane_string(r, pinfo, value_tree, hf_sane_option_string_value, "Option value: '%s'"); } else { proto_item_append_text(length_item, " (vector of length %d)", array_length / SANE_WORD_LENGTH); dissect_sane_word(r, value_tree, hf_sane_array_length, &array_length); for (int i = 0; i < array_length; i++) { if (value_type == SANE_TYPE_FIXED) { int value = 0; proto_item *numeric_value = dissect_sane_word(r, value_tree, hf_sane_option_numeric_value, &value); proto_item_append_text(numeric_value, " (%f)", ((double) value) / (1 << 16)); } else if (value_type == SANE_TYPE_INT) { int value = 0; proto_item *numeric_value = dissect_sane_word(r, value_tree, hf_sane_option_numeric_value, &value); proto_item_append_text(numeric_value, " (%d)", value); } else if (value_type == SANE_TYPE_BOOL) { dissect_sane_word(r, value_tree, hf_sane_option_boolean_value, NULL); } } } } static void dissect_sane_net_control_option_request(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { dissect_sane_word(r, tree, hf_sane_device_handle, NULL); dissect_sane_word(r, tree, hf_sane_option_index, NULL); dissect_sane_word(r, tree, hf_sane_option_control_action, NULL); dissect_control_option_value(r, pinfo, tree); } static void dissect_sane_net_authorize_request(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { dissect_sane_string(r, pinfo, tree, hf_sane_resource_name, "Authentication resource: %s"); dissect_sane_string(r, pinfo, tree, hf_sane_username, "Username: %s"); dissect_sane_string(r, pinfo, tree, hf_sane_password, "Password: %s"); } /** Dissects a message whose only payload is a device handle. */ static void dissect_sane_device_handle_request(tvb_sane_reader *r, proto_tree *tree) { dissect_sane_word(r, tree, hf_sane_device_handle, NULL); } static int dissect_sane_request(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { unsigned opcode = SANE_NET_UNKNOWN; dissect_sane_word(r, tree, hf_sane_opcode, &opcode); proto_item_append_text(tree, ": %s request", val_to_str(opcode, opcode_vals, "Unknown opcode (%u)")); col_append_fstr(pinfo->cinfo, COL_INFO, "%s request", val_to_str(opcode, opcode_vals, "Unknown opcode (%u)")); switch (opcode) { case SANE_NET_INIT: dissect_sane_net_init_request(r, pinfo, tree); break; case SANE_NET_GET_DEVICES: // no additional payload here break; case SANE_NET_OPEN: dissect_sane_net_open_request(r, pinfo, tree); break; case SANE_NET_CONTROL_OPTION: dissect_sane_net_control_option_request(r, pinfo, tree); break; case SANE_NET_CLOSE: case SANE_NET_START: case SANE_NET_CANCEL: case SANE_NET_GET_PARAMETERS: case SANE_NET_GET_OPTION_DESCRIPTORS: dissect_sane_device_handle_request(r, tree); break; case SANE_NET_AUTHORIZE: dissect_sane_net_authorize_request(r, pinfo, tree); break; } return r->bytes_read; } static proto_item * dissect_sane_status(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree, unsigned *status_ptr) { int offset = r->offset; unsigned status = SANE_STATUS_UNKNOWN; // Safe to ignore the return value here, we're guaranteed to have enough bytes to // read a word. tvb_read_sane_word(r, &status); proto_item_append_text(tree, " (%s)", val_to_str(status, status_values, "Unknown status (%u)")); col_append_fstr(pinfo->cinfo, COL_INFO, " (%s)", val_to_str(status, status_values, "Unknown (%u)")); proto_item *status_item = proto_tree_add_item(tree, hf_sane_status, r->tvb, offset, SANE_WORD_LENGTH, ENC_BIG_ENDIAN); proto_item_append_text(status_item, " (%s)", val_to_str(status, status_values, "Unknown (%u)")); if (status_ptr) { *status_ptr = status; } return status_item; } static void dissect_sane_net_init_response(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { unsigned status; dissect_sane_status(r, pinfo, tree, &status); int version = 0; proto_item *version_item = dissect_sane_word(r, tree, hf_sane_version, &version); proto_item *version_tree = proto_item_add_subtree(version_item, ett_sane_version); proto_item_append_text(version_item, " (major: %d, minor: %d, build: %d)", version >> 24, (version >> 16) & 0xff, version & 0xffff); proto_tree_add_item(version_tree, hf_sane_version_major, r->tvb, SANE_WORD_LENGTH, 1, ENC_NA); proto_tree_add_item(version_tree, hf_sane_version_minor, r->tvb, SANE_WORD_LENGTH + 1, 1, ENC_NA); proto_tree_add_item(version_tree, hf_sane_version_build, r->tvb, SANE_WORD_LENGTH + 2, 2, ENC_BIG_ENDIAN); } static void dissect_sane_net_open_response(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { unsigned status = SANE_STATUS_UNKNOWN; dissect_sane_status(r, pinfo, tree, &status); dissect_sane_word(r, tree, hf_sane_device_handle, NULL); dissect_sane_string(r, pinfo, tree, hf_sane_resource_name, "Authentication resource: '%s'"); } static void append_option_value(proto_item *item, int value, unsigned units, unsigned type) { switch (type) { case SANE_TYPE_INT: if (units) { proto_item_append_text(item, " (%d %s)", value, val_to_str_const(units, sane_option_unit_suffixes, "(unknown unit)")); } else { proto_item_append_text(item, " (%d)", value); } break; case SANE_TYPE_FIXED: { double fixed_val = ((double) value) / (1 << 16); if (units) { proto_item_append_text(item, " (%f %s)", fixed_val, val_to_str_const(units, sane_option_unit_suffixes, "(unknown unit)")); } else { proto_item_append_text(item, " (%f)", fixed_val); } break; } case SANE_TYPE_BOOL: proto_item_append_text(item, " (%s)", (value == 1) ? "True" : ((value == 0) ? "False" : "Invalid")); break; default: break; } } static void dissect_sane_net_get_option_descriptors_response(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { int option_count = 0; dissect_sane_word(r, tree, hf_sane_option_count, &option_count); for (int i = 0; i < option_count; i++) { int unit = 0; int type = 0; int start_offset = r->offset; proto_item *option_item = proto_tree_add_item(tree, hf_sane_option_descriptor, r->tvb, start_offset, 0, ENC_NA); proto_tree *option_tree = proto_item_add_subtree(option_item, ett_sane_option); proto_item_set_text(option_item, "Option descriptor %d", i); dissect_sane_word(r, option_tree, hf_sane_pointer_value, NULL); char *option_name = dissect_sane_string(r, pinfo, option_tree, hf_sane_option_name, "Option name: %s"); if (option_name && *option_name) { proto_item_append_text(option_item, " (%s)", option_name); } char *option_title = dissect_sane_string(r, pinfo, option_tree, hf_sane_option_title, "Option title: %s"); if (!(option_name && *option_name) && (option_title && *option_title)) { proto_item_append_text(option_item, " (%s)", option_title); } dissect_sane_string(r, pinfo, option_tree, hf_sane_option_description, "Option description: %s"); dissect_sane_word(r, option_tree, hf_sane_option_value_type, &type); dissect_sane_word(r, option_tree, hf_sane_option_unit, &unit); dissect_sane_word(r, option_tree, hf_sane_option_size, NULL); proto_tree_add_bitmask(option_tree, r->tvb, r->offset, hf_sane_option_capabilities, ett_sane_option_capabilities, sane_cap_bits, ENC_BIG_ENDIAN); /* XXX - Add consistency checks (expert items): * SANE_CAP_SOFT_SELECT set and SANE_CAP_HARD_SELECT set * SANE_CAP_SOFT_SELECT set and SANE_CAP_SOFT_DETECT not set */ tvb_skip_bytes(r, SANE_WORD_LENGTH); int constraint_start = r->offset; proto_item *constraint_item = proto_tree_add_item(option_tree, hf_sane_option_constraints, r->tvb, constraint_start, 0, ENC_NA); proto_tree *constraint_tree = proto_item_add_subtree(constraint_item, ett_sane_option_constraints); int constraint_type = SANE_NO_CONSTRAINT; dissect_sane_word(r, constraint_tree, hf_sane_option_constraint_type, &constraint_type); proto_item_set_text(constraint_item, "Constraint type: %s", val_to_str(constraint_type, sane_constraint_type_names, "Unknown (%u)")); int array_length = 0; int min = 0; int max = 0; int quant = 0; switch (constraint_type) { case SANE_CONSTRAINT_STRING_LIST: dissect_sane_word(r, constraint_tree, hf_sane_array_length, &array_length); for (int j = 0; j < array_length; j++) { dissect_sane_string(r, pinfo, constraint_tree, hf_sane_option_possible_string_value, "Possible value: %s"); } break; case SANE_CONSTRAINT_WORD_LIST: dissect_sane_word(r, constraint_tree, hf_sane_array_length, &array_length); for (int j = 0; j < array_length; j++) { int value = 0; proto_item *value_item = dissect_sane_word(r, constraint_tree, hf_sane_option_possible_word_value, &value); append_option_value(value_item, value, unit, type); } break; case SANE_CONSTRAINT_RANGE: dissect_sane_word(r, constraint_tree, hf_sane_pointer_value, NULL); proto_item *min_item = dissect_sane_word(r, constraint_tree, hf_sane_option_range_min, &min); append_option_value(min_item, min, unit, type); proto_item *max_item = dissect_sane_word(r, constraint_tree, hf_sane_option_range_max, &max); append_option_value(max_item, max, unit, type); proto_item *quant_item = dissect_sane_word(r, constraint_tree, hf_sane_option_range_quant, &quant); append_option_value(quant_item, quant, unit, type); break; } proto_item_set_len(constraint_item, r->offset - constraint_start); proto_item_set_len(option_item, r->offset - start_offset); } } static void dissect_sane_net_start_response(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { dissect_sane_status(r, pinfo, tree, NULL); dissect_sane_word(r, tree, hf_sane_data_port, NULL); dissect_sane_word(r, tree, hf_sane_byte_order, NULL); dissect_sane_string(r, pinfo, tree, hf_sane_resource_name, "Authentication resource: %s"); } static void dissect_sane_net_get_parameters_response(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { dissect_sane_status(r, pinfo, tree, NULL); dissect_sane_word(r, tree, hf_sane_frame_format, NULL); dissect_sane_word(r, tree, hf_sane_scan_is_last_frame, NULL); dissect_sane_word(r, tree, hf_sane_scan_bytes_per_line, NULL); dissect_sane_word(r, tree, hf_sane_scan_pixels_per_line, NULL); dissect_sane_word(r, tree, hf_sane_scan_line_count, NULL); dissect_sane_word(r, tree, hf_sane_scan_pixel_depth, NULL); } static void dissect_sane_net_control_option_response(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { dissect_sane_status(r, pinfo, tree, NULL); proto_tree_add_bitmask(tree, r->tvb, r->offset, hf_sane_control_option_info, ett_sane_control_option_info, sane_control_option_info_bits, ENC_BIG_ENDIAN); tvb_skip_bytes(r, SANE_WORD_LENGTH); dissect_control_option_value(r, pinfo, tree); dissect_sane_string(r, pinfo, tree, hf_sane_resource_name, "Authentication resource: %s"); } static void dissect_sane_dummy_response(tvb_sane_reader *r, proto_tree *tree) { dissect_sane_word(r, tree, hf_sane_dummy_value, NULL); } static void dissect_sane_net_get_devices_response(tvb_sane_reader *r, packet_info *pinfo, proto_tree *tree) { dissect_sane_status(r, pinfo, tree, NULL); int array_len = 0; dissect_sane_word(r, tree, hf_sane_array_length, &array_len); for (int i = 0; i < array_len - 1; i++) { int offset = r->offset; proto_item *device_item = proto_tree_add_item(tree, hf_sane_device_descriptor, r->tvb, r->offset, -1, ENC_NA); proto_tree *device_tree = proto_item_add_subtree(device_item, ett_sane_device_descriptor); proto_item_set_text(device_item, "Device[%d] descriptor", i); dissect_sane_word(r, device_tree, hf_sane_pointer_value, NULL); dissect_sane_string(r, pinfo, device_tree, hf_sane_device_name, "Device name: %s"); dissect_sane_string(r, pinfo, device_tree, hf_sane_device_vendor, "Device vendor: %s"); dissect_sane_string(r, pinfo, device_tree, hf_sane_device_model, "Device model: %s"); dissect_sane_string(r, pinfo, device_tree, hf_sane_device_type, "Device type: %s"); proto_item_set_len(device_item, r->offset - offset); } dissect_sane_word(r, tree, hf_sane_pointer_value, NULL); } static void dissect_sane_response(tvb_sane_reader *r, sane_session *sess, packet_info *pinfo, proto_tree *tree) { sane_rpc_code opcode = get_sane_expected_response_type(sess, pinfo); proto_item_append_text(tree, ": %s response", val_to_str(opcode, opcode_vals, "Unknown opcode (%u)")); col_append_fstr(pinfo->cinfo, COL_INFO, "%s response", val_to_str(opcode, opcode_vals, "Unknown opcode (%u)")); switch (opcode) { case SANE_NET_INIT: dissect_sane_net_init_response(r, pinfo, tree); break; case SANE_NET_OPEN: dissect_sane_net_open_response(r, pinfo, tree); break; case SANE_NET_GET_OPTION_DESCRIPTORS: dissect_sane_net_get_option_descriptors_response(r, pinfo, tree); break; case SANE_NET_START: dissect_sane_net_start_response(r, pinfo, tree); break; case SANE_NET_GET_PARAMETERS: dissect_sane_net_get_parameters_response(r, pinfo, tree); break; case SANE_NET_CONTROL_OPTION: dissect_sane_net_control_option_response(r, pinfo, tree); break; case SANE_NET_GET_DEVICES: dissect_sane_net_get_devices_response(r, pinfo, tree); break; case SANE_NET_CLOSE: case SANE_NET_CANCEL: case SANE_NET_AUTHORIZE: dissect_sane_dummy_response(r, tree); break; default: break; } } static int dissect_sane_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_) { tvb_sane_reader r = {.tvb = tvb, .bytes_read = 0, .offset = 0}; conversation_t *conv = find_or_create_conversation(pinfo); if (!conv) { return 0; } sane_session *sess = conversation_get_proto_data(conv, proto_sane); DISSECTOR_ASSERT_HINT(sess, "no session found"); col_set_str(pinfo->cinfo, COL_PROTOCOL, "SANE"); col_clear(pinfo->cinfo, COL_INFO); proto_item *sane_item = proto_tree_add_item(tree, proto_sane, r.tvb, 0, -1, ENC_NA); proto_tree *sane_tree = proto_item_add_subtree(sane_item, ett_sane); if (value_is_in_range(sane_server_ports, pinfo->destport)) { dissect_sane_request(&r, pinfo, sane_tree); } else { dissect_sane_response(&r, sess, pinfo, sane_tree); } proto_item_set_len(sane_item, r.bytes_read); return r.bytes_read; } /** * Returns the length, in bytes, of the SANE PDU beginning at the given offset * within the buffer. If the PDU appears to be a response from a client and its * type cannot be determined (e.g. because Wireshark never saw the request), * or if the PDU appears to be truncated and its length cannot be determined, * this function returns 0. */ static unsigned get_sane_pdu_len(packet_info *pinfo, tvbuff_t *tvb, int offset, void *data _U_) { tvb_sane_reader r = {.tvb = tvb, .offset = offset, .bytes_read = 0}; conversation_t *conv = find_or_create_conversation(pinfo); if (!conv) { return 0; } sane_session *sess = conversation_get_proto_data(conv, proto_sane); if (!sess) { sess = wmem_new0(wmem_file_scope(), sane_session); conversation_add_proto_data(conv, proto_sane, sess); } if (value_is_in_range(sane_server_ports, pinfo->destport)) { /* REQUEST */ unsigned opcode; WORD_OR_RETURN(&r, &opcode); sane_pdu pdu = { .is_request = true, .opcode = opcode, .packet_num = pinfo->num }; if (!PINFO_FD_VISITED(pinfo)) { sess->seen_request = true; if (opcode == SANE_NET_AUTHORIZE) { /* Just set this flag, so can remember op being authorised */ sess->auth = true; } else { /* Remember normal request */ sess->last_request = pdu; sess->auth = false; } } switch (opcode) { case SANE_NET_INIT: WORD_OR_RETURN(&r, NULL); STRING_OR_RETURN(&r); break; case SANE_NET_GET_DEVICES: case SANE_NET_EXIT: break; case SANE_NET_OPEN: STRING_OR_RETURN(&r); break; case SANE_NET_CLOSE: case SANE_NET_GET_OPTION_DESCRIPTORS: case SANE_NET_GET_PARAMETERS: case SANE_NET_START: case SANE_NET_CANCEL: WORD_OR_RETURN(&r, NULL); break; case SANE_NET_CONTROL_OPTION: for (int i = 0; i < 4; i++) { WORD_OR_RETURN(&r, NULL); } unsigned value_size; WORD_OR_RETURN(&r, &value_size); // Pointer to void, contains an extra word for whether the pointer is NULL if (tvb_skip_bytes(&r, SANE_WORD_LENGTH + value_size) == 0) { return 0; } break; case SANE_NET_AUTHORIZE: STRING_OR_RETURN(&r); STRING_OR_RETURN(&r); STRING_OR_RETURN(&r); break; } } else { /* RESPONSE */ sane_rpc_code opcode = get_sane_expected_response_type(sess, pinfo); unsigned array_len; switch (opcode) { case SANE_NET_INIT: for (int i = 0; i < 2; i++) { WORD_OR_RETURN(&r, NULL); } break; case SANE_NET_OPEN: // Status word WORD_OR_RETURN(&r, NULL); // Device handle WORD_OR_RETURN(&r, NULL); // Authentication resource name STRING_OR_RETURN(&r); break; case SANE_NET_GET_OPTION_DESCRIPTORS: WORD_OR_RETURN(&r, &array_len); for (unsigned i = 0; i < array_len; i++) { WORD_OR_RETURN(&r, NULL); // read name, title and description for (int j = 0; j < 3; j++) { STRING_OR_RETURN(&r); } for (int j = 0; j < 4; j++) { WORD_OR_RETURN(&r, NULL); } // constraint type unsigned constraint_type; WORD_OR_RETURN(&r, &constraint_type); unsigned string_count; unsigned value_list_length; switch (constraint_type) { case SANE_CONSTRAINT_STRING_LIST: WORD_OR_RETURN(&r, &string_count); for (unsigned j = 0; j < string_count; j++) { STRING_OR_RETURN(&r); } break; case SANE_CONSTRAINT_WORD_LIST: WORD_OR_RETURN(&r, &value_list_length); for (unsigned j = 0; j < value_list_length; j++) { WORD_OR_RETURN(&r, NULL); } break; case SANE_CONSTRAINT_RANGE: // Pointer to range, then min, max, quantization for (unsigned j = 0; j < 4; j++) { WORD_OR_RETURN(&r, NULL); } break; } } break; case SANE_NET_CONTROL_OPTION: // Expected record format: // SANE_Status status // SANE_Word info // SANE_Word value_type // SANE_Word value_size // void *value // SANE_String *resource // See http://sane-project.org/html/doc017.html#s5.2.6. for (int i = 0; i < 3; i++) { WORD_OR_RETURN(&r, NULL); } unsigned value_len; WORD_OR_RETURN(&r, &value_len); if (tvb_skip_bytes(&r, value_len + SANE_WORD_LENGTH) == 0) { return 0; } STRING_OR_RETURN(&r); break; case SANE_NET_GET_DEVICES: WORD_OR_RETURN(&r, NULL); unsigned device_count; WORD_OR_RETURN(&r, &device_count); for (unsigned i = 0; i < device_count - 1; i++) { WORD_OR_RETURN(&r, NULL); STRING_OR_RETURN(&r); STRING_OR_RETURN(&r); STRING_OR_RETURN(&r); STRING_OR_RETURN(&r); } WORD_OR_RETURN(&r, NULL); break; case SANE_NET_CLOSE: WORD_OR_RETURN(&r, NULL); break; case SANE_NET_START: for (int i = 0; i < 3; i++) { WORD_OR_RETURN(&r, NULL); } STRING_OR_RETURN(&r); break; case SANE_NET_GET_PARAMETERS: for (int i = 0; i < 7; i++) { WORD_OR_RETURN(&r, NULL); } break; case SANE_NET_CANCEL: case SANE_NET_AUTHORIZE: WORD_OR_RETURN(&r, NULL); break; default: break; } } return r.bytes_read; } static int dissect_sane(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data) { tcp_dissect_pdus(tvb, pinfo, tree, true, SANE_WORD_LENGTH, get_sane_pdu_len, dissect_sane_pdu, data); return (int) tvb_reported_length(tvb); } static void apply_sane_prefs(void) { sane_server_ports = prefs_get_range_value(SANE_MODULE_NAME, "tcp.port"); } void proto_register_sane(void) { static hf_register_info hf[] = { {&hf_sane_opcode, { "Opcode", "sane.opcode", FT_UINT32, BASE_DEC, VALS(opcode_vals), 0, "RPC request type", HFILL, }}, {&hf_sane_version, { "Version", "sane.version", FT_UINT32, BASE_HEX, NULL, 0, "Protocol version", HFILL, }}, {&hf_sane_version_major, { "Version Major Number", "sane.version.major", FT_UINT8, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_version_minor, { "Version Minor Number", "sane.version.minor", FT_UINT8, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_version_build, { "Version Build Number", "sane.version.build", FT_UINT16, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_username, { "Username", "sane.username", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_password, { "Password", "sane.password", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_string, { "String", "sane.string", FT_NONE, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_string_length, { "String length", "sane.string.length", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_array_length, { "Array length", "sane.array.length", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_device_descriptor, { "Device descriptor", "sane.device.descriptor", FT_NONE, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_device_name, { "Device name", "sane.device.name", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_device_vendor, { "Device vendor", "sane.device.vendor", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_device_model, { "Device model", "sane.device.model", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_device_type, { "Device type", "sane.device.type", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_resource_name, { "Resource name", "sane.resource.name", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_device_handle, { "Device handle", "sane.device.handle", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_index, { "Option index", "sane.option", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_control_action, { "Option control action", "sane.option.action", FT_UINT32, BASE_DEC, VALS(control_types), 0, NULL, HFILL, }}, {&hf_sane_option_length, { "Option value length", "sane.option.length", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_value_type, { "Option value type", "sane.option.type", FT_UINT32, BASE_DEC, VALS(sane_value_types), 0, NULL, HFILL, }}, {&hf_sane_status, { "Status", "sane.status", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_count, { "Option count", "sane.option_count", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_pointer_value, { "Pointer value", "sane.pointer_value", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_name, { "Option name", "sane.option.name", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_title, { "Option title", "sane.option.title", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_description, { "Option description", "sane.option.description", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_descriptor, { "Option descriptor", "sane.option.descriptor", FT_BYTES, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_unit, { "Option unit", "sane.option.unit", FT_UINT32, BASE_DEC, VALS(sane_option_units), 0, NULL, HFILL, }}, {&hf_sane_option_size, { "Option size", "sane.option.size", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_capabilities, { "Option capabilities", "sane.option.capabilities", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_capability_soft_select, { "Can be changed in software", "sane.option.soft_select", FT_BOOLEAN, 32, NULL, SANE_CAP_SOFT_SELECT, NULL, HFILL, }}, {&hf_sane_option_capability_hard_select, { "Requires user intervention to change", "sane.option.hard_select", FT_BOOLEAN, 32, NULL, SANE_CAP_HARD_SELECT, NULL, HFILL, }}, {&hf_sane_option_capability_soft_detect, { "Can be detected by software", "sane.option.soft_detect", FT_BOOLEAN, 32, NULL, SANE_CAP_SOFT_DETECT, NULL, HFILL, }}, {&hf_sane_option_capability_emulated, { "Emulated in software", "sane.option.emulated", FT_BOOLEAN, 32, NULL, SANE_CAP_EMULATED, NULL, HFILL, }}, {&hf_sane_option_capability_automatic, { "Can be set automatically", "sane.option.automatic", FT_BOOLEAN, 32, NULL, SANE_CAP_AUTOMATIC, NULL, HFILL, }}, {&hf_sane_option_capability_inactive, { "Inactive", "sane.option.inactive", FT_BOOLEAN, 32, NULL, SANE_CAP_INACTIVE, NULL, HFILL, }}, {&hf_sane_option_capability_advanced, { "Advanced option", "sane.option.advanced", FT_BOOLEAN, 32, NULL, SANE_CAP_ADVANCED, NULL, HFILL, }}, {&hf_sane_option_value, { "Option value", "sane.option.value", FT_NONE, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_string_value, { "Option string value", "sane.option.value.string", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_numeric_value, { "Option numeric value", "sane.option.value.numeric", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_boolean_value, { "Option boolean value", "sane.option.value.boolean", FT_BOOLEAN, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_constraints, { "Option constraints", "sane.option.constraints", FT_BYTES, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_constraint_type, { "Option constraint type", "sane.option.constraint_type", FT_UINT32, BASE_DEC, VALS(sane_constraint_type_names), 0, NULL, HFILL, }}, {&hf_sane_option_possible_string_value, { "Possible option string value", "sane.option.possible_string_value", FT_STRING, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_possible_word_value, { "Possible option word value", "sane.option.possible_word_value", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_range_min, { "Option minimum value", "sane.option.min_value", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_range_max, { "Option maximum value", "sane.option.max_value", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_option_range_quant, { "Option value quantization", "sane.option.quant", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_data_port, { "Image data port number", "sane.data_port", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_byte_order, { "Image data byte order", "sane.byte_order", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_frame_format, { "Image data frame format", "sane.scan.frame_format", FT_UINT32, BASE_DEC, VALS(sane_frame_format_names), 0, NULL, HFILL, }}, {&hf_sane_scan_line_count, { "Image data line count", "sane.scan.line_count", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_scan_pixel_depth, { "Image data pixel depth", "sane.scan.pixel_depth", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_scan_pixels_per_line, { "Image data pixels per line", "sane.scan.pixels_per_line", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_scan_bytes_per_line, { "Image data bytes per line", "sane.scan.bytes_per_line", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_scan_is_last_frame, { "Is last image data frame", "sane.scan.last_frame", FT_BOOLEAN, BASE_NONE, NULL, 0, NULL, HFILL, }}, {&hf_sane_dummy_value, { "Dummy value", "sane.dummy_value", FT_UINT32, BASE_DEC, NULL, 0, NULL, HFILL, }}, {&hf_sane_control_option_info, { "Control option info", "sane.control_option.info", FT_UINT32, BASE_HEX, NULL, 0, NULL, HFILL, }}, {&hf_sane_control_option_inexact, { "Inexact value selected", "sane.control_option.info.inexact", FT_BOOLEAN, 32, NULL, SANE_INFO_INEXACT, NULL, HFILL, }}, {&hf_sane_control_option_reload_options, { "Client should reload options", "sane.control_option.info.reload_options", FT_BOOLEAN, 32, NULL, SANE_INFO_RELOAD_OPTIONS, NULL, HFILL, }}, {&hf_sane_control_option_reload_params, { "Client should reload scan parameters", "sane.control_option.info.reload_params", FT_BOOLEAN, 32, NULL, SANE_INFO_RELOAD_PARAMS, NULL, HFILL, }}, }; static int *ett[] = { &ett_sane, &ett_sane_version, &ett_sane_string, &ett_sane_option, &ett_sane_option_value, &ett_sane_option_capabilities, &ett_sane_option_constraints, &ett_sane_control_option_info, &ett_sane_device_descriptor, }; module_t *sane_module; proto_sane = proto_register_protocol("Scanner Access Now Easy", "SANE", "sane"); proto_register_field_array(proto_sane, hf, array_length(hf)); proto_register_subtree_array(ett, array_length(ett)); register_dissector(SANE_MODULE_NAME, dissect_sane, proto_sane); /* * XXX - Required to be notified of server port changes, * while no other preferences are registered. */ sane_module = prefs_register_protocol(proto_sane, apply_sane_prefs); (void)sane_module; } void proto_reg_handoff_sane(void) { sane_handle = create_dissector_handle(dissect_sane, proto_sane); dissector_add_uint_range_with_preference("tcp.port", SANE_PORT, sane_handle); apply_sane_prefs(); } /* * 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: */