CT_EVENT receives notifications on event_sock after socket consolidation, but --buffer-size still configures and queries the separate control socket. A larger value therefore leaves the active event receive buffer unchanged while reporting success. Use event_sock for the forced setting, fallback setting and effective-size readback. Add an isolated real-kernel regression that traces all event masks and two requested sizes. The original fails because the option targets fd 3 while the multicast listener is fd 4; the fix passes all eight cases. The existing conntrack CLI suite also passes 286 cases. Prepared with AI assistance and verified against actual netlink syscalls. Fixes: 5ec684be0854 ("conntrack: consolidate socket open call") Signed-off-by: Yuen Su --- Validation (outside the commit message): - Public CI: https://github.com/ZyPulse-zy/packages/actions/runs/39094897174 - Real netlink syscalls: eight event cases / 72 assertions per binary; original mainline and 1.4.8 fail the exact wrong-socket assertion. - Actual Autotools builds and existing upstream CLI suites pass. Local current-mainline CLI suite: 286 cases. - ASan/UBSan tracing passes; LSan is disabled only because ptrace is unsupported by LSan. The default-LSan failure log is retained. - The OpenWrt 1.4.8 backport was refreshed by the actual SDK quilt target; official 25.12.0 GCC14.3 AArch64/musl APKs and dependencies build. SDK kernel configuration and Module.symvers hashes remain unchanged. - QEMU could not open NETLINK_NETFILTER (Protocol not supported), so target-userland runtime and hardware behavior are not claimed. src/conntrack.c | 6 +- tests/conntrack/test-event-buffer.py | 130 +++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 tests/conntrack/test-event-buffer.py diff --git a/src/conntrack.c b/src/conntrack.c index 940469d..977f130 100644 --- a/src/conntrack.c +++ b/src/conntrack.c @@ -3555,17 +3555,17 @@ static int do_command_ct(const char *progname, struct ct_cmd *cmd, socklen_t socklen = sizeof(socketbuffersize); - res = setsockopt(mnl_socket_get_fd(sock->mnl), + res = setsockopt(mnl_socket_get_fd(event_sock->mnl), SOL_SOCKET, SO_RCVBUFFORCE, &socketbuffersize, sizeof(socketbuffersize)); if (res < 0) { - setsockopt(mnl_socket_get_fd(sock->mnl), + setsockopt(mnl_socket_get_fd(event_sock->mnl), SOL_SOCKET, SO_RCVBUF, &socketbuffersize, sizeof(socketbuffersize)); } - getsockopt(mnl_socket_get_fd(sock->mnl), SOL_SOCKET, + getsockopt(mnl_socket_get_fd(event_sock->mnl), SOL_SOCKET, SO_RCVBUF, &socketbuffersize, &socklen); fprintf(stderr, "NOTICE: Netlink socket buffer size " "has been set to %zu bytes.\n", diff --git a/tests/conntrack/test-event-buffer.py b/tests/conntrack/test-event-buffer.py new file mode 100644 index 0000000..4a5adff --- /dev/null +++ b/tests/conntrack/test-event-buffer.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +"""Check -E --buffer-size on real netlink sockets in an isolated netns. + +Requires Python 3, strace and GNU timeout. Run as follows: + CONNTRACK_TEST_PARENT_NETNS=$(readlink /proc/self/ns/net) \ + unshare --user --map-root-user --net python3 test-event-buffer.py /path/to/conntrack +No connections are created, removed or modified. +""" + +import argparse +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("conntrack", type=Path) + parser.add_argument("--report", type=Path) + args = parser.parse_args() + binary = args.conntrack.resolve() + parent = os.environ.get("CONNTRACK_TEST_PARENT_NETNS") + if not parent or parent == os.readlink("/proc/self/ns/net") or os.geteuid() != 0: + raise SystemExit("Run in a new user/network namespace as documented") + report = {"passed": False, "checks": 0, "cases": []} + + def check(condition, description): + if not condition: + raise AssertionError(description) + report["checks"] += 1 + + try: + with tempfile.TemporaryDirectory(prefix="conntrack-event-buffer-") as temporary: + for mask, groups in [(None, 7), ("NEW", 1), ("UPDATE", 2), ("DESTROY", 4)]: + for requested in (32768, 1048576): + trace = Path(temporary) / "syscalls.log" + command = [ + "strace", + "-f", + "-o", + str(trace), + "-e", + "trace=socket,bind,setsockopt,getsockopt", + "timeout", + "--preserve-status", + "--signal=TERM", + "1", + str(binary), + "-E", + "--buffer-size", + str(requested), + ] + if mask: + command += ["--event-mask", mask] + result = subprocess.run(command, capture_output=True, text=True, timeout=5) + text = trace.read_text() + case = { + "mask": mask or "ALL", + "requested": requested, + "exit": result.returncode, + "stderr": result.stderr, + "trace": text, + } + report["cases"].append(case) + check(result.returncode == 0, "Event command did not shut down cleanly") + binds = re.findall( + r"bind\((\d+), .*nl_groups=(0x[0-9a-f]+|0+)\}, \d+\) = 0", text + ) + control = [int(fd) for fd, group in binds if int(group, 0) == 0] + events = [int(fd) for fd, group in binds if int(group, 0) == groups] + check( + len(control) == len(events) == 1, + "Expected control and event netlink sockets", + ) + event_fd = events[0] + check(event_fd != control[0], "Event listener must be a separate socket") + writes = re.findall( + r"setsockopt\((\d+), SOL_SOCKET, SO_RCVBUF(?:FORCE)?, \[(\d+)\].*", text + ) + check( + bool(writes) and all(int(fd) == event_fd for fd, _ in writes), + "--buffer-size changed the query socket instead of the event listener", + ) + check( + all(int(size) == requested for _, size in writes), "Requested size changed" + ) + check( + bool( + re.search( + r"setsockopt\(" + + str(event_fd) + + r", SOL_SOCKET, SO_RCVBUF(?:FORCE)?, .* = 0", + text, + ) + ), + "No receive-buffer option succeeded on the event listener", + ) + reads = re.findall(r"getsockopt\((\d+), SOL_SOCKET, SO_RCVBUF, \[(\d+)\]", text) + check( + len(reads) == 1 and int(reads[0][0]) == event_fd, + "Reported buffer size was read from the query socket", + ) + actual = int(reads[0][1]) + check(actual > 0, "Kernel returned an invalid buffer size") + check( + f"set to {actual} bytes" in result.stderr, + "Notice does not describe the kernel event socket readback", + ) + case.update( + event_fd=event_fd, + control_fd=control[0], + actual=actual, + force_fallback="EPERM" in text, + ) + report["passed"] = True + except BaseException as exc: + report["failure"] = str(exc) + raise + finally: + if args.report: + args.report.write_text(json.dumps(report, indent=2) + "\n") + print(f"PASS: {report['checks']} real-kernel assertions, {len(report['cases'])} event cases") + + +if __name__ == "__main__": + main() -- 2.53.0