When and how to use classic bpf effectively, and what benefits does it bring?

Original link: https://colobu.com/2023/09/27/when-and-how-to-use-classical-bpf/

Classical BPF (cBPF, Berkeley Packet Filter) is a technology used to filter network data packets. It is mounted on the critical path of the network stack like a hook and can filter or process network data packets according to preset rules before the data packets enter the protocol stack.

Compared with general software packet filtering solutions, Classical BPF has the following advantages:

  • High efficiency: Because it runs in the kernel space, it can avoid unnecessary switching between kernel mode and user mode, and also saves the overhead of multiple data copies.
  • Security: It cannot access system memory or modify data packets at will. It can only filter according to rules and will not cause security risks.
  • Flexible: Filtering rules can be dynamically updated, making the packet filtering function more flexible.

Classical BPF is usually used in network monitoring, firewalls, traffic control and other scenarios. It provides an efficient, secure and flexible solution for packet filtering. However, the function is relatively limited, and it can only filter packets but cannot modify them.

I have been doing network monitoring at Baidu for more than three years. We will use various methods to monitor the entire physical network of Baidu. These monitoring methods are different from ordinary TCP Server/Client or UDP programs. Generally, we will use raw Socket method is used for packet detection and network monitoring. In order to use raw socket efficiently and avoid copying all the packets in the kernel protocol layer to the application layer, we will use cBPF to filter the received packets. We only filter the packets from the kernel layer. Copy specific types of packets to the application layer, for example, only copy UDP protocol packets with destination ports between 20000 and 21000.

How to do it? Just use cBPF.

In my previous article , I used BPF to increase the throughput of Go network programs by 8 times and gave an example of using cBPF:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

conn, err := net.ListenPacket( “ip4:udp” , *addr)
if err != nil {
panic (err)
}
cc := conn.(*net.IPConn)
cc.SetReadBuffer (20 * 1024 * 1024 )
cc.SetWriteBuffer (20 * 1024 * 1024 )
pconn := ipv4.NewPacketConn(conn)
var assembled[]bpf.RawInstruction
if assembled, err = bpf.Assemble(filter); err != nil {
log.Print(err)
return
}
pconn.SetBPF(assembled)
handleConn(conn)

The filter can be set using SetBPF method of ipv4.PacketConn :


1
2
3
4
5
6
7
8

type Filter []bpf.Instruction
var filter = Filter{
bpf.LoadAbsolute{Off: 22 , Size: 2 }, // Load destination port
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32 (*port), SkipFalse: 1 }, // If the destination port != 8972, skip a line, and at the last one, the packet is discarded instruction
bpf.RetConstant{Val: 0 xffff}, // Return 0xffff, receive packet
bpf.RetConstant{Val: 0 x0}, // Returns 0 bytes, which means ignore this packet
}

Here we conduct a simple analysis based on the IP protocol. We haven’t done too many compatibility checks here, because we know that we are dealing with IPv4 packets, and there are no Option options in the packet:

The header of the IP is 20 bytes, and the payload is a UDP packet:

You can see that the first two bytes of UDP are the source port, and the next two bytes are the destination port.

So starting from the IP header, the 22nd to 24th bytes are the destination port, so bpf.LoadAbsolute{Off: 22, Size: 2}, is to read these two bytes and compare them with our values ​​to see Is it the value we expected?

So if you want to use cBPF , you must be able to write bpf.Instruction . You must be familiar with various protocols and bpf instructions.
I don’t want to learn! Tired, trouble! Error prone! Not easy to debug!

It doesn’t matter, I wrote a library. As long as you can use tcpdump/wireshark and use their filter writing method, you can write the corresponding instructions.

For example, for a command such as tcpdump -i any -nn -vvvv tcp port 8080 , its filter is tcp port 8080 You use the following function of this library:


1

raws, err := ParseTcpdumpFitlerExpr(layers.LinkTypeIPv4, “tcp port 8080” )

By calling this function you will get the compiled instruction []bpf.RawInstruction , and then call pconn.SetBPF(raws) .

If you want to get its Go code form, you can call s = CreateInstructionsFromExpr(layers.LinkTypeIPv4, "dst host 8.8.8.8 and icmp") ,
It will filter and retain only the packets whose destination IP address is 8.8.8.8 and are icmp. The generated instructions are as follows:


1
2
3
4
5
6
7
8
9

var filter = []bpf.Instruction {
bpf.LoadConstant{Dst: 0 ,Val: 0 },
bpf.LoadAbsolute{Off: 16 ,Size: 4 },
bpf.JumpIf{Cond: 1 ,Val: 134744072 ,SkipTrue: 3 } // 134744072 = 0x8080808,
bpf.LoadAbsolute{Off: 9 ,Size: 1 },
bpf.JumpIf{Cond: 1 ,Val: 1 ,SkipTrue: 1 },
bpf.RetConstant{Val: 0x1 },
bpf.RetConstant{Val: 0x0 },
}

bpf.LoadAbsolute{Off: 16,Size: 4}, is the destination IP address in the loaded IP header. Check whether it is equal to 8.8.8.8 . If so, check whether the protocol (odd: 9) is ICMP (icmp protocol number it’s 1).

So even if you are not familiar with various protocols, you can generate compiled bpf code based on the filter expression of tcpdump, or get Go language code snippets.

By the way, this library is Qianmo .

This article is reproduced from: https://colobu.com/2023/09/27/when-and-how-to-use-classical-bpf/
This site is for inclusion only, and the copyright belongs to the original author.