BPF 过滤器失败

问题描述

谁能建议为什么这个(经典的)BPF 程序有时会允许非 DHCP 响应数据包通过:

# Load the Ethertype field
BPF_LD | BPF_H | BPF_ABS    12
# And reject the packet if it's not 0x0800 (IPv4)
BPF_JMP | BPF_JEQ | BPF_K   0x0800    0    8

# Load the IP protocol field
BPF_LD | BPF_B | BPF_ABS    23
# And reject the packet if it's not 17 (UDP)
BPF_JMP | BPF_JEQ | BPF_K   17        0    6

# Check that the packet has not been fragmented
BPF_LD | BPF_H | BPF_ABS    20
BPF_JMP | BPF_JSET | BPF_K  0x1fff    4    0

# Load the IP header length field
BPF_LDX | BPF_B | BPF_MSH   14
# And load that offset + 16 to get the UDP destination port
BPF_LD | BPF_IND | BPF_H    16
# And reject the packet if the destination port is not 68
BPF_JMP | BPF_JEQ | BPF_K   68        0    1

# Accept the frame
BPF_RET | BPF_K             1500
# Reject the frame
BPF_RET | BPF_K             0

它不会让每一帧都通过,但在网络负载很重的情况下,它经常失败。我正在用这个 Python 3 程序测试它:

import ctypes
import struct
import socket
ETH_P_ALL = 0x0003
SO_ATTACH_FILTER = 26
SO_ATTACH_BPF = 50
filters = [
0x28,0x00,0x0c,0x15,0x08,0x30,0x17,0x06,0x11,0x28,0x14,0x45,0x04,0xff,0x1f,0xb1,0x0e,0x48,0x10,0x01,0x44,0xdc,0x05,]

filters = bytes(filters)

b = ctypes.create_string_buffer(filters)
mem_addr_of_filters = ctypes.addressof(b)
pf = struct.pack("HL",11,mem_addr_of_filters)
pf = bytes(pf)

def main():                                                                                     
    sock = socket.socket(socket.AF_PACKET,socket.soCK_RAW,socket.htons(ETH_P_ALL))            
    sock.bind(("eth0",ETH_P_ALL))                                                              
    sock.setsockopt(socket.soL_SOCKET,SO_ATTACH_FILTER,pf)                                    
#    sock.send(req)                                                                             
    sock.settimeout(1)                                                                          
    try:                                                                                        
        data = sock.recv(1500)                                                                  
                                                                                                
        if data[35] == 0x43:                                                                    
            return                                                                              
        print('Packet got through: 0x{:02x} 0x{:02x},0x{:02x},0x{:02x}'.format(data[12],data[13],data
    except:                   
        print('Timeout')                                                            
        return                                              
    sock.close()                                            
                              
for ii in range(1000):                                                                                   
    main()                    

如果我在将大核心文件 SCP 发送到运行该脚本的主机时执行此操作,则在大多数情况下(但不是全部)情况下,它不会达到一秒超时。在较轻的负载下,故障要少得多——例如,在套接字接收时在 ssh 链接上摆弄;有时它会通过 1000 次迭代而没有失败。

有问题的主机是 Linux 4.9.0。内核有 CONfig_BPF=y

编辑

对于同一问题的更简单版本,为什么这个 BPF 程序根本让任何数据包通过:

BPF_RET | BPF_K    0

编辑 2 上面的测试是在 ARM64 机器上进行的。我已经在 amd64 / Linux 5.9.0 上重新测试过。我仍然看到失败,尽管没有那么多。

解决方法

我得到了 a response on LKML 的解释。

问题是当帧到达界面时应用过滤器,而不是当它通过 recv() 传递到用户空间时。因此,在高负载下,帧会在使用 socket.socket(socket.AF_PACKET,socket.SOCK_RAW,socket.htons(ETH_P_ALL)) 创建套接字和使用 sock.setsockopt(socket.SOL_SOCKET,SO_ATTACH_FILTER,pf) 应用过滤器之间到达。这些帧在队列中;一旦应用过滤器,后续到达的数据包就会应用过滤器。

因此,一旦应用过滤器,就必须从套接字中“排出”所有排队的帧,然后才能依赖过滤器。