From 04005c07290a12f23443fd84c7dd3577d685c378 Mon Sep 17 00:00:00 2001 From: Maxim Sharabayko Date: Tue, 23 Oct 2018 18:27:38 +0200 Subject: Add Secure Reliable Transport dissector Bug: 15209 Change-Id: I4353a9d3961a8cd8e7bbc02167d8d6cb1fd76285 Reviewed-on: https://code.wireshark.org/review/30224 Petri-Dish: Pascal Quantin Tested-by: Petri Dish Buildbot Reviewed-by: Pascal Quantin --- docbook/release-notes.asciidoc | 1 + epan/dissectors/CMakeLists.txt | 1 + epan/dissectors/packet-srt.c | 1219 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1221 insertions(+) create mode 100644 epan/dissectors/packet-srt.c diff --git a/docbook/release-notes.asciidoc b/docbook/release-notes.asciidoc index 1e8ba6d17c..dac99ad2c0 100644 --- a/docbook/release-notes.asciidoc +++ b/docbook/release-notes.asciidoc @@ -96,6 +96,7 @@ PROXY (v2) protocol Ruby Distributed protocol Ruby Marshal format S101 Lawo Emberplus transport frame +Secure Reliable Transport protocol STCSIG (Spirent Test Center Signature decoding for Ethernet and FibreChannel, disabled by default) systemd Journal Export TeamSpeak 3 DNS diff --git a/epan/dissectors/CMakeLists.txt b/epan/dissectors/CMakeLists.txt index f8fc71ac99..3b4797db5d 100644 --- a/epan/dissectors/CMakeLists.txt +++ b/epan/dissectors/CMakeLists.txt @@ -1691,6 +1691,7 @@ set(DISSECTOR_SRC ${CMAKE_CURRENT_SOURCE_DIR}/packet-spray.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-sprt.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-srp.c + ${CMAKE_CURRENT_SOURCE_DIR}/packet-srt.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-srvloc.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-sscf-nni.c ${CMAKE_CURRENT_SOURCE_DIR}/packet-sscop.c diff --git a/epan/dissectors/packet-srt.c b/epan/dissectors/packet-srt.c new file mode 100644 index 0000000000..e91b634f42 --- /dev/null +++ b/epan/dissectors/packet-srt.c @@ -0,0 +1,1219 @@ +/* packet-srt.c + * Routines for Secure Reliable Transport Protocol dissection + * Copyright (c) 2018 Haivision Systems Inc. + * + * Wireshark - Network traffic analyzer + * By Gerald Combs + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* + * SRT is an open source video transport protocol and technology stack + * that optimizes streaming performance across unpredictable networks + * with secure streams and easy firewall traversal, bringing the best + * quality live video over the worst networks. + * + * http://www.srtalliance.org + */ + +#include +#include +#include +#include + +#include +#include +#include + +/* Prototypes */ +void proto_reg_handoff_srt(void); +void proto_register_srt(void); + +/* Initialize the protocol */ +static int proto_srt = -1; +static int hf_srt_iscontrol = -1; +static int hf_srt_type = -1; +static int hf_srt_exttype = -1; +static int hf_srt_exttype_none = -1; +static int hf_srt_seqno = -1; +static int hf_srt_ack_seqno = -1; +static int hf_srt_ackno = -1; +static int hf_srt_msgno = -1; +static int hf_srt_msgno_pb = -1; +static int hf_srt_msgno_inorder = -1; +static int hf_srt_msgno_enctypes = -1; +static int hf_srt_msgno_rexmit = -1; +static int hf_srt_timestamp = -1; +static int hf_srt_id = -1; +static int hf_srt_addinfo = -1; +static int hf_srt_rtt = -1; +static int hf_srt_rttvar = -1; +static int hf_srt_bufavail = -1; +static int hf_srt_rate = -1; +static int hf_srt_bandwidth = -1; +static int hf_srt_rcvrate = -1; + +/* SRT Handshake */ +static int hf_srt_handshake_version = -1; +static int hf_srt_handshake_type_v4 = -1; +static int hf_srt_handshake_enc_field_v5 = -1; +static int hf_srt_handshake_ext_field_v5 = -1; +static int hf_srt_handshake_ext_field_v5_flag_hsreq = -1; +static int hf_srt_handshake_ext_field_v5_flag_kmreq = -1; +static int hf_srt_handshake_ext_field_v5_flag_config = -1; +static int hf_srt_handshake_isn = -1; +static int hf_srt_handshake_mtu = -1; +static int hf_srt_handshake_flow_window = -1; +static int hf_srt_handshake_reqtype = -1; +static int hf_srt_handshake_id = -1; +static int hf_srt_handshake_cookie = -1; +static int hf_srt_handshake_peerip = -1; +/* SRT Handshake Extension */ +static int hf_srt_handshake_ext_version = -1; +static int hf_srt_handshake_ext_flags = -1; +static int hf_srt_handshake_ext_flag_tsbpd_snd = -1; +static int hf_srt_handshake_ext_flag_tsbpd_rcv = -1; +static int hf_srt_handshake_ext_flag_haicrypt = -1; +static int hf_srt_handshake_ext_flag_tlpkt_drop = -1; +static int hf_srt_handshake_ext_flag_nak_report = -1; +static int hf_srt_handshake_ext_flag_rexmit = -1; +static int hf_srt_handshake_ext_flag_stream = -1; + +static int hf_srt_srths_blocktype = -1; +static int hf_srt_srths_blocklen = -1; +static int hf_srt_srths_agent_latency = -1; // TSBPD delay +static int hf_srt_srths_peer_latency = -1; // TSBPD delay +static int hf_srt_srtkm_msg = -1; +static int hf_srt_srtkm_error = -1; +static int hf_srt_srths_sid = -1; +static int hf_srt_srths_conjestcontrol = -1; + +static gint ett_srt = -1; +static gint ett_srt_handshake_ext_flags = -1; +static gint ett_srt_handshake_ext_field_flags = -1; + +static expert_field ei_srt_nak_seqno = EI_INIT; +static expert_field ei_srt_hs_ext_hsreq_len = EI_INIT; +static expert_field ei_srt_hs_ext_type = EI_INIT; + +static dissector_handle_t srt_udp_handle; + + +/* Base Unit Strings Definitions */ +const unit_name_string units_pkts = { " pkts", NULL }; +const unit_name_string units_pkts_per_sec = { " pkts/s", NULL }; + + +/* This defines the firstmost bit of the packet, so it can stay this way. */ +#define SRT_TYPE_DATA 0 +#define SRT_TYPE_CONTROL 1 +#define SRT_CONTROL_MASK (~0x80000000) + +#define SRT_LOSS_SEQUENCE_FIRST 0x80000000 +#define SRT_LOSS_SEQUENCE_MASK (~SRT_LOSS_SEQUENCE_FIRST) + +enum UDTSockType +{ + SRT_UNDEFINED = 0, /* initial trap representation */ + SRT_STREAM = 1, + SRT_DGRAM = 2, + SRT_MAGIC_CODE = 0x4A17 +}; + +/* Handshake Extended Field Flags */ +#define SRT_OPT_FIELD_LEN 32 +#define SRT_OPT_TSBPDSND (1 << 0) +#define SRT_OPT_TSBPDRCV (1 << 1) +#define SRT_OPT_HAICRYPT (1 << 2) +#define SRT_OPT_TLPKTDROP (1 << 3) +#define SRT_OPT_NAKREPORT (1 << 4) +#define SRT_OPT_REXMITFLG (1 << 5) +#define SRT_OPT_STREAM (1 << 6) + + +/* Extended Handshake Flags */ +#define SRT_HS_V5_EXT_FIELD_LEN 16 +#define SRT_HS_V5_EXT_FIELD_HSREQ (1 << 0) +#define SRT_HS_V5_EXT_FIELD_KMREQ (1 << 1) +#define SRT_HS_V5_EXT_FIELD_CONFIG (1 << 2) +#define SRT_HS_V5_EXT_FIELD_MAGIC SRT_MAGIC_CODE + +/* Message number field and single bit flags */ +#define SRT_MSGNO_FF_FIRST_B (2 << (32-2)) +#define SRT_MSGNO_FF_LAST_B (1 << (32-2)) +#define SRT_MSGNO_FF_MASK (SRT_MSGNO_FF_FIRST_B | SRT_MSGNO_FF_LAST_B) + +enum PacketBoundary +{ + PB_SUBSEQUENT = 0, + /* 01: last packet of a message */ + PB_LAST = 1, + /* 10: first packet of a message */ + PB_FIRST = 2, + /* 11: solo message packet */ + PB_SOLO = 3, +}; + + +#define SRT_MSGNO_INORDER (1 << (32-3)) /* 0x20000000 */ + +#define SRT_MSGNO_ENCTYPE (3 << (32-5)) /* 0x18000000 */ + +#define SRT_MSGNO_EK_NONE 0 +#define SRT_MSGNO_EK_EVEN 1 +#define SRT_MSGNO_EK_ODD 2 + +#define SRT_MSGNO_REXMIT (1 << (32-6)) /* 0x04000000 */ + +/* Rest of the bits are for message sequence number */ +#define SRT_MSGNO_MSGNO_MASK 0x03ffffff + + +/* The message types used by UDT protocol. This is a part of UDT + * protocol and should never be changed. + */ +enum UDTMessageType +{ + UMSG_HANDSHAKE = 0, //< Connection Handshake. Control: see @a CHandShake. + UMSG_KEEPALIVE = 1, //< Keep-alive. + UMSG_ACK = 2, //< Acknowledgement. Control: past-the-end sequence number up to which packets have been received. + UMSG_LOSSREPORT = 3, //< Negative Acknowledgement (NACK). Control: Loss list. + UMSG_CGWARNING = 4, //< Congestion warning. + UMSG_SHUTDOWN = 5, //< Shutdown. + UMSG_ACKACK = 6, //< Acknowledgement of Acknowledgement. Add info: The ACK sequence number + UMSG_DROPREQ = 7, //< Message Drop Request. Add info: Message ID. Control Info: (first, last) number of the message. + UMSG_PEERERROR = 8, //< Signal from the Peer side. Add info: Error code. + /* ... add extra code types here */ + UMSG_END_OF_TYPES, + UMSG_EXT = 0x7FFF //< For the use of user-defined control packets. +}; + +// Adapted constants +#define SRT_CMD_HSREQ 1 +#define SRT_CMD_HSRSP 2 +#define SRT_CMD_KMREQ 3 +#define SRT_CMD_KMRSP 4 +#define SRT_CMD_SID 5 +#define SRT_CMD_CONJESTCTRL 6 + +enum SrtDataStruct +{ + SRT_HS_VERSION = 0, + SRT_HS_FLAGS, + SRT_HS_EXTRAS, + + // Keep it always last + SRT_HS__SIZE +}; + + +enum UDTRequestType +{ + URQ_AGREEMENT = -2, + URQ_CONCLUSION = -1, + URQ_WAVEAHAND = 0, + URQ_INDUCTION = 1, + + URQ_FAILURE_TYPES = 1000, + URQ_REJECT = 1002, + URQ_INVALID = 1004 +}; + + +enum SRT_KM_STATE +{ + SRT_KM_S_UNSECURED = 0, ///< No encryption + SRT_KM_S_SECURING = 1, ///< Stream encrypted, exchanging Keying Material + SRT_KM_S_SECURED = 2, ///< Stream encrypted, keying Material exchanged, decrypting ok. + SRT_KM_S_NOSECRET = 3, ///< Stream encrypted and no secret to decrypt Keying Material + SRT_KM_S_BADSECRET = 4 ///< Stream encrypted and wrong secret, cannot decrypt Keying Material +}; + + +void proto_register_srt(void); +void proto_reg_handoff_srt(void); + +static const value_string srt_ctrlmsg_types[] = { + {UMSG_HANDSHAKE, "UMSG_HANDSHAKE"}, + {UMSG_KEEPALIVE, "UMSG_KEEPALIVE"}, + {UMSG_ACK, "UMSG_ACK"}, + {UMSG_LOSSREPORT, "UMSG_LOSSREPORT"}, + {UMSG_CGWARNING, "UMSG_CGWARNING"}, + {UMSG_SHUTDOWN, "UMSG_SHUTDOWN"}, + {UMSG_ACKACK, "UMSG_ACKACK"}, + {UMSG_DROPREQ, "UMSG_DROPREQ"}, + {UMSG_PEERERROR, "UMSG_PEERERROR"}, + {UMSG_EXT, "UMSG_EXT"}, + + {0, NULL}, +}; + +static const value_string srt_ctrlmsg_exttypes[] = { + {SRT_CMD_HSREQ, "SRT_CMD_HSREQ"}, + {SRT_CMD_HSRSP, "SRT_CMD_HSRSP"}, + {SRT_CMD_KMREQ, "SRT_CMD_KMREQ"}, + {SRT_CMD_KMRSP, "SRT_CMD_KMRSP"}, + {SRT_CMD_SID, "SRT_CMD_SID"}, + {SRT_CMD_CONJESTCTRL, "SRT_CMD_CONJESTCTRL"}, + + { 0, NULL }, +}; + +static const value_string srt_hsv4_socket_types[] = { + {SRT_STREAM, "SRT_STREAM"}, + {SRT_DGRAM, "SRT_DGRAM"}, + {0, NULL}, +}; + + +static const value_string srt_handshake_enc_field[] = { + {0, "PBKEYLEN not advertised"}, + {2, "AES-128" }, + {3, "AES-192" }, + {4, "AES-256" }, + {0, NULL}, +}; + + +static const true_false_string srt_packet_types = { + "CONTROL", /* 1 */ + "DATA" /* 0 */ +}; + +static const value_string srt_pb_types[] = { + {PB_SUBSEQUENT, "PB_SUBSEQUENT"}, + {PB_LAST, "PB_LAST"}, + {PB_FIRST, "PB_FIRST"}, + {PB_SOLO, "PB_SOLO"}, + {0, NULL}, +}; + +static const value_string srt_msgno_enctypes[] = { + {SRT_MSGNO_EK_NONE, "Not encrypted"}, + {SRT_MSGNO_EK_EVEN, "Encrypted ith even key"}, + {SRT_MSGNO_EK_ODD, "Encrypted with odd key"}, + {0, NULL}, +}; + +static const true_false_string srt_msgno_rexmit = { + "Retransmited", /* 1 */ + "Original" /* 0 */ +}; + +static const value_string srt_hs_request_types[] = { + {URQ_INDUCTION, "URQ_INDUCTION (c/l invocation)"}, + {URQ_CONCLUSION, "URQ_CONCLUSION"}, + {URQ_WAVEAHAND, "URQ_WAVEAHAND (rendezvous invocation)"}, + {URQ_AGREEMENT, "URQ_AGREEMENT (rendezvous finalization)"}, + {URQ_REJECT, "!REJECT"}, + {URQ_INVALID, "!INVALID"}, + {0, NULL} +}; + +static const value_string srt_enc_kmstate[] = { + {SRT_KM_S_UNSECURED, "UNSECURED"}, + {SRT_KM_S_SECURING, "SECURING"}, + {SRT_KM_S_SECURED, "SECURED"}, + {SRT_KM_S_NOSECRET, "NOSECRET"}, + {SRT_KM_S_BADSECRET, "BADSECRET"}, + + {0, NULL}, +}; + + +/* + * XXX To be added later to extract correct IPv4/IPv6 address from 16 bytes of data + * static void srt_tree_add_ipaddr( proto_tree *tree, const int hf, tvbuff_t *tvb, gint offset) + * { + * + * } + */ + +#define IP_BUFFER_SIZE 64 + +static void srt_format_ip_address(gchar* dest, size_t dest_size, const gchar* ptr) +{ + /* Initial IPv4 check. + * The address is considered IPv4 if: + * byte[0] and byte[3] != 0 + * bytes[4...16] == 0 + */ + + ws_in4_addr ia4; + ws_in6_addr ia6; + guint32* p; + int i, j; + + if (ptr[0] != 0 && ptr[3] != 0) + { + for (i = 4; i < 16; ++i) + { + if ( ptr[i] == 0 ) + continue; + + /* This is not an IP4 */ + p = (guint32*) &ia6; + for (j = 0; j < 4; ++j) + p[j] = g_ntohl(((guint32*)ptr)[j]); + + ws_inet_ntop6(&ia6, dest, (guint) dest_size); + return; + } + } + + // There's one small problem: the contents of the handshake + // goes in LITTLE ENDIAN. That's an initial problem of UDT. + // The address must be inverted. + + // Here's IPv4, so invert only one l. + ia4 = g_ntohl(*((const guint32*)ptr)); + + ws_inet_ntop4(&ia4, dest, (guint) dest_size); + return; +} + + +static void srt_format_hs_ext_hsreq(proto_tree* tree, tvbuff_t* tvb, int baseoff) +{ + proto_item* pi; + guint32 version = 0; + pi = proto_tree_add_item_ret_uint(tree, hf_srt_handshake_ext_version, tvb, baseoff, 4, ENC_BIG_ENDIAN, &version); + + const int vminor = (version >> 8) & 0xff; + const int vmajor = (version >> 16) & 0xff; + const int vpatch = version & 0xff; + proto_item_append_text(pi, " (%d.%d.%d)", vmajor, vminor, vpatch); + + static const int * ext_hs_flags[] = { + &hf_srt_handshake_ext_flag_tsbpd_snd, + &hf_srt_handshake_ext_flag_tsbpd_rcv, + &hf_srt_handshake_ext_flag_haicrypt, + &hf_srt_handshake_ext_flag_tlpkt_drop, + &hf_srt_handshake_ext_flag_nak_report, + &hf_srt_handshake_ext_flag_rexmit, + &hf_srt_handshake_ext_flag_stream, + NULL + }; + + proto_tree_add_bitmask_with_flags(tree, tvb, baseoff + 4, hf_srt_handshake_ext_flags, + ett_srt_handshake_ext_flags, ext_hs_flags, ENC_NA, BMT_NO_APPEND); + + proto_tree_add_item(tree, hf_srt_srths_peer_latency, tvb, baseoff+8, 2, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_srths_agent_latency, tvb, baseoff+10, 2, ENC_BIG_ENDIAN); +} + +static void srt_format_kmx(proto_tree* tree, tvbuff_t* tvb, int baseoff, int blocklen) +{ + if (blocklen == 4) + { + // Error report. Format as KMX state. + proto_tree_add_item(tree, hf_srt_srtkm_error, tvb, baseoff, 4, ENC_NA); + } + else + { + proto_tree_add_item(tree, hf_srt_srtkm_msg, tvb, baseoff, blocklen, ENC_NA); + } +} + +// Wireshark dissector doesn't have a possibility to format enum-collected flags. +static void dissect_srt_hs_ext_field(proto_tree* tree, + tvbuff_t* tvb, int baseoff) +{ + static const gint ext_field_len = 2; + + const int bits = tvb_get_ntohs(tvb, baseoff); + if (bits == SRT_HS_V5_EXT_FIELD_MAGIC) + { + proto_item* pi = proto_tree_add_item(tree, hf_srt_handshake_ext_field_v5, + tvb, baseoff, ext_field_len, ENC_BIG_ENDIAN); + proto_item_append_text(pi, ": HSv5 MAGIC"); + return; + } + + static const int * ext_hs_ext_field_flags[] = { + &hf_srt_handshake_ext_field_v5_flag_hsreq, + &hf_srt_handshake_ext_field_v5_flag_kmreq, + &hf_srt_handshake_ext_field_v5_flag_config, + NULL + }; + + proto_tree_add_bitmask_with_flags(tree, tvb, baseoff, hf_srt_handshake_ext_field_v5, + ett_srt_handshake_ext_field_flags, ext_hs_ext_field_flags, ENC_NA, BMT_NO_APPEND); + + return; +} + + +static void format_text_reorder_32(proto_tree* tree, tvbuff_t* tvb, int hfinfo, int baseoff, int blocklen) +{ + wmem_strbuf_t *sid = wmem_strbuf_new(wmem_packet_scope(), ""); + for (int ii = 0; ii < blocklen; ii += 4) + { + const guint32 u = tvb_get_ntohl(tvb, baseoff + ii); + wmem_strbuf_append_c(sid, 0xFF & (u >> 0)); + wmem_strbuf_append_c(sid, 0xFF & (u >> 8)); + wmem_strbuf_append_c(sid, 0xFF & (u >> 16)); + wmem_strbuf_append_c(sid, 0xFF & (u >> 24)); + } + proto_tree_add_string(tree, hfinfo, tvb, + baseoff, blocklen, wmem_strbuf_get_str(sid)); +} + + +/* Code to actually dissect the packets + * + */ +void +dissect_srt_control_packet(tvbuff_t *tvb, packet_info* pinfo, + proto_tree *tree, proto_item *srt_item) +{ + guint32 type = 0; + guint32 exttype = 0; + + proto_tree_add_item_ret_uint(tree, hf_srt_type, tvb, 0, 2, + ENC_BIG_ENDIAN, &type); + + if ( type != UMSG_EXT ) + proto_tree_add_item(tree, hf_srt_exttype_none, tvb, 2, 2, + ENC_BIG_ENDIAN); + else + proto_tree_add_item_ret_uint(tree, hf_srt_exttype, tvb, 2, 2, + ENC_BIG_ENDIAN, &exttype); + + switch (type) + { + case UMSG_EXT: + col_add_fstr(pinfo->cinfo, COL_INFO, "Control/ext: %s socket: 0x%x", + val_to_str(exttype, srt_ctrlmsg_exttypes, + "Unknown SRT Control Type (0x%x)"), + tvb_get_ntohl(tvb, 12)); + break; + case UMSG_ACK: + col_add_fstr(pinfo->cinfo, COL_INFO, "Control: UMSG_ACK %d ackseq: %d socket: 0x%x", + tvb_get_ntohl(tvb, 4), + tvb_get_ntohl(tvb, 16), + tvb_get_ntohl(tvb, 12)); + break; + case UMSG_ACKACK: + col_add_fstr(pinfo->cinfo, COL_INFO, "Control: UMSG_ACKACK %d socket: 0x%x", + tvb_get_ntohl(tvb, 4), + tvb_get_ntohl(tvb, 12)); + break; + default: + col_add_fstr(pinfo->cinfo, COL_INFO, "Control: %s socket: 0x%x", + val_to_str(type, srt_ctrlmsg_types, + "Unknown UDT Control Type (%x)"), + tvb_get_ntohl(tvb, 12)); + break; + } + + switch (type) + { + case UMSG_ACK: + case UMSG_ACKACK: + proto_tree_add_item(tree, hf_srt_ackno, tvb, 4, 4, + ENC_BIG_ENDIAN); + break; + case UMSG_DROPREQ: + proto_tree_add_item(tree, hf_srt_msgno, tvb, 4, 4, + ENC_BIG_ENDIAN); + break; + default: + proto_tree_add_item(tree, hf_srt_addinfo, tvb, 4, 4, + ENC_BIG_ENDIAN); + break; + } + proto_tree_add_item(tree, hf_srt_timestamp, tvb, 8, 4, + ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_id, tvb, 12, 4, + ENC_BIG_ENDIAN); + + switch (type) + { + case UMSG_HANDSHAKE: + { + char ipbuf[IP_BUFFER_SIZE]; + + const int version = tvb_get_ntohl(tvb, 16); + const int final_length = tvb_reported_length(tvb); + int baselen = 64; + + /* This contains the handshake version (currently 4 or 5) */ + proto_tree_add_item(tree, hf_srt_handshake_version, tvb, + 16, 4, ENC_BIG_ENDIAN); + + /* Version 4 embraces both HSv4 listener URQ_INDUCTION response + * and HSv5 caller URQ_INDUCTION request. In both these cases the + * value is interpreted as socket type (UDT legacy). With version 5 + * the first message is the listener's URQ_INDUCTION response, where + * the layout in the type is already the MAGIC in the lower block, + * and ENC FLAGS in the upper block. The next caller's URQ_CONCLUSION + * will have SRT HS Extension block flags in the lower block. + */ + if (version == 4) + { + proto_tree_add_item(tree, hf_srt_handshake_type_v4, tvb, + 20, 4, ENC_BIG_ENDIAN); + } + else + { + /* Both the PBKEYLEN-ad and magic are used in HSv5 induction. */ + proto_tree_add_item(tree, hf_srt_handshake_enc_field_v5, tvb, + 20, 2, ENC_BIG_ENDIAN); + + dissect_srt_hs_ext_field(tree, tvb, 22); /* 2 bytes */ + } + + proto_tree_add_item(tree, hf_srt_handshake_isn, tvb, + 24, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_handshake_mtu, tvb, + 28, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_handshake_flow_window, tvb, + 32, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_handshake_reqtype, tvb, + 36, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_handshake_id, tvb, + 40, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_handshake_cookie, tvb, + 44, 4, ENC_BIG_ENDIAN); + + srt_format_ip_address(ipbuf, sizeof ipbuf, (const gchar *)tvb_memdup(wmem_packet_scope(), tvb, 48, 16)); + + proto_tree_add_string(tree, hf_srt_handshake_peerip, tvb, + 48, 16, ipbuf); + if (final_length > baselen) + { + /* Extract SRT handshake extension blocks + * and increase baselen accordingly. + */ + int begin = baselen; + for (;;) + { + const guint16 blockid = tvb_get_ntohs(tvb, begin); + const guint16 blocklen = tvb_get_ntohs(tvb, begin + 2); + + proto_tree_add_item(tree, hf_srt_srths_blocktype, tvb, + begin, 2, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_srths_blocklen, tvb, + begin+2, 2, ENC_BIG_ENDIAN); + + // Shift to the payload + begin += 4; + + switch (blockid) + { + case SRT_CMD_HSREQ: + case SRT_CMD_HSRSP: + if (blocklen == 3) + { + srt_format_hs_ext_hsreq(tree, tvb, begin); + } + else + { + /* blocklen should be 3, that corresponds to (3 * 4) = 12 bytes. + * Otherwise the format is unknown.*/ + proto_tree_add_expert_format(tree, pinfo, &ei_srt_hs_ext_hsreq_len, + tvb, begin, 4 * blocklen, "Actual length is %u", + blocklen); + } + break; + + case SRT_CMD_KMREQ: + case SRT_CMD_KMRSP: + // Rely on the extracted blocklen + srt_format_kmx(tree, tvb, begin, blocklen*4); + break; + + case SRT_CMD_SID: + format_text_reorder_32(tree, tvb, hf_srt_srths_sid, begin, 4 * blocklen); + break; + + case SRT_CMD_CONJESTCTRL: + format_text_reorder_32(tree, tvb, hf_srt_srths_conjestcontrol, begin, 4 * blocklen); + break; + + default: + proto_tree_add_expert_format(tree, pinfo, &ei_srt_hs_ext_type, + tvb, begin, 4 * blocklen, "Ext Type value is %u", + blockid); + break; + } + + /* Move the index pointer past the block and repeat. */ + begin += blocklen * 4; + + /* OK, once one block is done, interrupt the loop. */ + if (begin >= final_length) + break; + } + + baselen = begin; + } + + proto_item_set_len(srt_item, baselen); + } + break; + case UMSG_ACK: + { + guint len = tvb_reported_length(tvb); + + proto_tree_add_item(tree, hf_srt_ack_seqno, tvb, 4 * 4, 4, + ENC_BIG_ENDIAN); + + // Check for "Lite ACK" (size 4) + if (len <= (4 + 1) * 4) + { + proto_item_set_len(srt_item, (4 + 1) * 4); + } + else + { + proto_tree_add_item(tree, hf_srt_rtt, tvb, (4+1)*4, 4, + ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_rttvar, tvb, (4+2)*4, 4, + ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_bufavail, tvb, (4+3)*4, 4, + ENC_BIG_ENDIAN); + /* if not a light ack, decode the rate and link capacity */ + + if (len > (4 + 4) * 4) + { + proto_tree_add_item(tree, hf_srt_rate, tvb, (4 + 4) * 4, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_bandwidth, tvb, (4 + 5) * 4, 4, ENC_BIG_ENDIAN); + + // SRT Extra data. This can be version dependent, so + // test the length for each field. + if (len > (4 + 6) * 4) + { + proto_tree_add_item(tree, hf_srt_rcvrate, tvb, (4 + 6) * 4, 4, ENC_BIG_ENDIAN); + len = (4 + 7) * 4; + } + + proto_item_set_len(srt_item, (gint) len); + } + else + { + proto_item_set_len(srt_item, (4 + 4) * 4); + } + } + } + break; + case UMSG_LOSSREPORT: + { + guint len = tvb_reported_length(tvb); + guint pos; + guint32 val; + guint prev = 0; + for (pos = 16; pos < len; pos += 4) + { + val = tvb_get_ntohl(tvb, pos); + if (val & SRT_LOSS_SEQUENCE_FIRST) { + // Remember this as a beginning range + prev = val; + continue; + } + + // We have either a single value, or end-range here. + if (prev & SRT_LOSS_SEQUENCE_FIRST) { + // Was a range. Clear the state and display as range. + prev = 0; + proto_tree_add_expert_format(tree, pinfo, &ei_srt_nak_seqno, + tvb, pos-4, 8, "Loss sequence range: %u-%u", + (prev & SRT_LOSS_SEQUENCE_MASK), val); + } else { + // No from, so this is a freestanding loss value + proto_tree_add_expert_format(tree, pinfo, &ei_srt_nak_seqno, + tvb, pos, 4, "Loss sequence: %u", val); + } + } + + // Report possible errors + if (prev) + { + proto_tree_add_expert_format(tree, pinfo, &ei_srt_nak_seqno, + tvb, pos-4, 4, "ERROR: loss sequence range begin only: %u (%x)", + val & SRT_LOSS_SEQUENCE_MASK, val); + } + + proto_item_set_len(srt_item, len); + } + break; + + case UMSG_EXT: + switch (exttype) + { + case SRT_CMD_HSREQ: + case SRT_CMD_HSRSP: + srt_format_hs_ext_hsreq(tree, tvb, 16); + break; + + case SRT_CMD_KMREQ: + case SRT_CMD_KMRSP: + { + // This relies on value of HCRYPT_MSG_KM_MAX_SZ resulting from this above. + // Too strongly dependent on devel API, so using explicit 104. + int plen = tvb_reported_length(tvb) - 16; + if (plen > 104) + plen = 104; + srt_format_kmx(tree, tvb, 16, plen); + } + break; + + default: + break; + } + break; + + default: + // All other types have kinda "extra padding" + proto_tree_add_item(tree, hf_srt_addinfo, tvb, 16, 4, ENC_BIG_ENDIAN); + break; + } +} + + +/* Code to actually dissect the packets + * + * @return the amount of data this dissector was able to dissect + */ +static int +dissect_srt_udp(tvbuff_t *tvb, packet_info* pinfo, proto_tree *parent_tree, + void *data _U_) +{ + /* Other misc. local variables. */ + gboolean is_control = 0; + + col_set_str(pinfo->cinfo, COL_PROTOCOL, "SRT"); + col_clear (pinfo->cinfo, COL_INFO); + + proto_item *srt_item = proto_tree_add_item(parent_tree, proto_srt, tvb, + 0 /*start*/, -1 /*length*/, ENC_NA); + proto_tree *tree = proto_item_add_subtree(srt_item, ett_srt); + proto_tree_add_item_ret_boolean(tree, hf_srt_iscontrol, tvb, 0, 4, ENC_BIG_ENDIAN, &is_control); + + if (is_control) + { + dissect_srt_control_packet(tvb, pinfo, tree, srt_item); + } + else + { + /* otherwise, a data packet */ + tvbuff_t *next_tvb; + + col_add_fstr(pinfo->cinfo, COL_INFO, + "DATA: seqno: %u msgno: %u socket: 0x%x", + tvb_get_ntohl(tvb, 0), + tvb_get_ntohl(tvb, 4) & SRT_MSGNO_MSGNO_MASK, + tvb_get_ntohl(tvb, 12)); + + if (tree) + { + // Sequence number + proto_tree_add_item(tree, hf_srt_seqno, tvb, 0, 4, ENC_BIG_ENDIAN); + + proto_tree_add_item(tree, hf_srt_msgno_pb, tvb, 4, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_msgno_inorder, tvb, 4, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_msgno_enctypes, tvb, 4, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_msgno_rexmit, tvb, 4, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_msgno, tvb, 4, 4, ENC_BIG_ENDIAN); + + proto_tree_add_item(tree, hf_srt_timestamp, tvb, 8, 4, ENC_BIG_ENDIAN); + proto_tree_add_item(tree, hf_srt_id, tvb, 12, 4, ENC_BIG_ENDIAN); + + } + + next_tvb = tvb_new_subset_remaining(tvb, 16); + call_data_dissector(next_tvb, pinfo, tree); + } + + return tvb_reported_length(tvb); +} + + +static gboolean +dissect_srt_heur_udp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data) +{ + conversation_t *conv; + + /* Must have at least 24 captured bytes for heuristic check */ + if (tvb_captured_length(tvb) < 24) + return FALSE; + + /* detect handshake control packet */ + if (tvb_get_ntohl(tvb, 0) != (0x80000000 | UMSG_HANDSHAKE)) + return FALSE; + + /* must be version 4 or 5*/ + const guint32 version = tvb_get_ntohl(tvb, 16); + if (version != 4 && version != 5) + return FALSE; + + /* SRT: must be DGRAM. STREAM is not supported in SRT */ + if (version == 4 && tvb_get_ntohl(tvb, 20) != SRT_DGRAM) + return FALSE; + + conv = find_or_create_conversation(pinfo); + conversation_set_dissector(conv, srt_udp_handle); + dissect_srt_udp(tvb, pinfo, tree, data); + + return TRUE; +} + + +/* Register the protocol with Wireshark. + * + * This format is required because a script is used to build the C function that + * calls all the protocol registration. + */ +void proto_register_srt(void) +{ + expert_module_t *expert_srt; + + /* Setup list of header fields See Section 1.5 of README.dissector for + * details. */ + static hf_register_info hf[] = { + {&hf_srt_iscontrol, { + "Content", "srt.iscontrol", + FT_BOOLEAN, 32, + TFS(&srt_packet_types), 0x80000000, NULL, HFILL }}, + + {&hf_srt_type, { + "Msg Type", "srt.type", + FT_UINT16, BASE_HEX, + VALS(srt_ctrlmsg_types), 0x7fff, NULL, HFILL}}, + + {&hf_srt_exttype, { + "Extended type", "srt.exttype", + FT_UINT16, BASE_HEX, + VALS(srt_ctrlmsg_exttypes), 0, NULL, HFILL}}, + + {&hf_srt_exttype_none, { + "(no extended type)", "srt.exttype_none", + FT_UINT16, BASE_HEX, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_seqno, { + "Sequence Number", "srt.seqno", + FT_UINT32, BASE_DEC, + NULL, SRT_CONTROL_MASK, NULL, HFILL}}, + + {&hf_srt_addinfo, { + "(Unused)", "srt.addinfo", + FT_UINT32, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_msgno, { + "Message Number", "srt.msgno", + FT_UINT32, BASE_DEC, + NULL, SRT_MSGNO_MSGNO_MASK, NULL, HFILL}}, + + {&hf_srt_msgno_pb, { + "Packet Boundary", "srt.pb", + FT_UINT32, BASE_DEC, + VALS(srt_pb_types), SRT_MSGNO_FF_MASK, NULL, HFILL}}, + + {&hf_srt_msgno_inorder, { + "In-Order Indicator", "srt.msg.order", + FT_UINT32, BASE_DEC, + NULL, SRT_MSGNO_INORDER, NULL, HFILL}}, + + {&hf_srt_msgno_enctypes, { + "Encryption Status", "srt.msg.enc", + FT_UINT32, BASE_DEC, + VALS(srt_msgno_enctypes), SRT_MSGNO_ENCTYPE, NULL, HFILL }}, + + {&hf_srt_msgno_rexmit, { + "Sent as", "srt.msg.rexmit", + FT_BOOLEAN, 32, + TFS(&srt_msgno_rexmit), SRT_MSGNO_REXMIT, NULL, HFILL }}, + + {&hf_srt_timestamp, { + "Time Stamp", "srt.timestamp", + FT_UINT32, BASE_DEC_HEX, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_id, { + "Destination Socket ID", "srt.id", + FT_UINT32, BASE_HEX, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_ack_seqno, { + "ACKD_RCVLASTACK", "srt.ack_seqno", + FT_UINT32, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_ackno, { + "Ack Number", "srt.ackno", + FT_UINT32, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_rtt, { + "ACKD_RTT", "srt.rtt", + FT_UINT32, BASE_DEC | BASE_UNIT_STRING, + &units_microseconds, 0, NULL, HFILL}}, + + {&hf_srt_rttvar, { + "ACKD_RTTVAR", "srt.rttvar", + FT_UINT32, BASE_DEC | BASE_UNIT_STRING, + &units_microseconds, 0, NULL, HFILL}}, + + {&hf_srt_bufavail, { + "ACKD_BUFFERLEFT", "srt.rttvar", + FT_UINT32, BASE_DEC | BASE_UNIT_STRING, + &units_pkts, 0, NULL, HFILL}}, + + {&hf_srt_rate, { + "ACKD_RCVSPEED", "srt.rate", + FT_UINT32, BASE_DEC | BASE_UNIT_STRING, + &units_pkts_per_sec, 0, NULL, HFILL}}, + + {&hf_srt_bandwidth, { + "ACKD_BANDWIDTH", "srt.bw", + FT_UINT32, BASE_DEC | BASE_UNIT_STRING, + &units_pkts_per_sec, 0, NULL, HFILL}}, + + {&hf_srt_rcvrate, { + "ACKD_RCVRATE", "srt.rcvrate", + FT_UINT32, BASE_DEC | BASE_UNIT_STRING, + &units_byte_bytespsecond, 0, NULL, HFILL}}, + + {&hf_srt_handshake_version, { + "Handshake Version", "srt.hs.version", + FT_UINT32, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_type_v4, { + "(Legacy) Socket type", "srt.hs.socktype", + FT_UINT32, BASE_DEC, + VALS(srt_hsv4_socket_types), 0, NULL, + HFILL}}, + + {&hf_srt_handshake_enc_field_v5, { + "Encryption Field", "srt.hs.encfield", + FT_UINT16, BASE_HEX, + VALS(srt_handshake_enc_field), 0, NULL, + HFILL}}, + + {&hf_srt_handshake_ext_field_v5, { + "Extended Field", "srt.hs.extfield", + FT_UINT16, BASE_HEX, + NULL, 0, NULL, + HFILL}}, + + {&hf_srt_handshake_ext_field_v5_flag_hsreq, { + "HS_EXT_FIELD_HSREQ", "srt.hs.extfield.hsreq", + FT_BOOLEAN, SRT_HS_V5_EXT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_HS_V5_EXT_FIELD_HSREQ, + "Handshake request", + HFILL}}, + + {&hf_srt_handshake_ext_field_v5_flag_kmreq, { + "HS_EXT_FIELD_KMREQ", "srt.hs.extfield.kmreq", + FT_BOOLEAN, SRT_HS_V5_EXT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_HS_V5_EXT_FIELD_KMREQ, + "KM request", + HFILL}}, + + {&hf_srt_handshake_ext_field_v5_flag_config, { + "HS_EXT_FIELD_CONFIG", "srt.hs.extfield.config", + FT_BOOLEAN, SRT_HS_V5_EXT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_HS_V5_EXT_FIELD_CONFIG, + "Handshake has configuration", + HFILL}}, + + {&hf_srt_handshake_isn, { + "Initial Sequence Number", "srt.hs.isn", + FT_UINT32, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_mtu, { + "MTU", "srt.hs.mtu", + FT_UINT32, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_flow_window, { + "Flow Window", "srt.hs.flow_window", + FT_UINT32, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_reqtype, { + "Handshake Type", "srt.hs.reqtype", + FT_INT32, BASE_DEC, + VALS(srt_hs_request_types), 0, NULL, HFILL}}, + + {&hf_srt_handshake_id, { + "Socket ID", "srt.hs.id", + FT_UINT32, BASE_HEX, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_cookie, { + "SYN Cookie", "srt.hs.cookie", + FT_UINT32, BASE_HEX, + NULL, 0, NULL, HFILL}}, + {&hf_srt_handshake_peerip, { + /* FT_STRINGZ is used because the value + * is formatted to a temporary buffer first */ + "Peer IP Address", "srt.hs.peerip", + FT_STRINGZ, BASE_NONE, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_ext_version, { + "SRT Version", "srt.hs.version", + FT_UINT32, BASE_HEX, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_ext_flags, { + /* This uses custom format by appending the flag format string, + * while the value in hex is still printed. */ + "SRT Flags", "srt.hs.srtflags", + FT_UINT32, BASE_HEX, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_handshake_ext_flag_tsbpd_snd, { + "TSBPDSND", "srt.hs.srtflags.tsbpd_snd", + FT_BOOLEAN, SRT_OPT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_OPT_TSBPDSND, + "The party will be sending in TSBPD (Time Stamp Based Packet Delivery) mode", + HFILL}}, + + {&hf_srt_handshake_ext_flag_tsbpd_rcv, { + "TSBPDRCV", "srt.hs.srtflags.tsbpd_rcv", + FT_BOOLEAN, SRT_OPT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_OPT_TSBPDRCV, + "The party expects to receive in TSBPD (Time Stamp Based Packet Delivery) mode", + HFILL}}, + + {&hf_srt_handshake_ext_flag_haicrypt, { + "HAICRYPT", "srt.hs.srtflags.haicrypt", + FT_BOOLEAN, SRT_OPT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_OPT_HAICRYPT, + "The party includes haicrypt (legacy flag)", + HFILL}}, + + {&hf_srt_handshake_ext_flag_tlpkt_drop, { + "TLPKTDROP", "srt.hs.srtflags.tlpkt_drop", + FT_BOOLEAN, SRT_OPT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_OPT_TLPKTDROP, + "The party will do the Too-Late Packet Drop", + HFILL}}, + + {&hf_srt_handshake_ext_flag_nak_report, { + "NAKREPORT", "srt.hs.srtflags.nak_report", + FT_BOOLEAN, SRT_OPT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_OPT_NAKREPORT, + "The party will do periodic NAK reporting", + HFILL}}, + + {&hf_srt_handshake_ext_flag_rexmit, { + "REXMITFLG", "srt.hs.srtflags.rexmit", + FT_BOOLEAN, SRT_OPT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_OPT_REXMITFLG, + "The party uses the REXMIT flag", + HFILL}}, + + {&hf_srt_handshake_ext_flag_stream, { + "STREAM", "srt.hs.srtflags.stream", + FT_BOOLEAN, SRT_OPT_FIELD_LEN, TFS(&tfs_set_notset), + SRT_OPT_STREAM, + "The party uses stream type transmission", + HFILL}}, + + {&hf_srt_srths_blocktype, { + "SRT HS Extension type", "srt.hs.blocktype", + FT_UINT16, BASE_HEX, + VALS(srt_ctrlmsg_exttypes), 0, NULL, HFILL}}, + + {&hf_srt_srths_blocklen, { + "SRT HS Extension size (4-byte blocks)", "srt.hs.blocklen", + FT_UINT16, BASE_DEC, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_srths_agent_latency, { + "Latency", "srt.hs.agent_latency", + FT_UINT16, BASE_DEC | BASE_UNIT_STRING, + &units_milliseconds, 0, NULL, HFILL}}, + + {&hf_srt_srths_peer_latency, { + "Peer Latency", "srt.hs.peer_latency", + FT_UINT16, BASE_DEC | BASE_UNIT_STRING, + &units_milliseconds, 0, NULL, HFILL}}, + + {&hf_srt_srtkm_msg, { + "KMX Message (or KM State if 4 bytes)", "srt.km.msg", + FT_BYTES, BASE_NONE, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_srtkm_error, { + "KM State", "srt.km.error", + FT_UINT32, BASE_DEC, + VALS(srt_enc_kmstate), 0, NULL, HFILL}}, + + {&hf_srt_srths_sid, { + "Stream ID", "srt.hs.sid", + FT_STRING, BASE_NONE, + NULL, 0, NULL, HFILL}}, + + {&hf_srt_srths_conjestcontrol, { + "Congestion Control Type", "srt.hs.conjestctrl", + FT_STRING, BASE_NONE, + NULL, 0, NULL, HFILL}} + }; + + static gint *ett[] = { + &ett_srt, + &ett_srt_handshake_ext_flags, + &ett_srt_handshake_ext_field_flags + }; + + static ei_register_info ei[] = { + { &ei_srt_nak_seqno, + { "srt.nak_seqno", PI_SEQUENCE, PI_NOTE, + "Missing Sequence Number(s)", EXPFILL }}, + + { &ei_srt_hs_ext_hsreq_len, + { "srt.hs.ext.hsreq", PI_PROTOCOL, PI_WARN, + "Unknown HS Ext HSREQ length", EXPFILL }}, + + { &ei_srt_hs_ext_type, + { "srt.hs.ext.type", PI_PROTOCOL, PI_WARN, + "Unknown HS Ext Type", EXPFILL }}, + }; + + proto_srt = proto_register_protocol("SRT Protocol", "SRT", "srt"); + proto_register_field_array(proto_srt, hf, array_length(hf)); + proto_register_subtree_array(ett, array_length(ett)); + + expert_srt = expert_register_protocol(proto_srt); + expert_register_field_array(expert_srt, ei, array_length(ei)); + + register_dissector("srt", dissect_srt_udp, proto_srt); +} + + +void proto_reg_handoff_srt(void) +{ + srt_udp_handle = create_dissector_handle(dissect_srt_udp, proto_srt); + + /* register as heuristic dissector for UDP */ + heur_dissector_add("udp", dissect_srt_heur_udp, "SRT over UDP", + "srt_udp", proto_srt, HEURISTIC_ENABLE); + + /* Add a handle to the list of handles that *could* be used with this + table. That list is used by the "Decode As"/"-d" code in the UI. */ + dissector_add_for_decode_as("udp.port", srt_udp_handle); +} + + +/* + * 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: + */ -- cgit v1.2.3