eBPF:使用bcc调试BGP MD5(TCP MD5 Signature Option)过程

我为什么要做这个事情
在路由器上配置BGP之后,BGP进程会打印一些BGP日志,例如邻居UP,邻居Down

*Nov 24 09:18:40.993: %BGP-5-NBR_RESET: Neighbor 10.0.0.50 active reset (Peer closed the session)

这些其实还是BGP进程在做的事情。但如果配置了BGP MD5认证,遇到的报错就是这样

*Nov 24 09:14:05.326: %TCP-6-BADAUTH: No MD5 digest from 10.0.0.50(35796) to 10.0.0.231(179) tableid - 0

这个认证的过程是TCP协议栈处理的,那么异常也是内核抛出的,这其实就跟BGP应用没什么关系了。因此想研究一下这个过程。

BGP MD5认证

BGP的 MD5认证是在Peer双方都配置一串密码。其机制是双方都生成一个MD5签名附加在每一个TCP报文的option字段里。对面收到报文后会来校验签名,如果校验有问题,则会丢弃数据包并打印日志。
这种签名的机制是RFC2385 Protection of BGP Sessions via the TCP MD5 Signature Option 设计的机制。

RFC 2385 TCP MD5 Signature Option

这个option的长度是18字节,其中MD5签名占了16字节,还有1字节的类型和1字节的长度。

                   2 bytes             4 bytes
 +---------+---------+-------------------+
 | Kind=19 |Length=18|   MD5 digest...   |
 +---------+---------+-------------------+
 |                                       |
 +---------------------------------------+
 |                                       |
 +---------------------------------------+
 |                                       |
 +-------------------+-------------------+
 |                   |
 +-------------------+

计算签名需要使用四个信息:

  1. TCP 伪首部 , 按顺序是 源IP,目的IP,1字节的0填充,1字节协议号,2字节TCP报文长度。
  2. TCP 首部, 不包括选项,校验和预设为0.
  3. TCP 报文数据字段,如果有的话。
  4. 秘钥,例如BGP Password。

实验方式

一台Cisco路由器和一个装了Quagga的Linux机器。二者建立BGP邻居,之后配置BGP MD5 认证。

  • 10.0.0.50 - Quagga 内核版本 5.10.228-219.884.amzn2.x86_64
  • 10.0.0.231 - Cisco Router Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.9.1a, RELEASE SOFTWARE (fc2)

日志展示

先看一下原生的各种日志

路由器

密码不匹配的时候

*Nov 25 12:32:40.375: %TCP-6-BADAUTH: Invalid MD5 digest from 10.0.0.50(38918) to 10.0.0.231(179) tableid - 0

本端要求密码,对面没有配置密码的时候

*Nov 24 09:14:05.326: %TCP-6-BADAUTH: No MD5 digest from 10.0.0.50(35796) to 10.0.0.231(179) tableid - 0

经过抓包可以看到,只有路由器在179端口收到数据包的时候才会打印日志,如果是路由器去连接对方的179端口,并不会打印日志。

Quagga

密码不匹配的时候, /var/log/message 会输出下面的内容

Nov 25 12:36:08 ip-10-0-0-50 kernel: TCP: MD5 Hash failed for (10.0.0.231, 33440)->(10.0.0.50, 179) L3 index 0

对面没有配置密码的时候这个时候不会打印日志,会在一个计数器上+1。

[root@ip-10-0-0-50 ec2-user]# netstat -s | grep -i md5
    TCPMD5NotFound: 17037     -> 本端要求md5签名,但对面没有携带签名
    TCPMD5Unexpected: 88      -> 本端不要求md5, 但对方携带了。
    TCPMD5Failure: 409        -> 本端要求md4签名,但对方的签名不匹配。

调试过程

调试主要就是在装Quagga的Linux上进行的,路由器是黑盒所以不方便做。 Quagga目前启动了两个进程,zebra和bgpd。 zebra是核心进程,用于更新内核的路由规则,而bgpd是负责bgp协议工作的进程。
先找到bgpd的PID,用strace看一下能否找到和md5签名相关的调用。

