ebpf

点击查看原文

什么是 eBPF?

eBPF 是革命性技术, 起源于 linux 内核, 能够在操作系统内核中执行沙盒程序. 旨在不改变内核源码或加载内核模块的前提下安全便捷的扩展内核能力.

历史上, 由于内核拥有全局查看并控制整个操作系统的特权, 操作系统一直被认为是实现可观察性, 安全, 网络功能的理想地方. 同时, 由于其核心角色和对于稳定和安全的高要求, 操作系统很难演进. 因此, 传统上与在操作系统之外实现的功能相比, 操作系统级别的创新率较低.

overview

eBPF 从根本上改变了这种一成不变的状态. 通过允许在操作系统中执行沙盒程序, 开发者可以通过执行 eBPF 程序, 来给运行中的操作系统添加额外的能力. 就像在本地使用即时编译器(JIT)和验证引擎一样, 操作系统可以保证安全性和执行效率. 这催生了不少基于 eBPF 的项目, 涵盖了广泛的用例, 包括下一代网络、可观察性和安全功能.

今天, eBPF 被广泛用于各种用例: 在现代化的数据中心和云原生环境中提供高性能网络和负载均衡, 以较低的开销提取细粒度的可观察性安全数据, 帮助应用程序开发者追踪应用, 并能够在性能故障分析、预防性应用和容器运行时安全执法等方面提供帮助. 它的可能性是无限的, 关于 eBPF 的创新才刚开始.

什么是 eBPF.io?

eBPF.io 是以 eBPF 为主题, 每个人学习和协作的地方. eBPF 是一个开源社区, 每个人可以实践或者分享. 不论你是想阅读 eBPF 第一篇介绍文章, 还是发现更多阅读素材, 抑或是为变成 eBPF 主项目贡献者迈出第一步, eBPF.io 会一直陪伴你帮助你.

介绍 eBPF

下面的章节是关于 eBPF 的快速介绍. 如果你想了解更多, 查看 eBPF & XDP Reference Guide. 不管你是一名从事 eBPF 的开发者, 或是有兴趣使用 eBPF 作为解决方案, 理解基础概念和架构都是很有用的.

Hook 概览

eBPF 程序是事件驱动的, 能在内核或应用程序执行到一个特定的 hook 点时执行. 预定义的 hooks 包含系统调用, 函数出/入口, 内核追踪点, 网络事件等等.

syscall_hook

如果预定义 hook 不能满足需求, 也可以创建内核探针(kprobe)或者用户探针(uprobe), 在内核/用户应用程序的任何位置, 把探针附加到 eBPF 程序上.

eBPF 程序怎么写?

在很多场景中, 用户不需要直接使用 eBPF, 而是通过一些项目, 比如 cilium, bccbpftrace, 它们是 eBPF 上层的抽象, 提供了使用 eBPF 实现的特定功能, 用户无需直接编写 eBPF 程序.

clang

如果没有高级抽象, 就需要直接编写 eBPF 程序. Linux 内核要器加载字节码形式的 eBPF 程序. 虽然可以直接编写字节码, 但是更普遍的开发实践是借用像 LLVM 这样的编译器, 把伪 C 代码编译成字节码.

加载器 & 验证架构

当所需的钩子被识别后, 可以使用 bpf 系统调用将 eBPF 程序加载到 Linux 内核中. 这通常使用一个可用的 eBPF 工具库来完成. 下一节将介绍一些可用的开发工具链.

go

当程序加载到 Linux 内核中时, 它在附加到请求的钩子之前要经过两个步骤:

验证

这一步是为了确保 eBPF 程序安全执行. 它验证程序是否满足一些条件, 比如:

  • 加载 eBPF 程序的进程拥有所需的能力(特权). 除非启用非特权 eBPF, 否则只有特权进程才能加载 eBPF 程序.
  • 该程序不能崩溃或者以其他方式伤害操作系统.
  • 该程序必须总是能执行完(即程序不会死循环, 阻止后面的处理).

即时编译 (JIT)

该步骤将通用字节码翻译成机器特定的指令集, 以优化程序的执行速度. 这使 eBPF 程序像原生编译的内核代码或者像已加载的内核模块代码一样高效运行.

