WEBKT

用eBPF追踪特定Java进程的网络连接,揪出它都连了哪些IP

107 0 0 0

背景交代

最近,我遇到一个需求,需要监控某个Java进程的网络行为,想知道它到底连了哪些外部IP地址。一开始想抓包,但流量太大,而且只想看特定进程的,tcpdump不太方便。后来发现eBPF简直是神器,可以精确地跟踪内核里的网络调用,而且性能损耗极低。

eBPF简介

eBPF(Extended Berkeley Packet Filter)最初是用来做网络包过滤的,后来功能越来越强大,现在已经变成一个通用的内核跟踪和分析框架。你可以把它理解成一个内核“钩子”,在内核的关键路径上插入你自己的代码(eBPF程序),来收集信息、修改行为等等。

思路

我们的目标是追踪Java进程发起的网络连接,也就是connect系统调用。connect是进程发起TCP连接的关键步骤,我们可以hook这个函数,拿到目标IP地址和端口信息。具体步骤如下:

  1. 找到connect系统调用的内核探针点(kprobe)。
  2. 编写eBPF程序,hook住connect,过滤出目标Java进程的PID。
  3. connect的参数中提取目标IP地址。
  4. 将结果保存到eBPF map中,供用户态程序读取。

准备工作

  • 安装必要的工具链: 确保你的系统安装了libbpf、clang、llvm等eBPF开发工具。
  • 内核头文件: 需要安装内核头文件,因为eBPF程序需要访问内核数据结构。
  • bcc工具(可选): bcc是一个Python库,可以简化eBPF程序的开发和部署。虽然不是必须的,但强烈推荐使用。

代码实现 (使用bcc)

下面是一个使用bcc实现的简单例子:

from bcc import BPF
import socket
import struct

# 定义eBPF程序
program = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/inet_sock.h>
#include <linux/ip.h>

// 定义BPF map,用于存储结果
BPF_HASH(connect_info, u32, u32);

// kprobe connect 系统调用
int kprobe__inet_sock_connect(struct pt_regs *ctx, struct sock *sk)
{
    u32 pid = bpf_get_current_pid_tgid();
    // 替换成你的Java进程PID
    u32 target_pid = 12345; 

    if (pid != target_pid) {
        return 0; // 不是目标进程,直接返回
    }

    struct inet_sock *inet = inet_sk(sk);
    u32 daddr = inet->daddr; // 目标IP地址 (网络字节序)
    u32 dport = inet->dport; // 目标端口 (网络字节序)

    connect_info.insert(&daddr, &dport);

    return 0;
}
"""

# 创建BPF实例
bpf = BPF(text=program)

# 附加kprobe到inet_sock_connect函数
bpf.attach_kprobe(event="inet_sock_connect", fn_name="kprobe__inet_sock_connect")

print("Tracing connect calls for PID 12345... Press Ctrl+C to end")

# 循环读取BPF map中的数据
try:
    while True:
        for daddr, dport in bpf["connect_info"].items():
            # 将网络字节序的IP地址转换成字符串
            ip_address = socket.inet_ntoa(struct.pack(">I", daddr.value))
            port = socket.ntohs(dport.value)
            print(f"Connected to IP: {ip_address}, Port: {port}")
        bpf["connect_info"].clear()
        sleep(1)
except KeyboardInterrupt:
    pass

代码解释:

  • BPF_HASH(connect_info, u32, u32) 定义了一个eBPF map,用于存储IP地址和端口信息。Key是IP地址(u32),Value是端口(u32)。
  • kprobe__inet_sock_connect 这是kprobe的处理函数,当inet_sock_connect函数被调用时,这个函数会被执行。
  • bpf_get_current_pid_tgid() 获取当前进程的PID。
  • inet_sk(sk)->daddrsock结构体中获取目标IP地址(网络字节序)。
  • connect_info.insert(&daddr, &dport) 将IP地址和端口存入eBPF map。
  • 用户态程序: 用户态程序负责加载eBPF程序到内核,并循环读取eBPF map中的数据,将网络字节序的IP地址转换成字符串,并打印出来。

使用方法:

  1. 保存代码为connect_tracer.py
  2. target_pid变量替换成你的Java进程的PID。
  3. 以root权限运行python connect_tracer.py

进阶

  • 使用tracepoint代替kprobe tracepoint是内核提供的稳定跟踪点,比kprobe更可靠。你可以使用bpftrace工具来查找connect相关的tracepoint
  • 过滤更多的信息: 你可以在eBPF程序中过滤更多的信息,例如连接的协议类型、源IP地址等等。
  • 将结果发送到远程服务器: 你可以将eBPF程序收集到的数据发送到远程服务器,进行集中分析和监控。

注意事项

  • 权限: 运行eBPF程序需要root权限。
  • 内核版本: 不同的内核版本,内核数据结构可能会有所不同,需要根据实际情况调整代码。
  • 性能: 虽然eBPF性能损耗很低,但过度使用仍然可能影响系统性能,需要谨慎评估。

总结

eBPF是一个非常强大的内核跟踪和分析工具,可以用来解决各种各样的问题。用eBPF追踪特定Java进程的网络连接,可以帮助我们了解它的网络行为,排查网络问题。希望这个例子能帮助你入门eBPF,开启你的内核探索之旅!

内核老司机 eBPF网络追踪Java进程

评论点评