/* packet-applemidi.c * Routines for dissection of Apple network-midi session establishment. * Copyright 2006-2012, Tobias Erichsen * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * Copied from packet-data.c, README.developer, and various other files. * * SPDX-License-Identifier: GPL-2.0-or-later * * * Apple network-midi session establishment is a lightweight protocol for * providing a simple session establishment for MIDI-data sent in the form * of RTP-MIDI (RFC 4695 / 6295). Peers recognize each other using the * Apple Bonjour scheme with the service-name "_apple-midi._udp", establish * a connection using AppleMIDI (no official name, just an abbreviation) * and then send payload using RTP-MIDI. The implementation of this * dissector is based on the Apple implementation summary from May 6th, 2005 * and the extension from August 13th, 2010. * * 2010-11-29 * - initial version of dissector * 2012-02-24 * - implemented dynamic payloadtype support to automatically punt * the decoding to the RTP-MIDI dissector via the RTP dissector * - added new bitrate receive limit feature * * Here are some links: * * http://www.cs.berkeley.edu/~lazzaro/rtpmidi/ * https://tools.ietf.org/html/rfc4695 * https://tools.ietf.org/html/rfc6925 */ #include "config.h" #include #include #include "packet-rtp.h" void proto_register_applemidi(void); void proto_reg_handoff_applemidi(void); /* Definitions for protocol name during dissector-register */ #define APPLEMIDI_DISSECTOR_NAME "Apple Network-MIDI Session Protocol" #define APPLEMIDI_DISSECTOR_SHORTNAME "AppleMIDI" #define APPLEMIDI_DISSECTOR_ABBREVIATION "applemidi" /* Signature "Magic Value" for Apple network MIDI session establishment */ #define APPLEMIDI_PROTOCOL_SIGNATURE 0xffff /* Apple network MIDI valid commands */ #define APPLEMIDI_COMMAND_INVITATION 0x494e /* "IN" */ #define APPLEMIDI_COMMAND_INVITATION_REJECTED 0x4e4f /* "NO" */ #define APLLEMIDI_COMMAND_INVITATION_ACCEPTED 0x4f4b /* "OK" */ #define APPLEMIDI_COMMAND_ENDSESSION 0x4259 /* "BY" */ #define APPLEMIDI_COMMAND_SYNCHRONIZATION 0x434b /* "CK" */ #define APPLEMIDI_COMMAND_RECEIVER_FEEDBACK 0x5253 /* "RS" */ #define APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT 0x524c /* "RL" */ static int hf_applemidi_signature; static int hf_applemidi_command; static int hf_applemidi_protocol_version; static int hf_applemidi_token; static int hf_applemidi_ssrc; static int hf_applemidi_name; static int hf_applemidi_count; static int hf_applemidi_padding; static int hf_applemidi_timestamp1; static int hf_applemidi_timestamp2; static int hf_applemidi_timestamp3; static int hf_applemidi_sequence_num; static int hf_applemidi_rtp_sequence_num; static int hf_applemidi_rtp_bitrate_limit; static int hf_applemidi_unknown_data; static int ett_applemidi; static int ett_applemidi_seq_num; static const value_string applemidi_commands[] = { { APPLEMIDI_COMMAND_INVITATION, "Invitation" }, { APPLEMIDI_COMMAND_INVITATION_REJECTED, "Invitation Rejected" }, { APLLEMIDI_COMMAND_INVITATION_ACCEPTED, "Invitation Accepted" }, { APPLEMIDI_COMMAND_ENDSESSION, "End Session" }, { APPLEMIDI_COMMAND_SYNCHRONIZATION, "Synchronization" }, { APPLEMIDI_COMMAND_RECEIVER_FEEDBACK, "Receiver Feedback" }, { APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT, "Bitrate Receive Limit" }, { 0, NULL }, }; static int proto_applemidi; static dissector_handle_t applemidi_handle; static dissector_handle_t rtp_handle; static const char applemidi_unknown_command[] = "unknown command: 0x%04x"; static void dissect_applemidi_common( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, uint16_t command ) { proto_item *ti; uint16_t seq_num; uint8_t count; uint8_t *name; int offset = 0; int len; int string_size; proto_tree *applemidi_tree; proto_tree *applemidi_tree_seq_num; col_set_str( pinfo->cinfo, COL_PROTOCOL, APPLEMIDI_DISSECTOR_SHORTNAME ); col_add_str( pinfo->cinfo, COL_INFO, val_to_str( command, applemidi_commands, applemidi_unknown_command ) ); ti = proto_tree_add_item( tree, proto_applemidi, tvb, 0, -1, ENC_NA ); applemidi_tree = proto_item_add_subtree( ti, ett_applemidi ); proto_tree_add_item( applemidi_tree, hf_applemidi_signature, tvb, offset, 2, ENC_BIG_ENDIAN ); offset += 2; proto_tree_add_item( applemidi_tree, hf_applemidi_command, tvb, offset, 2, ENC_BIG_ENDIAN ); offset += 2; /* the format of packets for "IN", "NO", "OK" and "BY" is identical and contains * the protocol version, a random number generated by the initiator of the session, * the SSRC that is used by the respective sides RTP-entity and optionally the * name of the participant */ if ( ( APPLEMIDI_COMMAND_INVITATION == command ) || ( APPLEMIDI_COMMAND_INVITATION_REJECTED == command ) || ( APLLEMIDI_COMMAND_INVITATION_ACCEPTED == command ) || ( APPLEMIDI_COMMAND_ENDSESSION == command ) ) { proto_tree_add_item( applemidi_tree, hf_applemidi_protocol_version, tvb, offset, 4, ENC_BIG_ENDIAN ); offset += 4; proto_tree_add_item( applemidi_tree, hf_applemidi_token, tvb, offset, 4, ENC_BIG_ENDIAN ); offset += 4; proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN ); offset += 4; len = tvb_reported_length(tvb) - offset; /* Name is optional */ if ( len > 0 ) { name = tvb_get_string_enc( pinfo->pool, tvb, offset, len, ENC_UTF_8|ENC_NA ); string_size = (int)( strlen( name ) + 1 ); proto_tree_add_item( applemidi_tree, hf_applemidi_name, tvb, offset, string_size, ENC_UTF_8 ); col_append_fstr( pinfo->cinfo, COL_INFO, ": peer = \"%s\"", name ); offset += string_size; } /* the synchronization packet contains three 64bit timestamps, and a value to define how * many of the timestamps transmitted are valid */ } else if ( APPLEMIDI_COMMAND_SYNCHRONIZATION == command ) { proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN ); offset += 4; count = tvb_get_uint8( tvb, offset ); proto_tree_add_item( applemidi_tree, hf_applemidi_count, tvb, offset, 1, ENC_BIG_ENDIAN ); col_append_fstr( pinfo->cinfo, COL_INFO, ": count = %u", count ); offset += 1; proto_tree_add_item( applemidi_tree, hf_applemidi_padding, tvb, offset, 3, ENC_BIG_ENDIAN ); offset += 3; proto_tree_add_item( applemidi_tree, hf_applemidi_timestamp1, tvb, offset, 8, ENC_BIG_ENDIAN ); offset += 8; proto_tree_add_item( applemidi_tree, hf_applemidi_timestamp2, tvb, offset, 8, ENC_BIG_ENDIAN ); offset += 8; proto_tree_add_item( applemidi_tree, hf_applemidi_timestamp3, tvb, offset, 8, ENC_BIG_ENDIAN ); offset += 8; /* With the receiver feedback packet, the recipient can tell the sender up to what sequence * number in the RTP-stream the packets have been received; this can be used to shorten the * recovery-journal-section in the RTP-session */ } else if ( APPLEMIDI_COMMAND_RECEIVER_FEEDBACK == command ) { proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN ); offset += 4; ti = proto_tree_add_item( applemidi_tree, hf_applemidi_sequence_num, tvb, offset, 4, ENC_BIG_ENDIAN ); /* Apple includes a 32bit sequence-number, but the RTP-packet only specifies 16bit. * this subtree and subitem are added to be able to associate the sequence-number * here easier with the one specified in the corresponding RTP-packet */ applemidi_tree_seq_num = proto_item_add_subtree( ti, ett_applemidi_seq_num ); seq_num = tvb_get_ntohs( tvb, offset ); proto_tree_add_uint( applemidi_tree_seq_num, hf_applemidi_rtp_sequence_num, tvb, offset, 2, seq_num ); offset += 4; col_append_fstr( pinfo->cinfo, COL_INFO, ": seq = %u", seq_num ); /* With the bitrate receive limit packet, the recipient can tell the sender to limit the transmission to a certain bitrate. This is important if the peer is a gateway to a hardware-device that only supports a certain speed. Like the MIDI 1.0 DIN-cable MIDI-implementation which is limited to 31250. */ } else if ( APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT == command ) { proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN ); offset += 4; proto_tree_add_item( applemidi_tree, hf_applemidi_rtp_bitrate_limit, tvb, offset, 4, ENC_BIG_ENDIAN ); offset += 4; } /* If there is any remaining data (possibly because an unknown command was encountered), * we just dump it here */ len = tvb_reported_length_remaining( tvb, offset ); if ( len > 0 ) { proto_tree_add_item( applemidi_tree, hf_applemidi_unknown_data, tvb, offset, len, ENC_NA ); } } static bool test_applemidi(tvbuff_t *tvb, uint16_t *command_p, bool conversation_established ) { *command_p = 0xffff; /* An applemidi session protocol UDP-packet must start with the "magic value" of 0xffff ... */ if ( APPLEMIDI_PROTOCOL_SIGNATURE != tvb_get_ntohs( tvb, 0 ) ) return false; *command_p = tvb_get_ntohs( tvb, 2 ); /* If the conversation is establised (one prior packet with a valid known command) * we won't check the commands anymore - this way we still show new commands * Apple might introduce as "unknown" instead of punting to RTP-dissector */ if ( conversation_established ) { return true; } /* ... followed by packet-command: "IN", "NO", "OK", "BY", "CK" and "RS" and "RL" */ if ( ( APPLEMIDI_COMMAND_INVITATION == *command_p ) || ( APPLEMIDI_COMMAND_INVITATION_REJECTED == *command_p ) || ( APLLEMIDI_COMMAND_INVITATION_ACCEPTED == *command_p ) || ( APPLEMIDI_COMMAND_ENDSESSION == *command_p ) || ( APPLEMIDI_COMMAND_SYNCHRONIZATION == *command_p ) || ( APPLEMIDI_COMMAND_RECEIVER_FEEDBACK == *command_p ) || ( APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT == *command_p ) ) return true; return false; } /* dissect_applemidi() is called when a packet is seen from a previously identified applemidi conversation */ /* If the packet isn't a valid applemidi packet, assume it's an RTP-MIDI packet. */ static int dissect_applemidi( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_ ) { uint16_t command; if ( test_applemidi( tvb, &command, true ) ) dissect_applemidi_common( tvb, pinfo, tree, command ); else call_dissector( rtp_handle, tvb, pinfo, tree ); return tvb_captured_length(tvb); } static bool dissect_applemidi_heur( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_ ) { uint16_t command; conversation_t *p_conv; rtp_dyn_payload_t *rtp_dyn_payload; if ( tvb_captured_length( tvb ) < 4) return false; /* not enough bytes to check */ if ( !test_applemidi( tvb, &command, false ) ) { return false; } /* set dynamic payload-type 97 which is used by Apple for their RTP-MIDI implementation for this address/port-tuple to cause RTP-dissector to call the RTP-MIDI-dissector for payload-decoding */ rtp_dyn_payload = rtp_dyn_payload_new(); rtp_dyn_payload_insert(rtp_dyn_payload, 97, "rtp-midi", 10000, 1); rtp_add_address( pinfo, PT_UDP, &pinfo->src, pinfo->srcport, 0, APPLEMIDI_DISSECTOR_SHORTNAME, pinfo->num, false, rtp_dyn_payload); /* call dissect_applemidi() from now on for UDP packets on this "connection" it is important to do this step after calling rtp_add_address, otherwise all further packets will go directly to the RTP-dissector! */ p_conv = find_or_create_conversation(pinfo); conversation_set_dissector( p_conv, applemidi_handle ); /* punt to actual decoding */ dissect_applemidi_common( tvb, pinfo, tree, command ); return true; } void proto_register_applemidi( void ) { static hf_register_info hf[] = { { &hf_applemidi_signature, { "Signature", "applemidi.signature", FT_UINT16, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_command, { "Command", "applemidi.command", FT_UINT16, BASE_HEX, VALS( applemidi_commands ), 0x0, NULL, HFILL } }, { &hf_applemidi_protocol_version, { "Protocol Version", "applemidi.protocol_version", FT_UINT32, BASE_DEC, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_token, { "Initiator Token", "applemidi.initiator_token", FT_UINT32, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_ssrc, { "Sender SSRC", "applemidi.sender_ssrc", FT_UINT32, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_name, { "Name", "applemidi.name", FT_STRING, BASE_NONE, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_count, { "Count", "applemidi.count", FT_UINT8, BASE_DEC, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_padding, { "Padding", "applemidi.padding", FT_UINT24, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_timestamp1, { "Timestamp 1", "applemidi.timestamp1", FT_UINT64, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_timestamp2, { "Timestamp 2", "applemidi.timestamp2", FT_UINT64, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_timestamp3, { "Timestamp 3", "applemidi.timestamp3", FT_UINT64, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_sequence_num, { "Sequence Number", "applemidi.sequence_number", FT_UINT32, BASE_HEX, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_rtp_sequence_num, { "RTP Sequence Number", "applemidi.rtp_sequence_number", FT_UINT16, BASE_DEC, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_rtp_bitrate_limit, { "Bitrate limit", "applemidi.bitrate_limit", FT_UINT32, BASE_DEC, NULL, 0x0, NULL, HFILL } }, { &hf_applemidi_unknown_data, { "Unknown Data", "applemidi.unknown_data", FT_BYTES, BASE_NONE, NULL, 0x00, NULL, HFILL } }, }; static int *ett[] = { &ett_applemidi, &ett_applemidi_seq_num }; proto_applemidi = proto_register_protocol( APPLEMIDI_DISSECTOR_NAME, APPLEMIDI_DISSECTOR_SHORTNAME, APPLEMIDI_DISSECTOR_ABBREVIATION ); proto_register_field_array( proto_applemidi, hf, array_length( hf ) ); proto_register_subtree_array( ett, array_length( ett ) ); applemidi_handle = register_dissector( "applemidi", dissect_applemidi, proto_applemidi ); } void proto_reg_handoff_applemidi( void ) { /* If we cannot decode the data it will be RTP-MIDI since the Apple session protocol uses * two ports: the control-port and the MIDI-port. On both ports an invitation is being sent. * The second port is then used for the RTP-MIDI-data. So if we can't find valid AppleMidi * packets, it will be most likely RTP-MIDI... */ rtp_handle = find_dissector_add_dependency( "rtp", proto_applemidi ); heur_dissector_add( "udp", dissect_applemidi_heur, "Apple MIDI over UDP", "applemidi_udp", proto_applemidi, HEURISTIC_ENABLE ); } /* * Editor modelines - https://www.wireshark.org/tools/modelines.html * * Local variables: * c-basic-offset: 8 * tab-width: 8 * indent-tabs-mode: t * End: * * vi: set shiftwidth=8 tabstop=8 noexpandtab: * :indentSize=8:tabSize=8:noTabs=false: */