Skip to content

Commit

Permalink
OpenVPN: rework detection (#2199)
Browse files Browse the repository at this point in the history
Close #1873
  • Loading branch information
IvanNardi authored Dec 6, 2023
1 parent ad20846 commit f74cf16
Show file tree
Hide file tree
Showing 69 changed files with 349 additions and 189 deletions.
3 changes: 1 addition & 2 deletions src/include/ndpi_typedefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1454,8 +1454,7 @@ struct ndpi_flow_struct {


/* NDPI_PROTOCOL_OPENVPN */
u_int8_t ovpn_session_id[8];
u_int8_t ovpn_counter;
u_int8_t ovpn_session_id[2][8];

/* NDPI_PROTOCOL_TINC */
u_int8_t tinc_state;
Expand Down
265 changes: 169 additions & 96 deletions src/lib/protocols/openvpn.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "ndpi_api.h"
#include "ndpi_private.h"


/*
* OpenVPN TCP / UDP Detection - 128/160 hmac
*
Expand All @@ -34,10 +35,6 @@
* - packet ID
* - session ID
*
* Two (good) packets are needed to perform detection.
* - First packet from client: save session ID
* - Second packet from server: report saved session ID
*
* TODO
* - Support PSK only mode (instead of TLS)
* - Support PSK + TLS mode (PSK used for early authentication)
Expand All @@ -46,37 +43,82 @@
*/

#define P_CONTROL_HARD_RESET_CLIENT_V1 (0x01 << 3)
#define P_CONTROL_HARD_RESET_CLIENT_V2 (0x07 << 3)
#define P_CONTROL_HARD_RESET_SERVER_V1 (0x02 << 3)
#define P_CONTROL_V1 (0x04 << 3)
#define P_ACK_V1 (0x05 << 3)
#define P_CONTROL_HARD_RESET_CLIENT_V2 (0x07 << 3)
#define P_CONTROL_HARD_RESET_SERVER_V2 (0x08 << 3)
#define P_CONTROL_HARD_RESET_CLIENT_V3 (0x0A << 3)
#define P_CONTROL_WKC_V1 (0x0B << 3)

#define P_OPCODE_MASK 0xF8
#define P_SHA1_HMAC_SIZE 20
#define P_HMAC_128 16 // (RSA-)MD5, (RSA-)MD4, ..others
#define P_HMAC_160 20 // (RSA-|DSA-)SHA(1), ..others, SHA1 is openvpn default
#define P_HMAC_NONE 0 // No HMAC
#define P_HARD_RESET_PACKET_ID_OFFSET(hmac_size) (9 + hmac_size)
#define P_PACKET_ID_ARRAY_LEN_OFFSET(hmac_size) (P_HARD_RESET_PACKET_ID_OFFSET(hmac_size) + 8)
#define P_HARD_RESET_CLIENT_MAX_COUNT 5

static
#ifndef WIN32
inline
#endif
u_int32_t get_packet_id(const u_int8_t * payload, u_int8_t hms) {
#define P_PACKET_ID_ARRAY_LEN_OFFSET(hmac_size) (P_HARD_RESET_PACKET_ID_OFFSET(hmac_size) + 8 * (!!(hmac_size)))



static int is_opcode_valid(u_int8_t opcode)
{
/* Ignore:
* P_DATA_V1/2: they don't have any (useful) info in the header
* P_CONTROL_SOFT_RESET_V1: it is used to key renegotiation -> it is not at the beginning of the session
*/
return opcode == P_CONTROL_HARD_RESET_CLIENT_V1 ||
opcode == P_CONTROL_HARD_RESET_SERVER_V1 ||
opcode == P_CONTROL_V1 ||
opcode == P_ACK_V1 ||
opcode == P_CONTROL_HARD_RESET_CLIENT_V2 ||
opcode == P_CONTROL_HARD_RESET_SERVER_V2 ||
opcode == P_CONTROL_HARD_RESET_CLIENT_V3 ||
opcode == P_CONTROL_WKC_V1;
}

static u_int32_t get_packet_id(const u_int8_t * payload, u_int8_t hms) {
return(ntohl(*(u_int32_t*)(payload + P_HARD_RESET_PACKET_ID_OFFSET(hms))));
}

static
#ifndef WIN32
inline
#endif
int8_t check_pkid_and_detect_hmac_size(const u_int8_t * payload) {
/* From wireshark */
/* We check the leading 4 byte of a suspected hmac for 0x00 bytes,
if more than 1 byte out of the 4 provided contains 0x00, the
hmac is considered not valid, which suggests that no tls auth is used.
unfortunatly there is no other way to detect tls auth on the fly */
static int check_for_valid_hmac(u_int32_t hmac)
{
int c = 0;

if((hmac & 0x000000FF) == 0x00000000)
c++;
if((hmac & 0x0000FF00) == 0x00000000)
c++;
if ((hmac & 0x00FF0000) == 0x00000000)
c++;
if ((hmac & 0xFF000000) == 0x00000000)
c++;
if (c > 1)
return 0;
return 1;
}

static int8_t detect_hmac_size(const u_int8_t *payload, int payload_len) {
// try to guess
if(get_packet_id(payload, P_HMAC_160) == 1)
if((payload_len >= P_HARD_RESET_PACKET_ID_OFFSET(P_HMAC_160) + 4) &&
get_packet_id(payload, P_HMAC_160) == 1)
return P_HMAC_160;

if(get_packet_id(payload, P_HMAC_128) == 1)
if((payload_len >= P_HARD_RESET_PACKET_ID_OFFSET(P_HMAC_128) + 4) &&
get_packet_id(payload, P_HMAC_128) == 1)
return P_HMAC_128;


/* Heuristic from Wireshark, to detect no-HMAC flows (i.e. tls-crypt) */
if(payload_len >= 14 &&
!(payload[9] > 0 &&
check_for_valid_hmac(ntohl(*(u_int32_t*)(payload + 9)))))
return P_HMAC_NONE;

return(-1);
}

Expand All @@ -90,89 +132,120 @@ static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct
int8_t hmac_size;
int8_t failed = 0;
/* No u_ */int16_t ovpn_payload_len = packet->payload_packet_len;
int dir = packet->packet_direction;

/* Detection:
* (1) server and client resets matching (via session id -> remote session id)
* (2) consecutive packets (in both directions) with the same session id
* (3) asymmetric traffic
*/

if(ovpn_payload_len < 14 + 2 * (packet->tcp != NULL)) {
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
return;
}

if(ovpn_payload_len >= 40) {
// skip openvpn TCP transport packet size
if(packet->tcp != NULL)
ovpn_payload += 2, ovpn_payload_len -= 2;;

opcode = ovpn_payload[0] & P_OPCODE_MASK;

if(packet->udp) {
#ifdef DEBUG
printf("[packet_id: %u][opcode: %u][Packet ID: %d][%u <-> %u][len: %u]\n",
flow->num_processed_pkts,
opcode, check_pkid_and_detect_hmac_size(ovpn_payload),
htons(packet->udp->source), htons(packet->udp->dest), ovpn_payload_len);
#endif

if(
(flow->num_processed_pkts == 1)
&& (
((ovpn_payload_len == 112)
&& ((opcode == 168) || (opcode == 192))
)
|| ((ovpn_payload_len == 80)
&& ((opcode == 184) || (opcode == 88) || (opcode == 160) || (opcode == 168) || (opcode == 200)))
)) {
NDPI_LOG_INFO(ndpi_struct,"found openvpn\n");
/* Skip openvpn TCP transport packet size */
if(packet->tcp != NULL)
ovpn_payload += 2, ovpn_payload_len -= 2;

opcode = ovpn_payload[0] & P_OPCODE_MASK;
if(!is_opcode_valid(opcode)) {
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
return;
}
/* Maybe a strong assumption... */
if((ovpn_payload[0] & ~P_OPCODE_MASK) != 0) {
NDPI_LOG_DBG2(ndpi_struct, "Invalid key id\n");
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
return;
}
if(flow->packet_direction_counter[dir] == 1 &&
!(opcode == P_CONTROL_HARD_RESET_CLIENT_V1 ||
opcode == P_CONTROL_HARD_RESET_CLIENT_V2 ||
opcode == P_CONTROL_HARD_RESET_SERVER_V1 ||
opcode == P_CONTROL_HARD_RESET_SERVER_V2 ||
opcode == P_CONTROL_HARD_RESET_CLIENT_V3)) {
NDPI_LOG_DBG2(ndpi_struct, "Invalid first packet\n");
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
return;
}
if(flow->packet_direction_counter[dir] == 1 &&
packet->tcp &&
ntohs(*(u_int16_t *)(packet->payload)) != ovpn_payload_len) {
NDPI_LOG_DBG2(ndpi_struct, "Invalid tcp len on reset\n");
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
return;
}

NDPI_LOG_DBG2(ndpi_struct, "[packets %d/%d][opcode: %u][len: %u]\n",
flow->packet_direction_counter[dir],
flow->packet_direction_counter[!dir],
opcode, ovpn_payload_len);

if(flow->packet_direction_counter[dir] > 1) {
if(memcmp(flow->ovpn_session_id[dir], ovpn_payload + 1, 8) != 0) {
NDPI_LOG_DBG2(ndpi_struct, "Invalid session id on two consecutive pkts in the same dir\n");
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
return;
}
if(flow->packet_direction_counter[dir] >= 2 &&
flow->packet_direction_counter[!dir] >= 2) {
/* (2) */
NDPI_LOG_INFO(ndpi_struct,"found openvpn (session ids match on both direction)\n");
ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_OPENVPN, NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI);
return;
}
if(flow->packet_direction_counter[dir] >= 4 &&
flow->packet_direction_counter[!dir] == 0) {
/* (3) */
NDPI_LOG_INFO(ndpi_struct,"found openvpn (asymmetric)\n");
ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_OPENVPN, NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI);
return;
}

if(flow->ovpn_counter < P_HARD_RESET_CLIENT_MAX_COUNT && (opcode == P_CONTROL_HARD_RESET_CLIENT_V1 ||
opcode == P_CONTROL_HARD_RESET_CLIENT_V2)) {
if(check_pkid_and_detect_hmac_size(ovpn_payload) > 0) {
memcpy(flow->ovpn_session_id, ovpn_payload+1, 8);

NDPI_LOG_DBG2(ndpi_struct,
"session key: %02x%02x%02x%02x%02x%02x%02x%02x\n",
flow->ovpn_session_id[0], flow->ovpn_session_id[1], flow->ovpn_session_id[2], flow->ovpn_session_id[3],
flow->ovpn_session_id[4], flow->ovpn_session_id[5], flow->ovpn_session_id[6], flow->ovpn_session_id[7]);
} else {
memcpy(flow->ovpn_session_id[dir], ovpn_payload + 1, 8);
NDPI_LOG_DBG2(ndpi_struct, "Session key [%d]: 0x%lx\n", dir,
ndpi_ntohll(*(u_int64_t *)flow->ovpn_session_id[dir]));
}

/* (1) */
if(flow->packet_direction_counter[!dir] > 0 &&
(opcode == P_CONTROL_HARD_RESET_SERVER_V1 ||
opcode == P_CONTROL_HARD_RESET_SERVER_V2)) {

hmac_size = detect_hmac_size(ovpn_payload, ovpn_payload_len);
NDPI_LOG_DBG2(ndpi_struct, "hmac size %d\n", hmac_size);
failed = 1;
if(hmac_size >= 0 &&
P_PACKET_ID_ARRAY_LEN_OFFSET(hmac_size) < ovpn_payload_len) {
u_int16_t offset = P_PACKET_ID_ARRAY_LEN_OFFSET(hmac_size);

alen = ovpn_payload[offset];

if(alen > 0) {
offset += 1 + alen * 4;

if((offset + 8) <= ovpn_payload_len) {
session_remote = &ovpn_payload[offset];

if(memcmp(flow->ovpn_session_id[!dir], session_remote, 8) == 0) {
NDPI_LOG_INFO(ndpi_struct,"found openvpn\n");
ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_OPENVPN, NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI);
return;
} else {
NDPI_LOG_DBG2(ndpi_struct, "key mismatch 0x%lx\n", ndpi_ntohll(*(u_int64_t *)session_remote));
}
}
} else {
failed = 0; /* Server reset without remote session id field */
}
} else if(flow->ovpn_counter >= 1 && flow->ovpn_counter <= P_HARD_RESET_CLIENT_MAX_COUNT &&
(opcode == P_CONTROL_HARD_RESET_SERVER_V1 || opcode == P_CONTROL_HARD_RESET_SERVER_V2)) {

hmac_size = check_pkid_and_detect_hmac_size(ovpn_payload);

if(hmac_size > 0) {
u_int16_t offset = P_PACKET_ID_ARRAY_LEN_OFFSET(hmac_size);

alen = ovpn_payload[offset];

if (alen > 0) {
offset += 1 + alen * 4;

if((offset+8) <= ovpn_payload_len) {
session_remote = &ovpn_payload[offset];

if(memcmp(flow->ovpn_session_id, session_remote, 8) == 0) {
NDPI_LOG_INFO(ndpi_struct,"found openvpn\n");
ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_OPENVPN, NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI);
return;
} else {
NDPI_LOG_DBG2(ndpi_struct,
"key mismatch: %02x%02x%02x%02x%02x%02x%02x%02x\n",
session_remote[0], session_remote[1], session_remote[2], session_remote[3],
session_remote[4], session_remote[5], session_remote[6], session_remote[7]);
failed = 1;
}
} else
failed = 1;
} else
failed = 1;
} else
failed = 1;
} else
failed = 1;

flow->ovpn_counter++;

if(failed)
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
}
}

if(failed)
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);

if(flow->packet_counter > 5)
NDPI_EXCLUDE_PROTO(ndpi_struct, flow);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/cfgs/caches_cfg/result/ookla.pcap.out
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Guessed flow protos: 1
DPI Packets (TCP): 40 (6.67 pkts/flow)
Confidence Match by port : 1 (flows)
Confidence DPI : 5 (flows)
Num dissector calls: 528 (88.00 diss/flow)
Num dissector calls: 527 (87.83 diss/flow)
LRU cache ookla: 0/0/0 (insert/search/found)
LRU cache bittorrent: 0/3/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
Binary file added tests/cfgs/default/pcap/openvpn-tlscrypt.pcap
Binary file not shown.
Binary file modified tests/cfgs/default/pcap/openvpn.pcap
Binary file not shown.
Binary file added tests/cfgs/default/pcap/openvpn_nohmac.pcapng
Binary file not shown.
Binary file added tests/cfgs/default/pcap/openvpn_nohmac_tcp.pcapng
Binary file not shown.
2 changes: 1 addition & 1 deletion tests/cfgs/default/result/1kxun.pcap.out
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DPI Packets (UDP): 120 (1.21 pkts/flow)
Confidence Unknown : 14 (flows)
Confidence Match by port : 6 (flows)
Confidence DPI : 177 (flows)
Num dissector calls: 4628 (23.49 diss/flow)
Num dissector calls: 4626 (23.48 diss/flow)
LRU cache ookla: 0/0/0 (insert/search/found)
LRU cache bittorrent: 0/60/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
2 changes: 1 addition & 1 deletion tests/cfgs/default/result/6in6tunnel.pcap.out
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Guessed flow protos: 1

DPI Packets (UDP): 2 (2.00 pkts/flow)
Confidence Unknown : 1 (flows)
Num dissector calls: 137 (137.00 diss/flow)
Num dissector calls: 136 (136.00 diss/flow)
LRU cache ookla: 0/0/0 (insert/search/found)
LRU cache bittorrent: 0/3/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
2 changes: 1 addition & 1 deletion tests/cfgs/default/result/EAQ.pcap.out
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Guessed flow protos: 0
DPI Packets (TCP): 12 (6.00 pkts/flow)
DPI Packets (UDP): 116 (4.00 pkts/flow)
Confidence DPI : 31 (flows)
Num dissector calls: 4687 (151.19 diss/flow)
Num dissector calls: 4629 (149.32 diss/flow)
LRU cache ookla: 0/0/0 (insert/search/found)
LRU cache bittorrent: 0/0/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
2 changes: 1 addition & 1 deletion tests/cfgs/default/result/KakaoTalk_talk.pcap.out
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DPI Packets (UDP): 10 (2.00 pkts/flow)
Confidence Match by port : 8 (flows)
Confidence DPI : 11 (flows)
Confidence Match by IP : 1 (flows)
Num dissector calls: 1140 (57.00 diss/flow)
Num dissector calls: 1139 (56.95 diss/flow)
LRU cache ookla: 0/2/0 (insert/search/found)
LRU cache bittorrent: 0/27/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
2 changes: 1 addition & 1 deletion tests/cfgs/default/result/anyconnect-vpn.pcap.out
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ DPI Packets (other): 10 (1.00 pkts/flow)
Confidence Unknown : 2 (flows)
Confidence Match by port : 6 (flows)
Confidence DPI : 61 (flows)
Num dissector calls: 857 (12.42 diss/flow)
Num dissector calls: 851 (12.33 diss/flow)
LRU cache ookla: 0/0/0 (insert/search/found)
LRU cache bittorrent: 0/24/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
2 changes: 1 addition & 1 deletion tests/cfgs/default/result/cassandra.pcap.out
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Guessed flow protos: 0

DPI Packets (TCP): 16 (8.00 pkts/flow)
Confidence DPI : 2 (flows)
Num dissector calls: 314 (157.00 diss/flow)
Num dissector calls: 312 (156.00 diss/flow)
LRU cache ookla: 0/0/0 (insert/search/found)
LRU cache bittorrent: 0/0/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Guessed flow protos: 0

DPI Packets (UDP): 256 (1.04 pkts/flow)
Confidence DPI : 245 (flows)
Num dissector calls: 20467 (83.54 diss/flow)
Num dissector calls: 20463 (83.52 diss/flow)
LRU cache ookla: 0/0/0 (insert/search/found)
LRU cache bittorrent: 0/513/0 (insert/search/found)
LRU cache zoom: 0/0/0 (insert/search/found)
Expand Down
Loading

0 comments on commit f74cf16

Please sign in to comment.