Support remote packet capture over TLS. This requires OpenSSL to be available.
Signed-off-by: Stephen Hemminger <[email protected]> --- 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