Maps

eBPF 程序一个重要能力是: 能够共享收集的信息, 能够存储状态. 为了实现该能力, eBPF 程序借用 Maps 来存储/获取数据, 它支持丰富的数据结构. 通过系统调用, 可以从 eBPF 程序或者用户空间应用访问 maps.

map_architecture

为了解 map 类型的多样性, 下面是不完整的 map 类型列表. 这些类型的变量同时是 共享变量 和 per-CPU 变量.

  • Hash tables, Arrays 哈希表, 数组
  • LRU (Least Recently Used) 最近最少使用
  • Ring Buffer 环形缓冲区
  • Stack Trace 堆栈跟踪
  • LPM (Longest Prefix Match) 最长前缀匹配

帮助函数

eBPF 程序不能随意调用内核函数. 如果允许的话, 将会把 eBPF 程序绑定到特定的内核版本, 这会使程序的兼容性复杂化. 所以, eBPF 程序转而使用帮助函数, 它是内核提供的大家熟知的稳定的 API.

helper

可用的帮助函数还在持续发展中, 例如:

  • 生成随机数
  • 获取当前时间和日期
  • 访问 eBPF map
  • 获取 process/cgroup 上下文
  • 网络数据包处理和转发逻辑

尾调用 & 函数调用

eBPF 程序可以组合使用尾调用和函数调用(tail & function calls). 函数调用允许在 eBPF 程序中定义和调用函数. 尾调用可以调用执行其他 eBPF 程序, 并替换执行上下文, 类似于 execve() 系统调用对常规进程的操作方式.

tailcall

eBPF 安全

权利越大, 责任越大

eBPF 是一项伟大的技术, 当下在很多关键软件中都扮演了核心的角色. 在 eBPF 程序开发过程中, 当 eBPF 进入 Linux 内核时, eBPF 的安全性就变得异常重要. eBPF 的安全性通过下面几点来保证:

要求特权

除非开启非特权 eBPF, 所有企图加载 eBPF 程序到内核的进程必须在特权模式(root)下运行,或者必须获得 CAP_BPF 能力. 这意味着非授信的程序不能加载 eBPF 程序.

如果开启非特权 eBPF, 非特权进程可以加载特定的 eBPF 程序, 它们仅能使用被缩减的功能集合, 并且将受限制的访问内核.

验证器

如果进程允许加载 eBPF 程序, 所有的程序都要经过 eBPF 验证器, 验证器来确保程序本身的安全性. 这意味着:

  • 通过验证的程序一定会执行完, 比如, eBPF 程序不会卡住或死循环. eBPF 程序可以包含有边界的循环, 但是验证器要求, 循环必须具有可以被执行到的退出条件.
  • 程序不能使用任何未初始化的变量或者越界访问内存.
  • 程序必须在系统要求的大小范围内. 随意大的 eBPF 程序是无法加载的.
  • 程序必须具备有限的复杂性. 验证器会评估所有可能的执行路径, 并且必须在配置的复杂度范围内完成分析.

加固

完成验证之后, 根据 eBPF 程序是从特权进程还是非特权进程加载, 来决定是否加固的 eBPF 程序. 这包括:

  • 程序执行保护: 存有 eBPF 程序的内核内存是被保护的并且是只读的. 不管是内核 bug 或者是被恶意操纵, 内核都将崩溃, 而不是允许它继续执行损坏/被操纵的程序.
  • Mitigation against Spectre: Under speculation CPUs may mispredict branches and leave observable side effects that could be extracted through a side channel. 举几个例子: eBPF programs mask memory access in order to redirect access under transient instructions to controlled areas, the verifier also follows program paths accessible only under speculative execution and the JIT compiler emits Retpolines in case tail calls cannot be converted to direct calls.
  • 常量 blinding: 代码中的所有常量都被 blinded, 以防止 JIT spraying 攻击. 这可以避免: 当存在某种内核 bug 的情况下, 攻击者可以把可执行代码作为常量注入, 从而让攻击者跳转到 eBPF 程序的内存区域来执行代码.

抽象的运行时上下文