# strace -p 9192
****
setsockopt(15, SOL_TCP, TCP_MD5SIG, "\2\0\0\0\n\0\0\347\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 216) = 0
capset({version=_LINUX_CAPABILITY_VERSION_3, pid=0}, {effective=0, permitted=1<<CAP_NET_BIND_SERVICE|1<<CAP_NET_BROADCAST|1<<CAP_NET_ADMIN|1<<CAP_NET_RAW, inheritable=0}) = 0
fcntl(15, F_GETFL)                      = 0x2 (flags O_RDWR)
fcntl(15, F_SETFL, O_RDWR|O_NONBLOCK)   = 0
connect(15, {sa_family=AF_INET, sin_port=htons(179), sin_addr=inet_addr("10.0.0.231")}, 16) = -1 EINPROGRESS (Operation now in progress)

可以看到setsockopt设置了TCP_MD5SIG,这代表本端配置了MD5校验。不过这个是系统调用,我是想找到内核函数怎么处理的。tracepoint是针对一些系统调用,这些调用是内核代码中定义好的。

#  perf trace --event 'net:*' -p 9192
这个命令没有输出内容。

既然这样,需要看一下到底调用了哪些内核函数,这个时候可以使用kprobes。
kprobes允许用户定义回调函数,然后在内核函数中动态地插入探测点,当内核执行到指定的探测函数时,会调用该回调函数,用户即可收集所需的信息,同时内核最后还会回到原本的正常执行流程。
在eBPF中,也可以使用kprobe,在指定内核函数入口插入探测点,并执行用户定义的eBPF程序。
BCC是一个框架,它允许用户编写python程序,并将eBPF程序嵌入其中。该框架主要用于应用程序和系统的分析/跟踪等场景,其中eBPF程序用于收集统计数据或生成事件,而用户空间中的对应程序收集数据并以易理解的形式展示。运行python程序将生成eBPF字节码并将其加载到内核中。
因此下一步就编写python程序,跟踪相关的内核函数并打印。

为了找到相关的函数翻了一下源码,是tcp_v4_inbound_md5_hash这个函数负责tcp选项里的md5签名校验。
tcp_v4_inbound_md5_hash 中会首先查询是否期望对方发送md5签名,这是tcp_md5_do_lookup函数来做的; 其次会解析数据包中是否有这个md5签名的选项,这是tcp_parse_md5sig_option函数来做的。之后,会出现以下五种情况:

  1. 本端不期望md5签名,对端没有携带md5签名。
  2. 本端不期望md5签名,对端携带了md5签名。
  3. 本端期望md5签名,对端没有携带md5签名。
  4. 本端期望md5签名,对端携带了md5签名,但是校验md5签名没有通过。
  5. 本端期望md5签名,对端携带了md5签名,校验md5签名通过。

情况1和5是正常的情况。 2,3和4会使得对应的计数器+1,并且4还会打印日志。

BCC代码编写过程

tcp_v4_inbound_md5_hash 函数传入了sk和skb两个指针,从skb中获取数据包的IP地址信息和端口号信息。
tcp_v4_inbound_md5_hash内部还会调用 tcp_md5_do_lookup 和 tcp_parse_md5sig_option 这两个函数,拿到返回后去做判断。因此给这两个函数加kretprobe,获取其返回值。我曾经在两个不同的内核版本中运行过bcc程序,4.14版本一切正常,5.10版本会告诉我找不到tcp_md5_do_lookup探针,因此查了一下探针改为了__tcp_md5_do_lookup。

cannot attach kprobe, probe entry may not exist

[root@ip-10-0-0-50 ec2-user]# bpftrace -l "kretprobe:*" | grep tcp_md5_do_lookup
kretprobe:__tcp_md5_do_lookup
kretprobe:tcp_md5_do_lookup_exact

__tcp_md5_do_lookup
这些信息拿到后,传输给python程序进行进一步处理和输出。
tcp_v4_inbound_md5_hash这个函数是对于收到每一个TCP报文都会调用一次,而我只打算调试BGP的报文,因此需要对输出的内容做一下过滤,对于输出,原本是使用bpf_trace_printk,但它最多只能传三个参数,并且还有类型的限制。因此改用BPF_PERF_OUTPUT来输出。

运行效果

在Quagga的上配置BGP md5,但Cisco上没有配置md5,在Quagga上运行这个程序,可以得到下面的输出。

[root@ip-10-0-0-50 ec2-user]# python3 tcp.py 
TIME(s)            COMM             PID    SADDR  DADDR  SPORT  DPORT 
No MD5 digest
12.836720539       swapper/1        0      10.0.0.231      10.0.0.50       23876  179   
No MD5 digest
13.601122181       swapper/0        0      10.0.0.231      10.0.0.50       179    58978 
No MD5 digest

附上最终代码:

from __future__ import print_function
from bcc import BPF
from bcc.utils import printb
import socket
import struct
from ctypes import *

bpf_text = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>
#include <uapi/linux/icmp.h>
#include <linux/icmp.h>
#include <uapi/linux/ip.h>
#include <linux/ip.h>
#include <linux/tcp.h>

BPF_HASH(expect, u32);
BPF_HASH(location, u32);
struct pktdata{
    u32 pid;
    u64 ts;
    char comm[TASK_COMM_LEN];
    u16 sport;
    u16 dport;
    u32 saddr;
    u32 daddr;
};
BPF_PERF_OUTPUT(events);

int tcp_md5(struct pt_regs *ctx,struct sock *sk, struct sk_buff *skb)
{
    struct pktdata data = {};
    data.pid = bpf_get_current_pid_tgid();
    data.ts = bpf_ktime_get_ns();
    bpf_get_current_comm(&data.comm,sizeof(data.comm));
    struct tcphdr *tcp = (struct tcphdr *)(skb->data);
    data.sport = bpf_htons(tcp->source);
    data.dport = bpf_htons(tcp->dest);
    struct iphdr *iph = (struct iphdr *)(skb->head + skb->network_header);
    data.saddr = bpf_htonl(iph->saddr);
    data.daddr = bpf_htonl(iph->daddr);
    expect.update(&data.pid, &data.ts);
    location.update(&data.pid, &data.ts);
    events.perf_submit(ctx,&data,sizeof(data));
    return 0;
};

int tcp_md5_do_lookup_return(struct pt_regs *ctx)
{
    unsigned long long    ret = PT_REGS_RC(ctx);
    u32 pid = bpf_get_current_pid_tgid();
    expect.update(&pid, &ret);

    return 0;
};

int tcp_parse_md5sig_option_return(struct pt_regs *ctx)
{
    unsigned long long ret = PT_REGS_RC(ctx);
    u32 pid = bpf_get_current_pid_tgid();
    location.update(&pid, &ret);

    return 0;
};
"""
# initialize BPF
b = BPF(text=bpf_text)
b.attach_kprobe(event="tcp_v4_inbound_md5_hash", fn_name="tcp_md5")
b.attach_kretprobe(event = "__tcp_md5_do_lookup", fn_name = "tcp_md5_do_lookup_return")
b.attach_kretprobe(event = "tcp_parse_md5sig_option", fn_name = "tcp_parse_md5sig_option_return")

print("%-18s %-16s %-6s %-6s %-6s %-6s %-6s" % ("TIME(s)","COMM","PID","SADDR", "DADDR","SPORT", "DPORT"))
#end format output
expect = b["expect"]
location = b["location"]
start = 0
def print_event(cpu,data,size):
    global start
    event = b["events"].event(data)
    if start == 0:
        start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    # Convert IP from numeric to string.
    sipstr = socket.inet_ntoa(struct.pack('>I', event.saddr))
    dipstr = socket.inet_ntoa(struct.pack('>I', event.daddr))

    if ((sipstr == "10.0.0.231" or dipstr == "10.0.0.231") and (event.sport == 179 or event.dport == 179)):
        # Convert IP from string to bytes for printb
        sip = sipstr.encode('utf-8')
        dip = dipstr.encode('utf-8')
        try:
            for k,v in location.items():
                if int(expect[k].value) != 0 and int(location[k].value)==0:
                    print("No MD5 digest")
            printb(b"%-18.9f %-16s %-6d %-15s %-15s %-6d %-6d" % (time_s,event.comm,event.pid,sip, dip,event.sport,event.dport))
        except Exception as e:
            print(e)
            exit()



b["events"].open_perf_buffer(print_event)

exiting = False
while not exiting:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exiting = True


发表评论

  • OωO
  • |´・ω・)ノ
  • ヾ(≧∇≦*)ゝ
  • (☆ω☆)
  • (╯‵□′)╯︵┴─┴
  •  ̄﹃ ̄
  • (/ω\)
  • ∠(ᐛ」∠)_
  • (๑•̀ㅁ•́ฅ)
  • →_→
  • ୧(๑•̀⌄•́๑)૭
  • ٩(ˊᗜˋ*)و
  • (ノ°ο°)ノ
  • (´இ皿இ`)
  • ⌇●﹏●⌇
  • (ฅ´ω`ฅ)
  • (╯°A°)╯︵○○○
  • φ( ̄∇ ̄o)
  • (งᵒ̌皿ᵒ̌)ง⁼³₌₃
  • (ó﹏ò。)
  • Σ(っ°Д°;)っ
  • ╮(╯▽╰)╭
  • o(*
  • >﹏<
  • (。•ˇ‸ˇ•。)
  • 泡泡
  • 颜文字

*