Add trace tests for the landlock_deny_access_net tracepoint: denied
bind, allowed bind (no event), denied connect, bind field verification,
connect-after-bind field verification, and an unsandboxed baseline.

Cc: Günther Noack <[email protected]>
Cc: Tingmao Wang <[email protected]>
Signed-off-by: Mickaël Salaün <[email protected]>
---

Changes since v1:
- New patch.
---
 tools/testing/selftests/landlock/net_test.c | 547 +++++++++++++++++++-
 1 file changed, 546 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/landlock/net_test.c 
b/tools/testing/selftests/landlock/net_test.c
index 4c528154ea92..4fe41425995c 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -10,11 +10,12 @@
 #include <arpa/inet.h>
 #include <errno.h>
 #include <fcntl.h>
-#include <linux/landlock.h>
 #include <linux/in.h>
+#include <linux/landlock.h>
 #include <sched.h>
 #include <stdint.h>
 #include <string.h>
+#include <sys/mount.h>
 #include <sys/prctl.h>
 #include <sys/socket.h>
 #include <sys/syscall.h>
@@ -22,6 +23,9 @@
 
 #include "audit.h"
 #include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "net_test"
 
 const short sock_port_start = (1 << 10);
 
@@ -2026,4 +2030,545 @@ TEST_F(audit, connect)
        EXPECT_EQ(0, close(sock_fd));
 }
 
