diff options
author | Nardi Ivan <nardi.ivan@gmail.com> | 2020-08-20 18:26:12 +0200 |
---|---|---|
committer | Nardi Ivan <nardi.ivan@gmail.com> | 2020-08-21 22:04:55 +0200 |
commit | 23ec82b59dd4757508d9d99db05537d1278dd7d1 (patch) | |
tree | 7e60d6bbbf9a15fe02913063fda184d542c73411 /src | |
parent | fef199ad450d451d88d143d395dfcfd7906deefc (diff) |
Major rework of QUIC dissector
Improve support for GQUIC (up to Q046) and add support for Q050 and (IETF-)QUIC
Still no sub-classification for Q050 and QUIC
Diffstat (limited to 'src')
-rw-r--r-- | src/include/ndpi_api.h.in | 4 | ||||
-rw-r--r-- | src/lib/ndpi_serializer.c | 4 | ||||
-rw-r--r-- | src/lib/protocols/quic.c | 546 |
3 files changed, 438 insertions, 116 deletions
diff --git a/src/include/ndpi_api.h.in b/src/include/ndpi_api.h.in index e5d2ffad3..1141e054c 100644 --- a/src/include/ndpi_api.h.in +++ b/src/include/ndpi_api.h.in @@ -930,6 +930,10 @@ extern "C" { int ndpi_ptree_match_addr(ndpi_ptree_t *tree, const ndpi_ip_addr_t *addr, u_int32_t *user_data); void ndpi_ptree_destroy(ndpi_ptree_t *tree); + /* General purpose utilities */ + u_int64_t ndpi_htonll(u_int64_t v); + u_int64_t ndpi_ntohll(u_int64_t v); + /* DGA */ int ndpi_check_dga_name(struct ndpi_detection_module_struct *ndpi_str, struct ndpi_flow_struct *flow, diff --git a/src/lib/ndpi_serializer.c b/src/lib/ndpi_serializer.c index 87b2c06a8..83365e58c 100644 --- a/src/lib/ndpi_serializer.c +++ b/src/lib/ndpi_serializer.c @@ -40,7 +40,7 @@ /* ********************************** */ -static u_int64_t ndpi_htonll(u_int64_t v) { +u_int64_t ndpi_htonll(u_int64_t v) { union { u_int32_t lv[2]; u_int64_t llv; } u; u.lv[0] = htonl(v >> 32); @@ -51,7 +51,7 @@ static u_int64_t ndpi_htonll(u_int64_t v) { /* ********************************** */ -static u_int64_t ndpi_ntohll(u_int64_t v) { +u_int64_t ndpi_ntohll(u_int64_t v) { union { u_int32_t lv[2]; u_int64_t llv; } u; u.llv = v; diff --git a/src/lib/protocols/quic.c b/src/lib/protocols/quic.c index 6beac5443..dbbcb6c9f 100644 --- a/src/lib/protocols/quic.c +++ b/src/lib/protocols/quic.c @@ -27,142 +27,460 @@ #endif #include "ndpi_protocol_ids.h" - #define NDPI_CURRENT_PROTO NDPI_PROTOCOL_QUIC - #include "ndpi_api.h" -static int quic_ports(u_int16_t sport, u_int16_t dport) -{ - if ((sport == 443 || dport == 443 || sport == 80 || dport == 80) && - (sport != 123 && dport != 123)) - return 1; +/* This dissector handles GQUIC and IETF-QUIC both. + Main references: + * https://groups.google.com/a/chromium.org/g/proto-quic/c/wVHBir-uRU0?pli=1 + * https://groups.google.com/a/chromium.org/g/proto-quic/c/OAVgFqw2fko/m/jCbjP0AVAAAJ + * https://groups.google.com/a/chromium.org/g/proto-quic/c/OAVgFqw2fko/m/-NYxlh88AgAJ + * https://docs.google.com/document/d/1FcpCJGTDEMblAs-Bm5TYuqhHyUqeWpqrItw2vkMFsdY/edit + * https://tools.ietf.org/html/draft-ietf-quic-tls-29 + * https://tools.ietf.org/html/draft-ietf-quic-transport-29 +*/ + +/* Versions */ +#define V_Q024 0x51303234 +#define V_Q025 0x51303235 +#define V_Q030 0x51303330 +#define V_Q033 0x51303333 +#define V_Q034 0x51303334 +#define V_Q035 0x51303335 +#define V_Q037 0x51303337 +#define V_Q039 0x51303339 +#define V_Q043 0x51303433 +#define V_Q046 0x51303436 +#define V_Q050 0x51303530 +#define V_MVFST_22 0xfaceb001 +#define V_MVFST_27 0xfaceb002 + +#define QUIC_MAX_CID_LENGTH 20 + +static int is_version_gquic(uint32_t version) +{ + return ((version & 0xFFFFFF00) == 0x51303500) /* Q05X */ || + ((version & 0xFFFFFF00) == 0x51303400) /* Q04X */ || + ((version & 0xFFFFFF00) == 0x51303300) /* Q03X */ || + ((version & 0xFFFFFF00) == 0x51303200) /* Q02X */; +} +static int is_version_quic(uint32_t version) +{ + return ((version & 0xFFFFFF00) == 0xFF000000) /* IETF */ || + ((version & 0xFFFFF000) == 0xfaceb000) /* Facebook */; +} +static int is_version_valid(uint32_t version) +{ + return is_version_gquic(version) || is_version_quic(version); +} +static uint8_t get_u8_quic_ver(uint32_t version) +{ + if((version >> 8) == 0xff0000) + return (uint8_t)version; return 0; } +static int is_quic_ver_greater_than(uint32_t version, uint8_t min_version) +{ + return get_u8_quic_ver(version) >= min_version; +} +static uint8_t get_u8_gquic_ver(uint32_t version) +{ + if(is_version_gquic(version)) { + version = ntohl(((uint16_t)version) << 16); + return atoi((char *)&version); + } + return 0; +} +static int is_gquic_ver_less_than(uint32_t version, uint8_t max_version) +{ + uint8_t u8_ver = get_u8_gquic_ver(version); + return u8_ver && u8_ver <= max_version; +} +static int is_version_supported(uint32_t version) +{ + return (version == V_Q024 || + version == V_Q025 || + version == V_Q030 || + version == V_Q033 || + version == V_Q034 || + version == V_Q035 || + version == V_Q037 || + version == V_Q039 || + version == V_Q043 || + version == V_Q046 || + version == V_Q050 || + version == V_MVFST_22 || + version == V_MVFST_27 || + is_quic_ver_greater_than(version, 23)); +} -/* ***************************************************************** */ - -static int quic_len(u_int8_t l) { - switch(l) { +static int quic_len(const uint8_t *buf, uint64_t *value) +{ + *value = buf[0]; + switch((*value) >> 6) { case 0: - return(1); - break; + (*value) &= 0x3F; + return 1; case 1: - return(2); - break; + *value = ntohs(*(uint16_t *)buf) & 0x3FFF; + return 2; case 2: - return(4); - break; + *value = ntohl(*(uint32_t *)buf) & 0x3FFFFFFF; + return 4; case 3: - return(8); - break; + *value = ndpi_ntohll(*(uint64_t *)buf) & 0x3FFFFFFFFFFFFFFF; + return 8; + default: /* No Possible */ + return 0; } +} - return(0); /* NOTREACHED */ +static uint16_t gquic_get_u16(const uint8_t *buf, uint32_t version) +{ + if(version >= V_Q039) + return ntohs(*(uint16_t *)buf); + return (*(uint16_t *)buf); } -/* ***************************************************************** */ -void ndpi_search_quic(struct ndpi_detection_module_struct *ndpi_struct, - struct ndpi_flow_struct *flow) { - struct ndpi_packet_struct *packet = &flow->packet; - u_int32_t udp_len = packet->payload_packet_len; - u_int version_len = ((packet->payload[0] & 0x01) == 0) ? 0 : 4; - u_int cid_len = quic_len((packet->payload[0] & 0x0C) >> 2); - u_int seq_len = quic_len((packet->payload[0] & 0x30) >> 4); - u_int quic_hlen = 1 /* flags */ + version_len + seq_len + cid_len; - - NDPI_LOG_DBG(ndpi_struct, "search QUIC\n"); - - if(packet->udp != NULL - && (udp_len > (quic_hlen+4 /* QXXX */)) - // && ((packet->payload[0] & 0xC2) == 0x00) - && (quic_ports(ntohs(packet->udp->source), ntohs(packet->udp->dest))) - ) { - int i; - - if((packet->payload[1] == 'Q') - && (packet->payload[2] == '0') - && (packet->payload[3] == '4') - && (packet->payload[4] == '6')) { - - /* - TODO: Better handle Q046 - https://tools.ietf.org/html/draft-ietf-quic-invariants-04 - */ - quic_hlen = 18; - } else { - u_int16_t potential_stun_len = ntohs((*((u_int16_t*)&packet->payload[2]))); - - if((version_len > 0) && (packet->payload[1+cid_len] != 'Q')) - goto no_quic; - - if((version_len == 0) && ((packet->payload[0] & 0xC3 /* ignore CID len/packet number */) != 0)) - goto no_quic; - - - /* Heuristic to see if this packet could be a STUN packet */ - if((potential_stun_len /* STUN message len */ < udp_len) - && ((potential_stun_len+25 /* Attribute header overhead we assume is max */) /* STUN message len */ > udp_len)) - return; /* This could be STUN, let's skip this packet */ - - NDPI_LOG_INFO(ndpi_struct, "found QUIC\n"); - ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_QUIC, NDPI_PROTOCOL_UNKNOWN); - - if((udp_len > quic_hlen + 12) && (packet->payload[quic_hlen+12] != 0xA0)) - quic_hlen++; +static const uint8_t *get_crypto_data(struct ndpi_detection_module_struct *ndpi_struct, + struct ndpi_flow_struct *flow, + uint32_t version, + u_int8_t *clear_payload, uint32_t clear_payload_len, + uint64_t *crypto_data_len) +{ + const u_int8_t *crypto_data; + uint32_t counter; + uint8_t first_nonzero_payload_byte, offset_len; + uint64_t unused; + + counter = 0; + while(clear_payload[counter] == 0 && counter < clear_payload_len) + counter += 1; + if(counter >= clear_payload_len) + return NULL; + first_nonzero_payload_byte = clear_payload[counter]; + NDPI_LOG_DBG2(ndpi_struct, "first_nonzero_payload_byte 0x%x\n", first_nonzero_payload_byte); + if(is_gquic_ver_less_than(version, 46)) { + if(first_nonzero_payload_byte == 0x40 || + first_nonzero_payload_byte == 0x60) { + /* Probably an ACK/NACK frame: this CHLO is not the first one but try + decoding it nonetheless */ + counter += (first_nonzero_payload_byte == 0x40) ? 6 : 9; + if(counter >= clear_payload_len) + return NULL; + first_nonzero_payload_byte = clear_payload[counter]; } - - if(udp_len > (quic_hlen + 16 + 4)) { - if(!strncmp((char*)&packet->payload[quic_hlen+16], "CHLO" /* Client Hello */, 4)) { - /* Check if SNI (Server Name Identification) is present */ - for(i=quic_hlen+12; i<udp_len-3; i++) { - if((packet->payload[i] == 'S') - && (packet->payload[i+1] == 'N') - && (packet->payload[i+2] == 'I') - && (packet->payload[i+3] == 0)) { - u_int32_t offset = (*((u_int32_t*)&packet->payload[i+4])); - u_int32_t prev_offset = (*((u_int32_t*)&packet->payload[i-4])); - - if(offset > prev_offset) { - u_int32_t len = offset - prev_offset; - u_int32_t sni_offset = i+prev_offset+1; - - if(len < udp_len) { - while((sni_offset < udp_len) && (packet->payload[sni_offset] == '-')) - sni_offset++; - - if((sni_offset+len) < udp_len) { - u_int32_t max_len = sizeof(flow->host_server_name)-1, j = 0; - ndpi_protocol_match_result ret_match; - - if(len > max_len) len = max_len; - - while((len > 0) && (sni_offset < udp_len)) { - flow->host_server_name[j++] = packet->payload[sni_offset]; - sni_offset++, len--; - } - - ndpi_match_host_subprotocol(ndpi_struct, flow, - (char *)flow->host_server_name, - strlen((const char*)flow->host_server_name), - &ret_match, - NDPI_PROTOCOL_QUIC); - } - - break; - } - } - } - } + if((first_nonzero_payload_byte != 0xA0) && + (first_nonzero_payload_byte != 0xA4)) { + NDPI_LOG_DBG(ndpi_struct, "Unexpected frame 0x%x version 0x%x\n",\ + first_nonzero_payload_byte, version); + return NULL; + } + offset_len = (first_nonzero_payload_byte & 0x1C) >> 2; + if(offset_len > 0) + offset_len += 1; + if(counter + 2 + offset_len + 2 /*gquic_get_u16 reads 2 bytes */ > clear_payload_len) + return NULL; + if(clear_payload[counter + 1] != 0x01) { + NDPI_LOG_ERR(ndpi_struct, "Unexpected stream ID version 0x%x\n", version); + return NULL; + } + counter += 2 + offset_len; + *crypto_data_len = gquic_get_u16(&clear_payload[counter], version); + counter += 2; + crypto_data = &clear_payload[counter]; + + } else if(version == V_Q050) { + if(first_nonzero_payload_byte == 0x40 || + first_nonzero_payload_byte == 0x60) { + /* Probably an ACK/NACK frame: this CHLO is not the first one but try + decoding it nonetheless */ + counter += (first_nonzero_payload_byte == 0x40) ? 6 : 9; + if(counter >= clear_payload_len) + return NULL; + first_nonzero_payload_byte = clear_payload[counter]; + } + if(first_nonzero_payload_byte != 0x08) { + NDPI_LOG_DBG(ndpi_struct, "Unexpected frame 0x%x\n", first_nonzero_payload_byte); + return NULL; + } + counter += 1; + if(counter + 8 + 8 >= clear_payload_len) /* quic_len reads 8 bytes, at most */ + return NULL; + counter += quic_len(&clear_payload[counter], &unused); + counter += quic_len(&clear_payload[counter], crypto_data_len); + crypto_data = &clear_payload[counter]; + + } else { /* All other versions */ + if(first_nonzero_payload_byte != 0x06) { + if(first_nonzero_payload_byte != 0x02 && + first_nonzero_payload_byte != 0x1C) { + NDPI_LOG_ERR(ndpi_struct, "Unexpected frame 0x%x\n", first_nonzero_payload_byte); + } else { + NDPI_LOG_DBG(ndpi_struct, "Unexpected ACK/CC frame\n"); } + return NULL; + } + if(counter + 2 + 8 >= clear_payload_len) /* quic_len reads 8 bytes, at most */ + return NULL; + if(clear_payload[counter + 1] != 0x00) { + NDPI_LOG_ERR(ndpi_struct, "Unexpected crypto stream offset 0x%x\n", + clear_payload[counter + 1]); + return NULL; } + counter += 2; + counter += quic_len(&clear_payload[counter], crypto_data_len); + crypto_data = &clear_payload[counter]; + } + + if(*crypto_data_len + counter > clear_payload_len) { + NDPI_LOG_ERR(ndpi_struct, "Invalid length %lu + %d > %d version 0x%x\n", + *crypto_data_len, counter, clear_payload_len, version); + return NULL; + } + return crypto_data; +} + +static uint8_t *get_clear_payload(struct ndpi_detection_module_struct *ndpi_struct, + struct ndpi_flow_struct *flow, + uint32_t version, uint32_t *clear_payload_len) +{ + struct ndpi_packet_struct *packet = &flow->packet; + u_int8_t *clear_payload; + u_int8_t dest_conn_id_len, source_conn_id_len; + + if(is_gquic_ver_less_than(version, 43)) { + clear_payload = (uint8_t *)&packet->payload[26]; + *clear_payload_len = packet->payload_packet_len - 26; + /* Skip Private-flag field for version for < Q34 */ + if(is_gquic_ver_less_than(version, 33)) { + clear_payload += 1; + (*clear_payload_len) -= 1; + } + } else if(version == V_Q046) { + if(packet->payload[5] != 0x50) { + NDPI_LOG_DBG(ndpi_struct, "Q46 invalid conn id len 0x%x\n", + packet->payload[5]); + return NULL; + } + clear_payload = (uint8_t *)&packet->payload[30]; + *clear_payload_len = packet->payload_packet_len - 30; + } else { + dest_conn_id_len = packet->payload[5]; + if(dest_conn_id_len == 0 || + dest_conn_id_len > QUIC_MAX_CID_LENGTH) { + NDPI_LOG_DBG(ndpi_struct, "Packet 0x%x with dest_conn_id_len %d\n", + version, dest_conn_id_len); + return NULL; + } + source_conn_id_len = packet->payload[6 + dest_conn_id_len]; + if(source_conn_id_len > QUIC_MAX_CID_LENGTH) { + NDPI_LOG_DBG(ndpi_struct, "Packet 0x%x with source_conn_id_len %d\n", + version, source_conn_id_len); + return NULL; + } + /* TODO */ + clear_payload = NULL; + } + + return clear_payload; +} + +static void process_chlo(struct ndpi_detection_module_struct *ndpi_struct, + struct ndpi_flow_struct *flow, + const u_int8_t *crypto_data, uint32_t crypto_data_len) +{ + const uint8_t *tag; + uint32_t i; + uint16_t num_tags; + uint32_t prev_offset; + uint32_t tag_offset_start, offset, len, sni_len; + ndpi_protocol_match_result ret_match; + + if(crypto_data_len < 6) + return; + if(memcmp(crypto_data, "CHLO", 4) != 0) { + NDPI_LOG_ERR(ndpi_struct, "Unexpected handshake message"); + return; + } + num_tags = (*(uint16_t *)&crypto_data[4]); + + tag_offset_start = 8 + 8 * num_tags; + prev_offset = 0; + for(i = 0; i < num_tags; i++) { + if(8 + 8 * i + 8 >= crypto_data_len) + break; + tag = &crypto_data[8 + 8 * i]; + offset = *((u_int32_t *)&crypto_data[8 + 8 * i + 4]); + if(prev_offset > offset) + break; + len = offset - prev_offset; + if(tag_offset_start + prev_offset + len > crypto_data_len) + break; +#if 0 + printf("crypto_data_len %u prev_offset %u offset %u len %d\n", + crypto_data_len, prev_offset, offset, len); +#endif + if((memcmp(tag, "SNI\0", 4) == 0) && + (tag_offset_start + prev_offset + len < crypto_data_len)) { + sni_len = MIN(len, sizeof(flow->host_server_name) - 1); + memcpy(flow->host_server_name, + &crypto_data[tag_offset_start + prev_offset], sni_len); + + NDPI_LOG_DBG2(ndpi_struct, "SNI: [%s]\n", flow->host_server_name); + + ndpi_match_host_subprotocol(ndpi_struct, flow, + (char *)flow->host_server_name, + strlen((const char*)flow->host_server_name), + &ret_match, NDPI_PROTOCOL_QUIC); + return; + } + + prev_offset = offset; + } + if(i != num_tags) + NDPI_LOG_DBG(ndpi_struct, "Something went wrong in tags iteration\n"); +} + + +static int may_be_initial_pkt(struct ndpi_detection_module_struct *ndpi_struct, + struct ndpi_flow_struct *flow, + uint32_t *version) +{ + struct ndpi_packet_struct *packet = &flow->packet; + u_int8_t first_byte; + u_int8_t pub_bit1, pub_bit2, pub_bit3, pub_bit4, pub_bit5, pub_bit7, pub_bit8; + + /* According to draft-ietf-quic-transport-29: "Clients MUST ensure that UDP + datagrams containing Initial packets have UDP payloads of at least 1200 + bytes". Similar limit exists for previous versions */ + if(packet->payload_packet_len < 1200) { + return 0; + } + + first_byte = packet->payload[0]; + pub_bit1 = ((first_byte & 0x80) != 0); + pub_bit2 = ((first_byte & 0x40) != 0); + pub_bit3 = ((first_byte & 0x20) != 0); + pub_bit4 = ((first_byte & 0x10) != 0); + pub_bit5 = ((first_byte & 0x08) != 0); + pub_bit7 = ((first_byte & 0x02) != 0); + pub_bit8 = ((first_byte & 0x01) != 0); + + *version = 0; + if(pub_bit1) { + *version = ntohl(*((u_int32_t *)&packet->payload[1])); + } else if(pub_bit5 && !pub_bit2) { + if(!pub_bit8) { + NDPI_LOG_DBG2(ndpi_struct, "Packet without version\n") + } else { + *version = ntohl(*((u_int32_t *)&packet->payload[9])); + } + } + if(!is_version_valid(*version)) { + NDPI_LOG_DBG2(ndpi_struct, "Invalid version 0x%x\n", *version); + return 0; + } + + if(is_gquic_ver_less_than(*version, 43) && + (!pub_bit5 || pub_bit3 != 0 || pub_bit4 != 0)) { + NDPI_LOG_ERR(ndpi_struct, "Version 0x%x invalid flags 0x%x\n", + *version, first_byte); + return 0; + } + if((*version == V_Q046) && + (pub_bit7 != 1 || pub_bit8 != 1)) { + NDPI_LOG_ERR(ndpi_struct, "Q46 invalid flag 0x%x\n", first_byte); + return 0; + } + if((is_version_quic(*version) || (*version == V_Q046) || (*version == V_Q050)) && + (pub_bit3 != 0 || pub_bit4 != 0)) { + NDPI_LOG_DBG2(ndpi_struct, "Version 0x%x not Initial Packet\n", *version); + return 0; + } + + /* TODO: add some other checks to avoid false positives */ + + return 1; +} + +/* ***************************************************************** */ + +void ndpi_search_quic(struct ndpi_detection_module_struct *ndpi_struct, + struct ndpi_flow_struct *flow) +{ + u_int32_t version; + u_int8_t *clear_payload; + uint32_t clear_payload_len; + const u_int8_t *crypto_data; + uint64_t crypto_data_len; + int is_quic; + + NDPI_LOG_DBG2(ndpi_struct, "search QUIC\n"); + + /* Buffers: packet->payload ---> clear_payload ---> crypto_data */ + + /* + * 1) (Very) basic heuristic to check if it is a QUIC packet. + * The first packet of each QUIC session should contain a valid + * CHLO/ClientHello message and we need (only) it to sub-classify + * the flow. + * Detecting QUIC sessions where the first captured packet is not a + * CHLO/CH is VERY hard. Let's try avoiding it and let's see if + * anyone complains... + */ + + is_quic = may_be_initial_pkt(ndpi_struct, flow, &version); + if(!is_quic) { + NDPI_EXCLUDE_PROTO(ndpi_struct, flow); + return; + } + + /* + * 2) Ok, this packet seems to be QUIC + */ + + NDPI_LOG_INFO(ndpi_struct, "found QUIC\n"); + ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_QUIC, NDPI_PROTOCOL_UNKNOWN); + + /* + * 3) Skip not supported versions + */ + + if(!is_version_supported(version)) { + NDPI_LOG_ERR(ndpi_struct, "Unsupported version 0x%x\n", version) + NDPI_EXCLUDE_PROTO(ndpi_struct, flow); return; } - no_quic: - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); + /* + * 4) Extract the Payload from Initial Packets + */ + clear_payload = get_clear_payload(ndpi_struct, flow, version, &clear_payload_len); + if(!clear_payload) { + NDPI_EXCLUDE_PROTO(ndpi_struct, flow); + return; + } + + /* + * 5) Extract Crypto Data from the Payload + */ + crypto_data = get_crypto_data(ndpi_struct, flow, version, + clear_payload, clear_payload_len, + &crypto_data_len); + if(!crypto_data) { + NDPI_EXCLUDE_PROTO(ndpi_struct, flow); + return; + } + + /* + * 6) Process ClientHello/CHLO from the Crypto Data + */ + if(is_version_gquic(version)) { + process_chlo(ndpi_struct, flow, crypto_data, crypto_data_len); + } } /* ***************************************************************** */ |