eBPF 程序不能直接访问任意内核内存. 必须通过 eBPF 助手函数访问位于程序上下文之外的数据和数据结构. 这保证了一致性的数据访问, 并使任何此类访问均受制于 eBPF 程序的权限, 例如如果可以保证修改是安全的, 则允许运行的 eBPF 程序修改某些数据结构的数据. eBPF 程序不能随机修改内核中的数据结构.

为什么使用 eBPF?

可编程的力量

还记得 GeoCities 吗? 20年前, 网页几乎全都是用静态标记语言(HTML)写的, 网页基本上是一种应用程序(浏览器)能打开的文件. 再看今天, 网页已经变成了非常成熟的应用, 并且 WEB 已经取代了绝大部分编译语言写的应用. 是什么成就了这次革命?

geocities

简单来说, 就是引入 JavaScript 之后的可编程性. 它开启了一场大规模的革命, 几乎将浏览器变成了独立的操作系统.

为什么呢? 程序员不再受限于特定的浏览器版本. 没有去说服标准机构去定义更多需要的 HTML 标签, 相反, 而是提供了一些必要的构建模块, 将浏览器底层的演进和运行在其上层的应用进行分离. 这样说可能过于简单, 因为 HTML 的确做了不小的贡献, 也的确有所发展, 但是 HTML 本身的变革还不够.

在举这个例子并将其应用到 eBPF 之前, 让我们看一下对引入 JavaScript 至关重要的几个关键方面:

  • 安全性: 不受信任的代码在用户的浏览器中运行. 这是通过沙盒 JavaScript 程序和抽象对浏览器数据的访问来解决的.
  • 持续交付: 在不需要浏览器发新版本的情况下, 程序要能不断更新. 这得益于浏览器低级的(low-level)构建模块, 它能构建任意的逻辑.
  • 性能: 必须以最小的开销提供可编程性. 这得益于即时编译器(JIT).

上面说的所有内容, 在 eBPF 中都能找到:

eBPF 对 Linux 内核的影响

现在我们回到 eBPF. 为了理解 eBPF 可编程性在 Linux 内核上的影响, 我们来看张图片, 它有助于我们对 Linux 内核的架构进行理解, 并且能了解它是如何与应用程序和硬件进行交互的.

kernel_arch

Linux 内核的主要目的是抽象硬件或虚拟硬件, 并提供一致的 API(系统调用), 允许应用程序运行和共享资源. 为了实现这一点, 维护了大量的子系统和层来分配这些职责. 每个子系统通常允许某种级别的配置来满足不同的用户需求. 如果没办法通过配置满足某种需求, 则需要更改内核. 从历史上看, 有两种选择:

原生支持 内核模块
1. 更改内核源代码并说服 Linux 内核社区 1. 写一个新的内核模块
2. 等几年新内核版本上市 2. 定期修复它, 因为每个内核版本都可能破坏它
3. 由于缺乏安全边界, 有损坏 Linux 内核的风险

在不需要改变内核源码或者加载内核模块的情况下, eBPF 为重新编程内核行为提供了一种新的选择. 在很多地方, 这很像 JavaScript 和其他脚本语言, 它们让那些改变难度大, 成本高的系统开始演进.

开发工具链

有几个开发工具链来能够协助 eBPF 程序的开发和管理. 它们能满足用户的不同需求:

bcc

BCC 是一个框架, 能够让用户编写嵌入了 eBPF 程序的 python 程序. 该框架主要用来分析和跟踪应用/系统, eBPF 在其中主要负责收集统计数据或生成事件, 然后, 对应的用户空间程序会收集这些数据并以易读的方式进行展示. 运行 python 程序会生成 eBPF 字节码并将其加载进内核.

bcc

bpftrace

bpftrace 是 Linux eBPF 的高级跟踪语言, 可用于最新的 Linux 内核(4.x). bpftrace 使用 LLVM 作为后端将脚本编译为 eBPF 字节码,并利用 BCC 与 Linux eBPF 子系统以及现有的 Linux 跟踪功能进行交互: 内核动态跟踪(kprobes)、用户级动态跟踪(uprobes)和跟踪点(tracepoints). bpftrace 语言的灵感来自 awk、C 和以前的跟踪器(如 DTrace 和 SystemTap).

