Rewrite the TCP quarantine tutorial

This commit is contained in:
LinuxDev9002
2026-07-19 02:32:32 -07:00
parent 500bbcc714
commit 87fcf8348c
2 changed files with 549 additions and 285 deletions

View File

@@ -1,71 +1,46 @@
# eBPF Tutorial by Example: Quarantine an Established TCP Connection # eBPF Tutorial by Example: Targeted TCP Connection Quarantine
Your threat-intel feed just flagged `203.0.113.99` as a command-and-control server. Firewall rules and iptables block new connections immediately, but `netstat` shows three production workers already holding established TCP sessions to that address. Those sessions will keep exfiltrating data until the process closes them or the machine reboots. You need to sever the active connections right now, without restarting services, without killing processes, and without dropping unrelated traffic. Suppose an egress destination has just been added to a policy or threat-intelligence blocklist. A firewall blocks future connects, but `netstat` shows the server already has an active TCP session to that address. You want to preview and close that exact established session while preserving the process and unrelated connections.
This tutorial builds a small eBPF tool that scans the TCP sockets in the current network namespace, finds established IPv4 connections to an exact remote address and port, and destroys them with `bpf_sock_destroy`. The TCP BPF iterator sees only the sockets belonging to the network namespace of the process that loads it, not every socket on the host. A dry-run mode lets you verify what would be killed before you pull the trigger. This tutorial builds `tcp_quarantine`, a one-shot CLI that matches an exact IPv4 destination address and port, finds all `ESTABLISHED` TCP connections matching that tuple, and counts or destroys them. The default mode is dry-run, which reports matches. The `--apply` flag directs the BPF program to call the kernel's `bpf_sock_destroy` kfunc on each match. The TCP BPF iterator's scan scope is the network namespace of the loading process.
> Complete source: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/51-tcp-quarantine> > Complete source: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/51-tcp-quarantine>
## Quick demo ## End-to-End Flow
Build the tool and run it against a target destination: The tool uses a BPF TCP iterator to walk the kernel's TCP socket table. User space first validates the IPv4 address with `inet_pton` and parses the port number, then writes both parameters into the skeleton's read-only data section `.rodata` before loading the BPF program. The `destination.s_addr` stored by `inet_pton` is already in network byte order; the host-order port and apply boolean are written alongside it.
```bash The BPF program attaches as a TCP iterator via `SEC("iter/tcp")`. When user space reads the iterator file descriptor, the kernel invokes the callback once per TCP socket in the loading process's network namespace. The callback first increments `scanned` for each non-null `sk`, then selects `AF_INET` plus `TCP_ESTABLISHED` sockets into `established`. It reads `skc_daddr`, `skc_dport`, `skc_family`, and `skc_state` through `BPF_CORE_READ`, and CO-RE relocations allow the same compiled object to run on kernels with different layouts. Destination-address comparison uses the network-order value from `inet_pton`. Port comparison uses `bpf_htons(target_port)` because `skc_dport` is stored in network byte order.
cd src/51-tcp-quarantine
make clean Exact matches increment `matched`; dry-run mode returns after counting. Apply mode calls `bpf_sock_destroy(sk)`, incrementing `destroyed` on zero return or `failed` on nonzero. `bpf_sock_destroy` is declared as a strong `__ksym`, and the kernel resolves this required kfunc at load time.
make -j2
The shared `quarantine_stats` object lives in BSS as the result channel. The loader attaches the program with `bpf_program__attach_iter`, creates an iterator fd with `bpf_iter_create`, and reads that fd to EOF to drive traversal; the read buffer serves only to drive traversal, while counters pass through BSS. User space reads the five counters after consuming the iterator. The loader destroys the iterator link and skeleton on cleanup. It returns nonzero when loading or iteration fails, or when any `bpf_sock_destroy` call increments `failed`. The `--verbose` option enables libbpf debug messages.
## The Shared Header
Both the BPF program and user-space loader include this header to ensure the BSS layout of `quarantine_stats` stays consistent.
```c
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __TCP_QUARANTINE_H
#define __TCP_QUARANTINE_H
struct quarantine_stats {
unsigned long long scanned;
unsigned long long established;
unsigned long long matched;
unsigned long long destroyed;
unsigned long long failed;
};
#endif /* __TCP_QUARANTINE_H */
``` ```
Dry-run (safe, nothing is destroyed): The five counters record: total sockets scanned, IPv4 sockets in `ESTABLISHED` state, exact destination-and-port matches, sockets successfully destroyed in apply mode, and `bpf_sock_destroy` calls that returned nonzero. User space reads these counters after iteration completes and prints a single summary line.
```bash ## The BPF Program
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063
```
Apply (destroys matching sockets): The complete BPF program source follows.
```bash
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063 --apply
```
Real output from the test environment:
```console
mode=dry-run destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=0 failed=0
mode=apply destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=1 failed=0
PASS: dry-run preserved both connections; apply destroyed only the target
```
Substitute your own destination IP and port. The tool exits after one scan; it does not run continuously.
### Targeting a different network namespace
The TCP BPF iterator scans the network namespace of the process that loads the BPF program. To quarantine connections inside a container or network namespace other than your own, enter that namespace first:
```bash
sudo nsenter --net=/proc/<target-pid>/ns/net -- ./tcp_quarantine \
--destination 203.0.113.99 --port 443 --apply
```
The `<target-pid>` identifies any process in the target network namespace. Only the network namespace is entered; the binary and current working directory remain available from the caller's mount namespace. The tool does not scan other network namespaces automatically.
## How it works: end-to-end flow
The tool uses a BPF TCP iterator to walk the kernel's socket table. Here is the flow:
1. **User space parses the target.** The loader validates the IPv4 address with `inet_pton`, converts the port, and stores both into the BPF program's read-only data section before loading.
2. **BPF program loads and attaches as a TCP iterator.** The kernel calls the iterator callback once per TCP socket in the loading process's network namespace.
3. **The iterator filters.** For each socket it increments `scanned`, skips anything that isn't `AF_INET` + `TCP_ESTABLISHED`, then compares `skc_daddr` and `skc_dport` against the target.
4. **Dry-run vs. apply.** If `apply` is false (the default), matched sockets are counted but left alone. If true, the program calls `bpf_sock_destroy` on each match.
5. **User space reads statistics.** After consuming the iterator to completion, the loader reads counters from BSS and prints the single summary line.
## The BPF program
The complete BPF side fits in under 60 lines:
```c ```c
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
@@ -127,81 +102,261 @@ int quarantine_tcp(struct bpf_iter__tcp *ctx)
} }
``` ```
Key points: The program declares itself as a TCP iterator with `SEC("iter/tcp")`. The iterator context `bpf_iter__tcp` is provided by the kernel, with `sk_common` pointing to successive TCP sockets as the table is traversed. The three `const volatile` variables live in `.rodata`, passing the small fixed configuration directly through read-only global data; user space writes the target address, port, and apply flag after `open()` but before `load()`, and the verifier treats them as compile-time constants.
- **`const volatile` for parameters.** These variables live in the `.rodata` section. User space sets them after `open()` but before `load()`, so the verifier sees them as constants. This avoids maps for simple configuration. `bpf_sock_destroy` is declared via `extern ... __ksym` as a strong kfunc symbol. Strong-symbol resolution makes Linux 6.5+ kfunc availability a load-time prerequisite and completes relocation before traversal begins. Load diagnostics report errno plus the prerequisite set. The `BPF_CORE_READ` macro uses BTF to read `sock_common` fields, and CO-RE relocations ensure the same compiled object runs across kernel versions with differing layouts. Port comparison uses `bpf_htons(target_port)` because `skc_dport` is stored in network byte order.
- **`bpf_sock_destroy` as a kfunc.** Declared with `__ksym`, this is a required (strong) kfunc symbol. The kernel resolves it at load time. If the symbol is missing (kernel too old or kfunc not available), BPF loading fails at relocation. ## The User-Space Loader
- **`BPF_CORE_READ` for portability.** CO-RE relocations let the same compiled object run on kernels with different `sock_common` layouts, as long as BTF is available. The complete user-space loader source follows.
- **Network byte order for the port.** `skc_dport` is stored in network order, so the comparison uses `bpf_htons(target_port)`.
## The user-space loader
The loader handles argument parsing, BPF lifecycle, and iterator execution. The critical section:
```c ```c
skel = tcp_quarantine_bpf__open(); // SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
if (!skel) { #include <arpa/inet.h>
fprintf(stderr, "failed to open BPF skeleton\n"); #include <errno.h>
return 1; #include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include "tcp_quarantine.h"
#include "tcp_quarantine.skel.h"
static struct env {
const char *destination;
unsigned int port;
bool apply;
bool verbose;
} env;
static int libbpf_print_fn(enum libbpf_print_level level, const char *format,
va_list args)
{
if (level == LIBBPF_DEBUG && !env.verbose)
return 0;
return vfprintf(stderr, format, args);
} }
skel->rodata->target_addr = destination.s_addr; static void usage(const char *program)
skel->rodata->target_port = env.port; {
skel->rodata->apply = env.apply; fprintf(stderr,
"Usage: %s --destination IPv4 --port PORT [--apply] [--verbose]\n"
"\n"
"Find established TCP client connections to an exact destination.\n"
"The default is a safe dry run; --apply destroys matching sockets.\n"
"\n"
"Options:\n"
" -d, --destination IPv4 exact remote IPv4 address\n"
" -p, --port PORT exact remote TCP port (1-65535)\n"
" -a, --apply destroy matching sockets\n"
" -v, --verbose print libbpf diagnostics\n"
" -h, --help show this help\n",
program);
}
err = tcp_quarantine_bpf__load(skel); static int parse_port(const char *value, unsigned int *port)
if (err) { {
char *end = NULL;
unsigned long parsed;
errno = 0;
parsed = strtoul(value, &end, 10);
if (errno || !end || *end || parsed == 0 || parsed > 65535)
return -EINVAL;
*port = parsed;
return 0;
}
static int parse_args(int argc, char **argv)
{
static const struct option options[] = {
{ "destination", required_argument, NULL, 'd' },
{ "port", required_argument, NULL, 'p' },
{ "apply", no_argument, NULL, 'a' },
{ "verbose", no_argument, NULL, 'v' },
{ "help", no_argument, NULL, 'h' },
{},
};
int option;
while ((option = getopt_long(argc, argv, "d:p:avh", options, NULL)) != -1) {
switch (option) {
case 'd':
env.destination = optarg;
break;
case 'p':
if (parse_port(optarg, &env.port)) {
fprintf(stderr, "invalid TCP port: %s\n", optarg);
return -EINVAL;
}
break;
case 'a':
env.apply = true;
break;
case 'v':
env.verbose = true;
break;
case 'h':
usage(argv[0]);
exit(0);
default:
return -EINVAL;
}
}
if (!env.destination || !env.port || optind != argc)
return -EINVAL;
return 0;
}
static int run_iterator(struct bpf_program *program)
{
struct bpf_link *link;
char buffer[256];
int iter_fd, length, err;
link = bpf_program__attach_iter(program, NULL);
err = libbpf_get_error(link);
if (err) {
fprintf(stderr, "failed to attach TCP iterator: %s\n", strerror(-err));
return err;
}
iter_fd = bpf_iter_create(bpf_link__fd(link));
if (iter_fd < 0) {
err = iter_fd;
fprintf(stderr, "failed to create TCP iterator: %s\n", strerror(-err));
goto cleanup;
}
while ((length = read(iter_fd, buffer, sizeof(buffer))) > 0)
;
if (length < 0) {
err = -errno;
fprintf(stderr, "failed while scanning TCP sockets: %s\n", strerror(errno));
}
close(iter_fd);
cleanup:
bpf_link__destroy(link);
return err;
}
int main(int argc, char **argv)
{
struct tcp_quarantine_bpf *skel = NULL;
struct in_addr destination;
int err;
err = parse_args(argc, argv);
if (err) {
usage(argv[0]);
return 1;
}
if (inet_pton(AF_INET, env.destination, &destination) != 1) {
fprintf(stderr, "invalid IPv4 destination: %s\n", env.destination);
return 1;
}
libbpf_set_print(libbpf_print_fn);
skel = tcp_quarantine_bpf__open();
if (!skel) {
fprintf(stderr, "failed to open BPF skeleton\n");
return 1;
}
skel->rodata->target_addr = destination.s_addr;
skel->rodata->target_port = env.port;
skel->rodata->apply = env.apply;
err = tcp_quarantine_bpf__load(skel);
if (err) {
fprintf(stderr, fprintf(stderr,
"failed to load TCP quarantine program: %s\n" "failed to load TCP quarantine program: %s\n"
"This tool requires Linux 6.5+ with BTF, TCP BPF iterators, " "This tool requires Linux 6.5+ with BTF, TCP BPF iterators, "
"BPF JIT, and the bpf_sock_destroy kfunc.\n", "BPF JIT, and the bpf_sock_destroy kfunc.\n",
strerror(-err)); strerror(-err));
goto cleanup; goto cleanup;
}
err = run_iterator(skel->progs.quarantine_tcp);
if (err)
goto cleanup;
printf("mode=%s destination=%s:%u scanned=%llu established=%llu "
"matched=%llu destroyed=%llu failed=%llu\n",
env.apply ? "apply" : "dry-run", env.destination, env.port,
skel->bss->stats.scanned, skel->bss->stats.established,
skel->bss->stats.matched, skel->bss->stats.destroyed,
skel->bss->stats.failed);
if (skel->bss->stats.failed)
err = -EIO;
cleanup:
tcp_quarantine_bpf__destroy(skel);
return err != 0;
} }
``` ```
After loading, the iterator is attached and consumed: The loader's `main` entry parses command-line arguments. The port must be a decimal number from 1 to 65535, and the address is validated and converted to network byte order with `inet_pton`. After opening the skeleton, the three parameters are written to `.rodata`, and `load` completes BPF program loading. Load diagnostics include the kernel errno plus the prerequisite set: Linux 6.5+, BTF, TCP iterators, JIT, and the kfunc.
```c The `run_iterator` function attaches the BPF program with `bpf_program__attach_iter` and creates an iterator fd with `bpf_iter_create`. The loop reads that fd until EOF; each `read` drives the kernel to invoke the BPF callback for a batch of TCP sockets. The read buffer serves only to drive traversal, while counters pass through BSS. After iteration, the loader reads `skel->bss->stats` and prints a single summary line. If any `bpf_sock_destroy` call failed, the program exits nonzero.
link = bpf_program__attach_iter(program, NULL);
iter_fd = bpf_iter_create(bpf_link__fd(link)); ## Compilation and Execution
while ((length = read(iter_fd, buffer, sizeof(buffer))) > 0)
; Build the tool:
```bash
cd src/51-tcp-quarantine
make clean
make -j2
``` ```
Reading the iterator fd to EOF drives the kernel to call the BPF callback for every TCP socket. The read buffer content is unused here because the program communicates results through BSS counters rather than seq_file output. Dry-run mode reports matches; matching connections remain established:
## The shared header ```bash
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063
```c
struct quarantine_stats {
unsigned long long scanned;
unsigned long long established;
unsigned long long matched;
unsigned long long destroyed;
unsigned long long failed;
};
``` ```
Both the BPF program and user-space loader include this header so the BSS layout is consistent. Apply mode destroys matching sockets:
## Running the automated test ```bash
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063 --apply
```
The Python test (`tests/test_tcp_quarantine.py`) exercises four scenarios: Sample output from the automated test:
1. Invalid IPv4 address is rejected. ```console
2. Dry-run finds and reports the target but leaves it alive. mode=dry-run destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=0 failed=0
3. Apply mode destroys the target socket (verified by `send()` failing with `ECONNABORTED`, `ECONNRESET`, or `EPIPE`, matching `BROKEN_ERRORS` in the test). mode=apply destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=1 failed=0
4. A control connection to a different port survives both runs. PASS: dry-run preserved both connections; apply destroyed only the target
```
Socket-table counts and ephemeral port values vary by run. The assertions focus on: one exact match, dry-run preservation, targeted destruction, zero destroy failures, and control-connection survival.
Run the test:
```bash ```bash
sudo make test sudo make test
``` ```
This runs `python3 tests/test_tcp_quarantine.py ./tcp_quarantine` which creates ephemeral loopback connections, exercises the tool, and asserts the expected behavior. This runs `python3 tests/test_tcp_quarantine.py ./tcp_quarantine`. The test creates two loopback TCP connection pairs: a target port and an unrelated control port. It performs request/response round trips before running the tool. The test verifies: invalid destination `not-an-ip` returns nonzero and prints `invalid IPv4 destination`; dry-run output contains `mode=dry-run`, `matched=1`, and `destroyed=0`, and both connections still complete round trips; apply output contains `mode=apply`, `matched=1`, `destroyed=1`, and `failed=0`; the target client's `send()` or `getsockopt(SO_ERROR)` returns `ECONNABORTED`, `ECONNRESET`, or `EPIPE`; and the control connection continues to survive.
Runtime behavior was functionally tested on x86_64 with Linux 7.0.0-rc2+.
## Network Namespace
The TCP BPF iterator scans the network namespace of the process that loads the BPF program. To quarantine connections inside a container or another network namespace, enter that namespace first with `nsenter`:
```bash
sudo nsenter --net=/proc/<target-pid>/ns/net -- ./tcp_quarantine \
--destination 203.0.113.99 --port 443 --apply
```
`<target-pid>` is any process in the target network namespace. `nsenter --net` changes only the network namespace; the caller's mount namespace is retained, so the binary and working directory remain reachable. Namespace-boundary validation found `matched=0` from the caller namespace and `matched=1` after entering the isolated target namespace.
## Requirements ## Requirements
@@ -210,50 +365,27 @@ This runs `python3 tests/test_tcp_quarantine.py ./tcp_quarantine` which creates
| Kernel | Linux 6.5+ (where `bpf_sock_destroy` first appeared) | | Kernel | Linux 6.5+ (where `bpf_sock_destroy` first appeared) |
| BTF | Required (`CONFIG_DEBUG_INFO_BTF=y`) | | BTF | Required (`CONFIG_DEBUG_INFO_BTF=y`) |
| Kernel configs | `CONFIG_BPF=y`, `CONFIG_BPF_SYSCALL=y`, `CONFIG_BPF_JIT=y`, `CONFIG_BPF_EVENTS=y`, `CONFIG_DEBUG_INFO_BTF=y`, `CONFIG_INET=y`, `CONFIG_PROC_FS=y` | | Kernel configs | `CONFIG_BPF=y`, `CONFIG_BPF_SYSCALL=y`, `CONFIG_BPF_JIT=y`, `CONFIG_BPF_EVENTS=y`, `CONFIG_DEBUG_INFO_BTF=y`, `CONFIG_INET=y`, `CONFIG_PROC_FS=y` |
| BPF JIT runtime | Must be enabled at runtime (e.g. `net.core.bpf_jit_enable=1`). Programs that call kfuncs cannot fall back to the BPF interpreter. On kernels built with `CONFIG_BPF_JIT_ALWAYS_ON`, the sysctl may not exist because JIT is permanently enabled. | | BPF JIT | Must be enabled at runtime. Programs calling kfuncs use the JIT path; `CONFIG_BPF_JIT_ALWAYS_ON` kernels may omit the sysctl because JIT is permanently active. |
| Architecture | Tested on x86_64 | | Architecture | Tested on x86_64 |
| Privileges | Tested as root. A least-privilege deployment must determine the required capabilities and LSM policy for its kernel version and environment. | | Privileges | Tested as root |
| Hardware | None |
If loading fails, the loader prints the kernel errno followed by the kernel/BTF/iterator/JIT/kfunc requirements so you can identify which prerequisite is missing. Load diagnostics report the kernel errno plus the prerequisite set: Linux 6.5+, BTF, TCP BPF iterators, BPF JIT, and the kfunc.
## Limitations ## bpf_sock_destroy Semantics
This is a teaching tool, not a production agent. Specifically: The `bpf_sock_destroy` kfunc was introduced in Linux commit `4ddbcb886268af8d12a23e6640b39d1d9c652b1b` to let BPF programs forcibly close a socket from an iterator context. The kernel invokes the protocol-specific destroy path, and the error the application sees depends on which endpoint is destroyed and what operation it attempts next. The upstream TCP selftest expects `ECONNABORTED` on the client side and `ECONNRESET` on the server side; this lesson's test also accepts `EPIPE` as a send-side result.
- **IPv4 only.** There is no IPv6 path. ## Scope and Extensions
- **One-shot scan.** The tool exits after a single pass. It does not monitor for new connections.
- **No process attribution.** You cannot see which process owns the matched sockets.
- **Network-namespace scope, no cgroup selector.** The scan covers all TCP sockets in the loading process's network namespace, but there is no cgroup-level filter inside that namespace. A production controller that manages several namespaces must enter and run in each intended namespace or orchestrate them explicitly.
- **No authorization or audit log.** A production version would gate destruction behind policy approval and record an audit trail.
- **No UDP or packet inspection.** Only TCP established sockets are considered.
A real deployment would source targets from threat intelligence or policy, add RBAC controls, and integrate with an incident management system. This example performs one pass through the caller-selected network namespace, handling every established connection that matches one exact IPv4 destination tuple. Extensions can add IPv6, continuous policy input, process/cgroup attribution, multi-namespace orchestration, authorization, and audit records.
## How bpf_sock_destroy works ## Summary
The `bpf_sock_destroy` kfunc was introduced in Linux 6.5 to give BPF programs the ability to forcibly close a socket from within an iterator context. The original motivation was allowing network policy enforcement to terminate connections that violate updated rules without requiring cooperation from user space. After completing this tutorial, you can terminate all TCP connections matching a destination tuple while preserving the process and unrelated traffic. Dry-run mode previews the impact; apply mode destroys the matches. You understand how BPF iterators traverse the kernel's socket table, how kfuncs expose kernel functionality to BPF programs, and how CO-RE ensures cross-kernel compatibility. For more eBPF tutorials and examples, see <https://github.com/eunomia-bpf/bpf-developer-tutorial>.
When called on a TCP socket, the kernel invokes the protocol-specific destroy path. The exact error the owning application sees depends on which endpoint is destroyed and what operation it attempts next. The upstream TCP selftest expects `ECONNABORTED` on the client side and `ECONNRESET` on the server side; this lesson's test also accepts `EPIPE` as a send-side result. Do not assume a specific TCP state transition or packet-level behavior from these observations alone.
The kernel commit introducing this functionality: <https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b>
Upstream BPF selftests provide additional reference:
- BPF program: <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/sock_destroy_prog.c>
- Userspace test: <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/prog_tests/sock_destroy.c>
## What you can do now
After completing this tutorial, you can:
- Identify and terminate specific TCP connections by destination without killing processes.
- Safely preview the blast radius with dry-run before taking action.
- Understand how BPF iterators walk kernel data structures and how kfuncs extend BPF capabilities.
- Build on this pattern to create policy-driven connection enforcement tools.
## References ## References
- Tutorial repository: <https://github.com/eunomia-bpf/bpf-developer-tutorial>
- Tutorial website: <https://eunomia.dev/tutorials/>
- `bpf_sock_destroy` kernel commit: <https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b> - `bpf_sock_destroy` kernel commit: <https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b>
- Upstream sock_destroy selftest (BPF): <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/sock_destroy_prog.c> - Upstream sock_destroy selftest (BPF program): <https://github.com/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/sock_destroy_prog.c>
- Upstream sock_destroy selftest (userspace): <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/prog_tests/sock_destroy.c> - Upstream sock_destroy selftest (userspace test): <https://github.com/torvalds/linux/blob/master/tools/testing/selftests/bpf/prog_tests/sock_destroy.c>

View File

@@ -1,71 +1,46 @@
# eBPF 实践教程: 隔离已建立的 TCP 连接 # eBPF 教程:精准隔离已建立的 TCP 连接
你的威胁情报源刚刚把 `203.0.113.99` 标记为 C2 服务器。防火墙规则可以立即阻止新连接,但 `netstat` 显示三台生产节点已经和这个地址保持着活跃的 TCP 会话,数据仍在外泄。你需要立即切断这些连接,不重启服务,不杀死进程,也不影响无关流量。 假设某个出站目标地址刚被添加到威胁情报黑名单,防火墙已更新规则来阻止后续连接。然而 `netstat` 显示某台服务器上已经存在一条到该地址的活跃 TCP 会话,你需要精准关闭这条连接,同时保留进程和其他无关流量。
本教程构建一个小型 eBPF 工具:扫描当前网络命名空间中的 TCP 套接字,找到目标远端 IPv4 地址和端口完全匹配的 `ESTABLISHED` 连接,用 `bpf_sock_destroy` 将其销毁。TCP BPF 迭代器只能看到加载它的进程所在网络命名空间中的套接字,而非主机上的全部套接字。默认模式是 dry-run,确认影响范围后再执行销毁。 本教程构建 `tcp_quarantine`,一个单次执行的 eBPF 命令行工具,用于精确匹配指定的 IPv4 目标地址和端口,找到所有符合该元组的 `ESTABLISHED` TCP 连接并统计或销毁它们。默认模式是 dry-run,只报告匹配结果;`--apply` 模式调用内核的 `bpf_sock_destroy` kfunc 销毁每个匹配的套接字。BPF TCP 迭代器的扫描范围是加载它的进程所在的网络命名空间。
> 完整源码: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/51-tcp-quarantine> > 完整源码:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/51-tcp-quarantine>
## 快速演示 ## 端到端流程
编译工具: 工具使用 BPF TCP 迭代器遍历内核的 TCP 套接字表。用户态程序首先用 `inet_pton` 验证 IPv4 地址并解析端口号,将这两个参数写入 skeleton 的只读数据段 `.rodata`,然后加载 BPF 程序。`inet_pton` 存储的 `destination.s_addr` 已经是网络字节序,主机字节序的端口和 apply 布尔值也一并写入。
```bash BPF 程序使用 `SEC("iter/tcp")` 挂载为 TCP 迭代器。当用户态读取迭代器文件描述符时,内核会为加载进程所在网络命名空间中的每个 TCP 套接字调用一次回调。回调首先对每个非空 `sk` 递增 `scanned` 计数器,然后选出 `AF_INET` 且状态为 `TCP_ESTABLISHED` 的套接字计入 `established`。接着通过 `BPF_CORE_READ` 读取 `skc_daddr`、`skc_dport`、`skc_family` 和 `skc_state`,CO-RE 重定位保证同一编译产物可在不同内核布局上运行。目标地址直接比较网络字节序的值,端口比较使用 `bpf_htons(target_port)` 因为 `skc_dport` 以网络字节序存储。
cd src/51-tcp-quarantine
make clean 匹配的套接字递增 `matched`,dry-run 模式到此返回。apply 模式调用 `bpf_sock_destroy(sk)`,成功时递增 `destroyed`,返回非零时递增 `failed`。`bpf_sock_destroy` 通过 `__ksym` 声明为强 kfunc 符号,内核在加载时解析该符号。
make -j2
共享的 `quarantine_stats` 结构体位于 BSS 段,作为结果通道传递五个计数器。加载器用 `bpf_program__attach_iter` 挂载程序,`bpf_iter_create` 创建迭代器文件描述符,然后读到 EOF 驱动遍历;读取缓冲区仅用于驱动遍历,计数器通过 BSS 传递。加载器在退出时销毁迭代器 link 和 skeleton,加载或迭代失败、或 `bpf_sock_destroy` 有任何调用失败时返回非零退出码。`--verbose` 选项启用 libbpf 调试消息。
## 共享头文件
BPF 程序和用户态加载器都包含这个头文件,确保 BSS 段中 `quarantine_stats` 结构体的内存布局一致。
```c
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __TCP_QUARANTINE_H
#define __TCP_QUARANTINE_H
struct quarantine_stats {
unsigned long long scanned;
unsigned long long established;
unsigned long long matched;
unsigned long long destroyed;
unsigned long long failed;
};
#endif /* __TCP_QUARANTINE_H */
``` ```
Dry-run 模式(安全,不会销毁任何连接): 五个计数器分别记录扫描的套接字总数、处于 `ESTABLISHED` 状态的 IPv4 套接字数、目标地址和端口完全匹配的数量、apply 模式下成功销毁的数量、以及 `bpf_sock_destroy` 返回非零的数量。用户态在迭代完成后读取这些计数器并打印单行汇总。
```bash
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063
```
Apply 模式(销毁匹配的套接字):
```bash
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063 --apply
```
测试环境的真实输出:
```console
mode=dry-run destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=0 failed=0
mode=apply destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=1 failed=0
PASS: dry-run preserved both connections; apply destroyed only the target
```
请将目标 IP 和端口替换为你要隔离的实际地址。该工具执行一次扫描后退出,不会持续运行。
### 在其他网络命名空间中使用
TCP BPF 迭代器扫描的是加载 BPF 程序的进程所在的网络命名空间。要隔离容器或其他网络命名空间中的连接,先进入该命名空间:
```bash
sudo nsenter --net=/proc/<target-pid>/ns/net -- ./tcp_quarantine \
--destination 203.0.113.99 --port 443 --apply
```
`<target-pid>` 是目标网络命名空间中任意进程的 PID。这里只进入网络命名空间,二进制文件和当前工作目录仍来自调用者的 mount 命名空间。该工具不会自动扫描其他网络命名空间。
## 工作原理:端到端流程
工具使用 BPF TCP iterator 遍历内核套接字表,流程如下:
1. **用户态解析目标参数。** 加载器用 `inet_pton` 验证 IPv4 地址,转换端口号,在加载 BPF 程序之前写入只读数据段。
2. **BPF 程序作为 TCP iterator 加载并挂载。** 内核对加载进程所在网络命名空间中的每个 TCP 套接字调用一次迭代器回调。
3. **迭代器过滤。** 对每个套接字递增 `scanned` 计数器,跳过非 `AF_INET` 或非 `TCP_ESTABLISHED` 的条目,然后比较 `skc_daddr` 和 `skc_dport` 是否与目标匹配。
4. **Dry-run 与 apply。** 如果 `apply` 为 false(默认),匹配的套接字只被计数。如果为 true,程序调用 `bpf_sock_destroy` 销毁它。
5. **用户态读取统计。** 迭代器消费完毕后,加载器从 BSS 段读取计数器并打印汇总行。
## BPF 程序 ## BPF 程序
完整的 BPF 侧代码不到 60 行: 以下是完整的 BPF 程序源码。
```c ```c
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
@@ -127,133 +102,290 @@ int quarantine_tcp(struct bpf_iter__tcp *ctx)
} }
``` ```
要点: 程序使用 `SEC("iter/tcp")` 声明为 TCP 迭代器类型。迭代器上下文 `bpf_iter__tcp` 由内核提供,其中 `sk_common` 字段在遍历套接字表时依次指向每个 TCP 套接字。三个 `const volatile` 变量位于 `.rodata` 段,通过只读全局数据直接传递小规模固定配置;用户态在 `open()` 之后、`load()` 之前写入目标地址、端口和 apply 标志,验证器将它们视为编译期常量。
- **`const volatile` 用于参数传递。** 这些变量位于 `.rodata` 段,用户态在 `open()` 之后、`load()` 之前设置它们,验证器将其视为常量。这种方式比使用 map 更简洁。 `bpf_sock_destroy` 通过 `extern ... __ksym` 声明为强 kfunc 符号。强符号解析使 Linux 6.5+ 的 kfunc 可用性成为加载时前置条件,重定位在遍历开始前完成。加载诊断报告 errno 以及前置条件集合。`BPF_CORE_READ` 宏利用 BTF 信息读取 `sock_common` 结构体的字段,CO-RE 重定位保证同一编译产物可以在不同内核版本和字段布局上运行。端口比较使用 `bpf_htons(target_port)` 是因为 `skc_dport` 以网络字节序存储。
- **`bpf_sock_destroy` 作为 kfunc 声明。** 通过 `__ksym` 标注为必需(强)符号,内核在加载时解析。如果内核中缺少该符号(版本过低或 kfunc 不可用),BPF 加载会在重定位阶段失败。
- **`BPF_CORE_READ` 保证可移植性。** CO-RE 重定位使同一编译产物能在不同 `sock_common` 布局的内核上运行,前提是有 BTF。
- **端口比较使用网络字节序。** `skc_dport` 以网络字节序存储,因此比较时使用 `bpf_htons(target_port)`。
## 用户态加载器 ## 用户态加载器
加载器负责参数解析、BPF 生命周期管理和迭代器执行。核心代码: 以下是完整的用户态加载器源码。
```c ```c
skel = tcp_quarantine_bpf__open(); // SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
if (!skel) { #include <arpa/inet.h>
fprintf(stderr, "failed to open BPF skeleton\n"); #include <errno.h>
return 1; #include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include "tcp_quarantine.h"
#include "tcp_quarantine.skel.h"
static struct env {
const char *destination;
unsigned int port;
bool apply;
bool verbose;
} env;
static int libbpf_print_fn(enum libbpf_print_level level, const char *format,
va_list args)
{
if (level == LIBBPF_DEBUG && !env.verbose)
return 0;
return vfprintf(stderr, format, args);
} }
skel->rodata->target_addr = destination.s_addr; static void usage(const char *program)
skel->rodata->target_port = env.port; {
skel->rodata->apply = env.apply; fprintf(stderr,
"Usage: %s --destination IPv4 --port PORT [--apply] [--verbose]\n"
"\n"
"Find established TCP client connections to an exact destination.\n"
"The default is a safe dry run; --apply destroys matching sockets.\n"
"\n"
"Options:\n"
" -d, --destination IPv4 exact remote IPv4 address\n"
" -p, --port PORT exact remote TCP port (1-65535)\n"
" -a, --apply destroy matching sockets\n"
" -v, --verbose print libbpf diagnostics\n"
" -h, --help show this help\n",
program);
}
err = tcp_quarantine_bpf__load(skel); static int parse_port(const char *value, unsigned int *port)
if (err) { {
char *end = NULL;
unsigned long parsed;
errno = 0;
parsed = strtoul(value, &end, 10);
if (errno || !end || *end || parsed == 0 || parsed > 65535)
return -EINVAL;
*port = parsed;
return 0;
}
static int parse_args(int argc, char **argv)
{
static const struct option options[] = {
{ "destination", required_argument, NULL, 'd' },
{ "port", required_argument, NULL, 'p' },
{ "apply", no_argument, NULL, 'a' },
{ "verbose", no_argument, NULL, 'v' },
{ "help", no_argument, NULL, 'h' },
{},
};
int option;
while ((option = getopt_long(argc, argv, "d:p:avh", options, NULL)) != -1) {
switch (option) {
case 'd':
env.destination = optarg;
break;
case 'p':
if (parse_port(optarg, &env.port)) {
fprintf(stderr, "invalid TCP port: %s\n", optarg);
return -EINVAL;
}
break;
case 'a':
env.apply = true;
break;
case 'v':
env.verbose = true;
break;
case 'h':
usage(argv[0]);
exit(0);
default:
return -EINVAL;
}
}
if (!env.destination || !env.port || optind != argc)
return -EINVAL;
return 0;
}
static int run_iterator(struct bpf_program *program)
{
struct bpf_link *link;
char buffer[256];
int iter_fd, length, err;
link = bpf_program__attach_iter(program, NULL);
err = libbpf_get_error(link);
if (err) {
fprintf(stderr, "failed to attach TCP iterator: %s\n", strerror(-err));
return err;
}
iter_fd = bpf_iter_create(bpf_link__fd(link));
if (iter_fd < 0) {
err = iter_fd;
fprintf(stderr, "failed to create TCP iterator: %s\n", strerror(-err));
goto cleanup;
}
while ((length = read(iter_fd, buffer, sizeof(buffer))) > 0)
;
if (length < 0) {
err = -errno;
fprintf(stderr, "failed while scanning TCP sockets: %s\n", strerror(errno));
}
close(iter_fd);
cleanup:
bpf_link__destroy(link);
return err;
}
int main(int argc, char **argv)
{
struct tcp_quarantine_bpf *skel = NULL;
struct in_addr destination;
int err;
err = parse_args(argc, argv);
if (err) {
usage(argv[0]);
return 1;
}
if (inet_pton(AF_INET, env.destination, &destination) != 1) {
fprintf(stderr, "invalid IPv4 destination: %s\n", env.destination);
return 1;
}
libbpf_set_print(libbpf_print_fn);
skel = tcp_quarantine_bpf__open();
if (!skel) {
fprintf(stderr, "failed to open BPF skeleton\n");
return 1;
}
skel->rodata->target_addr = destination.s_addr;
skel->rodata->target_port = env.port;
skel->rodata->apply = env.apply;
err = tcp_quarantine_bpf__load(skel);
if (err) {
fprintf(stderr, fprintf(stderr,
"failed to load TCP quarantine program: %s\n" "failed to load TCP quarantine program: %s\n"
"This tool requires Linux 6.5+ with BTF, TCP BPF iterators, " "This tool requires Linux 6.5+ with BTF, TCP BPF iterators, "
"BPF JIT, and the bpf_sock_destroy kfunc.\n", "BPF JIT, and the bpf_sock_destroy kfunc.\n",
strerror(-err)); strerror(-err));
goto cleanup; goto cleanup;
}
err = run_iterator(skel->progs.quarantine_tcp);
if (err)
goto cleanup;
printf("mode=%s destination=%s:%u scanned=%llu established=%llu "
"matched=%llu destroyed=%llu failed=%llu\n",
env.apply ? "apply" : "dry-run", env.destination, env.port,
skel->bss->stats.scanned, skel->bss->stats.established,
skel->bss->stats.matched, skel->bss->stats.destroyed,
skel->bss->stats.failed);
if (skel->bss->stats.failed)
err = -EIO;
cleanup:
tcp_quarantine_bpf__destroy(skel);
return err != 0;
} }
``` ```
加载完成后,挂载并消费迭代器: 加载器在 `main` 入口首先解析命令行参数,端口必须是 1 到 65535 之间的十进制数,地址用 `inet_pton` 验证并转换为网络字节序。打开 skeleton 后,三个参数写入 `.rodata` 段,然后调用 `load` 完成 BPF 程序加载。加载诊断同时给出内核 errno 以及 Linux 6.5+、BTF、TCP 迭代器、JIT 和 kfunc 的前置条件集合。
```c `run_iterator` 函数用 `bpf_program__attach_iter` 挂载 BPF 程序,用 `bpf_iter_create` 创建迭代器文件描述符。循环读取该 fd 直到 EOF,每次 `read` 驱动内核为若干 TCP 套接字调用 BPF 回调;读取缓冲区仅用于驱动遍历,计数器通过 BSS 传递。迭代完成后,加载器从 `skel->bss->stats` 读取计数器并打印单行汇总。如果有任何 `bpf_sock_destroy` 调用失败,程序返回非零退出码。
link = bpf_program__attach_iter(program, NULL);
iter_fd = bpf_iter_create(bpf_link__fd(link)); ## 编译与执行
while ((length = read(iter_fd, buffer, sizeof(buffer))) > 0)
; 编译工具:
```bash
cd src/51-tcp-quarantine
make clean
make -j2
``` ```
对迭代器 fd 读到 EOF 会驱动内核为每个 TCP 套接字调用 BPF 回调。读取缓冲区的内容在这里不使用,因为程序通过 BSS 计数器传递结果,而非 seq_file 输出。 Dry-run 模式报告匹配结果,匹配的连接保持已建立状态:
## 共享头文件 ```bash
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063
```c
struct quarantine_stats {
unsigned long long scanned;
unsigned long long established;
unsigned long long matched;
unsigned long long destroyed;
unsigned long long failed;
};
``` ```
BPF 程序和用户态加载器都包含这个头文件,保证 BSS 布局一致。 Apply 模式销毁匹配的套接字:
## 运行自动化测试 ```bash
sudo ./tcp_quarantine --destination 127.0.0.1 --port 42063 --apply
```
Python 测试 (`tests/test_tcp_quarantine.py`) 验证四个场景: 自动化测试的输出示例:
1. 无效 IPv4 地址被拒绝。 ```console
2. Dry-run 发现并报告目标但不销毁。 mode=dry-run destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=0 failed=0
3. Apply 模式销毁目标套接字(通过 `send()` 返回 `ECONNABORTED`、`ECONNRESET` 或 `EPIPE` 验证,与测试中的 `BROKEN_ERRORS` 一致)。 mode=apply destination=127.0.0.1:42063 scanned=6 established=4 matched=1 destroyed=1 failed=0
4. 不同端口的控制连接在两次执行中均存活。 PASS: dry-run preserved both connections; apply destroyed only the target
```
套接字表计数和临时端口号随每次运行而变化,测试断言关注的是:匹配数为 1、dry-run 保留连接、apply 销毁目标、销毁失败数为 0、控制连接存活。
运行测试:
```bash ```bash
sudo make test sudo make test
``` ```
该命令执行 `python3 tests/test_tcp_quarantine.py ./tcp_quarantine`,创建临时的回环连接,运行工具,并断言预期行为。 该命令执行 `python3 tests/test_tcp_quarantine.py ./tcp_quarantine`。测试创建两组回环 TCP 连接对:一个目标端口和一个无关的控制端口,先执行请求/响应往返,再运行工具验证行为。测试检查:无效目标 `not-an-ip` 返回非零并打印 `invalid IPv4 destination`;dry-run 输出包含 `mode=dry-run`、`matched=1`、`destroyed=0`,且两个连接仍能完成往返;apply 输出包含 `mode=apply`、`matched=1`、`destroyed=1`、`failed=0`;目标客户端 `send()` 或 `getsockopt(SO_ERROR)` 返回 `ECONNABORTED`、`ECONNRESET` 或 `EPIPE`;控制连接继续存活。
运行时行为已在 x86_64、Linux 7.0.0-rc2+ 上完成功能测试。
## 网络命名空间
TCP BPF 迭代器扫描的是加载 BPF 程序的进程所在的网络命名空间。要隔离容器或其他网络命名空间中的连接,先用 `nsenter` 进入目标命名空间:
```bash
sudo nsenter --net=/proc/<target-pid>/ns/net -- ./tcp_quarantine \
--destination 203.0.113.99 --port 443 --apply
```
`<target-pid>` 是目标网络命名空间中任意进程的 PID。`nsenter --net` 只改变网络命名空间,调用者的 mount 命名空间保持不变,因此二进制文件和当前工作目录仍可访问。功能验证发现:在调用者命名空间运行时 `matched=0`,进入隔离目标命名空间后 `matched=1`。
## 环境要求 ## 环境要求
| 要求 | 详情 | | 要求 | 详情 |
|---|---| |---|---|
| 内核版本 | Linux 6.5+(`bpf_sock_destroy` 首次出现的版本) | | 内核版本 | Linux 6.5+(`bpf_sock_destroy` 首次引入) |
| BTF | 必须启用 (`CONFIG_DEBUG_INFO_BTF=y`) | | BTF | 必须启用 (`CONFIG_DEBUG_INFO_BTF=y`) |
| 内核配置 | `CONFIG_BPF=y`, `CONFIG_BPF_SYSCALL=y`, `CONFIG_BPF_JIT=y`, `CONFIG_BPF_EVENTS=y`, `CONFIG_DEBUG_INFO_BTF=y`, `CONFIG_INET=y`, `CONFIG_PROC_FS=y` | | 内核配置 | `CONFIG_BPF=y`, `CONFIG_BPF_SYSCALL=y`, `CONFIG_BPF_JIT=y`, `CONFIG_BPF_EVENTS=y`, `CONFIG_DEBUG_INFO_BTF=y`, `CONFIG_INET=y`, `CONFIG_PROC_FS=y` |
| BPF JIT 运行时 | 必须在运行时启用(例如 `net.core.bpf_jit_enable=1`)。调用 kfunc 的程序无法回退到 BPF 解释器。如果内核编译时启用了 `CONFIG_BPF_JIT_ALWAYS_ON`,该 sysctl 可能不存在,因为 JIT 已永久启用。 | | BPF JIT | 运行时必须启用。调用 kfunc 的程序使用 JIT 路径;`CONFIG_BPF_JIT_ALWAYS_ON` 内核无需 sysctl 设置,因为 JIT 已永久启用。 |
| 架构 | 已在 x86_64 上测试 | | 架构 | 已在 x86_64 上测试 |
| 权限 | 已在 root 下测试。最小权限部署需要根据具体内核版本和环境确定所需的 capabilities 和 LSM 策略。 | | 权限 | 已在 root 下测试 |
| 硬件 | 无特殊要求 |
加载失败时,加载器会打印内核 errno 以及内核/BTF/迭代器/JIT/kfunc 的要求信息,帮助定位缺失的前置条件。 加载诊断报告内核 errno 以及 Linux 6.5+、BTF、TCP BPF 迭代器、BPF JIT 和 kfunc 的前置条件集合。
## 局限性 ## bpf_sock_destroy 语义
这是一个教学工具,不是生产级 agent。具体限制: `bpf_sock_destroy` kfunc 由 Linux commit `4ddbcb886268af8d12a23e6640b39d1d9c652b1b` 引入,允许 BPF 程序在迭代器上下文中强制关闭套接字。内核执行协议特定的销毁路径,应用层看到的错误取决于被销毁的端点和下一个操作。上游 TCP selftest 预期客户端收到 `ECONNABORTED`、服务端收到 `ECONNRESET`;本教程的测试还接受发送端的 `EPIPE`。
- **仅支持 IPv4。** 没有 IPv6 路径。 ## 扩展方向
- **单次扫描。** 工具执行一次后退出,不会监控新建连接。
- **无进程归属信息。** 无法看到匹配套接字属于哪个进程。
- **网络命名空间范围,无 cgroup 筛选。** 扫描覆盖加载进程所在网络命名空间中的所有 TCP 套接字,但命名空间内部没有 cgroup 级别的过滤。管理多个命名空间的生产控制器需要分别进入每个目标命名空间运行,或显式编排。
- **无授权和审计日志。** 生产版本应通过策略审批控制销毁操作并记录审计轨迹。
- **不支持 UDP 或数据包检测。** 仅处理 TCP ESTABLISHED 套接字。
实际部署中应从威胁情报或策略系统获取目标,增加 RBAC 控制,并集成事件管理系统。 本示例在调用者所选网络命名空间中执行单次遍历,处理所有匹配一个精确 IPv4 目标元组的已建立连接。后续扩展可添加 IPv6 支持、持续策略输入、进程/cgroup 归属、多命名空间编排、授权控制和审计记录。
## bpf_sock_destroy 原理 ## 总结
`bpf_sock_destroy` kfunc 在 Linux 6.5 中引入,使 BPF 程序能在迭代器上下文中强制关闭套接字。最初的动机是让网络策略执行器能终止违反新规则的连接,而不需要用户态配合。 完成本教程后,你可以按目标元组精确终止所有匹配的 TCP 连接,同时保留进程和无关流量。dry-run 模式预览影响范围,apply 模式执行销毁。你理解了 BPF 迭代器如何遍历内核套接字表、kfunc 如何从 BPF 程序调用内核函数、以及 CO-RE 如何保证跨内核兼容性。更多 eBPF 教程和示例请参见 <https://github.com/eunomia-bpf/bpf-developer-tutorial>。
调用时,内核执行协议特定的销毁路径。应用层看到的具体错误取决于被销毁的是哪个端点以及下一次执行的操作。上游 TCP selftest 中,客户端预期收到 `ECONNABORTED`,服务端预期收到 `ECONNRESET`;本教程的测试还接受 `EPIPE` 作为发送端的结果。不要仅凭这些观察推断特定的 TCP 状态转换或报文行为。
引入此功能的内核 commit: <https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b>
上游 BPF selftest 提供额外参考:
- BPF 程序: <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/sock_destroy_prog.c>
- 用户态测试: <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/prog_tests/sock_destroy.c>
## 你现在能做什么
完成本教程后,你可以:
- 按目标地址精确终止特定 TCP 连接,无需杀死进程。
- 用 dry-run 安全预览影响范围后再执行操作。
- 理解 BPF iterator 如何遍历内核数据结构,以及 kfunc 如何扩展 BPF 能力。
- 在此模式基础上构建策略驱动的连接管控工具。
## 参考资料 ## 参考资料
- 教程仓库: <https://github.com/eunomia-bpf/bpf-developer-tutorial> - `bpf_sock_destroy` 内核 commit:<https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b>
- 教程网站: <https://eunomia.dev/tutorials/> - 上游 sock_destroy selftest(BPF 程序):<https://github.com/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/sock_destroy_prog.c>
- `bpf_sock_destroy` 内核 commit: <https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b> - 上游 sock_destroy selftest(用户态测试):<https://github.com/torvalds/linux/blob/master/tools/testing/selftests/bpf/prog_tests/sock_destroy.c>
- 上游 sock_destroy selftest (BPF): <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/sock_destroy_prog.c>
- 上游 sock_destroy selftest (用户态): <https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/prog_tests/sock_destroy.c>