+/* Trace tests */
+
+/* clang-format off */
+FIXTURE(trace_net) {
+       /* clang-format on */
+       int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_net)
+{
+       int ret;
+
+       set_cap(_metadata, CAP_SYS_ADMIN);
+       ASSERT_EQ(0, unshare(CLONE_NEWNS));
+       ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+       ret = tracefs_fixture_setup();
+       if (ret) {
+               clear_cap(_metadata, CAP_SYS_ADMIN);
+               self->tracefs_ok = 0;
+               SKIP(return, "tracefs not available");
+       }
+       self->tracefs_ok = 1;
+
+       ASSERT_EQ(0,
+                 tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
+       ASSERT_EQ(0, tracefs_clear());
+       clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_net)
+{
+       if (!self->tracefs_ok)
+               return;
+
+       set_cap(_metadata, CAP_SYS_ADMIN);
+       tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
+       tracefs_fixture_teardown();
+       clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Baseline: verifies that without Landlock, the bind succeeds and no
+ * deny_access_net trace event fires.
+ */
+/* clang-format off */
+FIXTURE_VARIANT(trace_net)
+{
+       /* clang-format on */
+       bool sandbox;
+       int bind_port_offset; /* 0 = allowed port, 1 = denied port */
+       int expect_denied;
+};
+
+/* Unsandboxed: no Landlock, bind should succeed with no events. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_net, unsandboxed) {
+       /* clang-format on */
+       .sandbox = false,
+       .bind_port_offset = 0,
+       .expect_denied = 0,
+};
+
+/* Denied: sandboxed, bind to port not in ruleset. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_net, bind_denied) {
+       /* clang-format on */
+       .sandbox = true,
+       .bind_port_offset = 1,
+       .expect_denied = 1,
+};
+
+/* Allowed: sandboxed, bind to port in ruleset. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_net, bind_allowed) {
+       /* clang-format on */
+       .sandbox = true,
+       .bind_port_offset = 0,
+       .expect_denied = 0,
+};
+
+TEST_F(trace_net, deny_access_net_bind)
+{
+       char *buf;
+       int count, status;
+       pid_t child;
+
+       if (!self->tracefs_ok)
+               SKIP(return, "tracefs not available");
+
+       ASSERT_EQ(0, tracefs_clear_buf());
+
+       child = fork();
+       ASSERT_LE(0, child);
+
+       if (child == 0) {
+               struct sockaddr_in addr = {
+                       .sin_family = AF_INET,
+                       .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+               };
+               int sock_fd;
+
+               if (variant->sandbox) {
+                       struct landlock_ruleset_attr ruleset_attr = {
+                               .handled_access_net =
+                                       LANDLOCK_ACCESS_NET_BIND_TCP,
+                       };
+                       struct landlock_net_port_attr port_attr = {
+                               .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+                               .port = sock_port_start,
+                       };
+                       int ruleset_fd;
+
+                       ruleset_fd = landlock_create_ruleset(
+                               &ruleset_attr, sizeof(ruleset_attr), 0);
+                       if (ruleset_fd < 0)
+                               _exit(1);
+
+                       if (landlock_add_rule(ruleset_fd,
+                                             LANDLOCK_RULE_NET_PORT,
+                                             &port_attr, 0)) {
+                               close(ruleset_fd);
+                               _exit(1);
+                       }
+
+                       prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+                       if (landlock_restrict_self(ruleset_fd, 0)) {
+                               close(ruleset_fd);
+                               _exit(1);
+                       }
+                       close(ruleset_fd);
+               }
+
+               sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+               if (sock_fd < 0)
+                       _exit(1);
+
+               addr.sin_port =
+                       htons(sock_port_start + variant->bind_port_offset);
+               if (variant->expect_denied) {
+                       /* Bind should be denied. */
+                       if (bind(sock_fd, (struct sockaddr *)&addr,
+                                sizeof(addr)) == 0) {
+                               close(sock_fd);
+                               _exit(2);
+                       }
+                       if (errno != EACCES) {
+                               close(sock_fd);
+                               _exit(3);
+                       }
+               } else {
+                       /* Bind should succeed. */
+                       if (bind(sock_fd, (struct sockaddr *)&addr,
+                                sizeof(addr))) {
+                               close(sock_fd);
+                               _exit(2);
+                       }
+               }
+               close(sock_fd);
+               _exit(0);
+       }
+
+       ASSERT_EQ(child, waitpid(child, &status, 0));
+       ASSERT_TRUE(WIFEXITED(status));
+       EXPECT_EQ(0, WEXITSTATUS(status));
+
+       buf = tracefs_read_buf();
+       ASSERT_NE(NULL, buf);
+
+       count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK));
+       if (variant->expect_denied) {
+               EXPECT_LE(variant->expect_denied, count)
+               {
+                       TH_LOG("Expected deny_access_net event, got %d\n%s",
+                              count, buf);
+               }
+       } else {
+               EXPECT_EQ(0, count)
+               {
+                       TH_LOG("Expected 0 deny_access_net events, "
+                              "got %d\n%s",
+                              count, buf);
+               }
+       }
+
+       free(buf);
+}
+
+/* Connect and field-check tests use a separate fixture without variants. */
+
+/* clang-format off */
+FIXTURE(trace_net_connect) {
+       /* clang-format on */
+       int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_net_connect)
+{
+       int ret;
+
+       set_cap(_metadata, CAP_SYS_ADMIN);
+       ASSERT_EQ(0, unshare(CLONE_NEWNS));
+       ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+       ret = tracefs_fixture_setup();
+       if (ret) {
+               clear_cap(_metadata, CAP_SYS_ADMIN);
+               self->tracefs_ok = 0;
+               SKIP(return, "tracefs not available");
+       }
+       self->tracefs_ok = 1;
+
+       ASSERT_EQ(0,
+                 tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
+       ASSERT_EQ(0, tracefs_clear());
+       clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_net_connect)
+{
+       if (!self->tracefs_ok)
+               return;
+
+       set_cap(_metadata, CAP_SYS_ADMIN);
+       tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
+       tracefs_fixture_teardown();
+       clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Verifies that a denied connect emits a deny_access_net trace event with
+ * sport=0 and dport=<denied_port>.
+ */
+TEST_F(trace_net_connect, deny_access_net_connect_denied)
+{
+       pid_t child;
+       int status;
+       char *buf;
+       char field[64], expected[16];
+
+       if (!self->tracefs_ok)
+               SKIP(return, "tracefs not available");
+
+       child = fork();
+       ASSERT_LE(0, child);
+
+       if (child == 0) {
+               struct landlock_ruleset_attr ruleset_attr = {
+                       .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+               };
+               struct landlock_net_port_attr port_attr = {
+                       .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+                       .port = sock_port_start,
+               };
+               struct sockaddr_in addr = {
+                       .sin_family = AF_INET,
+                       .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+               };
+               int ruleset_fd, sock_fd;
+
+               ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+                                                    sizeof(ruleset_attr), 0);
+               if (ruleset_fd < 0)
+                       _exit(1);
+
+               if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+                                     &port_attr, 0)) {
+                       close(ruleset_fd);
+                       _exit(1);
+               }
+
+               prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+               if (landlock_restrict_self(ruleset_fd, 0)) {
+                       close(ruleset_fd);
+                       _exit(1);
+               }
+               close(ruleset_fd);
+
+               /* Connect to denied port. */
+               sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+               if (sock_fd < 0)
+                       _exit(1);
+
+               addr.sin_port = htons(sock_port_start + 1);
+               if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) ==
+                   0) {
+                       close(sock_fd);
+                       _exit(2);
+               }
+               if (errno != EACCES) {
+                       close(sock_fd);
+                       _exit(3);
+               }
+               close(sock_fd);
+               _exit(0);
+       }
+
+       ASSERT_EQ(child, waitpid(child, &status, 0));
+       ASSERT_TRUE(WIFEXITED(status));
+       EXPECT_EQ(0, WEXITSTATUS(status));
+
+       buf = tracefs_read_buf();
+       ASSERT_NE(NULL, buf);
+
+       EXPECT_LE(1, tracefs_count_matches(buf,
+                                          REGEX_DENY_ACCESS_NET(TRACE_TASK)));
+
+       /*
+        * Verify dport is the denied port and sport is 0.  The port
+        * value must be in host endianness, matching the UAPI convention
+        * (landlock_net_port_attr.port).  On little-endian,
+        * htons(sock_port_start + 1) would produce a different decimal
+        * value, so this comparison also catches byte-order bugs.
+        */
+       ASSERT_EQ(0,
+                 tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+                                       "sport", field, sizeof(field)));
+       EXPECT_STREQ("0", field);
+
+       ASSERT_EQ(0,
+                 tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+                                       "dport", field, sizeof(field)));
+       snprintf(expected, sizeof(expected), "%llu",
+                (unsigned long long)(sock_port_start + 1));
+       EXPECT_STREQ(expected, field);
+
+       free(buf);
+}
+
+/* Verifies that a denied bind emits sport=<port> dport=0. */
+TEST_F(trace_net_connect, deny_access_net_bind_fields)
+{
+       pid_t child;
+       int status;
+       char *buf;
+       char field[64], expected[16];
+
+       if (!self->tracefs_ok)
+               SKIP(return, "tracefs not available");
+
+       child = fork();
+       ASSERT_LE(0, child);
+
+       if (child == 0) {
+               struct landlock_ruleset_attr ruleset_attr = {
+                       .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+               };
+               struct landlock_net_port_attr port_attr = {
+                       .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+                       .port = sock_port_start,
+               };
+               struct sockaddr_in addr = {
+                       .sin_family = AF_INET,
+                       .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+               };
+               int ruleset_fd, sock_fd;
+
+               ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+                                                    sizeof(ruleset_attr), 0);
+               if (ruleset_fd < 0)
+                       _exit(1);
+
+               if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+                                     &port_attr, 0)) {
+                       close(ruleset_fd);
+                       _exit(1);
+               }
+
+               prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+               if (landlock_restrict_self(ruleset_fd, 0)) {
+                       close(ruleset_fd);
+                       _exit(1);
+               }
+               close(ruleset_fd);
+
+               /* Bind to denied port. */
+               sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+               if (sock_fd < 0)
+                       _exit(1);
+
+               addr.sin_port = htons(sock_port_start + 1);
+               if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) ==
+                   0) {
+                       close(sock_fd);
+                       _exit(2);
+               }
+               if (errno != EACCES) {
+                       close(sock_fd);
+                       _exit(3);
+               }
+               close(sock_fd);
+               _exit(0);
+       }
+
+       ASSERT_EQ(child, waitpid(child, &status, 0));
+       ASSERT_TRUE(WIFEXITED(status));
+       EXPECT_EQ(0, WEXITSTATUS(status));
+
+       buf = tracefs_read_buf();
+       ASSERT_NE(NULL, buf);
+
+       EXPECT_LE(1, tracefs_count_matches(buf,
+                                          REGEX_DENY_ACCESS_NET(TRACE_TASK)));
+
+       /* Verify sport is the denied port and dport is 0. */
+       ASSERT_EQ(0,
+                 tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+                                       "dport", field, sizeof(field)));
+       EXPECT_STREQ("0", field);
+
+       ASSERT_EQ(0,
+                 tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+                                       "sport", field, sizeof(field)));
+       snprintf(expected, sizeof(expected), "%llu",
+                (unsigned long long)(sock_port_start + 1));
+       EXPECT_STREQ(expected, field);
+
+       free(buf);
+}
+
+/*
+ * Verifies that a denied connect after a successful bind shows sport=0 and
+ * dport=<denied_port>.  The bind succeeds (allowed port), then the connect is
+ * denied.  sport=0 because the denied operation is connect, not bind.
+ */
+TEST_F(trace_net_connect, deny_access_net_connect_after_bind)
+{
+       pid_t child;
+       int status;
+       char *buf;
+       char field[64], expected[16];
+
+       if (!self->tracefs_ok)
+               SKIP(return, "tracefs not available");
+
+       child = fork();
+       ASSERT_LE(0, child);
+
+       if (child == 0) {
+               struct landlock_ruleset_attr ruleset_attr = {
+                       .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
+                                             LANDLOCK_ACCESS_NET_CONNECT_TCP,
+               };
+               struct landlock_net_port_attr port_attr;
+               struct sockaddr_in bind_addr = {
+                       .sin_family = AF_INET,
+                       .sin_port = htons(sock_port_start),
+                       .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+               };
+               struct sockaddr_in conn_addr = {
+                       .sin_family = AF_INET,
+                       .sin_port = htons(sock_port_start + 1),
+                       .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+               };
+               int ruleset_fd, sock_fd, optval = 1;
+
+               ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+                                                    sizeof(ruleset_attr), 0);
+               if (ruleset_fd < 0)
+                       _exit(1);
+
+               /* Allow bind and connect on sock_port_start only. */
+               port_attr.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
+                                          LANDLOCK_ACCESS_NET_CONNECT_TCP;
+               port_attr.port = sock_port_start;
+               if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+                                     &port_attr, 0)) {
+                       close(ruleset_fd);
+                       _exit(1);
+               }
+
+               prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+               if (landlock_restrict_self(ruleset_fd, 0)) {
+                       close(ruleset_fd);
+                       _exit(1);
+               }
+               close(ruleset_fd);
+
+               sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+               if (sock_fd < 0)
+                       _exit(1);
+               setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval,
+                          sizeof(optval));
+
+               /* Bind to allowed port (succeeds, no trace event). */
+               if (bind(sock_fd, (struct sockaddr *)&bind_addr,
+                        sizeof(bind_addr))) {
+                       close(sock_fd);
+                       _exit(1);
+               }
+
+               /* Connect to denied port (fails, emits trace event). */
+               if (connect(sock_fd, (struct sockaddr *)&conn_addr,
+                           sizeof(conn_addr)) == 0) {
+                       close(sock_fd);
+                       _exit(2);
+               }
+               if (errno != EACCES) {
+                       close(sock_fd);
+                       _exit(3);
+               }
+               close(sock_fd);
+               _exit(0);
+       }
+
+       ASSERT_EQ(child, waitpid(child, &status, 0));
+       ASSERT_TRUE(WIFEXITED(status));
+       EXPECT_EQ(0, WEXITSTATUS(status));
+
+       buf = tracefs_read_buf();
+       ASSERT_NE(NULL, buf);
+
+       EXPECT_LE(1, tracefs_count_matches(buf,
+                                          REGEX_DENY_ACCESS_NET(TRACE_TASK)));
+
+       /*
+        * The denied operation is connect, so sport=0 and dport=<denied_port>,
+        * regardless of the prior bind.
+        */
+       ASSERT_EQ(0,
+                 tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+                                       "sport", field, sizeof(field)));
+       EXPECT_STREQ("0", field);
+
+       ASSERT_EQ(0,
+                 tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+                                       "dport", field, sizeof(field)));
+       snprintf(expected, sizeof(expected), "%llu",
+                (unsigned long long)(sock_port_start + 1));
+       EXPECT_STREQ(expected, field);
+
+       free(buf);
+}
+
+/*
+ * IPv6 network trace tests are intentionally elided.  IPv6 hook dispatch uses
+ * the same current_check_access_socket() code path as IPv4, validated by the
+ * audit tests in this file.  The trace events use the same 
blockers/sport/dport
+ * fields regardless of address family.
+ */
+
 TEST_HARNESS_MAIN
-- 
2.53.0


Reply via email to