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
next prev 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