目录
代码分析
comm 数据部分
BPF 代码部分
功能说明
rb 结构分析
ip_is_fragment 函数分析
bpf_skb_load_bytes函数分析
GRE协议说明
用户代码部分
功能说明
open_raw_sock& 原始套接字
setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd))
执行效果
分片与不分片的处理难度说明
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2022 Jacky Yin */
#ifndef __SOCKFILTER_H
#define __SOCKFILTER_H
struct so_event {
__be32 src_addr;
__be32 dst_addr;
union {
__be32 ports;
__be16 port16[2];
};
__u32 ip_proto;
__u32 pkt_type;
__u32 ifindex;
};
#endif /* __SOCKFILTER_H */
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2022 Jacky Yin */
#include
#include
#include
#include
#include
#include
#include
#include "sockfilter.h"
#define IP_MF 0x2000
#define IP_OFFSET 0x1FFF
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
static inline int ip_is_fragment(struct __sk_buff *skb, __u32 nhoff)
{
__u16 frag_off;
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);
frag_off = __bpf_ntohs(frag_off);
return frag_off & (IP_MF | IP_OFFSET);
}
SEC("socket")
int socket_handler(struct __sk_buff *skb)
{
struct so_event *e;
__u8 verlen;
__u16 proto;
__u32 nhoff = ETH_HLEN;
bpf_skb_load_bytes(skb, 12, &proto, 2);
proto = __bpf_ntohs(proto);
if (proto != ETH_P_IP)
return 0;
if (ip_is_fragment(skb, nhoff))
return 0;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &e->ip_proto, 1);
if (e->ip_proto != IPPROTO_GRE) {
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
}
bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
bpf_skb_load_bytes(skb, nhoff + ((verlen & 0xF) << 2), &(e->ports), 4);
e->pkt_type = skb->pkt_type;
e->ifindex = skb->ifindex;
bpf_ringbuf_submit(e, 0);
return skb->len;
}
从接收到的数据包中提取一些元数据,并将这些元数据保存到一个ring buffer中。具体来说,它提取的元数据包括:
这个BPF程序只处理非分片的IP数据包,并且只处理以太网协议类型为IP的数据包。
rb是一个ring buffer类型的map,最大容量为256 * 1024项。ring buffer是一种先进先出(FIFO)的数据结构,用于保存从数据包中提取的元数据。
在之前的案例中也提到过这个结构,就是用于和用户空间之间通信的
ip_is_fragment函数的主要作用是检查给定的IP数据包是否是一个分片。在IP协议中,如果一个数据包过大,超过了最大传输单元(MTU),那么这个数据包会被分片(fragmented)成多个更小的数据包以进行传输。这个过程可以在发送端进行,也可以在路由过程中由路由器进行。每个分片都是一个完整的IP数据包,包含了完整的IP头,但只包含了原始数据包的一部分数据。
ip_is_fragment函数通过检查IP头中的"fragment offset"字段来判断一个数据包是否是分片。这个字段的结构如下:
所以,如果一个数据包是分片,那么它的"More Fragments"位是1,或者它的"Fragment Offset"不是0。在ip_is_fragment函数中,这两个条件被合并在一起,所以只要这个字段的值和IP_MF或IP_OFFSET进行位与操作的结果不是0,那么这个函数就会返回1,表示这个数据包是分片。否则,这个函数就会返回0,表示这个数据包不是分片。
注意,这个函数只能判断一个数据包是否是分片,但不能判断这个分片是原始数据包的哪一部分。为了完整地重组一个分片的数据包,需要收集所有的分片,然后按照它们的"Fragment Offset"字段的值将它们按正确的顺序组合在一起。
if (ip_is_fragment(skb, nhoff))
return 0;
如果ip_is_fragment函数返回true(或者非零),那么这段代码就会执行return 0;,意味着该函数在这个点上结束,且返回值为0。这是因为该BPF程序只处理非分片的IP数据包。
static inline void bpf_skb_load_bytes(const struct __sk_buff *skb, u32 off, void *to, u32 len);
这个函数的参数包括:
bpf_skb_load_bytes函数的功能是从数据包的指定偏移量开始,读取指定长度的数据,并将这些数据复制到指定的缓冲区。这个函数可以用于读取数据包的任何部分,包括数据包头(例如IP头,TCP头等)和数据包的数据部分。
注意,bpf_skb_load_bytes函数只能在BPF程序中使用,不能在普通的C程序中使用。这是因为这个函数在运行时将被BPF虚拟机转换为对底层数据结构的直接操作。
bpf_skb_load_bytes(skb, 12, &proto, 2);
这行代码是在使用bpf_skb_load_bytes函数从数据包中读取两个字节的数据,这两个字节位于数据包的偏移量为12的位置。这两个字节被解释为网络层协议类型(proto),并且它们被复制到变量proto中。
在以太网帧的标准结构中,偏移量为12的位置是帧类型字段的开始位置。该字段用于表示以太网帧的有效载荷(Payload)中的协议类型。例如,如果这个字段的值是0x0800,那么表示有效载荷中的数据是一个IP数据包;如果这个字段的值是0x86DD,那么表示有效载荷中的数据是一个IPv6数据包。
因此,这行代码的目的是读取以太网帧的类型字段,然后检查这个字段的值。如果这个字段的值表示有效载荷中的数据是一个IP数据包,那么socket_handler函数将继续处理这个数据包;否则,socket_handler函数将忽略这个数据包。
在这个代码中是不处理GRE协议的
GRE,全称为Generic Routing Encapsulation(通用路由封装),是一个网络层协议,用于在两个网络节点之间封装任何网络层协议的数据包,使其能够经过不同的网络传输。
GRE协议可用于在两个网络之间建立直接的、点对点的连接,即使这两个网络在地理上相隔很远。这使得GRE协议成为建立VPN(虚拟专用网络)的一种常用技术。
GRE协议的一大优点是它能够封装几乎所有的网络层协议,包括IPv4、IPv6、IPX等。这使得GRE协议非常灵活,能够应用于各种网络环境中。
然而,GRE协议也有一些缺点。例如,GRE协议没有内置的加密功能,因此,如果需要保证数据的安全性,那么就需要额外实现加密功能。此外,由于GRE协议需要额外的头部信息来封装原始的数据包,因此,它会增加网络的带宽使用和处理开销。
GRE是由IETF(互联网工程任务组)在RFC 2784中定义的。
但是因为如下原因,一般不会处理GRE协议。
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2022 Jacky Yin */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "sockfilter.h"
#include "sockfilter.skel.h"
static struct env {
const char *interface;
} env;
const char argp_program_doc[] =
"BPF socket filter demo application.\n"
"\n"
"This program watch network packet of specified interface and print out src/dst\n"
"information.\n"
"\n"
"Currently only IPv4 is supported.\n"
"\n"
"USAGE: ./sockfilter [-i ]\n";
static const struct argp_option opts[] = {
{ "interface", 'i', "INTERFACE", 0, "Network interface to attach" },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
switch (key) {
case 'i':
env.interface = arg;
break;
case ARGP_KEY_ARG:
argp_usage(state);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static const struct argp argp = {
.options = opts,
.parser = parse_arg,
.doc = argp_program_doc,
};
static const char *ipproto_mapping[IPPROTO_MAX] = {
[IPPROTO_IP] = "IP", [IPPROTO_ICMP] = "ICMP", [IPPROTO_IGMP] = "IGMP",
[IPPROTO_IPIP] = "IPIP", [IPPROTO_TCP] = "TCP", [IPPROTO_EGP] = "EGP",
[IPPROTO_PUP] = "PUP", [IPPROTO_UDP] = "UDP", [IPPROTO_IDP] = "IDP",
[IPPROTO_TP] = "TP", [IPPROTO_DCCP] = "DCCP", [IPPROTO_IPV6] = "IPV6",
[IPPROTO_RSVP] = "RSVP", [IPPROTO_GRE] = "GRE", [IPPROTO_ESP] = "ESP",
[IPPROTO_AH] = "AH", [IPPROTO_MTP] = "MTP", [IPPROTO_BEETPH] = "BEETPH",
[IPPROTO_ENCAP] = "ENCAP", [IPPROTO_PIM] = "PIM", [IPPROTO_COMP] = "COMP",
[IPPROTO_SCTP] = "SCTP", [IPPROTO_UDPLITE] = "UDPLITE", [IPPROTO_MPLS] = "MPLS",
[IPPROTO_RAW] = "RAW"
};
static int open_raw_sock(const char *name)
{
struct sockaddr_ll sll;
int sock;
sock = socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));
if (sock < 0) {
fprintf(stderr, "Failed to create raw socket\n");
return -1;
}
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = if_nametoindex(name);
sll.sll_protocol = htons(ETH_P_ALL);
if (bind(sock, (struct sockaddr *)&sll, sizeof(sll)) < 0) {
fprintf(stderr, "Failed to bind to %s: %s\n", name, strerror(errno));
close(sock);
return -1;
}
return sock;
}
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
static inline void ltoa(uint32_t addr, char *dst)
{
snprintf(dst, 16, "%u.%u.%u.%u", (addr >> 24) & 0xFF, (addr >> 16) & 0xFF,
(addr >> 8) & 0xFF, (addr & 0xFF));
}
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct so_event *e = data;
char ifname[IF_NAMESIZE];
char sstr[16] = {}, dstr[16] = {};
if (e->pkt_type != PACKET_HOST)
return 0;
if (e->ip_proto < 0 || e->ip_proto >= IPPROTO_MAX)
return 0;
if (!if_indextoname(e->ifindex, ifname))
return 0;
ltoa(ntohl(e->src_addr), sstr);
ltoa(ntohl(e->dst_addr), dstr);
printf("interface: %s\tprotocol: %s\t%s:%d(src) -> %s:%d(dst)\n", ifname,
ipproto_mapping[e->ip_proto], sstr, ntohs(e->port16[0]), dstr, ntohs(e->port16[1]));
return 0;
}
static volatile bool exiting = false;
static void sig_handler(int sig)
{
exiting = true;
}
int main(int argc, char **argv)
{
struct ring_buffer *rb = NULL;
struct sockfilter_bpf *skel;
int err, prog_fd, sock;
env.interface = "lo";
/* Parse command line arguments */
err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
if (err)
return -err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Cleaner handling of Ctrl-C */
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* Load and verify BPF programs*/
skel = sockfilter_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
/* Set up ring buffer polling */
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
/* Create raw socket for localhost interface */
sock = open_raw_sock(env.interface);
if (sock < 0) {
err = -2;
fprintf(stderr, "Failed to open raw socket\n");
goto cleanup;
}
/* Attach BPF program to raw socket */
prog_fd = bpf_program__fd(skel->progs.socket_handler);
if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd))) {
err = -3;
fprintf(stderr, "Failed to attach to raw socket\n");
goto cleanup;
}
/* Process events */
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR) {
err = 0;
break;
}
if (err < 0) {
fprintf(stderr, "Error polling perf buffer: %d\n", err);
break;
}
sleep(1);
}
cleanup:
ring_buffer__free(rb);
sockfilter_bpf__destroy(skel);
return -err;
}
用于分析网络数据包的 BPF (Berkeley Packet Filter) 程序。程序会侦听指定的网络接口,并打印出源和目标的信息。目前,只支持处理 IPv4 协议。
open_raw_sock() 函数的主要功能是创建一个原始套接字,并将其绑定到指定的网络接口上。这个函数的主要步骤如下:
原始套接字(Raw Socket)允许在更低的网络层次(如 IP 或 Ethernet 层次)上进行通信。使用原始套接字,应用程序可以自行处理或生成协议头部,例如,IP 头或 TCP 头,而不是由操作系统的网络栈来处理。这使得原始套接字非常适合于实现网络监控工具,或者自定义协议。
相比之下,普通的套接字(如 TCP 或 UDP 套接字)是在更高的网络层次(如传输层)上进行通信。这些套接字由操作系统的网络栈自动处理协议头部,并为应用程序提供了一个简单的读写接口。
需要注意的是,由于原始套接字能够直接访问底层网络协议,使用原始套接字通常需要相应的权限(如 root 权限)。
这段代码使用 setsockopt() 函数将 BPF(Berkeley Packet Filter)程序附加到原始套接字上。
下面是每个参数的详细解释:
当这段代码执行后,所有通过 sock socket 接收的数据包将首先通过 prog_fd 指向的 BPF 程序进行处理。也就是说,BPF 程序能够在数据包被 socket 读取之前对其进行过滤或修改。
interface: lo protocol: TCP 127.0.0.1:56738(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:57856(dst)
interface: lo protocol: TCP 127.0.0.1:57856(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:57856(dst)
interface: lo protocol: TCP 127.0.0.1:57856(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:57856(dst)
interface: lo protocol: TCP 127.0.0.1:57856(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:56738(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:56738(dst)
interface: lo protocol: TCP 127.0.0.1:56738(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:56738(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:52778(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:52762(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:56738(dst)
interface: lo protocol: TCP 127.0.0.1:56738(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:52778(dst)
interface: lo protocol: TCP 127.0.0.1:52778(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:52778(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:52778(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:52762(dst)
interface: lo protocol: TCP 127.0.0.1:52762(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:52762(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:52762(dst)
interface: lo protocol: TCP 127.0.0.1:52778(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:52762(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:57856(dst)
interface: lo protocol: TCP 127.0.0.1:57856(src) -> 127.0.0.1:37351(dst)
interface: lo protocol: TCP 127.0.0.1:37351(src) -> 127.0.0.1:57856(dst)
interface: lo protocol: TCP 127.0.0.1:57856(src) -> 127.0.0.1:37351(dst)