bpftrace

eBPF Go 类库

eBPF Go 库提供了一个通用的 eBPF 库, 它将获取 eBPF 字节码的过程与 eBPF 程序的加载和管理分离. eBPF 程序通常是通过编写高级语言创建的, 然后使用 clang/LLVM 编译器编译为 eBPF 字节码.

go

libbpf C/C++ 类库

libbpf 库是一个基于 C/C++ 的通用 eBPF 库. 它提供给应用程序一种易用的 API 来抽象化 BPF 系统调用, 并将 eBPF 字节码(clang/LLVM 编译器生成)加载到内核的过程与之分离.

libbpf

阅读更多

如果你想学习更多的 eBPF 知识, 阅读下面的材料:

文档

教程

发言

基础

深入

Cilium

Hubble

图书

文章 & 博客

这篇文档主要演示了 opensnoop(Linux eBPF/bcc) 工具的使用.

示例

opensnoop 在系统范围内跟踪 open() 系统调用,并打印各种详细信息.

示例输出:

# ./opensnoop
PID    COMM      FD ERR PATH
17326  <...>      7   0 /sys/kernel/debug/tracing/trace_pipe
1576   snmpd      9   0 /proc/net/dev
1576   snmpd     11   0 /proc/net/if_inet6
1576   snmpd     11   0 /proc/sys/net/ipv4/neigh/eth0/retrans_time_ms
1576   snmpd     11   0 /proc/sys/net/ipv6/neigh/eth0/retrans_time_ms
1576   snmpd     11   0 /proc/sys/net/ipv6/conf/eth0/forwarding
1576   snmpd     11   0 /proc/sys/net/ipv6/neigh/eth0/base_reachable_time_ms
1576   snmpd     11   0 /proc/sys/net/ipv4/neigh/lo/retrans_time_ms
1576   snmpd     11   0 /proc/sys/net/ipv6/neigh/lo/retrans_time_ms
1576   snmpd     11   0 /proc/sys/net/ipv6/conf/lo/forwarding
1576   snmpd     11   0 /proc/sys/net/ipv6/neigh/lo/base_reachable_time_ms
1576   snmpd      9   0 /proc/diskstats
1576   snmpd      9   0 /proc/stat
1576   snmpd      9   0 /proc/vmstat
1956   supervise  9   0 supervise/status.new
1956   supervise  9   0 supervise/status.new
17358  run        3   0 /etc/ld.so.cache
17358  run        3   0 /lib/x86_64-linux-gnu/libtinfo.so.5
17358  run        3   0 /lib/x86_64-linux-gnu/libdl.so.2
17358  run        3   0 /lib/x86_64-linux-gnu/libc.so.6
17358  run       -1   6 /dev/tty
17358  run        3   0 /proc/meminfo
17358  run        3   0 /etc/nsswitch.conf
17358  run        3   0 /etc/ld.so.cache
17358  run        3   0 /lib/x86_64-linux-gnu/libnss_compat.so.2
17358  run        3   0 /lib/x86_64-linux-gnu/libnsl.so.1
17358  run        3   0 /etc/ld.so.cache
17358  run        3   0 /lib/x86_64-linux-gnu/libnss_nis.so.2
17358  run        3   0 /lib/x86_64-linux-gnu/libnss_files.so.2
17358  run        3   0 /etc/passwd
17358  run        3   0 ./run
^C

在跟踪时,snmpd 进程打开了各种 /proc 文件(读取指标). 另外, 一个 “run” 进程读取各种库和配置文件(看起来像 正在启动: 一个新进程).

如果在应用程序启动期间使用, opensnoop 可用于发现配置和日志文件.

过滤 PID

-p 选项可用于在内核中过滤 PID. 这里我将它与 -T 一起使用来打印时间戳:

$ ./opensnoop -Tp 1956
TIME(s)       PID    COMM               FD ERR PATH
0.000000000   1956   supervise           9   0 supervise/status.new
0.000289999   1956   supervise           9   0 supervise/status.new
1.023068000   1956   supervise           9   0 supervise/status.new
1.023381997   1956   supervise           9   0 supervise/status.new
2.046030000   1956   supervise           9   0 supervise/status.new
2.046363000   1956   supervise           9   0 supervise/status.new
3.068203997   1956   supervise           9   0 supervise/status.new
3.068544999   1956   supervise           9   0 supervise/status.new

