DPDK-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Stephen Hemminger <stephen@networkplumber.org>
To: dev@dpdk.org
Cc: Stephen Hemminger <stephen@networkplumber.org>,
	Reshma Pattan <reshma.pattan@intel.com>
Subject: [PATCH v4 3/4] app/rpcapd: add TLS support
Date: Wed, 30 Sep 2026 19:35:28 -0700	[thread overview]
Message-ID: <20261001025853.319860-4-stephen@networkplumber.org> (raw)
In-Reply-To: <20261001025853.319860-1-stephen@networkplumber.org>

Support remote packet capture over TLS.
This requires OpenSSL to be available.

Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
 app/rpcapd/capture.c                   |  15 ++
 app/rpcapd/main.c                      | 154 +++++++++++++-
 app/rpcapd/meson.build                 |  15 ++
 app/rpcapd/rpcap-protocol.h            |   3 +
 app/rpcapd/rpcapd.h                    |  20 +-
 app/rpcapd/session.c                   | 182 +++++++++++++++--
 app/rpcapd/sock.c                      |  52 ++++-
 app/rpcapd/tls.c                       | 273 +++++++++++++++++++++++++
 doc/guides/rel_notes/release_26_11.rst |   2 +
 doc/guides/tools/rpcapd.rst            | 119 +++++++++--
 10 files changed, 789 insertions(+), 46 deletions(-)
 create mode 100644 app/rpcapd/tls.c

diff --git a/app/rpcapd/capture.c b/app/rpcapd/capture.c
index 17fce3fbca..b168216246 100644
--- a/app/rpcapd/capture.c
+++ b/app/rpcapd/capture.c
@@ -168,6 +168,7 @@ stop_capture(struct session *s)
 	rte_free(s->prm);
 	s->prm = NULL;
 	if (s->data.fd >= 0) {
+		tls_close(&s->data);
 		close(s->data.fd);
 		s->data.fd = -1;
 	}
@@ -312,6 +313,14 @@ handle_startcap(const struct conn *c, uint32_t plen, struct session *s)
 
 	s->data.fd = data_fd;
 
+	/* The client starts its handshake as soon as it has connected,
+	 * so promote before anything is sent.
+	 */
+	if (use_tls && tls_accept(&s->data) < 0) {
+		stop_capture(s);
+		return -1;
+	}
+
 	RPCAPD_LOG(NOTICE,
 		   "capture started on %s (snaplen %u, data port %u)",
 		   s->name, s->snaplen, data_port);
@@ -427,6 +436,12 @@ check_socket_status(const struct conn *ctrl)
 {
 	struct pollfd pfd = { .fd = ctrl->fd, .events = POLLIN };
 
+	/* A request may already be decrypted and waiting out of sight
+	 * of poll(), sharing a TLS record with an earlier one.
+	 */
+	if (tls_pending(ctrl))
+		return 1;
+
 	if (poll(&pfd, 1, 0) < 0) {
 		if (errno == EINTR)
 			return 0;
diff --git a/app/rpcapd/main.c b/app/rpcapd/main.c
index 7b7288785d..0cf6c3f4ba 100644
--- a/app/rpcapd/main.c
+++ b/app/rpcapd/main.c
@@ -62,13 +62,17 @@ static int bind_family = AF_UNSPEC;
 static const char *debug_file;		/* --debug-file argument */
 static unsigned int debug_log;		/* -D count: raise RPCAPD log verbosity */
 uint32_t send_timeout = DATA_SEND_TIMEOUT_SEC;	/* 0 means no limit */
+bool use_tls;				/* -S */
+bool null_auth_ok;			/* -n */
+static const char *tls_certfile;	/* -X argument */
+static const char *tls_keyfile;		/* -K argument */
 
 struct sockaddr_storage listen_addr;
 socklen_t               listen_addrlen;
 
 RTE_ATOMIC(bool) quit_signal;
 
-static bool
+bool
 is_loopback(const struct sockaddr_storage *ss)
 {
 	if (ss->ss_family == AF_INET) {
@@ -121,6 +125,62 @@ signal_handler(int sig __rte_unused)
 	rte_atomic_store_explicit(&quit_signal, true, rte_memory_order_relaxed);
 }
 
+/*
+ * TLS is not negotiated in the rpcap protocol, so a mismatch has to be
+ * detected from the first byte: an rpcap message starts with the
+ * protocol version 0, a TLS handshake with content type 22.
+ */
+#define TLS_RECORD_TYPE_HANDSHAKE	22
+
+/* How long a client has to send its first byte. */
+#define FIRST_BYTE_TIMEOUT_MS		(10 * 1000)
+
+static int
+setup_tls(struct conn *ctrl)
+{
+	uint8_t first;
+
+	/* Bounded wait: only one client is served at a time, so a peer
+	 * that connects and says nothing must not hold the daemon.
+	 */
+	switch (wait_readable(ctrl, FIRST_BYTE_TIMEOUT_MS)) {
+	case 1:
+		break;
+	case 0:
+		RPCAPD_LOG(NOTICE, "client sent nothing within %u seconds, closing",
+			FIRST_BYTE_TIMEOUT_MS / 1000);
+		return -1;
+	default:
+		return -1;
+	}
+
+	if (recv(ctrl->fd, &first, 1, MSG_PEEK) != 1)
+		return -1;
+
+	if (!use_tls) {
+		if (first == TLS_RECORD_TYPE_HANDSHAKE) {
+			tls_reject_handshake(ctrl->fd);
+			return -1;
+		}
+		return 0;
+	}
+
+	if (first != TLS_RECORD_TYPE_HANDSHAKE) {
+		struct rpcap_header hdr;
+
+		/* Reply in the clear; it is all the client will understand. */
+		RPCAPD_LOG(WARNING, "rejecting plaintext client: server requires TLS");
+		if (recv_full(ctrl, &hdr, sizeof(hdr)) == 0)
+			rpcap_discard(ctrl, rte_be_to_cpu_32(hdr.plen));
+
+		rpcap_send_error(ctrl, PCAP_ERR_TLS_REQUIRED,
+				 "TLS is required by this server; use rpcaps://");
+		return -1;
+	}
+
+	return tls_accept(ctrl);
+}
+
 /* Service a single client until it disconnects. */
 static void
 handle_client(int ctrl_fd)
@@ -134,6 +194,7 @@ handle_client(int ctrl_fd)
 	/* Remembered so the data connection can be restricted to this peer. */
 	if (getpeername(ctrl_fd, (struct sockaddr *)&peer, &peerlen) != 0) {
 		RPCAPD_LOG(ERR, "getpeername: %s", strerror(errno));
+		close(ctrl_fd);
 		return;
 	}
 	s.peer = peer;
@@ -141,6 +202,15 @@ handle_client(int ctrl_fd)
 		    host, sizeof(host), NULL, 0, NI_NUMERICHOST);
 	RPCAPD_LOG(NOTICE, "client %s connected", host);
 
+	if (!use_tls && !is_loopback(&peer))
+		RPCAPD_LOG(ERR,
+			"remote client %s is connected without TLS; "
+			"captured traffic and any credentials are exposed to the network",
+			host);
+
+	if (setup_tls(&ctrl) < 0)
+		goto done;
+
 	while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) {
 		struct rpcap_header hdr;
 		uint32_t plen;
@@ -165,12 +235,24 @@ handle_client(int ctrl_fd)
 			continue;
 		}
 
+		/* Nothing but authentication is served until it succeeds. */
+		if (!s.authenticated && hdr.type != RPCAP_MSG_AUTH_REQ &&
+		    hdr.type != RPCAP_MSG_CLOSE) {
+			RPCAPD_LOG(NOTICE, "request 0x%02x before authentication",
+				hdr.type);
+			if (rpcap_discard(&ctrl, plen) < 0 ||
+			    rpcap_send_error(&ctrl, PCAP_ERR_AUTH,
+					     "not authenticated") < 0)
+				goto done;
+			continue;
+		}
+
 		switch (hdr.type) {
 		case RPCAP_MSG_AUTH_REQ:
 			/* libpcap treats a zero-length AUTH_REPLY as "version
 			 * 0 only, same byte order".
 			 */
-			if (handle_auth(&ctrl, plen) < 0)
+			if (handle_auth(&ctrl, plen, &s) < 0)
 				goto done;
 			break;
 		case RPCAP_MSG_FINDALLIF_REQ:
@@ -210,6 +292,7 @@ handle_client(int ctrl_fd)
 	}
 done:
 	stop_capture(&s);
+	tls_close(&ctrl);
 	close(ctrl_fd);
 	RPCAPD_LOG(NOTICE, "client %s disconnected", host);
 }
@@ -239,10 +322,10 @@ open_listen_socket(uint16_t port)
 
 	RPCAPD_LOG(NOTICE, "listening on %s port %u", host, listen_port);
 
-	if (!is_loopback(&listen_addr))
-		RPCAPD_LOG(WARNING,
-			"non-loopback address %s; "
-			"rpcap is unauthenticated and unencrypted, captured traffic is exposed to the network",
+	if (!is_loopback(&listen_addr) && !use_tls)
+		RPCAPD_LOG(ERR,
+			"listening on non-loopback address %s without TLS; "
+			"captured traffic will be exposed to the network, use -S",
 			host);
 
 	if (listen(fd, 1) < 0)
@@ -261,6 +344,12 @@ usage(FILE *f, const char *progname)
 		"  -4                    use only IPv4\n"
 		"  -6                    use only IPv6\n"
 		"  -N <ring size>        ring size in packets (default %u)\n"
+#ifdef RTE_HAS_OPENSSL
+		"  -S, --tls             encrypt connections with TLS (rpcaps://)\n"
+		"  -X, --cert <file>     server certificate chain, PEM (needs -S)\n"
+		"  -K, --key <file>      server private key, PEM (needs -S)\n"
+#endif
+		"  -n, --null-auth       permit unauthenticated remote clients\n"
 		"  -D, --debug           increase log verbosity (-D info, -DD debug)\n"
 		"      --debug-file <f>  redirect log output to file <f> (append mode)\n"
 		"      --send-timeout <s> seconds a data send may block before the\n"
@@ -271,9 +360,9 @@ usage(FILE *f, const char *progname)
 		"      --lcore=<core>    CPU core to run on (default: any)\n"
 		"      --file-prefix=<p> prefix to use for multi-process\n"
 		"\n"
-		"WARNING: rpcap is unauthenticated and unencrypted.  Binding to\n"
-		"any non-loopback address exposes captured traffic to the\n"
-		"network.  Not for production use.\n",
+		"Remote clients must authenticate with a system username and\n"
+		"password, and must use TLS to send it.  Loopback clients may\n"
+		"connect unauthenticated.  Not for production use.\n",
 		RPCAP_DEFAULT_NETPORT, DEFAULT_RING_SIZE,
 		DATA_SEND_TIMEOUT_SEC);
 }
@@ -297,6 +386,12 @@ parse_opts(int argc, char **argv)
 	static const struct option long_options[] = {
 		{ "port",         required_argument, NULL, 'p' },
 		{ "bind",         required_argument, NULL, 'b' },
+		{ "null-auth",    no_argument,       NULL, 'n' },
+#ifdef RTE_HAS_OPENSSL
+		{ "tls",          no_argument,       NULL, 'S' },
+		{ "cert",         required_argument, NULL, 'X' },
+		{ "key",          required_argument, NULL, 'K' },
+#endif
 		{ "debug",        no_argument,       NULL, 'D' },
 		{ "help",         no_argument,       NULL, 'h' },
 		{ "version",      no_argument,       NULL, OPT_VERSION },
@@ -308,8 +403,11 @@ parse_opts(int argc, char **argv)
 	};
 	int option_index, c;
 
