HTTP SLI 调研 (3)

黎 浩然/ 29 6 月, 2022/ 扩展的伯克利数据包过滤器/EXTENDEDBERKELLEYPACKETFILTER/EBPF, 计算机/COMPUTER/ 0 comments

HTTP服务端READ和WRITE的对应

对于HTTPS,下面以OpenSSL加密库为例:

首先:

  • 同一个ConnectionSocket内在时间上相邻的HTTP请求(read)与响应(write)是对应的
  • (Socket的)文件描述符在同一个进程内部是不同线程共享的,在不同进程之间是隔离的

因此:

  • 可以通过(进程ID,socket描述符)二元组决定是否为同一个socket

以OpenSSL为例,

加密的HTTP数据在解密后调用SSL_read函数从SSL/TLS连接读取到buffer中:

#include <openssl/ssl.h>

int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);
int SSL_read(SSL *ssl, void *buf, int num);

明文的HTTP数据在加密前调用SSL_write函数写入到SSL/TLS连接中:

#include <openssl/ssl.h>

int SSL_write(SSL *ssl, const void *buf, int num);

通过在SSL_read和SSL_write注入用户态探针,可以检查其是否HTTP相关的请求和响应。

  • 如何检查是否为同一个进程? ebpf提供了bpf_get_current_pid_tgid函数来查看内核视角到PID(包括进程ID和线程ID)
  • 如何检查是否为同一个socket? 注意到SSL_read和SSL_write函数都有SSL结构体指针,通过一下函数可以获得socket的描述符 #include <openssl/ssl.h> int SSL_get_fd(const SSL *ssl); int SSL_get_rfd(const SSL *ssl); int SSL_get_wfd(const SSL *ssl);

从WRITE到内核态的跟踪

TCP_ACK

在TCP层收到客户端对服务端发出的HTTP响应的ACK后,内核最终会调用tcp_ack函数。

如下,tcp_ack函数的第二个参数skb为来自客户端的ACK数据包。要确定这个tcp_ack是否对应我们服务器的HTTP响应,仅仅通过sock结构体来判断是不够的, 因为这个ACK可能是针对该TCP连接的其它数据包。如,重复的ACK。

但是tcp_ack函数最终会调用tcp_clean_rtx_queue,尝试将已经被ack的数据包从重传队列中删除。

在TCP层中包含数据的片段一经发送,片段的一份复制就放在名为重传队列的数据结构中,此时启动重传计时器。

tcp_clean_rtx_queue函数会遍历重传队列中的skb:

被ACK的skb最终会调用tcp_rtx_queue_unlink_and_free函数删除:

如果HTTP数据是明文传输的,我们可以考虑对这个tcp_rtx_queue_unlink_and_free函数注入内核态探针。通过skb我们可以在内核中tcp数据包,从而确定这个被删除的数据包是不是服务器发出的http响应。

上图为struct sk_buff结构体中用于存放部分数据包的线性数据区域。

但对于加密的HTTPS流量,上述方法行不通,因为skb中的数据是加密的。目前没有找到更好方法

Nginx源代码调研

通过阅读Nginx的源代码,对于http和https请求和响应可以观察到以上的函数调用过程。

两个函数都是在用户态下的单个线程下完成的,因此可以通过线程标识符判断是否对应。

再在tcp_rtx_queue_unlink_and_free注入内核态探针从而得到tcp处理时间和网络延迟。

第一阶段开发思路

  • 在recv函数和writev函数处注入用户态探针,抓取HTTP请求和响应报文
  • 通过线程ID及recv和writev函数调用的时序关系将HTTP请求与响应对应
  • 在tcp_rtx_queue_unlink_and_free注入内核态探针抓包,对比writev发出的HTTP响应数据包,计算内核处理时间和网络延迟。
dev_queue_xmit
tcp_ack
tcp_rate_skb_delivered
Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*