一、前言
不要认为是一些小的demo而忽略对它的学习,往往一个复杂的代码,复杂的工程是一个个小demo拼接成的。
ebpf 网络过滤器这门技术在网络监控领域用的非常多,ebpf 编程我现在用的比较多的其实只有几个探针,kprobe、uprobe 以及linux网络过滤器。
今天读了 <<Linux内核观测技术BPF>>以及linux 内核源码中的sock_user1.c sock_kern.c ,linux内核的demo我认为写的还是很简单,而且这本书里面写的也很简单,打算对书里和linux内核的demo里的例子做一下实践。
内核代码很复杂,以后有空再看,今晚对linux 网络过滤器这块部分进行了实践。
心中一直有个疑问,好多网文都说他是cbpf的革新,那么cbpf filter 在pcap 中有过滤数据包能力,那ebpf是如何做到的呢?
二、实践
场景1:ebpf 统计网络包
ebpf程序
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
/* Packet types */
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __BPF_LEGACY__
#define __BPF_LEGACY__
/* llvm builtin functions that eBPF C program may use to
* emit BPF_LD_ABS and BPF_LD_IND instructions
*/
unsigned long long load_byte(void *skb,
unsigned long long off) asm("llvm.bpf.load.byte");
unsigned long long load_half(void *skb,
unsigned long long off) asm("llvm.bpf.load.half");
unsigned long long load_word(void *skb,
unsigned long long off) asm("llvm.bpf.load.word");
#endif
#define PACKET_OUTGOING 4 /* Outgoing of any type */
#define ETH_ALEN 6 /* Octets in one ethernet addr */
#define ETH_TLEN 2 /* Octets in ethernet type field */
#define ETH_HLEN 14 /* Total octets in header. */
#define ETH_ZLEN 60 /* Min. octets in frame sans FCS */
#define ETH_DATA_LEN 1500 /* Max. octets in payload */
#define ETH_FRAME_LEN 1514 /* Max. octets in frame sans FCS */
#define ETH_FCS_LEN 4 /* Octets in the FCS */
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, long);
__uint(max_entries, 256);
} my_map SEC(".maps");
SEC("socket/test")
int bpf_prog1(struct __sk_buff *skb)
{
int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
long *value;
if (skb->pkt_type != PACKET_OUTGOING)
return -1;
value = bpf_map_lookup_elem(&my_map, &index);
if (value)
__sync_fetch_and_add(value, skb->len);
return 0;
}
char _license[] SEC("license") = "GPL";
编译ebpf程序:
/usr/bin/clang-14 -g -O2 -target bpf -D__TARGET_ARCH_x86_64 -c kern1.c -o kernel_write.o
user 部分程序
其实很简单 创建一个链路套接字,然后把我们的bpf程序使用SO_ATTACH_BPF附着上去就可以了
common.h
/* SPDX-License-Identifier: GPL-2.0 */
#include <stdlib.h>
#include <stdio.h>
#include <linux/unistd.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <linux/if_ether.h>
#include <net/if.h>
#include <linux/if_packet.h>
#include <arpa/inet.h>
static inline 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) {
printf("cannot 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) {
printf("bind to %s: %s\n", name, strerror(errno));
close(sock);
return -1;
}
return sock;
}
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <bpf/bpf.h>
#include <assert.h>
#include <bpf/libbpf.h>
#include <net/ethernet.h> /* the L2 protocols */// ETH_P_ALL
#include <linux/if_packet.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/ip.h> // ip head
#include <netinet/tcp.h> // ip head
#include <netinet/udp.h> // udp head
#include <signal.h>
#include <string.h>
#include "common.h"
#define PACKET_SIZE 1000
#ifndef ETH_P_WSMP
#define ETH_P_WSMP 0x88DC
#endif
int main(int argc, char **argv)
{
char msg[255];
struct bpf_object *obj;
struct bpf_program *prog;
int map_fd, prog_fd;
char filename[256];
int i, sock, err;
FILE *f;
obj = bpf_object__open_file("kernel_write.o", NULL);
if (libbpf_get_error(obj))
return 1;
prog = bpf_object__find_program_by_name(obj, "bpf_prog1");
bpf_program__set_type(prog, BPF_PROG_TYPE_SOCKET_FILTER);
err = bpf_object__load(obj);
if (err)
return 1;
prog_fd = bpf_program__fd(prog);
map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");
sock = open_raw_sock("lo");
if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd,
sizeof(prog_fd))) {
fprintf(stderr, "ERROR: bpf_program__attach failed\n");
return -1;
}
f = popen("ping -4 -c5 localhost", "r");
(void) f;
for (i = 0; i < 5; i++) {
long long tcp_cnt, udp_cnt, icmp_cnt;
int key;
key = IPPROTO_TCP;
assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0);
key = IPPROTO_UDP;
assert(bpf_map_lookup_elem(map_fd, &key, &udp_cnt) == 0);
key = IPPROTO_ICMP;
assert(bpf_map_lookup_elem(map_fd, &key, &icmp_cnt) == 0);
printf("TCP %lld UDP %lld ICMP %lld bytes\n",
tcp_cnt, udp_cnt, icmp_cnt);
sleep(1);
}
return 0;
}
编译程序:
g++ user.c -l bpf -o user
运行程序:
./user
运行结果:
root@zhanglei-HP-ZHAN-66-Pro-14-inch-G5-Notebook-PC:/home/zhanglei/data/ebpf-test# ./user
TCP 0 UDP 0 ICMP 0 bytes
TCP 152 UDP 0 ICMP 196 bytes
TCP 304 UDP 0 ICMP 392 bytes
TCP 456 UDP 0 ICMP 588 bytes
TCP 1890 UDP 0 ICMP 784 bytes
结论:
ebpf 程序具有统计网络流量包的作用
场景2、不同的返回值对RAW SOCKET的影响
我们将user 部分程序进行修改,attach之后调用readfrom 从原始套接字上继续读数据,看到不同的返回值对recv 所产生的影响
user程序:
tcp.h:
//
// Created by root on 22-11-11.
//
#include <net/ethernet.h> /* the L2 protocols */// ETH_P_ALL
#include <linux/if_packet.h>
#include <linux/if.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/ip.h> // ip head
#include <netinet/tcp.h> // ip head
#include <netinet/udp.h> // udp head
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
struct vlan_8021q_header {
u_int16_t priority_cfi_vid;
u_int16_t ether_type;
};
#include "tcp.h"
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <bpf/bpf.h>
#include <assert.h>
#include <bpf/libbpf.h>
#include <net/ethernet.h> /* the L2 protocols */// ETH_P_ALL
#include <linux/if_packet.h>
#include <linux/if.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/ip.h> // ip head
#include <netinet/tcp.h> // ip head
#include <netinet/udp.h> // udp head
#include <signal.h>
static int running = 1;
void sigint(int signum)
{
if (!running){
exit(EXIT_FAILURE);
}
running = 0;
}
void sigalarm(int signum)
{
if (!running){
exit(EXIT_FAILURE);
}
running = 0;
}
#define PACKET_SIZE 1000
#ifndef ETH_P_WSMP
#define ETH_P_WSMP 0x88DC
#endif
void print_string_hex(char *buf, unsigned short length)
{
int i =0;
printf("\n\t\t");
for( i = 0; i < length; i++){
printf(" %02X", (buf[i]&0xFF));
if( (i + 1) % 16 == 0 )
{
printf("\n\t\t");
}
}
printf("\n\n");
}
// 校验和函数
unsigned short csum(unsigned short *buf, int nwords)
{
unsigned long sum;
for(sum=0; nwords>0; nwords--)
sum += *buf++;
sum = (sum >> 16) + (sum &0xffff);
sum += (sum >> 16);
return (unsigned short)(~sum);
}
// 发送mac层数据
int main()
{
struct sockaddr_ll stTagAddr;
memset(&stTagAddr, 0 , sizeof(stTagAddr));
stTagAddr.sll_family = AF_PACKET;//填写AF_PACKET,不再经协议层处理
stTagAddr.sll_protocol = htons(ETH_P_ALL);
int ret;
struct ifreq req;
int sd;
sd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP));//这个sd就是用来获取eth0的index,完了就关闭
if(sd == -1)
{
printf("error to create socket pf_inet:%s\n", strerror(errno));
return -1;
}
// todo 此处需要更改为你使用的网卡名称
char *eth_name = "lo";
strncpy(req.ifr_name, eth_name, strlen(eth_name));//通过设备名称获取index
ret=ioctl(sd, SIOCGIFINDEX, &req);
if(ret <0)
{
perror("SIOCGIFHWADDR");
return -1;
}
// 获取mac地址
struct ifreq if_mac;
memset(&if_mac, 0, sizeof(struct ifreq));
strncpy(if_mac.ifr_name, eth_name, strlen(eth_name));
if (ioctl(sd, SIOCGIFHWADDR, &if_mac) < 0)
{
perror("SIOCGIFHWADDR");
return -1;
}
close(sd);
if (ret==-1)
{
printf("Get eth0 index err:%d=>%s \n", errno, strerror(errno));
return -1;
}
// int SockFd = socket(PF_PACKET, SOCK_RAW, htons(VSTRONG_PROTOCOL));
int SockFd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (-1 == SockFd)
{
printf("create socket error:%d.\n",errno);
return -11;
}
struct sockaddr_ll s_addr;
/* prepare sockaddr_ll */
memset(&s_addr, 0, sizeof(s_addr));
s_addr.sll_family = AF_PACKET;
s_addr.sll_protocol = htons(ETH_P_IP);
s_addr.sll_ifindex = req.ifr_ifindex;
/* bind to interface */
if (bind(SockFd, (struct sockaddr *) &s_addr, sizeof(s_addr)) == -1)
{
perror("error to bind:");
exit(EXIT_FAILURE);
}
//处理信号
/* enable signal */
signal(SIGINT, sigint);
signal(SIGALRM, sigalarm);
int recv = 0;
uint8_t buff[PACKET_SIZE];
printf("wait recv data\n");
char msg[255];
struct bpf_object *obj;
struct bpf_program *prog;
int map_fd, prog_fd;
char filename[256];
int i, sock, err;
FILE *f;
obj = bpf_object__open_file("kernel_write.o", NULL);
if (libbpf_get_error(obj))
return 1;
prog = bpf_object__find_program_by_name(obj, "bpf_prog1");
bpf_program__set_type(prog, BPF_PROG_TYPE_SOCKET_FILTER);
err = bpf_object__load(obj);
if (err)
return 1;
prog_fd = bpf_program__fd(prog);
map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");
if (setsockopt(SockFd, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd,
sizeof(prog_fd))) {
fprintf(stderr, "ERROR: bpf_program__attach failed\n");
return -1;
}
while(1)
{
recv = recvfrom(SockFd, buff, PACKET_SIZE, 0, NULL, NULL);
printf("recv :%d \n", recv);
}
close(SockFd);
}
ebpf程序
1、返回-1
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
/* Packet types */
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __BPF_LEGACY__
#define __BPF_LEGACY__
/* llvm builtin functions that eBPF C program may use to
* emit BPF_LD_ABS and BPF_LD_IND instructions
*/
unsigned long long load_byte(void *skb,
unsigned long long off) asm("llvm.bpf.load.byte");
unsigned long long load_half(void *skb,
unsigned long long off) asm("llvm.bpf.load.half");
unsigned long long load_word(void *skb,
unsigned long long off) asm("llvm.bpf.load.word");
#endif
#define PACKET_OUTGOING 4 /* Outgoing of any type */
#define ETH_ALEN 6 /* Octets in one ethernet addr */
#define ETH_TLEN 2 /* Octets in ethernet type field */
#define ETH_HLEN 14 /* Total octets in header. */
#define ETH_ZLEN 60 /* Min. octets in frame sans FCS */
#define ETH_DATA_LEN 1500 /* Max. octets in payload */
#define ETH_FRAME_LEN 1514 /* Max. octets in frame sans FCS */
#define ETH_FCS_LEN 4 /* Octets in the FCS */
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, long);
__uint(max_entries, 256);
} my_map SEC(".maps");
SEC("socket/test")
int bpf_prog1(struct __sk_buff *skb)
{
return -1;
}
char _license[] SEC("license") = "GPL";
读取数据包:
root@zhanglei-HP-ZHAN-66-Pro-14-inch-G5-Notebook-PC:/home/zhanglei/data/ebpf-test# ./user
wait recv data
recv :86
recv :66
recv :86
recv :66
recv :374
recv :374
recv :384
recv :66
recv :1000
recv :66
recv :1000
recv :73
recv :384
recv :66
recv :1000
recv :66
recv :1000
recv :73
recv :66
recv :86
recv :66
recv :86
recv :66
总结:
返回-1 并不会过滤链路套接字的任何数据包
2.返回0
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
/* Packet types */
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __BPF_LEGACY__
#define __BPF_LEGACY__
/* llvm builtin functions that eBPF C program may use to
* emit BPF_LD_ABS and BPF_LD_IND instructions
*/
unsigned long long load_byte(void *skb,
unsigned long long off) asm("llvm.bpf.load.byte");
unsigned long long load_half(void *skb,
unsigned long long off) asm("llvm.bpf.load.half");
unsigned long long load_word(void *skb,
unsigned long long off) asm("llvm.bpf.load.word");
#endif
#define PACKET_OUTGOING 4 /* Outgoing of any type */
#define ETH_ALEN 6 /* Octets in one ethernet addr */
#define ETH_TLEN 2 /* Octets in ethernet type field */
#define ETH_HLEN 14 /* Total octets in header. */
#define ETH_ZLEN 60 /* Min. octets in frame sans FCS */
#define ETH_DATA_LEN 1500 /* Max. octets in payload */
#define ETH_FRAME_LEN 1514 /* Max. octets in frame sans FCS */
#define ETH_FCS_LEN 4 /* Octets in the FCS */
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, long);
__uint(max_entries, 256);
} my_map SEC(".maps");
SEC("socket/test")
int bpf_prog1(struct __sk_buff *skb)
{
return 0;
}
char _license[] SEC("license") = "GPL";
然后运行user程序
root@zhanglei-HP-ZHAN-66-Pro-14-inch-G5-Notebook-PC:/home/zhanglei/data/ebpf-test# ./user
wait recv data
总结:
如果返回0,那么他会过滤到链路套接字的数据包
3. 返回1
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
/* Packet types */
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __BPF_LEGACY__
#define __BPF_LEGACY__
/* llvm builtin functions that eBPF C program may use to
* emit BPF_LD_ABS and BPF_LD_IND instructions
*/
unsigned long long load_byte(void *skb,
unsigned long long off) asm("llvm.bpf.load.byte");
unsigned long long load_half(void *skb,
unsigned long long off) asm("llvm.bpf.load.half");
unsigned long long load_word(void *skb,
unsigned long long off) asm("llvm.bpf.load.word");
#endif
#define PACKET_OUTGOING 4 /* Outgoing of any type */
#define ETH_ALEN 6 /* Octets in one ethernet addr */
#define ETH_TLEN 2 /* Octets in ethernet type field */
#define ETH_HLEN 14 /* Total octets in header. */
#define ETH_ZLEN 60 /* Min. octets in frame sans FCS */
#define ETH_DATA_LEN 1500 /* Max. octets in payload */
#define ETH_FRAME_LEN 1514 /* Max. octets in frame sans FCS */
#define ETH_FCS_LEN 4 /* Octets in the FCS */
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, long);
__uint(max_entries, 256);
} my_map SEC(".maps");
SEC("socket/test")
int bpf_prog1(struct __sk_buff *skb)
{
return 1;
}
char _license[] SEC("license") = "GPL";
运行结果:
root@zhanglei-HP-ZHAN-66-Pro-14-inch-G5-Notebook-PC:/home/zhanglei/data/ebpf-test# ./user
wait recv data
recv :1
recv :1
recv :1
recv :1
recv :1
recv :1
recv :1
recv :1
recv :1
recv :1
recv :1
recv :1
总结:
返回1 那么会造成recvfrom 只读到了1个字节,也就是说返回的数字,就代表recvfrom读到的字节数,如果是-1代表不过滤
三、总结
ebpf 网络过滤器具有统计监控以及数据过滤的功能,下次有空我会写个关于分类过滤器的调研