这表明 supervise 进程每秒打开2次 status.new 文件.

包含/过滤 UID

-U 选项在输出中包含 UID:

# ./opensnoop -U
UID   PID    COMM               FD ERR PATH
0     27063  vminfo              5   0 /var/run/utmp
103   628    dbus-daemon        -1   2 /usr/local/share/dbus-1/system-services
103   628    dbus-daemon        18   0 /usr/share/dbus-1/system-services
103   628    dbus-daemon        -1   2 /lib/dbus-1/system-services

-u 选项过滤 UID:

# ./opensnoop -Uu 1000
UID   PID    COMM               FD ERR PATH
1000  30240  ls                  3   0 /etc/ld.so.cache
1000  30240  ls                  3   0 /lib/x86_64-linux-gnu/libselinux.so.1
1000  30240  ls                  3   0 /lib/x86_64-linux-gnu/libc.so.6
1000  30240  ls                  3   0 /lib/x86_64-linux-gnu/libpcre.so.3
1000  30240  ls                  3   0 /lib/x86_64-linux-gnu/libdl.so.2
1000  30240  ls                  3   0 /lib/x86_64-linux-gnu/libpthread.so.0

过滤失败的 opens

-x 选项仅打印失败的 opens:

# ./opensnoop -x
PID    COMM      FD ERR PATH
18372  run       -1   6 /dev/tty
18373  run       -1   6 /dev/tty
18373  multilog  -1  13 lock
18372  multilog  -1  13 lock
18384  df        -1   2 /usr/share/locale/en_US.UTF-8/LC_MESSAGES/coreutils.mo
18384  df        -1   2 /usr/share/locale/en_US.utf8/LC_MESSAGES/coreutils.mo
18384  df        -1   2 /usr/share/locale/en_US/LC_MESSAGES/coreutils.mo
18384  df        -1   2 /usr/share/locale/en.UTF-8/LC_MESSAGES/coreutils.mo
18384  df        -1   2 /usr/share/locale/en.utf8/LC_MESSAGES/coreutils.mo
18384  df        -1   2 /usr/share/locale/en/LC_MESSAGES/coreutils.mo
18385  run       -1   6 /dev/tty
18386  run       -1   6 /dev/tty

这里捕获了一个 df 命令无法打开 coreutils.mo 文件, 并尝试从其他目录打开.

列 ERR 表示系统错误码, 2 代表 ENOENT: no such file or directory.

可以使用 -d 选项设置最长跟踪持续时间. 例如, 要跟踪 2秒:

# ./opensnoop -d 2
PID    COMM               FD ERR PATH
2191   indicator-multi    11   0 /sys/block
2191   indicator-multi    11   0 /sys/block
2191   indicator-multi    11   0 /sys/block
2191   indicator-multi    11   0 /sys/block
2191   indicator-multi    11   0 /sys/block

过滤进程名称

-n 选项可用于过滤进程名称(部分匹配):

# ./opensnoop -n ed

PID    COMM               FD ERR PATH
2679   sed                 3   0 /etc/ld.so.cache
2679   sed                 3   0 /lib/x86_64-linux-gnu/libselinux.so.1
2679   sed                 3   0 /lib/x86_64-linux-gnu/libc.so.6
2679   sed                 3   0 /lib/x86_64-linux-gnu/libpcre.so.3
2679   sed                 3   0 /lib/x86_64-linux-gnu/libdl.so.2
2679   sed                 3   0 /lib/x86_64-linux-gnu/libpthread.so.0
2679   sed                 3   0 /proc/filesystems
2679   sed                 3   0 /usr/lib/locale/locale-archive
2679   sed                -1   2
2679   sed                 3   0 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
2679   sed                 3   0 /dev/null
2680   sed                 3   0 /etc/ld.so.cache
2680   sed                 3   0 /lib/x86_64-linux-gnu/libselinux.so.1
2680   sed                 3   0 /lib/x86_64-linux-gnu/libc.so.6
2680   sed                 3   0 /lib/x86_64-linux-gnu/libpcre.so.3
2680   sed                 3   0 /lib/x86_64-linux-gnu/libdl.so.2
2680   sed                 3   0 /lib/x86_64-linux-gnu/libpthread.so.0
2680   sed                 3   0 /proc/filesystems
2680   sed                 3   0 /usr/lib/locale/locale-archive
2680   sed                -1   2
^C

