blob: 3155a737443d2a2f4b7e50c5ea0c15be35de5783 [file] [log] [blame]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UNIT_TEST
// Include vmlinux.h at first to declare all kernel types.
#include "include/patchpanel/vmlinux/vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#else
#include "patchpanel/bpf/unit_test_utils.h"
#endif // UNIT_TEST
// The return values used in this program. In the context of iptables, a
// non-zero return value means a match.
#define RET_IPTABLES_MATCHED (1)
#define RET_IPTABLES_NOT_MATCHED (0)
// We cannot include some kernel headers directly since it may cause a conflict
// with vmlinux.h. Define some consts directly here.
#define ETH_HLEN 14
#define ETH_P_IP 0x0800
#define ETH_P_IPV6 0x86DD
#define IPPROTO_UDP 17
#ifndef UNIT_TEST
// This is a HACK on 4.14 kernel: bpf_skb_load_bytes_relative() does not exist
// before 4.18 kernels. We define the constant used by it here just to make the
// compilation working, and this program won't be loaded on that kernel.
// TODO(b/311751709#comment4): Remove this after M122.
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 18, 0)
#define BPF_HDR_START_NET (1)
#endif
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 20, 0)
// A partial copy of the definition in kernel/v4.19/include/uapi/linux/bpf.h.
// This is a HACK on 4.19 kernel: the vmlinux.h above doesn't contain the
// definition of this struct, and we also cannot include the header directly due
// to the reason mentioned above. Note that eBPF is not enabled on 4.19 kernel,
// so we haven't verified and also don't need to verify if this code works.
struct __sk_buff {
u32 len;
u32 pkt_type;
u32 mark;
u32 queue_mapping;
u32 protocol;
// There are more fields, but we don't care them in the current code.
};
#endif
#endif // UNIT_TEST
const char LICENSE[] SEC("license") = "Dual BSD/GPL";
// Notes for the implementation in this file:
// - For the helper functions, they must be marked as
// `__attribute__((always_inline))`, otherwise the program cannot be loaded by
// libbpf.
// - bpf_skb_load_bytes_relative() will return an error if we try to access an
// invalid index in the packet contents, so we don't need to do the
// out-of-bounds check before calling it.
// Assumes the packet is an IPv4 packet, parses the payload and returns the
// matching result (RET_IPTABLES_MATCHED or RET_IPTABLES_NOT_MATCHED).
static __attribute__((always_inline)) int handle_ipv4(struct __sk_buff* skb);
// Assumes the packet is an IPv6 packet, parses the payload and returns the
// matching result (RET_IPTABLES_MATCHED or RET_IPTABLES_NOT_MATCHED).
static __attribute__((always_inline)) int handle_ipv6(struct __sk_buff* skb);
// Assumes the packet is an UDP packet and the UDP header starts from
// `base_offset_to_net`, parses the payload and returns the matching result
// (RET_IPTABLES_MATCHED or RET_IPTABLES_NOT_MATCHED).
static __attribute__((always_inline)) int handle_udp(struct __sk_buff* skb,
u32 base_offset_to_net);
// Assumes the packet is an DTLS packet and the DTLS contents starts from
// `base_offset_to_net`, parses the payload and returns the matching result
// (RET_IPTABLES_MATCHED or RET_IPTABLES_NOT_MATCHED).
static __attribute__((always_inline)) int handle_dtls(struct __sk_buff* skb,
u32 base_offset_to_net);
static int handle_ipv4(struct __sk_buff* skb) {
u8 proto;
if (bpf_skb_load_bytes_relative(skb, offsetof(struct iphdr, protocol), &proto,
sizeof(proto), BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
if (proto == IPPROTO_UDP) {
return handle_udp(skb, sizeof(struct iphdr));
}
return RET_IPTABLES_NOT_MATCHED;
}
static int handle_ipv6(struct __sk_buff* skb) {
u8 proto;
if (bpf_skb_load_bytes_relative(skb, offsetof(struct ipv6hdr, nexthdr),
&proto, sizeof(proto),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
if (proto == IPPROTO_UDP) {
return handle_udp(skb, sizeof(struct ipv6hdr));
}
return RET_IPTABLES_NOT_MATCHED;
}
// The packet is a UDP packet and the UDP header start from
// `base_offset_to_net`.
static int handle_udp(struct __sk_buff* skb, u32 base_offset_to_net) {
u32 offset = base_offset_to_net;
// Read the UDP dst port, check if the port is the standard STUN port (3478)
// to decide whether we need to parse the STUN payload. This has both false
// positive and false negative: a TURN/STUN server may not use this port, and
// this port may be used by other applications.
const int kSTUNPort = 3478;
u16 dport;
if (bpf_skb_load_bytes_relative(skb, offset + offsetof(struct udphdr, dest),
&dport, sizeof(dport),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
// Skip the UDP header.
offset += sizeof(struct udphdr);
// Assume it's not an STUN packet. Try parsing DTLS directly.
if (dport != bpf_htons(kSTUNPort)) {
return handle_dtls(skb, offset);
}
// Assume it's an STUN packet. Skip the STUN header at first.
const int kSTUNHeaderSize = 20;
offset += kSTUNHeaderSize;
// The payload of a STUN packet is a list of attributes. Try to find the DATA
// attribute which may contains the DTLS payload. See RFC 8489 for the
// structure of the STUN packet in detail.
const int kDataAttributeType = 0x13;
// Assume that the DATA attribute is in the first 5 attributes.
#pragma clang loop unroll(full)
for (int i = 0; i < 5; ++i) {
// Each attributes contain 3 fields: type, length of value, and value.
struct stun_attribute_header {
u16 type;
u16 len;
} attr_hdr;
if (bpf_skb_load_bytes_relative(skb, offset, &attr_hdr, sizeof(attr_hdr),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
offset += sizeof(struct stun_attribute_header);
if (attr_hdr.type == bpf_htons(kDataAttributeType)) {
return handle_dtls(skb, offset);
}
// Move to next attribute.
offset += bpf_htons(attr_hdr.len);
}
return RET_IPTABLES_NOT_MATCHED;
}
// Checks if the given two types represent DTLS version 1.0, which is "254.255".
static __attribute__((always_inline)) int is_dtls_version_1_0(
const u8 version[2]) {
return version[0] == 254 && version[1] == 255;
}
// Checks if the given two types represent DTLS version 1.2, which is "254.253".
static __attribute__((always_inline)) int is_dtls_version_1_2(
const u8 version[2]) {
return version[0] == 254 && version[1] == 253;
}
static int handle_dtls(struct __sk_buff* skb, u32 base_offset_to_net) {
u32 offset = base_offset_to_net;
// Check the first 3 bytes in the payload. If this is a DTLS client hello
// message we care about, these 3 bytes should be fixed.
struct {
u8 type;
u8 version[2];
} dtls_hdr;
if (bpf_skb_load_bytes_relative(skb, offset, &dtls_hdr, sizeof(dtls_hdr),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
// Defined in TLS RFC (RFC 8446 for TLS 1.3).
const int kTypeHandshakePacket = 22;
// Version can be either 1.0 or 1.2 for the client hello packet.
if (dtls_hdr.type != kTypeHandshakePacket ||
!(is_dtls_version_1_0(dtls_hdr.version) ||
is_dtls_version_1_2(dtls_hdr.version))) {
return RET_IPTABLES_NOT_MATCHED;
}
// Move to the fragment field, which should contain a Handshake struct.
const int kFragmentOffsetInDTLSPlainText = 13;
offset += kFragmentOffsetInDTLSPlainText;
// Parse the Handshake struct (defined in RFC 6347 Section 4.2.2 for DTLS
// 1.2) and check if it's a client hello packet.
u8 msgType;
if (bpf_skb_load_bytes_relative(skb, offset, &msgType, sizeof(msgType),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
const int kMsgTypeClientHello = 1;
if (msgType != kMsgTypeClientHello) {
return RET_IPTABLES_NOT_MATCHED;
}
// Move to the body field, which should contain a ClientHello struct.
const int kBodyOffsetInHandshake = 12;
offset += kBodyOffsetInHandshake;
// Parse the ClientHello struct (defined in RFC 6347 Section 4.7.2 for DTLS
// 1.2). The first 2 bytes should be the version, we only care about 1.2 here
// now.
u8 version[2];
if (bpf_skb_load_bytes_relative(skb, offset, version, sizeof(version),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
if (!is_dtls_version_1_2(version)) {
return RET_IPTABLES_NOT_MATCHED;
}
// In the ClientHello struct, we only care about the extensions field, but
// there are several fields with variable length before that. We need to get
// the lengths of these fields and jump over them.
// Move to session_id field.
const int kSessionIDOffsetInClientHello = 34;
offset += kSessionIDOffsetInClientHello;
// Jump over session_id.
u8 session_id_size;
if (bpf_skb_load_bytes_relative(skb, offset, &session_id_size,
sizeof(session_id_size),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
offset += sizeof(session_id_size) + session_id_size;
// Jump over cookie.
u8 cookie_size;
if (bpf_skb_load_bytes_relative(skb, offset, &cookie_size,
sizeof(cookie_size), BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
offset += sizeof(cookie_size) + cookie_size;
// Jump over cipher_suites.
u16 cipher_suites_size;
if (bpf_skb_load_bytes_relative(skb, offset, &cipher_suites_size,
sizeof(cipher_suites_size),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
offset += sizeof(cipher_suites_size) + bpf_htons(cipher_suites_size);
// Jump over compression_methods.
u8 compression_methods_size;
if (bpf_skb_load_bytes_relative(skb, offset, &compression_methods_size,
sizeof(compression_methods_size),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
offset += sizeof(compression_methods_size) + compression_methods_size;
// We reach the extensions field here. The first two bytes are for its length.
// We don't need to parse the length so just skip them.
offset += 2;
// Parse the extensions field and try to find the "use_srtp" extension in it.
// Assume that it is in the first 10 extensions.
#pragma clang loop unroll(full)
for (int i = 0; i < 10; ++i) {
// See RFC 5246 Section 7.4.1.4 for the definition of struct Extension.
struct {
u16 type;
u16 length;
// There is data after length but we don't care about it.
} extension;
if (bpf_skb_load_bytes_relative(skb, offset, &extension, sizeof(extension),
BPF_HDR_START_NET) < 0) {
return RET_IPTABLES_NOT_MATCHED;
}
const int kExtentionTypeUseSRTP = 14;
if (extension.type == bpf_htons(kExtentionTypeUseSRTP)) {
// Found the SRTP extension.
return RET_IPTABLES_MATCHED;
}
offset += sizeof(extension) + bpf_htons(extension.length);
}
return RET_IPTABLES_NOT_MATCHED;
}
// A BPF_PROG_TYPE_SOCKET_FILTER type eBPF program attached by iptables with the
// bpf module which checks if the packet is a DTLS client hello packet and
// contains the "use_srtp" extension (which is strong indicator that this
// connection will be used for sending multimedia streams in WebRTC). This
// program is used for detecting if the connection is a WebRTC connection.
//
// See the resources for more information:
// - "WebRTC detection" section in go/cros-wifi-qos-dd for the high-level idea
// of this program.
// - "bpf" section in `man iptables-extensions` for the (e)BPF usage in
// iptables.
SEC("socket")
int match_dtls_srtp(struct __sk_buff* skb) {
if (skb->protocol == bpf_htons(ETH_P_IP)) {
return handle_ipv4(skb);
} else if (skb->protocol == bpf_htons(ETH_P_IPV6)) {
return handle_ipv6(skb);
}
return RET_IPTABLES_NOT_MATCHED;
}