diff mbox series

[v2,1/6] landlock: Add UDP bind+connect access control

Message ID 20241214184540.3835222-2-matthieu@buffet.re (mailing list archive)
State Handled Elsewhere
Headers show
Series landlock: Add UDP access control support | expand

Commit Message

Matthieu Buffet Dec. 14, 2024, 6:45 p.m. UTC
If an app doesn't need to be able to open UDP sockets, it should be
denied the right to create UDP sockets altogether (via seccomp and/or
https://github.com/landlock-lsm/linux/issues/6 when it lands).
For apps using UDP, add support for two more fine-grained access rights:

- LANDLOCK_ACCESS_NET_CONNECT_UDP, to gate the possibility to connect()
  a UDP socket. For client apps (those which want to avoid specifying a
  destination for each datagram in sendmsg()), and for a few servers
  (those creating per-client sockets, which want to receive traffic only
  from a specific address)

- LANDLOCK_ACCESS_NET_BIND_UDP, to gate the possibility to bind() a UDP
  socket. For most servers (to start listening for datagrams on a
  non-ephemeral port) and can be useful for some client applications (to
  set the source port of future datagrams, e.g. mDNS requires to use
  source port 5353)

No restriction is enforced on send()/recv() to preserve performance.
The security boundary is to prevent acquiring a bound/connected socket.

Bump ABI to v7 to allow userland to detect and use these new restrictions.

Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
 include/uapi/linux/landlock.h | 53 ++++++++++++++++++++++++++---------
 security/landlock/limits.h    |  2 +-
 security/landlock/net.c       | 49 ++++++++++++++++++++++----------
 security/landlock/syscalls.c  |  2 +-
 4 files changed, 76 insertions(+), 30 deletions(-)
diff mbox series

Patch

diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 33745642f787..3f7b8e85822d 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -119,12 +119,15 @@  struct landlock_net_port_attr {
 	 *
 	 * It should be noted that port 0 passed to :manpage:`bind(2)` will bind
 	 * to an available port from the ephemeral port range.  This can be
-	 * configured with the ``/proc/sys/net/ipv4/ip_local_port_range`` sysctl
-	 * (also used for IPv6).
+	 * configured globally with the
+	 * ``/proc/sys/net/ipv4/ip_local_port_range`` sysctl (also used for
+	 * IPv6), and, within that first range, on a per-socket basis using
+	 * ``setsockopt(IP_LOCAL_PORT_RANGE)``.
 	 *
-	 * A Landlock rule with port 0 and the ``LANDLOCK_ACCESS_NET_BIND_TCP``
-	 * right means that requesting to bind on port 0 is allowed and it will
-	 * automatically translate to binding on the related port range.
+	 * A Landlock rule with port 0 and the %LANDLOCK_ACCESS_NET_BIND_TCP
+	 * or %LANDLOCK_ACCESS_NET_BIND_UDP right means that requesting to
+	 * bind on port 0 is allowed and it will automatically translate to
+	 * binding on the ephemeral port range.
 	 */
 	__u64 port;
 };
@@ -267,18 +270,42 @@  struct landlock_net_port_attr {
  * Network flags
  * ~~~~~~~~~~~~~~~~
  *
- * These flags enable to restrict a sandboxed process to a set of network
- * actions. This is supported since the Landlock ABI version 4.
- *
- * The following access rights apply to TCP port numbers:
- *
- * - %LANDLOCK_ACCESS_NET_BIND_TCP: Bind a TCP socket to a local port.
- * - %LANDLOCK_ACCESS_NET_CONNECT_TCP: Connect an active TCP socket to
- *   a remote port.
+ * These flags enable to restrict which network-related actions a sandboxed
+ * process can take. TCP support was added in Landlock ABI version 4, and UDP
+ * support in version 7.
+ *
+ * TCP access rights:
+ *
+ * - %LANDLOCK_ACCESS_NET_BIND_TCP: bind sockets to the given local port,
+ *   for servers that will listen() on that port, or for clients that want
+ *   to open connections with that specific source port instead of using a
+ *   kernel-assigned random ephemeral one
+ * - %LANDLOCK_ACCESS_NET_CONNECT_TCP: connect client sockets to servers
+ *   listening on that remote port
+ *
+ * UDP access rights:
+ *
+ * - %LANDLOCK_ACCESS_NET_BIND_UDP: bind sockets to the given local port,
+ *   for servers that will listen() on that port, or for clients that want
+ *   to send datagrams with that specific source port instead of using a
+ *   kernel-assigned random ephemeral one
+ * - %LANDLOCK_ACCESS_NET_CONNECT_UDP: connect sockets to the given remote
+ *   port, either for clients that will send datagrams to that destination
+ *   (and want to send them faster without specifying an explicit address
+ *   every time), or for servers that want to filter which client address
+ *   they want to receive datagrams from (e.g. creating a client-specific
+ *   socket)
+ *
+ * Note that binding on port 0 means binding to an ephemeral
+ * kernel-assigned port, in the range configured in
+ * ``/proc/sys/net/ipv4/ip_local_port_range`` globally (and, within that
+ * range, on a per-socket basis with ``setsockopt(IP_LOCAL_PORT_RANGE)``).
  */
 /* clang-format off */
 #define LANDLOCK_ACCESS_NET_BIND_TCP			(1ULL << 0)
 #define LANDLOCK_ACCESS_NET_CONNECT_TCP			(1ULL << 1)
+#define LANDLOCK_ACCESS_NET_BIND_UDP			(1ULL << 2)
+#define LANDLOCK_ACCESS_NET_CONNECT_UDP			(1ULL << 3)
 /* clang-format on */
 
 /**
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 15f7606066c8..ca90c1c56458 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -22,7 +22,7 @@ 
 #define LANDLOCK_MASK_ACCESS_FS		((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_FS		__const_hweight64(LANDLOCK_MASK_ACCESS_FS)
 
-#define LANDLOCK_LAST_ACCESS_NET	LANDLOCK_ACCESS_NET_CONNECT_TCP
+#define LANDLOCK_LAST_ACCESS_NET	LANDLOCK_ACCESS_NET_CONNECT_UDP
 #define LANDLOCK_MASK_ACCESS_NET	((LANDLOCK_LAST_ACCESS_NET << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_NET		__const_hweight64(LANDLOCK_MASK_ACCESS_NET)
 
diff --git a/security/landlock/net.c b/security/landlock/net.c
index d5dcc4407a19..1c5cf2ddb7c1 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -63,10 +63,6 @@  static int current_check_access_socket(struct socket *const sock,
 	if (WARN_ON_ONCE(dom->num_layers < 1))
 		return -EACCES;
 
-	/* Checks if it's a (potential) TCP socket. */
-	if (sock->type != SOCK_STREAM)
-		return 0;
-
 	/* Checks for minimal header length to safely read sa_family. */
 	if (addrlen < offsetofend(typeof(*address), sa_family))
 		return -EINVAL;
@@ -94,17 +90,19 @@  static int current_check_access_socket(struct socket *const sock,
 	/* Specific AF_UNSPEC handling. */
 	if (address->sa_family == AF_UNSPEC) {
 		/*
-		 * Connecting to an address with AF_UNSPEC dissolves the TCP
-		 * association, which have the same effect as closing the
-		 * connection while retaining the socket object (i.e., the file
-		 * descriptor).  As for dropping privileges, closing
-		 * connections is always allowed.
-		 *
-		 * For a TCP access control system, this request is legitimate.
+		 * Connecting to an address with AF_UNSPEC dissolves the
+		 * remote association while retaining the socket object
+		 * (i.e., the file descriptor). For TCP, it has the same
+		 * effect as closing the connection. For UDP, it removes
+		 * any preset destination for future datagrams.
+		 * Like dropping privileges, these actions are always
+		 * allowed: access control is performed when bind()ing or
+		 * connect()ing.
 		 * Let the network stack handle potential inconsistencies and
 		 * return -EINVAL if needed.
 		 */
-		if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP)
+		if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP ||
+		    access_request == LANDLOCK_ACCESS_NET_CONNECT_UDP)
 			return 0;
 
 		/*
@@ -118,7 +116,8 @@  static int current_check_access_socket(struct socket *const sock,
 		 * checks, but it is safer to return a proper error and test
 		 * consistency thanks to kselftest.
 		 */
-		if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) {
+		if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
+		    access_request == LANDLOCK_ACCESS_NET_BIND_UDP) {
 			/* addrlen has already been checked for AF_UNSPEC. */
 			const struct sockaddr_in *const sockaddr =
 				(struct sockaddr_in *)address;
@@ -159,16 +158,36 @@  static int current_check_access_socket(struct socket *const sock,
 static int hook_socket_bind(struct socket *const sock,
 			    struct sockaddr *const address, const int addrlen)
 {
+	access_mask_t access_request;
+
+	/* Checks if it's a (potential) TCP socket. */
+	if (sock->type == SOCK_STREAM)
+		access_request = LANDLOCK_ACCESS_NET_BIND_TCP;
+	else if (sk_is_udp(sock->sk))
+		access_request = LANDLOCK_ACCESS_NET_BIND_UDP;
+	else
+		return 0;
+
 	return current_check_access_socket(sock, address, addrlen,
-					   LANDLOCK_ACCESS_NET_BIND_TCP);
+					   access_request);
 }
 
 static int hook_socket_connect(struct socket *const sock,
 			       struct sockaddr *const address,
 			       const int addrlen)
 {
+	access_mask_t access_request;
+
+	/* Checks if it's a (potential) TCP socket. */
+	if (sock->type == SOCK_STREAM)
+		access_request = LANDLOCK_ACCESS_NET_CONNECT_TCP;
+	else if (sk_is_udp(sock->sk))
+		access_request = LANDLOCK_ACCESS_NET_CONNECT_UDP;
+	else
+		return 0;
+
 	return current_check_access_socket(sock, address, addrlen,
-					   LANDLOCK_ACCESS_NET_CONNECT_TCP);
+					   access_request);
 }
 
 static struct security_hook_list landlock_hooks[] __ro_after_init = {
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index c097d356fa45..200f771fa3a4 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -150,7 +150,7 @@  static const struct file_operations ruleset_fops = {
 	.write = fop_dummy_write,
 };
 
-#define LANDLOCK_ABI_VERSION 6
+#define LANDLOCK_ABI_VERSION 7
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset