Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Socket filter to track HTTP requests for non Go #122

Merged
merged 20 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,308 changes: 1,308 additions & 0 deletions bpf/headers/bpf_builtins.h

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions bpf/headers/compiler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
/* Copyright Authors of Cilium */

#ifndef __BPF_COMPILER_H_
#define __BPF_COMPILER_H_

#ifndef __maybe_unused
# define __maybe_unused __attribute__((__unused__))
#endif

#ifndef __nobuiltin
# if __clang_major__ >= 10
# define __nobuiltin(X) __attribute__((no_builtin(X)))
# else
# define __nobuiltin(X)
# endif
#endif

#ifndef __throw_build_bug
# define __throw_build_bug() __builtin_trap()
#endif

#ifndef barrier
# define barrier() asm volatile("": : :"memory")
#endif

#ifndef barrier_data
# define barrier_data(ptr) asm volatile("": :"r"(ptr) :"memory")
#endif

/* The LOAD_CONSTANT macro is used to define a named constant that will be replaced
* at runtime by the Go code. This replaces usage of a bpf_map for storing values, which
* eliminates a bpf_map_lookup_elem per kprobe hit. The constants are best accessed with a
* dedicated inlined function.
*/
#define LOAD_CONSTANT(param, var) asm("%0 = " param " ll" : "=r"(var))

#endif /* __BPF_COMPILER_H_ */
21 changes: 21 additions & 0 deletions bpf/http_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,25 @@
// Taken from errno.h
#define EINPROGRESS 115 /* Operation now in progress */

// Taken from uapi/linux/if_ether.h
#define ETH_HLEN 14 /* Total octets in header. */
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
#define ETH_P_IPV6 0x86DD /* IPv6 over bluebook */


// Taken from uapi/linux/in.h
#define IPPROTO_TCP 6 /* Transmission Control Protocol */

// Taken from linux/include/net/tcp.h
#define TCPHDR_FIN 0x01
#define TCPHDR_SYN 0x02
#define TCPHDR_RST 0x04
#define TCPHDR_PSH 0x08
#define TCPHDR_ACK 0x10
#define TCPHDR_URG 0x20
#define TCPHDR_ECE 0x40
#define TCPHDR_CWR 0x80

#define DEFAULT_HTTPS_PORT 443

#endif
102 changes: 86 additions & 16 deletions bpf/http_sock.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
#include "bpf_dbg.h"
#include "pid.h"
#include "sockaddr.h"
#include "tcp_info.h"
#include "ringbuf.h"
#include "http_sock.h"

char __license[] SEC("license") = "Dual MIT/GPL";

Expand All @@ -25,16 +27,6 @@ struct {
__type(value, sock_args_t);
} active_connect_args SEC(".maps");

// Keeps track of active accept or connect connection infos
// From this table we extract the PID of the process and filter
// HTTP calls we are not interested in
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, http_connection_info_t);
__type(value, u64); // PID_TID group
__uint(max_entries, MAX_CONCURRENT_REQUESTS);
} filtered_connections SEC(".maps");

// Helps us track process names of processes that exited.
// Only used with system wide instrumentation
struct {
Expand Down Expand Up @@ -85,7 +77,7 @@ int BPF_KRETPROBE(kretprobe_sys_accept4, uint fd)
return 0;
}

bpf_dbg_printk("=== accept 4 ret id=%d ===", id);
//bpf_dbg_printk("=== accept 4 ret id=%d ===", id);

// The file descriptor is the value returned from the accept4 syscall.
// If we got a negative file descriptor we don't have a connection
Expand All @@ -95,17 +87,20 @@ int BPF_KRETPROBE(kretprobe_sys_accept4, uint fd)

sock_args_t *args = bpf_map_lookup_elem(&active_accept_args, &id);
if (!args) {
bpf_dbg_printk("No sock info %d", id);
//bpf_dbg_printk("No sock info %d", id);
goto cleanup;
}

http_connection_info_t info = {};
connection_info_t info = {};

if (parse_accept_socket_info(args, &info)) {
sort_connection_info(&info);
dbg_print_http_connection_info(&info);

bpf_map_update_elem(&filtered_connections, &info, &id, BPF_ANY); // On purpose BPF_ANY, we want to overwrite stale
http_connection_metadata_t meta = {};
meta.id = id;
meta.type = EVENT_HTTP_REQUEST;
bpf_map_update_elem(&filtered_connections, &info, &meta, BPF_ANY); // On purpose BPF_ANY, we want to overwrite stale
}

cleanup:
Expand Down Expand Up @@ -161,13 +156,17 @@ int BPF_KRETPROBE(kretprobe_sys_connect, int fd)
goto cleanup;
}

http_connection_info_t info = {};
connection_info_t info = {};

if (parse_connect_sock_info(args, &info)) {
bpf_dbg_printk("=== connect ret id=%d, pid=%d ===", id, pid_from_pid_tgid(id));
sort_connection_info(&info);
dbg_print_http_connection_info(&info);

bpf_map_update_elem(&filtered_connections, &info, &id, BPF_ANY); // On purpose BPF_ANY, we want to overwrite stale
http_connection_metadata_t meta = {};
meta.id = id;
meta.type = EVENT_HTTP_CLIENT;
bpf_map_update_elem(&filtered_connections, &info, &meta, BPF_ANY); // On purpose BPF_ANY, we want to overwrite stale
}

cleanup:
Expand All @@ -194,3 +193,74 @@ int BPF_KPROBE(kprobe_sys_exit, int status) {

return 0;
}

SEC("socket/http_filter")
int socket__http_filter(struct __sk_buff *skb) {
protocol_info_t tcp = {};
connection_info_t conn = {};

if (!read_sk_buff(skb, &tcp, &conn)) {
return 0;
}

// ignore ACK packets
if (tcp_ack(&tcp)) {
return 0;
}

// ignore empty packets, unless it's TCP FIN or TCP RST
if (!tcp_close(&tcp) && tcp_empty(&tcp, skb)) {
return 0;
}

// sorting must happen here, before we check or set dups
sort_connection_info(&conn);

// ignore duplicate sequences
if (tcp_dup(&conn, &tcp)) {
return 0;
}

// we don't support HTTPs yet, quick check for client HTTP calls being SSL, so we don't bother parsing
if (conn.s_port == DEFAULT_HTTPS_PORT || conn.d_port == DEFAULT_HTTPS_PORT) {
return 0;
}

// we don't want to read the whole buffer for every packed that passes our checks, we read only a bit and check if it's trully HTTP request/response.
char buf[MIN_HTTP_SIZE] = {0};
bpf_skb_load_bytes(skb, tcp.hdr_len, (void *)buf, sizeof(buf));
// technically the read should be reversed, but eBPF verifier complains on read with variable length
u32 len = skb->len - tcp.hdr_len;
if (len > MIN_HTTP_SIZE) {
len = MIN_HTTP_SIZE;
}

bpf_dbg_printk("=== http_filter len=%d %s ===", len, buf);
dbg_print_http_connection_info(&conn);

u8 packet_type = 0;
if (is_http(buf, len, &packet_type) || tcp_close(&tcp)) { // we must check tcp_close second, a packet can be a close and a response
http_info_t info = {0};
info.conn_info = conn;

http_connection_metadata_t *meta = NULL;
if (packet_type) {
u32 full_len = skb->len - tcp.hdr_len;
if (full_len > FULL_BUF_SIZE) {
full_len = FULL_BUF_SIZE;
}
read_skb_bytes(skb, tcp.hdr_len, info.buf, full_len);
if (packet_type == PACKET_TYPE_RESPONSE) {
// if we are filtering by application, ignore the packets not for this connection
meta = bpf_map_lookup_elem(&filtered_connections, &conn);
bpf_dbg_printk("Response meta=%lx", meta);
if (!meta) {
return 0;
}
}
}
process_http(&info, &tcp, packet_type, info.buf, meta);
}

return 0;
}
167 changes: 167 additions & 0 deletions bpf/http_sock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#ifndef HTTP_SOCK_HELPERS
#define HTTP_SOCK_HELPERS

#include "common.h"
#include "bpf_helpers.h"
#include "bpf_builtins.h"
#include "http_types.h"
#include "ringbuf.h"
#include "pid.h"

#define MIN_HTTP_SIZE 12 // HTTP/1.1 CCC is the smallest valid request we can have
#define RESPONSE_STATUS_POS 9 // HTTP/1.1 <--

#define PACKET_TYPE_REQUEST 1
#define PACKET_TYPE_RESPONSE 2

// Keeps track of the tcp sequences we've seen for a connection
// With multiple network interfaces the same sequence can be seen again
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, connection_info_t);
__type(value, u32); // the TCP sequence
__uint(max_entries, MAX_CONCURRENT_REQUESTS);
} http_tcp_seq SEC(".maps");

// Keeps track of active accept or connect connection infos
// From this table we extract the PID of the process and filter
// HTTP calls we are not interested in
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, connection_info_t);
__type(value, http_connection_metadata_t); // PID_TID group and connection type
__uint(max_entries, MAX_CONCURRENT_REQUESTS);
} filtered_connections SEC(".maps");

// Keeps track of the ongoing http connections we match for request/response
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, connection_info_t);
__type(value, http_info_t);
__uint(max_entries, MAX_CONCURRENT_REQUESTS);
} ongoing_http SEC(".maps");

static __always_inline bool tcp_dup(connection_info_t *http, protocol_info_t *tcp) {
u32 *prev_seq = bpf_map_lookup_elem(&http_tcp_seq, http);

if (prev_seq && (*prev_seq == tcp->seq)) {
return true;
}

bpf_map_update_elem(&http_tcp_seq, http, &tcp->seq, BPF_ANY);
return false;
}