这里捕获了 “sed” 命令,是因为命令中使用了命令名称部分匹配 “-n ed”

过滤标志

-e 选项能打印出额外的列; 例如,以下输出包含传递给 open(2) 的标志(以八进制表示):

# ./opensnoop -e
PID    COMM               FD ERR FLAGS    PATH
28512  sshd               10   0 00101101 /proc/self/oom_score_adj
28512  sshd                3   0 02100000 /etc/ld.so.cache
28512  sshd                3   0 02100000 /lib/x86_64-linux-gnu/libwrap.so.0
28512  sshd                3   0 02100000 /lib/x86_64-linux-gnu/libaudit.so.1
28512  sshd                3   0 02100000 /lib/x86_64-linux-gnu/libpam.so.0
28512  sshd                3   0 02100000 /lib/x86_64-linux-gnu/libselinux.so.1
28512  sshd                3   0 02100000 /lib/x86_64-linux-gnu/libsystemd.so.0
28512  sshd                3   0 02100000 /usr/lib/x86_64-linux-gnu/libcrypto.so.1.0.2
28512  sshd                3   0 02100000 /lib/x86_64-linux-gnu/libutil.so.1

-f 选项能基于 open(2) 调用的标志进行过滤, 比如:

# ./opensnoop -e -f O_WRONLY -f O_RDWR
PID    COMM               FD ERR FLAGS    PATH
28084  clear_console       3   0 00100002 /dev/tty
28084  clear_console      -1  13 00100002 /dev/tty0
28084  clear_console      -1  13 00100001 /dev/tty0
28084  clear_console      -1  13 00100002 /dev/console
28084  clear_console      -1  13 00100001 /dev/console
28051  sshd                8   0 02100002 /var/run/utmp
28051  sshd                7   0 00100001 /var/log/wtmp

基于 cgroup 集进行过滤

–cgroupmap 选项基于 cgroup 集进行过滤, 它用于使用外部创建的映射.

# ./opensnoop --cgroupmap /sys/fs/bpf/test01

更多信息, 查看 docs/special_filtering.md

USAGE 说明

# ./opensnoop -h
usage: opensnoop.py [-h] [-T] [-U] [-x] [-p PID] [-t TID]
                    [--cgroupmap CGROUPMAP] [--mntnsmap MNTNSMAP] [-u UID]
                    [-d DURATION] [-n NAME] [-e] [-f FLAG_FILTER]

跟踪 open() 系统调用

optional arguments:
  -h, --help            show this help message and exit
  -T, --timestamp       include timestamp on output
  -U, --print-uid       include UID on output
  -x, --failed          only show failed opens
  -p PID, --pid PID     trace this PID only
  -t TID, --tid TID     trace this TID only
  --cgroupmap CGROUPMAP
                        trace cgroups in this BPF map only
  --mntnsmap MNTNSMAP   trace mount namespaces in this BPF map on
  -u UID, --uid UID     trace this UID only
  -d DURATION, --duration DURATION
                        total duration of trace in seconds
  -n NAME, --name NAME  only print process names containing this name
  -e, --extended_fields
                        show extended fields
  -f FLAG_FILTER, --flag_filter FLAG_FILTER
                        filter on flags argument (e.g., O_WRONLY)

examples:
    ./opensnoop           # trace all open() syscalls
    ./opensnoop -T        # include timestamps
    ./opensnoop -U        # include UID
    ./opensnoop -x        # only show failed opens
    ./opensnoop -p 181    # only trace PID 181
    ./opensnoop -t 123    # only trace TID 123
    ./opensnoop -u 1000   # only trace UID 1000
    ./opensnoop -d 10     # trace for 10 seconds only
    ./opensnoop -n main   # only print process names containing "main"
    ./opensnoop -e        # show extended fields
    ./opensnoop -f O_WRONLY -f O_RDWR  # only print calls for writing
    ./opensnoop --cgroupmap mappath  # only trace cgroups in this BPF map
    ./opensnoop --mntnsmap mappath   # only trace mount namespaces in the map

这篇文档主要演示了 tcplife(Linux eBPF/bcc) 工具的使用.

示例

tcplife 总结了在跟踪期间打开和关闭的 TCP 会话. 比如:

# ./tcplife
PID   COMM       LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS
22597 recordProg 127.0.0.1       46644 127.0.0.1       28527     0     0 0.23
3277  redis-serv 127.0.0.1       28527 127.0.0.1       46644     0     0 0.28
22598 curl       100.66.3.172    61620 52.205.89.26    80        0     1 91.79
22604 curl       100.66.3.172    44400 52.204.43.121   80        0     1 121.38
22624 recordProg 127.0.0.1       46648 127.0.0.1       28527     0     0 0.22
3277  redis-serv 127.0.0.1       28527 127.0.0.1       46648     0     0 0.27
22647 recordProg 127.0.0.1       46650 127.0.0.1       28527     0     0 0.21
3277  redis-serv 127.0.0.1       28527 127.0.0.1       46650     0     0 0.26
[...]

这捕获了一个程序 “recordProg”, 它建立了一些到 “redis-serv” 的短暂的 TCP 连接, 每个连接持续大约 0.25 毫秒. 还有几个 “curl” 会话也被跟踪, 连接到端口 80, 持续了 91 和 121 毫秒.

此工具对于工作负载表征和流量统计很有用: 识别正在发生的连接以及传输的字节.

在这个例子中, 我上传了一个 10 Mbyte 的文件到服务器, 然后再次使用 scp 下载:

# ./tcplife
PID   COMM       LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS
7715  recordProg 127.0.0.1       50894 127.0.0.1       28527     0     0 0.25
3277  redis-serv 127.0.0.1       28527 127.0.0.1       50894     0     0 0.30
7619  sshd       100.66.3.172    22    100.127.64.230  63033     5 10255 3066.79
7770  recordProg 127.0.0.1       50896 127.0.0.1       28527     0     0 0.20
3277  redis-serv 127.0.0.1       28527 127.0.0.1       50896     0     0 0.24
7793  recordProg 127.0.0.1       50898 127.0.0.1       28527     0     0 0.23
3277  redis-serv 127.0.0.1       28527 127.0.0.1       50898     0     0 0.27
7847  recordProg 127.0.0.1       50900 127.0.0.1       28527     0     0 0.24
3277  redis-serv 127.0.0.1       28527 127.0.0.1       50900     0     0 0.29
7870  recordProg 127.0.0.1       50902 127.0.0.1       28527     0     0 0.29
3277  redis-serv 127.0.0.1       28527 127.0.0.1       50902     0     0 0.30
7798  sshd       100.66.3.172    22    100.127.64.230  64925 10265     6 2176.15
[...]

可以看到 sshd 接收了 10 MB, 然后再传输出去. 看起来, 接收(3.07 秒)比传输(2.18 秒)慢.

宽显

进程名称被截断为 10 个字符. 通过使用宽选项 -w, 列宽变为 16 个字符. IP 地址栏也更宽, 以适合 IPv6 地址:

# ./tcplife -w
PID   COMM             IP LADDR                      LPORT RADDR                      RPORT  TX_KB  RX_KB MS
26315 recordProgramSt  4  127.0.0.1                  44188 127.0.0.1                  28527      0      0 0.21
3277  redis-server     4  127.0.0.1                  28527 127.0.0.1                  44188      0      0 0.26
26320 ssh              6  fe80::8a3:9dff:fed5:6b19   22440 fe80::8a3:9dff:fed5:6b19   22         1      1 457.52
26321 sshd             6  fe80::8a3:9dff:fed5:6b19   22    fe80::8a3:9dff:fed5:6b19   22440      1      1 458.69
26341 recordProgramSt  4  127.0.0.1                  44192 127.0.0.1                  28527      0      0 0.27
3277  redis-server     4  127.0.0.1                  28527 127.0.0.1                  44192      0      0 0.32

