
在 Linux 系统中,eBPF 技术已经广泛应用于网络、观测、安全和性能优化等各种场景。随着这些实践的发展,围绕 eBPF 已经形成了一个横跨 Linux 内核、开发工具、开发库以及各类产品的生态系统。尽管最初 eBPF 技术是在 Linux 内核中实现的,但 eBPF 在 Linux 系统中解决的这些问题在 Windows 等其他操作系统中也同样存在。
如何在 Windows 系统中安装 eBPF 运行时。在安装之前需要提醒的是,由于 Windows 系统要求在内核模式运行的所有软件都必须进行数字签名,而 Windows eBPF 暂时还在测试阶段,还没有发布已签名的稳定版本,因而只能在打开测试签名或者连接并运行内核调试器的系统上工作。所以,在安装之前,你需要先打开测试签名模式并重启系统。
第一步,打开测试签名并重启系统
bcdedit.exe -set TESTSIGNING ON在执行命令时,请注意:
第二步,安装 Windows eBPF 运行时
以管理员模式打开 Powershell,执行下面的命令,下载并安装 eBPF 运行时。
# 安装 Visual C++ 可再发行程序包
Start-BitsTransfer https://aka.ms/vs/17/release/vc_redist.x64.exe
Start-Process -FilePath .\vc_redist.x64.exe
# 安装 eBPF 运行时
Start-BitsTransfer https://github.com/microsoft/ebpf-for-windows/releases/download/v0.12.0/ebpf-for-windows.0.12.0.msi
# 安装时请选择安装 Runtime Components 和 Development Components
msiexec.exe /I ebpf-for-windows.0.12.0.msi注意,在安装时请把默认目录修改到 C:\ebpf-for-windows 或者其他不带空格的目录中(以下的内容都以 C:\ebpf-for-windows 为例讲解)。
接着,执行 Get-Service eBPFSvc 确认 eBPF 程序已经正常启动。如果一切正常,你将看到如下输出:
PS C:\> Get-Service eBPFSvc
Status Name DisplayName
------ ---- -----------
Running eBPFSvc eBPFSvc第三步,安装 Windows eBPF 开发工具
以管理员模式打开一个新的 Powershell 终端,执行下面的命令。
# 安装 Chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# 安装 eBPF 开发工具
Invoke-WebRequest 'https://raw.githubusercontent.com/microsoft/ebpf-for-windows/main/scripts/Setup-DevEnv.ps1' -OutFile $env:TEMP\Setup-DeveEnv.ps1
&"$env:TEMP\Setup-DeveEnv.ps1"由于 Chocolatey 暂时还不支持安装 WDK for Windows 11,所以你还需要执行下面的命令,手动安装 WDK for Windows 11。
Start-BitsTransfer "https://go.microsoft.com/fwlink/?linkid=2196230" -Destination wdksetup.exe
Start-Process -FilePath .\wdksetup.exe注意,在安装完成时,需要注意勾选 “Install Windows Driver Kit Visual Studio Extension” 选项,这样才能在 Visual Studio 中使用 WDK。
Windows eBPF 程序的开发过程也是类似的,只是在具体的步骤上稍微不同,这个过程可以通过以下几个步骤完成:
1、开发 eBPF 内核程序,包括数据结构定义、BPF 映射创建和更新、内核挂载事件的处理等。
eBPF 内核程序的开发包括数据结构定义、BPF 映射创建和更新、内核挂载事件的处理等。同 Linux 类似,数据结构的定义一般放在一个公共的头文件中,方便后续在 eBPF 内核程序和用户态程序中复用。
创建一个 conn_tracker.h,内容如下所示:
#include "net/ip.h"
// IP 地址数据结构
typedef struct _ip_address
{
union
{
uint32_t ipv4;
ipv6_address_t ipv6;
};
} ip_address_t;
// 连接信息数据结构
typedef struct _connection_tuple
{
ip_address_t src_ip;
uint16_t src_port;
ip_address_t dst_ip;
uint16_t dst_port;
uint32_t protocol;
uint32_t compartment_id;
uint64_t interface_luid;
} connection_tuple_t;
// 连接历史信息数据结构
typedef struct _connection_history
{
connection_tuple_t tuple;
bool is_ipv4;
uint64_t start_time;
uint64_t end_time;
} connection_history_t;有了数据结构之后,接下来就可以用这些数据结构定义我们需要的 eBPF 映射了。创建一个 conn_track.c,内容如下所示:
// 网络连接映射,KEY: 连接信息,VALUE:连接开始时间
SEC("maps")
struct bpf_map_def connection_map = {
.type = BPF_MAP_TYPE_LRU_HASH,
.key_size = sizeof(connection_tuple_t),
.value_size = sizeof(uint64_t),
.max_entries = 1024};
// 网络连接历史映射,用于用户态程序获取连接历史信息
SEC("maps")
struct bpf_map_def history_map = {
.type = BPF_MAP_TYPE_RINGBUF,
.max_entries = 256 * 1024};再接着就是对 eBPF 内核事件的处理了,这里我们选择 sock_ops 事件,即在网络套接字操作时触发的事件。
// eBPF 内核事件处理程序入口,段名字必须为 sockops
SEC("sockops")
int connection_tracker(bpf_sock_ops_t* ctx)
{
int result = 0;
bool connected;
switch (ctx->op) {
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
connected = true;
break;
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
connected = true;
break;
case BPF_SOCK_OPS_CONNECTION_DELETED_CB:
connected = false;
break;
default:
result = -1;
}
if (result == 0)
handle_connection(ctx, (ctx->family == AF_INET), connected);
return 0;
}
// 处理网络连接信息的主函数,将新的网络连接信息写入 history_map 中。
__attribute__((always_inline)) void
handle_connection(bpf_sock_ops_t* ctx, bool is_ipv4, bool connected)
{
connection_tuple_t key = {0};
sock_ops_to_connection_tuple(ctx, is_ipv4, &key);
uint64_t now = bpf_ktime_get_ns();
if (connected) {
// 将连接信息写入 connection_map 中
bpf_map_update_elem(&connection_map, &key, &now, 0);
} else {
// 从 connection_map 中删除后,将连接信息写入 history_map 中
uint64_t* start_time = (uint64_t*)bpf_map_lookup_and_delete_elem(&connection_map, &key);
if (start_time) {
log_tuple(&key, is_ipv4, false);
connection_history_t history;
// Memset is required due to padding within this struct.
__builtin_memset(&history, 0, sizeof(history));
history.tuple = key;
history.is_ipv4 = is_ipv4;
history.start_time = *start_time;
history.end_time = now;
bpf_ringbuf_output(&history_map, &history, sizeof(history), 0);
}
}
}
// 数据结构转换函数,从 bpf_sock_ops_t 提取需要的连接信息
__attribute__((always_inline)) void
sock_ops_to_connection_tuple(bpf_sock_ops_t* ctx, bool is_ipv4, connection_tuple_t* tuple)
{
tuple->compartment_id = ctx->compartment_id;
tuple->interface_luid = ctx->interface_luid;
tuple->src_port = ctx->local_port;
tuple->dst_port = ctx->remote_port;
tuple->protocol = ctx->protocol;
if (is_ipv4) {
tuple->src_ip.ipv4 = ctx->local_ip4;
tuple->dst_ip.ipv4 = ctx->remote_ip4;
} else {
void* ip6 = NULL;
ip6 = ctx->local_ip6;
__builtin_memcpy(tuple->src_ip.ipv6, ip6, sizeof(tuple->src_ip.ipv6));
ip6 = ctx->remote_ip6;
__builtin_memcpy(tuple->dst_ip.ipv6, ip6, sizeof(tuple->dst_ip.ipv6));
}
}2、编译 eBPF 程序为字节码,即调用 clang 将 eBPF 代码编译为字节码。
eBPF 内核程序开发完成后,接着就可以调用 clang 命令,将其编译为字节码了。这一步比较简单,打开 Powershell 终端,执行下面的命令即可。
clang -I 'C:\ebpf-for-windows\include' -target bpf -Werror -O2 -g -c conn_track.c -o conn_track.o3、开发 eBPF 用户态程序,包括 eBPF 程序加载、挂载到内核跟踪点,以及通过 eBPF 映射获取和打印执行结果等。
最后,就是开发 eBPF 用户态程序了。这一步包括 eBPF 字节码加载、挂载到内核跟踪点,以及通过 eBPF 映射获取和打印执行结果等。
创建一个新的 conn_tracker.cpp 文件,省略一些错误处理逻辑,主要内容如下所示:
// 引用头文件,注意 <windows.h> 需要放在最前面
#include <windows.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <ebpf_api.h>
int main(int argc, char** argv)
{
// 加载 eBPF 字节码
struct bpf_object* object = bpf_object__open("conn_track.o");
if (!object) {
std::cerr << "bpf_object__open for conn_track.o failed:" << errno << std::endl;
return 1;
}
if (bpf_object__load(object) < 0) {
std::cerr << "bpf_object__load for conn_track.o failed:" << errno << std::endl;
return 1;
}
// 挂载到 sock_ops
auto program = bpf_object__find_program_by_name(object, "connection_tracker");
if (!program) {
std::cerr << "bpf_object__find_program_by_name for \"connection_tracker\"failed:" << errno << std::endl;
return 1;
}
auto link = bpf_program__attach(program);
if (!link) {
std::cerr << "BPF program conn_track.o failed to attach:" << errno << std::endl;
return 1;
}
// 挂载 BPF 映射处理函数
bpf_map* map = bpf_object__find_map_by_name(object, "history_map");
if (!map) {
std::cerr << "Unable to locate history map:" << errno << std::endl;
return 1;
}
auto ring = ring_buffer__new(bpf_map__fd(map), conn_track_history_callback, nullptr, nullptr);
if (!ring) {
std::cerr << "Unable to create ring buffer:" << errno << std::endl;
return 1;
}
// 等待 Ctrl-C 结束程序
...
}
// 读取 history_map 事件并打印连接信息到终端
int conn_track_history_callback(void* ctx, void* data, size_t size)
{
if (size == sizeof(connection_history_t)) {
auto history = reinterpret_cast<connection_history_t*>(data);
auto source = ip_address_to_string(history->is_ipv4, history->tuple.src_ip, history->tuple.interface_luid) + ":" +
std::to_string(htons(history->tuple.src_port));
auto dest = ip_address_to_string(history->is_ipv4, history->tuple.dst_ip, history->tuple.interface_luid) + ":" +
std::to_string(htons(history->tuple.dst_port));
double duration = static_cast<double>(history->end_time);
duration -= static_cast<double>(history->start_time);
duration /= 1e9;
std::cout <<source << "==>" << dest << "\t" << _protocol[history->tuple.protocol] << "\t" << duration
<< std::endl;
}
return 0;
}
// 转换 IP 地址和网卡 UID 到字符串格式
std::string ip_address_to_string(bool ipv4, const ip_address_t& ip_address, const uint64_t interface_luid)
{
std::string buffer;
if (ipv4) {
buffer.resize(MAX_IPV4_ADDRESS_LENGTH);
in_addr addr;
addr.S_un.S_addr = ip_address.ipv4;
auto end = RtlIpv4AddressToStringA(&addr, buffer.data());
buffer.resize(end - buffer.data());
} else {
buffer.resize(MAX_IPV6_ADDRESS_LENGTH);
in_addr6 addr;
memcpy(addr.u.Byte, ip_address.ipv6, sizeof(ip_address.ipv6));
auto end = RtlIpv6AddressToStringA(&addr, buffer.data());
buffer.resize(end - buffer.data());
}
buffer += "%" + interface_luid_to_name(interface_luid);
return "[" + trim(buffer) + "]";
}从这段代码中你会发现有很多陌生的函数,这是由于 Windows 库函数与 Linux 不同导致的。比如,Windows 使用 RtlIpv4AddressToStringA() 把 IPv4 地址转换为可读的字符串格式,而 Linux 则使用 inet_ntop() 函数。
代码开发完成后,还需要最后编译为二进制可执行文件才可运行。在 Windows 系统上,我们通常使用 Visual Studio 来开发、编译和调试程序。
构建成功后,打开一个新的 Powershell 终端,执行下面的命令运行 eBPF 程序。
# 切换到项目的 Release 目录中
cd .\x64\Release
# 执行跟踪程序
.\conn_tracker.exe接着,打开另一个 Powershell 终端,随机访问几个网络服务,比如解析 baidu.com 的 IP 地址并访问它。
nslookup.exe baidu.com 114.114.114.114
curl.exe baidu.com再回到第一个终端,你可以看到如下输出:
# 格式为源 IP、网卡、源端口 => 目的 IP、网卡、端口 协议 连接延迟
[192.168.0.3%ethernet_32769]:49853==>[114.114.114.114%ethernet_32769]:53 UDP 0.195494
[192.168.0.3%ethernet_32769]:49854==>[114.114.114.114%ethernet_32769]:53 UDP 0.199999
[192.168.0.3%ethernet_32769]:49855==>[114.114.114.114%ethernet_32769]:53 UDP 0.195471
[192.168.0.3%ethernet_32769]:54537==>[110.242.68.66%ethernet_32769]:80 TCP 0.212876到这里,一个完整的网络跟踪 eBPF 程序就开发完成了。你可以看到,Windows eBPF 程序的开发流程同 Linux 非常类似,主要也是开发 eBPF 内核程序、编译 eBPF 程序为字节码,最后再到用户态程序中加载和挂载 eBPF 字节码,并通过 eBPF 映射同内核态 eBPF 程序进行交互。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。