static __always_inline bool is_http(char *p, u32 len, u8 *packet_type) {
if (len < MIN_HTTP_SIZE) {
return false;
}
//HTTP
if ((p[0] == 'H') && (p[1] == 'T') && (p[2] == 'T') && (p[3] == 'P')) {
*packet_type = PACKET_TYPE_RESPONSE;
} else if (
((p[0] == 'G') && (p[1] == 'E') && (p[2] == 'T') && (p[3] == ' ') && (p[4] == '/')) || // GET
((p[0] == 'P') && (p[1] == 'O') && (p[2] == 'S') && (p[3] == 'T') && (p[4] == ' ') && (p[5] == '/')) || // POST
((p[0] == 'P') && (p[1] == 'U') && (p[2] == 'T') && (p[3] == ' ') && (p[4] == '/')) || // PUT
((p[0] == 'P') && (p[1] == 'A') && (p[2] == 'T') && (p[3] == 'C') && (p[4] == 'H') && (p[5] == ' ') && (p[6] == '/')) || // PATCH
((p[0] == 'D') && (p[1] == 'E') && (p[2] == 'L') && (p[3] == 'E') && (p[4] == 'T') && (p[5] == 'E') && (p[6] == ' ') && (p[7] == '/')) || // DELETE
((p[0] == 'H') && (p[1] == 'E') && (p[2] == 'A') && (p[3] == 'D') && (p[4] == ' ') && (p[5] == '/')) // HEAD
) {
*packet_type = PACKET_TYPE_REQUEST;
}
// OPTIONS? do we care?

return true;
}

static __always_inline void read_skb_bytes(const void *skb, u32 offset, unsigned char *buf, const u32 len) {
u32 max = offset + len;
int b = 0;
for (; b < (FULL_BUF_SIZE/BUF_COPY_BLOCK_SIZE); b++) {
if ((offset + (BUF_COPY_BLOCK_SIZE - 1)) >= max) {
break;
}
bpf_skb_load_bytes(skb, offset, (void *)(&buf[b * BUF_COPY_BLOCK_SIZE]), BUF_COPY_BLOCK_SIZE);
offset += BUF_COPY_BLOCK_SIZE;
}

if ((b * BUF_COPY_BLOCK_SIZE) >= len) {
return;
}

// This code is messy to make sure the eBPF verifier is happy. I had to cast to signed 64bit.
s64 remainder = (s64)max - (s64)offset;

if (remainder <= 0) {
return;
}

int remaining_to_copy = (remainder < (BUF_COPY_BLOCK_SIZE - 1)) ? remainder : (BUF_COPY_BLOCK_SIZE - 1);
int space_in_buffer = (len < (b * BUF_COPY_BLOCK_SIZE)) ? 0 : len - (b * BUF_COPY_BLOCK_SIZE);

if (remaining_to_copy <= space_in_buffer) {
bpf_skb_load_bytes(skb, offset, (void *)(&buf[b * BUF_COPY_BLOCK_SIZE]), remaining_to_copy);
}
}

static __always_inline http_info_t *get_or_set_http_info(http_info_t *info, u8 packet_type) {
if (packet_type == PACKET_TYPE_REQUEST) {
bpf_map_update_elem(&ongoing_http, &info->conn_info, info, BPF_NOEXIST); // noexist is critical here for correct keepalive functionality
}

return bpf_map_lookup_elem(&ongoing_http, &info->conn_info);
}

static __always_inline bool still_responding(http_info_t *info) {
return info->status != 0;
}

static __always_inline void process_http_request(http_info_t *info, unsigned char *buf) {
bpf_memcpy(info->buf, buf, FULL_BUF_SIZE);
info->start_monotime_ns = bpf_ktime_get_ns();
}

static __always_inline void process_http_response(http_info_t *info, unsigned char *buf, http_connection_metadata_t *meta) {
info->pid = pid_from_pid_tgid(meta->id);
info->type = meta->type;
info->status = 0;
info->status += (buf[RESPONSE_STATUS_POS] - '0') * 100;
info->status += (buf[RESPONSE_STATUS_POS + 1] - '0') * 10;
info->status += (buf[RESPONSE_STATUS_POS + 2] - '0');
}

static __always_inline void process_http(http_info_t *in, protocol_info_t *tcp, u8 packet_type, unsigned char *buf, http_connection_metadata_t *meta) {
http_info_t *info = get_or_set_http_info(in, packet_type);
if (!info) {
return;
}

if (packet_type == PACKET_TYPE_REQUEST) {
process_http_request(info, buf);
} else if (packet_type == PACKET_TYPE_RESPONSE) {
process_http_response(info, buf, meta);
}

if (still_responding(info)) {
info->end_monotime_ns = bpf_ktime_get_ns();
}

if (tcp_close(tcp)) {
if (info->start_monotime_ns != 0 && info->status != 0 && info->pid != 0) {
http_info_t *trace = bpf_ringbuf_reserve(&events, sizeof(http_info_t), 0);
if (trace) {
bpf_dbg_printk("Sending trace %lx, closing connection", info);

bpf_memcpy(trace, info, sizeof(http_info_t));
bpf_ringbuf_submit(trace, get_flags());
}

bpf_map_delete_elem(&ongoing_http, &info->conn_info);
// bpf_map_delete_elem(&filtered_connections, &info->conn_info); // don't clean this up, doesn't work with keepalive
// we don't explicitly clean-up the http_tcp_seq, we need to still monitor for dups
}
}

}

#endif
Loading