-	while ((c = getopt_long(argc, argv, "hD46p:b:N:",
-				long_options, &option_index)) != -1) {
+	while ((c = getopt_long(argc, argv, "hnD46p:b:N:"
+#ifdef RTE_HAS_OPENSSL
+				"SX:K:"
+#endif
+				, long_options, &option_index)) != -1) {
 		switch (c) {
 		case 'p': {
 			unsigned long u = strtoul(optarg, NULL, 0);
@@ -328,6 +426,20 @@ parse_opts(int argc, char **argv)
 		case '6':
 			bind_family = AF_INET6;
 			break;
+		case 'n':
+			null_auth_ok = true;
+			break;
+#ifdef RTE_HAS_OPENSSL
+		case 'S':
+			use_tls = true;
+			break;
+		case 'X':
+			tls_certfile = optarg;
+			break;
+		case 'K':
+			tls_keyfile = optarg;
+			break;
+#endif
 		case 'N': {
 			unsigned long u = strtoul(optarg, NULL, 0);
 
@@ -394,6 +506,22 @@ parse_opts(int argc, char **argv)
 
 	/* Resolve the bind address now that -4/-6/-b have been seen. */
 	parse_bind_addr();
+
+	/* There is no sensible default for either: libpcap's rpcapd looks
+	 * for cert.pem and key.pem in the current directory, which is not
+	 * something a daemon started as root should do.
+	 */
+	if (use_tls && (tls_certfile == NULL || tls_keyfile == NULL))
+		rte_exit(EXIT_FAILURE,
+			 "TLS needs both a certificate (-X) and a private key (-K)\n");
+
+	if (!use_tls && (tls_certfile != NULL || tls_keyfile != NULL))
+		rte_exit(EXIT_FAILURE,
+			 "A certificate or key was given without -S\n");
+
+	if (null_auth_ok && !is_loopback(&listen_addr))
+		RPCAPD_LOG(ERR,
+			"-n allows any client that can reach this port to capture traffic");
 }
 
 /*
@@ -548,6 +676,10 @@ main(int argc, char **argv)
 	if (rte_eth_dev_count_avail() == 0)
 		rte_exit(EXIT_FAILURE, "No Ethernet ports found\n");
 
+	/* Fail here rather than on the first client's handshake. */
+	if (use_tls && tls_init(tls_certfile, tls_keyfile) < 0)
+		rte_exit(EXIT_FAILURE, "TLS setup failed\n");
+
 	sigaction(SIGTERM, &action, NULL);
 	sigaction(SIGINT, &action, NULL);
 
diff --git a/app/rpcapd/meson.build b/app/rpcapd/meson.build
index 61f4dc0a95..42b9eec854 100644
--- a/app/rpcapd/meson.build
+++ b/app/rpcapd/meson.build
@@ -14,12 +14,27 @@ if not dpdk_conf.has('RTE_HAS_LIBPCAP')
     subdir_done()
 endif
 
+# password authentication uses crypt(3)
+libcrypt_dep = cc.find_library('crypt', required: false)
+if not libcrypt_dep.found()
+    build = false
+    reason = 'missing dependency, "libcrypt"'
+    subdir_done()
+endif
+
 sources = files(
         'capture.c',
         'filter.c',
         'main.c',
         'session.c',
         'sock.c',
+        'tls.c',
 )
 ext_deps += pcap_dep
+ext_deps += libcrypt_dep
 deps += ['ethdev', 'pdump', 'bpf', 'pcapng']
+
+# TLS support is optional; tls.c builds as stubs without it
+if dpdk_conf.has('RTE_HAS_OPENSSL')
+    ext_deps += openssl_dep
+endif
diff --git a/app/rpcapd/rpcap-protocol.h b/app/rpcapd/rpcap-protocol.h
index 438fd8dd84..7fc1ec5db5 100644
--- a/app/rpcapd/rpcap-protocol.h
+++ b/app/rpcapd/rpcap-protocol.h
@@ -42,7 +42,10 @@
 #define RPCAP_MSG_STATS_REPLY	     (RPCAP_MSG_STATS_REQ	 | RPCAP_MSG_IS_REPLY)
 
 /* Error codes carried in the 'value' field of RPCAP_MSG_ERROR */
+#define PCAP_ERR_AUTH               3	/* generic authentication error */
 #define PCAP_ERR_WRONGVER          17
+#define PCAP_ERR_AUTH_FAILED       18	/* credentials were not accepted */
+#define PCAP_ERR_TLS_REQUIRED      19	/* server will only speak TLS */
 #define PCAP_ERR_AUTH_TYPE_NOTSUP  20
 
 /* Authentication types in rpcap_auth.type */
diff --git a/app/rpcapd/rpcapd.h b/app/rpcapd/rpcapd.h
index df38231bfb..3eea5c08e2 100644
--- a/app/rpcapd/rpcapd.h
+++ b/app/rpcapd/rpcapd.h
@@ -21,6 +21,7 @@
 struct rte_bpf_prm;
 struct rte_mempool;
 struct rte_ring;
+struct ssl_st;
 
 #define RTE_LOGTYPE_RPCAPD RTE_LOGTYPE_USER1
 #define RPCAPD_LOG(level, ...) \
@@ -36,9 +37,10 @@ struct rte_ring;
  */
 #define MAX_CAPTURE_LEN		(DEFAULT_SNAPLEN + 2 * sizeof(struct rte_vlan_hdr))
 
-/* A connection to the client. */
+/* A connection to the client; ssl is NULL when not encrypted. */
 struct conn {
 	int fd;
+	struct ssl_st *ssl;
 };
 
 /* Per-client capture session state. */
@@ -50,6 +52,7 @@ struct session {
 	uint32_t snaplen;
 	uint32_t npkt;				/* packet sequence for rpcap_pkthdr */
 	uint32_t pdump_flags;			/* direction bits handed to pdump */
+	bool     authenticated;			/* AUTH_REQ has succeeded */
 	bool     opened;			/* OPEN_REQ has selected a port */
 	bool     capture_on;
 	bool     promisc_set;			/* we enabled promiscuous mode */
@@ -64,6 +67,8 @@ extern RTE_ATOMIC(bool) quit_signal;
 /* Command-line settings needed outside of main.c */
 extern uint32_t ring_size;
 extern uint32_t send_timeout;		/* seconds; 0 means no limit */
+extern bool use_tls;			/* -S: encrypt both connections */
+extern bool null_auth_ok;		/* -n: permit null auth off loopback */
 
 /* Address the control socket is bound to; the data socket uses the same
  * address with an ephemeral port.
@@ -71,6 +76,8 @@ extern uint32_t send_timeout;		/* seconds; 0 means no limit */
 extern struct sockaddr_storage listen_addr;
 extern socklen_t               listen_addrlen;
 
+bool is_loopback(const struct sockaddr_storage *ss);
+
 /* sock.c: transport and message framing */
 int wait_readable(const struct conn *c, int timeout_ms);
 int accept_timeout(int listen_fd, int timeout_ms);
@@ -85,8 +92,17 @@ int rpcap_discard(const struct conn *c, uint32_t plen);
 void set_sockaddr_port(struct sockaddr_storage *ss, uint16_t port);
 uint16_t get_sockaddr_port(const struct sockaddr_storage *ss);
 
+/* tls.c: TLS transport, stubbed out when built without OpenSSL */
+int tls_init(const char *certfile, const char *keyfile);
+int tls_accept(struct conn *c);
+void tls_close(struct conn *c);
+int tls_send(struct ssl_st *ssl, const void *buf, size_t len);
+int tls_recv(struct ssl_st *ssl, void *buf, size_t len);
+bool tls_pending(const struct conn *c);
+void tls_reject_handshake(int fd);
+
 /* session.c: control requests handled before a capture starts */
-int handle_auth(const struct conn *c, uint32_t plen);
+int handle_auth(const struct conn *c, uint32_t plen, struct session *s);
 int handle_findallif(const struct conn *c);
 int handle_open(const struct conn *c, uint32_t plen, struct session *s);
 
diff --git a/app/rpcapd/session.c b/app/rpcapd/session.c
index cc14f26335..61600cb286 100644
--- a/app/rpcapd/session.c
+++ b/app/rpcapd/session.c
@@ -5,8 +5,13 @@
  * the interface list, and selecting an interface.
  */
 
+#include <crypt.h>
+#include <errno.h>
+#include <pwd.h>
+#include <shadow.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 
 #include <rte_byteorder.h>
 #include <rte_ethdev.h>
@@ -14,6 +19,12 @@
 #include "rpcap-protocol.h"
 #include "rpcapd.h"
 
+/* Slow down a client working through a password list. */
+#define AUTH_FAIL_DELAY_SEC	1
+
+/* Bound what a client can make us allocate for credentials. */
+#define MAX_CREDENTIAL_LEN	256
+
 /* Build and send the list of available DPDK ports. */
 int
 handle_findallif(const struct conn *c)
@@ -67,37 +78,182 @@ handle_findallif(const struct conn *c)
 }
 
 /*
- * AUTH_REQ: check the authentication type only.
- *
- * There is no credential store, so a username and password cannot be
- * verified; refuse them rather than reply that they were accepted.
+ * Check credentials against the system password database, as libpcap's
+ * rpcapd does.  Privileges are not dropped afterwards, since that would
+ * break the capture, so this authenticates without authorising.
+ * Returns 0 if the credentials are good.
+ */
+static int
+check_password(const char *user, const char *password)
+{
+	const struct passwd *pw;
+	const struct spwd *sp;
+	const char *hash;
+	char *result;
+
+	pw = getpwnam(user);
+	if (pw == NULL) {
+		RPCAPD_LOG(NOTICE, "authentication failed: no such user");
+		return -1;
+	}
+
+	/* The password database only holds a placeholder when the real
+	 * hash lives in the shadow file.
+	 */
+	sp = getspnam(user);
+	hash = (sp != NULL) ? sp->sp_pwdp : pw->pw_passwd;
+
+	/* Not a hash: the account is locked ('!' or '*') or has no
+	 * password.  Either way there is nothing to check against.
+	 */
+	if (hash == NULL || *hash != '$') {
+		RPCAPD_LOG(NOTICE,
+			   "authentication failed: account has no usable password "
+			   "(is /etc/shadow readable?)");
+		return -1;
+	}
+
+	errno = 0;
+	result = crypt(password, hash);
+	if (result == NULL) {
+		RPCAPD_LOG(ERR, "crypt failed: %s",
+			   errno != 0 ? strerror(errno) : "unknown error");
+		return -1;
+	}
+
+	if (strcmp(result, hash) != 0) {
+		RPCAPD_LOG(NOTICE, "authentication failed: wrong password");
+		return -1;
+	}
+
+	return 0;
+}
+
+/*
+ * Wipe a credential before freeing it.  explicit_bzero() because the
+ * compiler may drop a memset() before free() as a dead store.
+ */
+static void
+free_credential(char *cred)
+{
+	if (cred != NULL) {
+		explicit_bzero(cred, strlen(cred));
+		free(cred);
+	}
+}
+
+/* Read a length-prefixed credential out of the AUTH_REQ payload. */
+static int
+recv_credential(const struct conn *c, uint32_t len, uint32_t *plen, char **out)
+{
+	char *buf;
+
+	if (len > *plen || len > MAX_CREDENTIAL_LEN)
+		return -1;
+
+	buf = malloc(len + 1);
+	if (buf == NULL)
+		return -1;
+
+	if (recv_full(c, buf, len) < 0) {
+		explicit_bzero(buf, len);
+		free(buf);
+		return -1;
+	}
+	buf[len] = '\0';
+	*plen -= len;
+	*out = buf;
+	return 0;
+}
+
+/*
+ * AUTH_REQ: null authentication is accepted from a loopback peer only;
+ * a remote client needs a username and password, unless -n was given.
  */
 int
-handle_auth(const struct conn *c, uint32_t plen)
+handle_auth(const struct conn *c, uint32_t plen, struct session *s)
 {
+	char *user = NULL, *password = NULL;
 	struct rpcap_auth auth;
 	uint16_t type;
+	int rc;
+
+	s->authenticated = false;
 
 	if (plen < sizeof(auth)) {
 		rpcap_discard(c, plen);
-		return rpcap_send_error(c, 0, "short authentication request");
+		return rpcap_send_error(c, PCAP_ERR_AUTH, "short authentication request");
 	}
 
 	if (recv_full(c, &auth, sizeof(auth)) < 0)
 		return -1;
-
-	/* Discard any username and password that followed. */
-	if (rpcap_discard(c, plen - sizeof(auth)) < 0)
-		return -1;
+	plen -= sizeof(auth);
 
 	type = rte_be_to_cpu_16(auth.type);
-	if (type != RPCAP_RMTAUTH_NULL) {
+	switch (type) {
+	case RPCAP_RMTAUTH_NULL:
+		if (rpcap_discard(c, plen) < 0)
+			return -1;
+
+		if (!is_loopback(&s->peer) && !null_auth_ok) {
+			RPCAPD_LOG(NOTICE,
+				   "rejecting null authentication from remote client");
+			return rpcap_send_error(c, PCAP_ERR_AUTH_FAILED,
+						"this server requires a username and "
+						"password for remote clients");
+		}
+		break;
+
+	case RPCAP_RMTAUTH_PWD:
+		if (recv_credential(c, rte_be_to_cpu_16(auth.slen1), &plen, &user) < 0 ||
+		    recv_credential(c, rte_be_to_cpu_16(auth.slen2), &plen, &password) < 0) {
+			free_credential(user);
+			return -1;
+		}
+
+		if (rpcap_discard(c, plen) < 0) {
+			free_credential(user);
+			free_credential(password);
+			return -1;
+		}
+
+		/* Refuse before checking, so a rejected password has not
+		 * already crossed the network in the clear.
+		 */
+		if (c->ssl == NULL && !is_loopback(&s->peer)) {
+			free_credential(user);
+			free_credential(password);
+			RPCAPD_LOG(NOTICE,
+				   "refusing password authentication on an unencrypted connection");
+			return rpcap_send_error(c, PCAP_ERR_AUTH_FAILED,
+						"this server will not accept a password "
+						"over an unencrypted connection; "
+						"use rpcaps://");
+		}
+
+		rc = check_password(user, password);
+		free_credential(user);
+		free_credential(password);
+
+		if (rc != 0) {
+			/* Delay a guess, and do not say which of the two
+			 * was wrong.
+			 */
+			sleep(AUTH_FAIL_DELAY_SEC);
+			return rpcap_send_error(c, PCAP_ERR_AUTH_FAILED,
+						"authentication failed");
+		}
+		break;
+
+	default:
+		if (rpcap_discard(c, plen) < 0)
+			return -1;
 		RPCAPD_LOG(NOTICE, "rejecting authentication type %u", type);
 		return rpcap_send_error(c, PCAP_ERR_AUTH_TYPE_NOTSUP,
-					"this server cannot check credentials; "
-					"connect without a username or password");
+					"authentication type not supported");
 	}
 
+	s->authenticated = true;
 	return rpcap_send_msg(c, RPCAP_MSG_AUTH_REPLY, 0, NULL, 0);
 }
 
diff --git a/app/rpcapd/sock.c b/app/rpcapd/sock.c
index 179c1a31c1..49c30c5370 100644
--- a/app/rpcapd/sock.c
+++ b/app/rpcapd/sock.c
@@ -18,6 +18,7 @@
 
 #include <rte_byteorder.h>
 #include <rte_common.h>
+#include <rte_mbuf.h>
 #include <rte_stdatomic.h>
 
 #include "rpcap-protocol.h"
@@ -59,6 +60,10 @@ wait_readable(const struct conn *c, int timeout_ms)
 {
 	struct pollfd pfd = { .fd = c->fd, .events = POLLIN };
 
+	/* Decrypted bytes buffered in the SSL object are invisible to poll() */
+	if (tls_pending(c))
+		return 1;
+
 	while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) {
 		int wait_ms = POLL_INTERVAL_MS;
 		int rc;
@@ -207,7 +212,11 @@ recv_full(const struct conn *c, void *buf, size_t len)
 		if (wait_readable(c, -1) != 1)
 			return -1;
 
-		n = recv(c->fd, p, len, 0);
+		if (c->ssl != NULL)
+			n = tls_recv(c->ssl, p, len);
+		else
+			n = recv(c->fd, p, len, 0);
+
 		if (n < 0 && errno == EINTR)
 			continue;
 
@@ -220,6 +229,44 @@ recv_full(const struct conn *c, void *buf, size_t len)
 	return 0;
 }
 
+/*
+ * No scatter/gather write in TLS, and SSL_write() gives each call its
+ * own record, so gather into one buffer rather than paying record
+ * overhead per piece.
+ */
+static int
+send_iov_tls(struct ssl_st *ssl, const struct iovec *iov, int iovcnt)
+{
+	uint8_t buf[sizeof(struct rpcap_header) + sizeof(struct rpcap_pkthdr) +
+		    MAX_CAPTURE_LEN];
+	const uint8_t *p = buf;
+	size_t len = 0;
+	int i;
+
+	for (i = 0; i < iovcnt; i++) {
+		if (len + iov[i].iov_len > sizeof(buf)) {
+			/* Cannot happen: buf is sized for both headers plus
+			 * MAX_CAPTURE_LEN.
+			 */
+			RPCAPD_LOG(ERR, "message too large for TLS buffer");
+			errno = EMSGSIZE;
+			return -1;
+		}
+		memcpy(buf + len, iov[i].iov_base, iov[i].iov_len);
+		len += iov[i].iov_len;
+	}
+
+	while (len > 0) {
+		int n = tls_send(ssl, p, len);
+
+		if (n <= 0)
+			return -1;
+		p += n;
+		len -= n;
+	}
+	return 0;
+}
+
 /*
  * Send all of iov, resending the remainder if sendmsg() reports a short
  * count (possible when the connection breaks or a signal arrives after
@@ -233,6 +280,9 @@ send_iov_full(const struct conn *c, struct iovec *iov, int iovcnt, int flags)
 		.msg_iovlen = iovcnt,
 	};
 
+	if (c->ssl != NULL)
+		return send_iov_tls(c->ssl, iov, iovcnt);
+
 	while (msg.msg_iovlen > 0) {
 		ssize_t n = sendmsg(c->fd, &msg, flags | MSG_NOSIGNAL);
 
diff --git a/app/rpcapd/tls.c b/app/rpcapd/tls.c
new file mode 100644
index 0000000000..f9671f63b4
--- /dev/null
+++ b/app/rpcapd/tls.c
@@ -0,0 +1,273 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ *
+ * TLS transport for the rpcaps:// scheme.  Both the control and the
+ * data connection are promoted.  Built as stubs when DPDK was
+ * configured without OpenSSL.
+ */
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <unistd.h>
+
+#include "rpcapd.h"
+
+/* How long a peer may take to complete a handshake. */
+#define TLS_HANDSHAKE_TIMEOUT_SEC	10
+
+#ifdef RTE_HAS_OPENSSL
+
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+
+static SSL_CTX *tls_ctx;
+
+static const char *
+tls_strerror(void)
+{
+	unsigned long e = ERR_get_error();
+
+	return (e != 0) ? ERR_reason_error_string(e) : "unknown error";
+}
+
+/*
+ * Build the server context at startup, so a bad certificate fails here
+ * rather than on the first client's handshake.
+ */
+int
+tls_init(const char *certfile, const char *keyfile)
+{
+	tls_ctx = SSL_CTX_new(TLS_server_method());
+	if (tls_ctx == NULL) {
+		RPCAPD_LOG(ERR, "cannot create TLS context: %s", tls_strerror());
+		return -1;
+	}
+
+	if (SSL_CTX_set_min_proto_version(tls_ctx, TLS1_2_VERSION) != 1) {
+		RPCAPD_LOG(ERR, "cannot set minimum TLS version: %s", tls_strerror());
+		return -1;
+	}
+
+	/* Hides a renegotiation from SSL_read()/SSL_write(). */
+	SSL_CTX_set_mode(tls_ctx, SSL_MODE_AUTO_RETRY);
+
+	if (SSL_CTX_use_certificate_chain_file(tls_ctx, certfile) != 1) {
+		RPCAPD_LOG(ERR, "cannot read certificate file '%s': %s",
+			   certfile, tls_strerror());
+		return -1;
+	}
+
+	if (SSL_CTX_use_PrivateKey_file(tls_ctx, keyfile, SSL_FILETYPE_PEM) != 1) {
+		RPCAPD_LOG(ERR, "cannot read private key file '%s': %s",
+			   keyfile, tls_strerror());
+		return -1;
+	}
+
+	if (SSL_CTX_check_private_key(tls_ctx) != 1) {
+		RPCAPD_LOG(ERR, "private key '%s' does not match certificate '%s'",
+			   keyfile, certfile);
+		return -1;
+	}
+
+	return 0;
+}
+
+/*
+ * SSL_accept() on a blocking socket waits indefinitely, and only one
+ * client is served at a time, so bound the handshake with socket
+ * timeouts.
+ */
+static int
+set_handshake_timeout(int fd, time_t seconds)
+{
+	struct timeval tv = { .tv_sec = seconds };
+
+	if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0 ||
+	    setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) {
+		RPCAPD_LOG(NOTICE, "cannot set TLS handshake timeout: %s",
+			   strerror(errno));
+		return -1;
+	}
+	return 0;
+}
+
+int
+tls_accept(struct conn *c)
+{
+	SSL *ssl = SSL_new(tls_ctx);
+	bool timed = set_handshake_timeout(c->fd, TLS_HANDSHAKE_TIMEOUT_SEC) == 0;
+
+	if (ssl == NULL) {
+		RPCAPD_LOG(ERR, "SSL_new: %s", tls_strerror());
+		return -1;
+	}
+
+	if (SSL_set_fd(ssl, c->fd) != 1) {
+		RPCAPD_LOG(ERR, "SSL_set_fd: %s", tls_strerror());
+		SSL_free(ssl);
+		return -1;
+	}
+
+	if (SSL_accept(ssl) != 1) {
+		/* A timeout surfaces as a syscall error on the read. */
+		if (errno == EAGAIN || errno == EWOULDBLOCK)
+			RPCAPD_LOG(ERR, "TLS handshake timed out after %u seconds",
+				   TLS_HANDSHAKE_TIMEOUT_SEC);
+		else
+			RPCAPD_LOG(ERR, "TLS handshake failed: %s", tls_strerror());
+		SSL_free(ssl);
+		return -1;
+	}
+
+	/* Back to blocking for the session. */
+	if (timed)
+		set_handshake_timeout(c->fd, 0);
+
+	RPCAPD_LOG(DEBUG, "TLS established: %s %s",
+		   SSL_get_version(ssl), SSL_get_cipher(ssl));
+	c->ssl = ssl;
+	return 0;
+}
+
+/* Send the close_notify alert so the client does not report a truncated
+ * stream.  The caller still owns the socket.
+ */
+void
+tls_close(struct conn *c)
+{
+	if (c->ssl == NULL)
+		return;
+
+	SSL_shutdown(c->ssl);
+	SSL_free(c->ssl);
+	c->ssl = NULL;
+}
+
+/*
+ * Map an SSL error onto the send()/recv() contract the callers expect:
+ * byte count on success, -1 with errno set on failure.
+ */
+static int
+tls_error(SSL *ssl, int ret, const char *what)
+{
+	int err = SSL_get_error(ssl, ret);
+
+	switch (err) {
+	case SSL_ERROR_ZERO_RETURN:
+		/* Clean shutdown by the peer: an orderly EOF. */
+		return 0;
+	case SSL_ERROR_SYSCALL:
+		/* errno is already set, unless the peer just vanished. */
+		if (errno == 0)
+			errno = ECONNRESET;
+		return -1;
+	case SSL_ERROR_WANT_READ:
+	case SSL_ERROR_WANT_WRITE:
+		errno = EAGAIN;
+		return -1;
+	default:
+		RPCAPD_LOG(DEBUG, "%s: %s", what, tls_strerror());
+		errno = EPROTO;
+		return -1;
+	}
+}
+
+int
+tls_send(struct ssl_st *ssl, const void *buf, size_t len)
+{
+	int ret = SSL_write(ssl, buf, len);
+
+	if (ret > 0)
+		return ret;
+	return tls_error(ssl, ret, "SSL_write");
+}
+
+int
+tls_recv(struct ssl_st *ssl, void *buf, size_t len)
+{
+	int ret = SSL_read(ssl, buf, len);
+
+	if (ret > 0)
+		return ret;
+	return tls_error(ssl, ret, "SSL_read");
+}
+
+/*
+ * One TLS record can hold several rpcap messages, and once read off the
+ * socket the rest sit in the SSL object where poll() cannot see them.
+ * Every wait must check this first.
+ */
+bool
+tls_pending(const struct conn *c)
+{
+	return c->ssl != NULL && SSL_pending(c->ssl) > 0;
+}
+
+#else /* !RTE_HAS_OPENSSL */
+
+int
+tls_init(const char *certfile __rte_unused, const char *keyfile __rte_unused)
+{
+	RPCAPD_LOG(ERR, "built without OpenSSL, TLS is not available");
+	return -1;
+}
+
+int
+tls_accept(struct conn *c __rte_unused)
+{
+	return -1;
+}
+
+void
+tls_close(struct conn *c __rte_unused)
+{
+}
+
+int
+tls_send(struct ssl_st *ssl __rte_unused, const void *buf __rte_unused,
+	 size_t len __rte_unused)
+{
+	errno = ENOTSUP;
+	return -1;
+}
+
+int
+tls_recv(struct ssl_st *ssl __rte_unused, void *buf __rte_unused,
+	 size_t len __rte_unused)
+{
+	errno = ENOTSUP;
+	return -1;
+}
+
+bool
+tls_pending(const struct conn *c __rte_unused)
+{
+	return false;
+}
+
+#endif /* RTE_HAS_OPENSSL */
+
+/*
+ * Turn away a handshake from a daemon without -S.  Written straight to
+ * the socket since there is no SSL context to generate it with.
+ */
+void
+tls_reject_handshake(int fd)
+{
+	static const uint8_t alert[] = {
+		21,	/* content type: alert */
+		3, 3,	/* legacy record version: TLS 1.2 */
+		0, 2,	/* payload length */
+		2,	/* level: fatal */
+		40,	/* description: handshake_failure */
+	};
+
+	RPCAPD_LOG(WARNING, "rejecting TLS handshake: server is not using TLS");
+	if (write(fd, alert, sizeof(alert)) != (ssize_t)sizeof(alert))
+		RPCAPD_LOG(DEBUG, "could not send TLS alert: %s", strerror(errno));
+}
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index 8e107b48b6..b1ecaa3574 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -147,6 +147,8 @@ New Features
 
   Added the ``dpdk-rpcapd`` application, which implements the rpcap
   protocol to allow live capture in tcpdump and Wireshark.
+  Remote clients can be authenticated with a system username and password,
+  and connections encrypted with TLS when built with OpenSSL.
 
 
 Removed Items
diff --git a/doc/guides/tools/rpcapd.rst b/doc/guides/tools/rpcapd.rst
index a8b026a409..f5d292682c 100644
--- a/doc/guides/tools/rpcapd.rst
+++ b/doc/guides/tools/rpcapd.rst
@@ -18,19 +18,21 @@ the libpcap project's ``rpcapd``.
 See
 https://github.com/the-tcpdump-group/libpcap/tree/master/rpcapd
 for the reference implementation.
-Clients connect to ``dpdk-rpcapd`` using a ``rpcap://`` URL,
+Clients connect to ``dpdk-rpcapd`` using a ``rpcap://`` URL, or
+``rpcaps://`` for a TLS-encrypted connection,
 request the list of available interfaces(which are the ports of the DPDK primary),
 open one, and stream packets from it.
 
 .. warning::
 
-   ``dpdk-rpcapd`` listens on an unauthenticated, unencrypted TCP port
-   (default 2002).  Anyone able to reach the port can list DPDK ports
-   and capture all traffic flowing through them.  The default bind
-   address is ``127.0.0.1``, so the listener is not reachable from
-   other hosts; overriding this with ``--bind`` exposes captured
-   traffic to anyone who can reach that address.  **Do not run
-   ``dpdk-rpcapd`` on a production system.**
+   Anyone who can authenticate to ``dpdk-rpcapd`` can capture all
+   traffic flowing through the ports of the DPDK primary process.  The
+   default bind address is ``127.0.0.1``, so the listener is not
+   reachable from other hosts.  A client on the loopback address may
+   connect without credentials; a client from any other address must
+   authenticate with a system username and password and must use TLS to
+   send it, unless ``-n`` was given.  See `Authentication`_ and `TLS`_.
+   **Do not run ``dpdk-rpcapd`` on a production system.**
 
 
 Running the Application
@@ -63,6 +65,27 @@ The application has a small set of command-line options:
     Size of the per-session capture ring in packets.  Default is 2048.
     Rounded up to a power of two if necessary.
 
+*   ``-S``, ``--tls``
+
+    Encrypt both the control and the data connection with TLS.  Clients
+    must then use a ``rpcaps://`` URL.  Requires ``-X`` and ``-K``.
+    Available only when DPDK was built with OpenSSL.
+
+*   ``-X <file>``, ``--cert <file>``
+
+    Server certificate chain in PEM format.  Only meaningful with
+    ``-S``, and required by it.
+
+*   ``-K <file>``, ``--key <file>``
+
+    Server private key in PEM format.  Required with ``-S``; there is
+    no default.
+
+*   ``-n``, ``--null-auth``
+
+    Permit null authentication from any address, not just loopback.
+    Usually used with ``-l``.
+
 *   ``-D``, ``--debug``
 
     Increase log verbosity.  A single ``-D`` adds informational
@@ -102,6 +125,64 @@ secondary process and does not need EAL options on its command line for
 typical use.
 
 
+Authentication
+--------------
+
+A client on the loopback address may connect without credentials, which
+is what a ``rpcap://`` URL with no userinfo does.
+
+A client from any other address must supply a system username and
+password, checked against the host password database as the reference
+``rpcapd`` does.  Accounts without a usable password hash, such as
+locked accounts, are refused.
+
+A password is only accepted over an encrypted connection, so remote
+password authentication requires ``-S`` as well.  A password sent in
+the clear is refused without being checked.
+
+Credentials are checked but no privileges are dropped, so this
+authenticates a client without authorising it: any account that can log
+in has the same access to every port of the primary process.
+
+``-n`` waives the check and lets any client connect unauthenticated,
+from any address.
+
+
+TLS
+---
+
+``-S`` encrypts both the control and the data connection, and is
+available only when DPDK was built with OpenSSL.
+
+TLS is not negotiated in the rpcap protocol: the client decides from
+its URL scheme and the daemon from ``-S``, so the two have to be
+configured to agree.  A mismatch is reported rather than left to fail
+as a protocol error.
+
+A certificate and key can be generated for testing with:
+
+.. code-block:: console
+
+    openssl req -x509 -newkey rsa:2048 -nodes -days 30 \
+        -keyout key.pem -out cert.pem -subj /CN=localhost
+
+Start the daemon with them:
+
+.. code-block:: console
+
+    sudo ./<build_dir>/app/dpdk-rpcapd -S -X cert.pem -K key.pem
+
+Then connect with a ``rpcaps://`` URL:
+
+.. code-block:: console
+
+    sudo /usr/local/sbin/tcpdump -i rpcaps://localhost:2002/net_tap0 -nn -c 20
+
+A client does not validate a self-signed certificate unless told to
+trust it, so the connection is encrypted but the server is not
+authenticated.
+
+
 Client Setup
 ------------
 
@@ -174,17 +255,17 @@ in this initial version:
     Subsequent clients are queued by the listening socket but not
     serviced until the first disconnects.
 
-*   **No authentication.** Password authentication is refused with
-    ``PCAP_ERR_AUTH_TYPE_NOTSUP``; clients must connect without
-    credentials, which is what a ``rpcap://`` URL with no userinfo does.
-    With the default loopback bind, reaching the port already requires
-    an account on the host.
-
-*   **No TLS.** The ``-S`` option of the reference ``rpcapd`` is not
-    implemented, so the connection is always in the clear.  This is
-    reasonable for the default loopback bind, where the traffic never
-    leaves the host, but means ``--bind`` to any other address sends
-    captured packets over the network unencrypted.
+*   **TLS needs OpenSSL.** ``-S`` is only available when DPDK was built
+    with OpenSSL support.
+
+*   **Authentication does not restrict access.** Credentials are
+    checked, but the daemon keeps the root privileges it needs for
+    ``pdump`` instead of dropping to the authenticated user, so every
+    account that can log in has the same access to every port.
+
+*   **No client certificates.** TLS authenticates the server to the
+    client and encrypts the connection; the client is identified only
+    by its password.
 
 *   **TCP data transport only.** A client requesting UDP is refused.
 
-- 
2.53.0


  parent reply	other threads:[~2026-10-01  2:59 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-08 21:07 [PATCH] examples/rpcapd: demo version of packet capture daemon Stephen Hemminger
2026-09-20 18:59 ` [PATCH v2] " Stephen Hemminger
2026-09-21  9:51   ` Marat Khalili
2026-09-21 15:57     ` Stephen Hemminger
2026-09-21 15:58     ` Stephen Hemminger
2026-09-21 16:43       ` Marat Khalili
2026-09-21 17:31         ` Stephen Hemminger
2026-09-21 17:53           ` Marat Khalili
2026-09-21 16:17     ` Stephen Hemminger
2026-09-22 18:45   ` Stephen Hemminger
2026-09-22 21:31 ` [PATCH v3] " Stephen Hemminger
2026-09-28 16:18   ` Marat Khalili
2026-09-28 17:24     ` Stephen Hemminger
2026-10-01  2:35 ` [PATCH v4 0/4] add rpcap remote " Stephen Hemminger
2026-10-01  2:35   ` [PATCH v4 1/4] pcapng: add API to read back capture mbuf header Stephen Hemminger
2026-10-01  2:35   ` [PATCH v4 2/4] app/rpcapd: remote pcap daemon Stephen Hemminger
2026-10-01 18:50     ` Marat Khalili
2026-10-01  2:35   ` Stephen Hemminger [this message]
2026-10-01  2:35   ` [PATCH v4 4/4] app/rpcapd: add host list option Stephen Hemminger
2026-10-01 18:50   ` [PATCH v4 0/4] add rpcap remote capture daemon Marat Khalili
2026-10-01 23:00     ` Stephen Hemminger

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261001025853.319860-4-stephen@networkplumber.org \
    --to=stephen@networkplumber.org \
    --cc=dev@dpdk.org \
    --cc=reshma.pattan@intel.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox