mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-10-29 03:34:44 +08:00
* Add targeted TCP connection quarantine tool * Document TCP quarantine workflow * Refresh package indexes before CI install * Document TCP quarantine namespace scope * Require BPF events for TCP iterator * Add production fsession latency tracer * Document fsession latency workflow * Reject empty fsession CLI values * Preserve successful fsession exit status * Document fsession deadline regression test * Drain fsession events before summary * Document fsession PID and shutdown semantics * Simplify fsession polling control flow * Add bounded BPF egress pacer tutorial * Test queued packet cleanup on signal * Document the BPF egress pacer * Polish and review the BPF egress pacer * Refactor egress pacer option parsing * Add executable image inspector lesson * Build the exec image inspector in CI * Document executable image inspection * Handle re-exec chains in the image inspector * Polish exec image inspector guidance * Reduce exec inspector wait complexity * Rewrite the egress pacer tutorial * Rewrite the exec image inspector tutorial * Polish the exec image inspector tutorial * Correct the egress pacer tutorial * Make the exec tutorial Chinese natural * Fix egress tutorial prose checks * Finish the exec tutorial style pass * Clarify exec tutorial runtime evidence * Polish exec tutorial scope wording * Rewrite the egress pacer tutorial for readers * Regenerate tutorial indexes * Teach the egress pacer component by component * Use standard Markdown in the egress tutorial * Strengthen the egress pacer tutorial narrative * jiji * Use positive framing in the egress tutorial * Rewrite the exec image inspector tutorial * Rewrite the TCP quarantine tutorial * Add the egress pacer lifecycle diagram * Refresh TCP tutorial indexes * Add the exec image inspection flow diagram * Use one source directory link * Rewrite the fsession latency tutorial * Add fsession feature background * Add BPF qdisc feature background * Add TCP iterator feature background * Add exec inspection feature background * Require feature background in tutorial openings * Polish fsession background prose * Simplify repository tutorial skills * Update exec inspector tooling revisions * Restructure tutorials 51-54 following advanced guideline - Update SKILL.md with strict section ordering and "why" requirements - Rewrite README.zh.md for tutorials 51-54: - Add background sections explaining why traditional approaches fail - Move compilation/execution sections after code analysis - Add high-level mechanism explanation before code - Use paragraph style instead of bullet lists * Consolidate skill guidelines and remove duplicates - Update write-bpf-production-tutorial skill to reference 47, 48, 49 - Add structure requirements section with "Why" section emphasis - Remove duplicate advanced-tutorial-guidelines.md (use scripts/guideline_advance.md instead) * Rewrite English READMEs 51-54 to match Chinese structure Restructure all four English tutorials to match their Chinese counterparts: - Add "Why this approach is needed" sections explaining traditional limitations - Add high-level mechanism sections before code - Move compilation/execution sections after code analysis - Add environment requirements tables - Use consistent section ordering across all tutorials * Regenerate TOC after tutorial title changes Run generate_toc.py to sync SUMMARY and README files with updated tutorial titles from the restructuring. * Regenerate compatibility tables after title changes --------- Co-authored-by: LinuxDev9002 <i@littlefisher.me>
301 lines
10 KiB
Python
301 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Deterministic KVM integration checks for the BPF egress pacer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import errno
|
|
import os
|
|
import re
|
|
import select
|
|
import signal
|
|
import socket
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
|
|
TX_INTERFACE = "epac_tx"
|
|
RX_INTERFACE = "epac_rx"
|
|
ETHERTYPE = 0x88B5
|
|
MAGIC = b"EPAC"
|
|
FRAME_SIZE = 1024
|
|
ATTEMPTS = 40
|
|
RATE_KBPS = 64
|
|
QUEUE_LIMIT = 8
|
|
|
|
SUMMARY = re.compile(
|
|
r"SUMMARY enqueued=(\d+) dequeued=(\d+) policy_dropped=(\d+) "
|
|
r"cleanup_dropped=(\d+) bytes_dequeued=(\d+) max_qlen=(\d+)"
|
|
)
|
|
|
|
|
|
def run(*command: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
command,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
check=check,
|
|
)
|
|
|
|
|
|
def delete_test_link() -> None:
|
|
run("ip", "link", "del", TX_INTERFACE, check=False)
|
|
|
|
|
|
def create_test_link() -> None:
|
|
delete_test_link()
|
|
run("ip", "link", "add", TX_INTERFACE, "type", "veth", "peer", "name", RX_INTERFACE)
|
|
run("ip", "link", "set", "dev", TX_INTERFACE, "addrgenmode", "none")
|
|
run("ip", "link", "set", "dev", RX_INTERFACE, "addrgenmode", "none")
|
|
run("ip", "link", "set", "dev", TX_INTERFACE, "up")
|
|
run("ip", "link", "set", "dev", RX_INTERFACE, "up")
|
|
|
|
|
|
def wait_until_ready(process: subprocess.Popen[str]) -> list[str]:
|
|
lines: list[str] = []
|
|
deadline = time.monotonic() + 8
|
|
assert process.stdout is not None
|
|
|
|
while time.monotonic() < deadline:
|
|
readable, _, _ = select.select([process.stdout], [], [], 0.25)
|
|
if readable:
|
|
line = process.stdout.readline()
|
|
if line:
|
|
lines.append(line)
|
|
if line.startswith("READY "):
|
|
return lines
|
|
if process.poll() is not None:
|
|
break
|
|
|
|
process.kill()
|
|
remainder, _ = process.communicate()
|
|
raise AssertionError("pacer did not become ready:\n" + "".join(lines) + remainder)
|
|
|
|
|
|
def receive_frames(
|
|
stop: threading.Event,
|
|
ready: threading.Event,
|
|
times: list[float],
|
|
errors: list[Exception],
|
|
) -> None:
|
|
receiver: socket.socket | None = None
|
|
try:
|
|
receiver = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETHERTYPE))
|
|
receiver.bind((RX_INTERFACE, 0))
|
|
receiver.settimeout(0.1)
|
|
ready.set()
|
|
while not stop.is_set():
|
|
try:
|
|
frame = receiver.recv(2048)
|
|
except TimeoutError:
|
|
continue
|
|
if len(frame) >= 18 and frame[12:14] == struct.pack("!H", ETHERTYPE):
|
|
if frame[14:18] == MAGIC:
|
|
times.append(time.monotonic())
|
|
except Exception as error:
|
|
errors.append(error)
|
|
ready.set()
|
|
finally:
|
|
if receiver is not None:
|
|
receiver.close()
|
|
|
|
|
|
def send_burst() -> int:
|
|
sender = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
|
|
sender.bind((TX_INTERFACE, 0))
|
|
send_errors = 0
|
|
try:
|
|
for sequence in range(ATTEMPTS):
|
|
header = b"\xff" * 6 + b"\x02\x00\x00\x00\x00\x01" + struct.pack("!H", ETHERTYPE)
|
|
payload = MAGIC + struct.pack("!I", sequence)
|
|
frame = header + payload + b"x" * (FRAME_SIZE - len(header) - len(payload))
|
|
try:
|
|
sender.send(frame)
|
|
except OSError as error:
|
|
if error.errno != errno.ENOBUFS:
|
|
raise
|
|
send_errors += 1
|
|
finally:
|
|
sender.close()
|
|
return send_errors
|
|
|
|
|
|
def parse_summary(output: str) -> dict[str, int]:
|
|
match = SUMMARY.search(output)
|
|
if not match:
|
|
raise AssertionError("missing summary:\n" + output)
|
|
names = (
|
|
"enqueued",
|
|
"dequeued",
|
|
"policy_dropped",
|
|
"cleanup_dropped",
|
|
"bytes_dequeued",
|
|
"max_qlen",
|
|
)
|
|
return dict(zip(names, map(int, match.groups())))
|
|
|
|
|
|
def main() -> int:
|
|
binary = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else "./egress_pacer")
|
|
stop = threading.Event()
|
|
receiver: threading.Thread | None = None
|
|
process: subprocess.Popen[str] | None = None
|
|
|
|
invalid = run(binary, "--interface", "definitely_missing", "--duration", "1", check=False)
|
|
assert invalid.returncode != 0
|
|
assert "interface does not exist" in invalid.stdout
|
|
|
|
create_test_link()
|
|
try:
|
|
run("tc", "qdisc", "add", "dev", TX_INTERFACE, "root", "handle", "1:",
|
|
"pfifo", "limit", "16")
|
|
conflict = run(
|
|
binary,
|
|
"--interface", TX_INTERFACE,
|
|
"--rate-kbps", str(RATE_KBPS),
|
|
"--queue-limit", str(QUEUE_LIMIT),
|
|
"--duration", "1",
|
|
check=False,
|
|
)
|
|
assert conflict.returncode != 0, conflict.stdout
|
|
assert "refusing to replace" in conflict.stdout, conflict.stdout
|
|
run("tc", "qdisc", "del", "dev", TX_INTERFACE, "root")
|
|
|
|
receiver_ready = threading.Event()
|
|
receive_times: list[float] = []
|
|
receiver_errors: list[Exception] = []
|
|
receiver = threading.Thread(
|
|
target=receive_frames,
|
|
args=(stop, receiver_ready, receive_times, receiver_errors),
|
|
)
|
|
receiver.start()
|
|
assert receiver_ready.wait(timeout=2)
|
|
assert not receiver_errors, receiver_errors
|
|
|
|
process = subprocess.Popen(
|
|
[
|
|
binary,
|
|
"--interface", TX_INTERFACE,
|
|
"--rate-kbps", str(RATE_KBPS),
|
|
"--queue-limit", str(QUEUE_LIMIT),
|
|
"--duration", "2",
|
|
],
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
bufsize=1,
|
|
)
|
|
lines = wait_until_ready(process)
|
|
send_errors = send_burst()
|
|
remainder, _ = process.communicate(timeout=8)
|
|
output = "".join(lines) + remainder
|
|
stop.set()
|
|
receiver.join(timeout=2)
|
|
assert not receiver.is_alive()
|
|
assert not receiver_errors, receiver_errors
|
|
assert process.returncode == 0, output
|
|
|
|
stats = parse_summary(output)
|
|
assert stats["enqueued"] + stats["policy_dropped"] == ATTEMPTS, stats
|
|
assert stats["policy_dropped"] > 0, stats
|
|
assert stats["dequeued"] == stats["enqueued"], stats
|
|
assert stats["cleanup_dropped"] == 0, stats
|
|
assert stats["max_qlen"] <= QUEUE_LIMIT, stats
|
|
assert stats["max_qlen"] >= 2, stats
|
|
assert len(receive_times) == stats["dequeued"], (len(receive_times), stats)
|
|
assert len(receive_times) >= 5, receive_times
|
|
|
|
span = receive_times[-1] - receive_times[0]
|
|
assert span >= 0.4, span
|
|
observed_kbps = (len(receive_times) - 1) * FRAME_SIZE * 8 / span / 1000
|
|
assert observed_kbps <= RATE_KBPS * 1.5, observed_kbps
|
|
|
|
qdisc_after = run("tc", "qdisc", "show", "dev", TX_INTERFACE).stdout
|
|
assert "bpf_pacer" not in qdisc_after, qdisc_after
|
|
|
|
process = subprocess.Popen(
|
|
[
|
|
binary,
|
|
"--interface", TX_INTERFACE,
|
|
"--rate-kbps", str(RATE_KBPS),
|
|
"--queue-limit", str(QUEUE_LIMIT),
|
|
"--duration", "30",
|
|
],
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
bufsize=1,
|
|
)
|
|
signal_lines = wait_until_ready(process)
|
|
send_burst()
|
|
process.terminate()
|
|
signal_remainder, _ = process.communicate(timeout=4)
|
|
signal_output = "".join(signal_lines) + signal_remainder
|
|
assert process.returncode == 0, signal_output
|
|
signal_stats = parse_summary(signal_output)
|
|
assert signal_stats["enqueued"] + signal_stats["policy_dropped"] == ATTEMPTS, signal_stats
|
|
assert signal_stats["cleanup_dropped"] > 0, signal_stats
|
|
assert signal_stats["enqueued"] == (
|
|
signal_stats["dequeued"] + signal_stats["cleanup_dropped"]
|
|
), signal_stats
|
|
qdisc_after_signal = run("tc", "qdisc", "show", "dev", TX_INTERFACE).stdout
|
|
assert "bpf_pacer" not in qdisc_after_signal, qdisc_after_signal
|
|
|
|
process = subprocess.Popen(
|
|
[
|
|
binary,
|
|
"--interface", TX_INTERFACE,
|
|
"--rate-kbps", str(RATE_KBPS),
|
|
"--queue-limit", str(QUEUE_LIMIT),
|
|
"--duration", "30",
|
|
],
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
bufsize=1,
|
|
)
|
|
wait_until_ready(process)
|
|
process.kill()
|
|
process.communicate(timeout=4)
|
|
assert process.returncode == -signal.SIGKILL, process.returncode
|
|
qdisc_after_kill = run("tc", "qdisc", "show", "dev", TX_INTERFACE).stdout
|
|
assert "bpf_pacer" in qdisc_after_kill, qdisc_after_kill
|
|
run("tc", "qdisc", "del", "dev", TX_INTERFACE, "root")
|
|
qdisc_after_recovery = run("tc", "qdisc", "show", "dev", TX_INTERFACE).stdout
|
|
assert "bpf_pacer" not in qdisc_after_recovery, qdisc_after_recovery
|
|
|
|
ready_line = next(line for line in output.splitlines() if line.startswith("READY "))
|
|
summary_line = next(line for line in output.splitlines() if line.startswith("SUMMARY "))
|
|
print(ready_line)
|
|
print(summary_line)
|
|
print(
|
|
f"TEST-SUMMARY attempts={ATTEMPTS} received={len(receive_times)} "
|
|
f"send_errors={send_errors} span_ms={span * 1000:.0f} "
|
|
f"observed_kbps={observed_kbps:.1f}"
|
|
)
|
|
print(
|
|
"PASS: conflict refusal, bounded drops, pacing, accounting, "
|
|
"normal/signal cleanup, and SIGKILL recovery succeeded"
|
|
)
|
|
finally:
|
|
if process is not None and process.poll() is None:
|
|
process.terminate()
|
|
try:
|
|
process.communicate(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.communicate()
|
|
stop.set()
|
|
if receiver is not None:
|
|
receiver.join(timeout=2)
|
|
delete_test_link()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|