添加时间戳

可以使用 -t 添加时间戳:

# ./tcplife -t
TIME(s)   PID   COMM       LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS
0.000000  5973  recordProg 127.0.0.1       47986 127.0.0.1       28527     0     0 0.25
0.000059  3277  redis-serv 127.0.0.1       28527 127.0.0.1       47986     0     0 0.29
1.022454  5996  recordProg 127.0.0.1       47988 127.0.0.1       28527     0     0 0.23
1.022513  3277  redis-serv 127.0.0.1       28527 127.0.0.1       47988     0     0 0.27
2.044868  6019  recordProg 127.0.0.1       47990 127.0.0.1       28527     0     0 0.24
2.044924  3277  redis-serv 127.0.0.1       28527 127.0.0.1       47990     0     0 0.28
3.069136  6042  recordProg 127.0.0.1       47992 127.0.0.1       28527     0     0 0.22
3.069204  3277  redis-serv 127.0.0.1       28527 127.0.0.1       47992     0     0 0.28

这表明 recordProg 进程每秒连接一次.

另外 -T 选项可以使用 HH:MM:SS 格式时间戳.

逗号分隔列表

-s 选项可以指定逗号分隔的列表模式. 这里同时使用了 -t 和 -T 类型的时间戳:

# ./tcplife -stT
TIME,TIME(s),PID,COMM,IP,LADDR,LPORT,RADDR,RPORT,TX_KB,RX_KB,MS
23:39:38,0.000000,7335,recordProgramSt,4,127.0.0.1,48098,127.0.0.1,28527,0,0,0.26
23:39:38,0.000064,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48098,0,0,0.32
23:39:39,1.025078,7358,recordProgramSt,4,127.0.0.1,48100,127.0.0.1,28527,0,0,0.25
23:39:39,1.025141,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48100,0,0,0.30
23:39:41,2.040949,7381,recordProgramSt,4,127.0.0.1,48102,127.0.0.1,28527,0,0,0.24
23:39:41,2.041011,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48102,0,0,0.29
23:39:42,3.067848,7404,recordProgramSt,4,127.0.0.1,48104,127.0.0.1,28527,0,0,0.30
23:39:42,3.067914,3277,redis-server,4,127.0.0.1,28527,127.0.0.1,48104,0,0,0.35
[...]

端口过滤

还有过滤本地/远端端口的选项. 这里就过滤了本地 22 和 80 端口.

# ./tcplife.py -L 22,80
PID   COMM       LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS
8301  sshd       100.66.3.172    22    100.127.64.230  58671     3     3 1448.52
[...]

USAGE 说明

# ./tcplife.py -h
usage: tcplife.py [-h] [-T] [-t] [-w] [-s] [-p PID] [-L LOCALPORT]
                  [-D REMOTEPORT] [-4 | -6]

跟踪 TCP 会话的生命周期并进行总结

optional arguments:
  -h, --help            show this help message and exit
  -T, --time            include time column on output (HH:MM:SS)
  -t, --timestamp       include timestamp on output (seconds)
  -w, --wide            wide column output (fits IPv6 addresses)
  -s, --csv             comma separated values output
  -p PID, --pid PID     trace this PID only
  -L LOCALPORT, --localport LOCALPORT
                        comma-separated list of local ports to trace.
  -D REMOTEPORT, --remoteport REMOTEPORT
                        comma-separated list of remote ports to trace.
  -4, --ipv4            trace IPv4 family only
  -6, --ipv6            trace IPv6 family only

examples:
    ./tcplife           # trace all TCP connect()s
    ./tcplife -t        # include time column (HH:MM:SS)
    ./tcplife -w        # wider columns (fit IPv6)
    ./tcplife -stT      # csv output, with times & timestamps
    ./tcplife -p 181    # only trace PID 181
    ./tcplife -L 80     # only trace local port 80
    ./tcplife -L 80,81  # only trace local ports 80 and 81
    ./tcplife -D 80     # only trace remote port 80
    ./tcplife -4        # only trace IPv4 family
    ./tcplife -6        # only trace IPv6 family