diff --git a/README b/README index 9392ef5..a35e7ef 100644 --- a/README +++ b/README @@ -35,6 +35,10 @@ following libraries to be installed: * libnl3 * libyaml +The Linux UAPI headers must be v5.10 or later. tlshd reads the +kernel's generic netlink attribute policy to detect optional +handshake features, and the definitions for that arrived in v5.10. + ## Installation See [NEWS](NEWS) to see what has changed in the latest release, diff --git a/README.md b/README.md index 9392ef5..a35e7ef 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ following libraries to be installed: * libnl3 * libyaml +The Linux UAPI headers must be v5.10 or later. tlshd reads the +kernel's generic netlink attribute policy to detect optional +handshake features, and the definitions for that arrived in v5.10. + ## Installation See [NEWS](NEWS) to see what has changed in the latest release, diff --git a/configure.ac b/configure.ac index a17cef8..7db41bc 100644 --- a/configure.ac +++ b/configure.ac @@ -136,6 +136,17 @@ if test "x$have_tls_tx_max_payload_len" = xyes ; then AC_DEFINE([HAVE_TLS_TX_MAX_PAYLOAD_LEN], [1], [Define to 1 if linux/tls.h defines TLS_TX_MAX_PAYLOAD_LEN]) fi +AC_MSG_CHECKING(for CTRL_ATTR_OP_POLICY in linux/genetlink.h) +AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[ #include ]], + [[ (void) CTRL_ATTR_OP_POLICY; ]])], + [ have_ctrl_attr_op_policy=yes ], + [ have_ctrl_attr_op_policy=no ]) +AC_MSG_RESULT([$have_ctrl_attr_op_policy]) +if test "x$have_ctrl_attr_op_policy" = xno ; then + AC_MSG_ERROR([Linux UAPI headers v5.10 or later are required]) +fi + AC_SUBST([AM_CPPFLAGS]) AC_CONFIG_FILES([Makefile \ diff --git a/src/tlshd/client.c b/src/tlshd/client.c index 4ed30ca..30bfa57 100644 --- a/src/tlshd/client.c +++ b/src/tlshd/client.c @@ -96,6 +96,16 @@ static void tlshd_tls13_client_anon_handshake(struct tlshd_handshake_parms *parm unsigned int flags; int ret; + /* + * Without a peer name, GnuTLS verifies the certificate chain + * but not who presented it. session_status is already EIO, so + * the early return fails the kernel's request. + */ + if (!parms->peername) { + tlshd_log_error("No peer name: cannot verify the server's identity"); + return; + } + ret = gnutls_certificate_allocate_credentials(&xcred); if (ret != GNUTLS_E_SUCCESS) { tlshd_log_gnutls_error(ret); @@ -417,6 +427,11 @@ static void tlshd_tls13_client_x509_handshake(struct tlshd_handshake_parms *parm unsigned int flags; int ret; + if (!parms->peername) { + tlshd_log_error("No peer name: cannot verify the server's identity"); + return; + } + ret = gnutls_certificate_allocate_credentials(&xcred); if (ret != GNUTLS_E_SUCCESS) { tlshd_log_gnutls_error(ret); @@ -646,6 +661,11 @@ static void tlshd_quic_client_set_x509_session(struct tlshd_quic_conn *conn) gnutls_session_t session; int ret; + if (!parms->peername) { + tlshd_log_error("No peer name: cannot verify the server's identity"); + return; + } + if (conn->cert_req != TLSHD_QUIC_NO_CERT_AUTH) { if (!tlshd_x509_client_get_certs(parms) || !tlshd_x509_client_get_privkey(parms)) { tlshd_log_error("Failed to get cert or privkey"); @@ -683,12 +703,10 @@ static void tlshd_quic_client_set_x509_session(struct tlshd_quic_conn *conn) ret = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, cred); if (ret) goto err_session; - if (parms->peername) { - ret = gnutls_server_name_set(session, GNUTLS_NAME_DNS, - parms->peername, strlen(parms->peername)); - if (ret) - goto err_session; - } + ret = gnutls_server_name_set(session, GNUTLS_NAME_DNS, + parms->peername, strlen(parms->peername)); + if (ret) + goto err_session; conn->session = session; return; diff --git a/src/tlshd/netlink.c b/src/tlshd/netlink.c index 0f8de30..52984c5 100644 --- a/src/tlshd/netlink.c +++ b/src/tlshd/netlink.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -213,97 +214,191 @@ static void tlshd_genl_sock_close(struct nl_sock *nls) } /** - * @brief Probe whether the kernel supports a specific netlink attribute - * @param[in] nls Netlink socket - * @param[in] cmd Netlink command (e.g., HANDSHAKE_CMD_DONE) - * @param[in] attr_type Attribute type to test - * - * Sends a test message with the specified attribute and minimal - * required fields. The kernel rejects the message for having invalid - * required fields, but this determines whether it parsed the optional - * attribute without error. - * - * @retval true Kernel accepts this attribute type - * @retval false Kernel rejected the attribute as unsupported + * @struct tlshd_op_policy + * @brief The attribute types a kernel accepts for one netlink command */ -static bool tlshd_probe_attr(struct nl_sock *nls, int cmd, int attr_type) +struct tlshd_op_policy { + int cmd; /**< Command being probed */ + uint32_t policy_id; /**< Policy index the kernel assigned */ + bool have_id; /**< policy_id has been read */ + uint32_t attrs; /**< Accepted attribute types */ +}; + +/** + * @def TLSHD_OP_POLICY_ATTRS + * Number of attribute types that fit in tlshd_op_policy::attrs + */ +#define TLSHD_OP_POLICY_ATTRS (32) + +/* + * A DONE attribute numbered past the width of that bitmask gives this + * typedef a negative size. The build then fails where the attribute is + * added, rather than the daemon quietly reporting it unsupported. + */ +typedef char tlshd_done_attrs_fit + [HANDSHAKE_A_DONE_MAX < TLSHD_OP_POLICY_ATTRS ? 1 : -1]; + +/** + * @var struct nla_policy tlshd_ctrl_op_policy + * Netlink policy for the per-command nests in CTRL_ATTR_OP_POLICY + */ +#if LIBNL_VER_NUM >= LIBNL_VER(3,5) +static const struct nla_policy +#else +static struct nla_policy +#endif +tlshd_ctrl_op_policy[CTRL_ATTR_POLICY_MAX + 1] = { + [CTRL_ATTR_POLICY_DO] = { .type = NLA_U32, }, + [CTRL_ATTR_POLICY_DUMP] = { .type = NLA_U32, }, +}; + +/** + * @brief Collect one command's attribute policy from a dump message + * @param[in] msg Message to be processed + * @param[in,out] arg struct tlshd_op_policy to be filled in + * + * The kernel reports a policy in two parts. A CTRL_ATTR_OP_POLICY + * message maps the command to a policy index, and the CTRL_ATTR_POLICY + * messages that follow carry one attribute apiece, nested under that + * index. An attribute the policy rejects is left out of the dump, so + * the presence of a nest is the answer this probe wants. + * + * @retval NL_SKIP Skip this message. + */ +static int tlshd_policy_valid_handler(struct nl_msg *msg, void *arg) +{ + struct tlshd_op_policy *policy = arg; + struct nlattr *tb[CTRL_ATTR_MAX + 1]; + struct nlattr *pol, *attr; + int rem, rem2; + + if (genlmsg_parse(nlmsg_hdr(msg), 0, tb, CTRL_ATTR_MAX, NULL) < 0) + return NL_SKIP; + + if (tb[CTRL_ATTR_OP_POLICY]) { + nla_for_each_nested(pol, tb[CTRL_ATTR_OP_POLICY], rem) { + struct nlattr *op[CTRL_ATTR_POLICY_MAX + 1]; + + if (nla_type(pol) != policy->cmd) + continue; + if (nla_parse_nested(op, CTRL_ATTR_POLICY_MAX, pol, + tlshd_ctrl_op_policy) < 0) + continue; + if (op[CTRL_ATTR_POLICY_DO]) { + policy->policy_id = + nla_get_u32(op[CTRL_ATTR_POLICY_DO]); + policy->have_id = true; + } + } + } + + if (tb[CTRL_ATTR_POLICY] && policy->have_id) { + nla_for_each_nested(pol, tb[CTRL_ATTR_POLICY], rem) { + if ((uint32_t)nla_type(pol) != policy->policy_id) + continue; + nla_for_each_nested(attr, pol, rem2) { + int type = nla_type(attr); + + if (type > 0 && type < TLSHD_OP_POLICY_ATTRS) + policy->attrs |= 1U << type; + } + } + } + + return NL_SKIP; +} + +/** + * @brief Retrieve the attribute policy the kernel applies to a command + * @param[in] cmd Netlink command (e.g., HANDSHAKE_CMD_DONE) + * @param[out] policy Filled in with the accepted attribute types + * + * @retval true The kernel reported a policy for this command + * @retval false No policy could be retrieved + */ +static bool tlshd_get_op_policy(int cmd, struct tlshd_op_policy *policy) { + struct nl_sock *nls; struct nl_msg *msg; - int family_id, err; - bool supported; + bool ret = false; + int err; - family_id = genl_ctrl_resolve(nls, HANDSHAKE_FAMILY_NAME); - if (family_id < 0) - return false; + memset(policy, 0, sizeof(*policy)); + policy->cmd = cmd; - msg = nlmsg_alloc(); - if (!msg) + if (tlshd_genl_sock_open(&nls)) return false; - genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, family_id, 0, - NLM_F_REQUEST, cmd, HANDSHAKE_FAMILY_VERSION); + nl_socket_modify_cb(nls, NL_CB_VALID, NL_CB_CUSTOM, + tlshd_policy_valid_handler, policy); - switch (cmd) { - case HANDSHAKE_CMD_DONE: - nla_put_u32(msg, HANDSHAKE_A_DONE_STATUS, 0); - nla_put_u32(msg, HANDSHAKE_A_DONE_SOCKFD, -1); - break; - default: - nlmsg_free(msg); - return false; + msg = nlmsg_alloc(); + if (!msg) { + tlshd_log_error("Failed to allocate message buffer."); + goto out_close; } - switch (attr_type) { - case HANDSHAKE_A_DONE_TAG: - nla_put_string(msg, attr_type, "__probe__"); - break; - case HANDSHAKE_A_DONE_REMOTE_AUTH: - nla_put_s32(msg, attr_type, 0); - break; - default: - tlshd_log_error("Attribute %d not supported", attr_type); - nlmsg_free(msg); - return false; + if (!genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, GENL_ID_CTRL, 0, + NLM_F_DUMP, CTRL_CMD_GETPOLICY, 1)) { + tlshd_log_error("Failed to set up message header."); + goto out_msgfree; + } + + err = nla_put_string(msg, CTRL_ATTR_FAMILY_NAME, + HANDSHAKE_FAMILY_NAME); + if (err < 0) { + tlshd_log_nl_error("nla_put family name", err); + goto out_msgfree; + } + err = nla_put_u32(msg, CTRL_ATTR_OP, cmd); + if (err < 0) { + tlshd_log_nl_error("nla_put op", err); + goto out_msgfree; } err = nl_send_auto(nls, msg); - nlmsg_free(msg); + if (err < 0) { + tlshd_log_nl_error("nl_send_auto", err); + goto out_msgfree; + } + + err = nl_recvmsgs_default(nls); + if (err < 0) { + tlshd_log_nl_error("CTRL_CMD_GETPOLICY", err); + goto out_msgfree; + } + + ret = policy->have_id; - /* - * nl_send_auto() returns the number of bytes sent on success, - * or a negative error code on failure. Treat any failure as - * the attribute being unsupported; a positive return indicates - * the kernel accepted the message containing this attribute. - */ - supported = (err >= 0); - /* Drain kernel response to prevent stale data on socket reuse */ - nl_recvmsgs_default(nls); - - return supported; +out_msgfree: + nlmsg_free(msg); +out_close: + tlshd_genl_sock_close(nls); + return ret; } /** * @brief Detect which optional netlink attributes the kernel supports - * @param[in] nls Netlink socket * - * Probes the kernel to determine which optional handshake netlink - * attributes are supported. Results are cached in tlshd_kernel_caps - * for use throughout the daemon lifetime. Unsupported attributes are - * not included in subsequent netlink messages to avoid rejection. + * Reads the kernel's attribute policy for HANDSHAKE_CMD_DONE. Results + * are cached in tlshd_kernel_caps for use throughout the daemon + * lifetime. Unsupported attributes are not included in subsequent + * netlink messages to avoid rejection. A kernel that reports no policy + * leaves every capability off, which is the conservative choice. * - * This function should be called once during initialization, after - * connecting to the handshake netlink family but before processing - * any handshake requests. + * This function should be called once during initialization, before + * processing any handshake requests. */ -static void tlshd_detect_kernel_caps(struct nl_sock *nls) +static void tlshd_detect_kernel_caps(void) { - tlshd_kernel_caps.done_tag = - tlshd_probe_attr(nls, HANDSHAKE_CMD_DONE, - HANDSHAKE_A_DONE_TAG); + struct tlshd_op_policy policy; - tlshd_kernel_caps.done_remote_auth = - tlshd_probe_attr(nls, HANDSHAKE_CMD_DONE, - HANDSHAKE_A_DONE_REMOTE_AUTH); + if (tlshd_get_op_policy(HANDSHAKE_CMD_DONE, &policy)) { + tlshd_kernel_caps.done_tag = + policy.attrs & (1U << HANDSHAKE_A_DONE_TAG); + tlshd_kernel_caps.done_remote_auth = + policy.attrs & (1U << HANDSHAKE_A_DONE_REMOTE_AUTH); + } tlshd_log_notice("Kernel capabilities: " "session_tags=%s remote_peerids=%s", @@ -355,9 +450,7 @@ static int tlshd_sig_poll_fd; * @param[in] msg A netlink event to be handled * @param[in] arg Additional arguments * - * @retval NL_OK Proceed with the next message * @retval NL_SKIP Skip this message. - * @retval NL_STOP Stop and discard remaining messages. */ static int tlshd_genl_event_handler(struct nl_msg *msg, __attribute__ ((unused)) void *arg) @@ -432,7 +525,7 @@ void tlshd_genl_dispatch(void) } /* Detect which optional netlink attributes the kernel supports */ - tlshd_detect_kernel_caps(tlshd_notification_nls); + tlshd_detect_kernel_caps(); if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) { tlshd_log_perror("signal"); @@ -574,9 +667,11 @@ static void tlshd_parse_certificate(struct tlshd_handshake_parms *parms, * @param[in] msg Message to be processed * @param[out] arg Handshake parms to be filled in * - * @retval NL_OK Proceed with the next message + * libnl reports NL_STOP to its caller as success, so a failure to + * parse the message has to return a negative libnl error code. + * * @retval NL_SKIP Skip this message. - * @retval NL_STOP Stop and discard remaining messages. + * @retval <0 Negative libnl error code */ static int tlshd_genl_valid_handler(struct nl_msg *msg, void *arg) { @@ -594,7 +689,7 @@ static int tlshd_genl_valid_handler(struct nl_msg *msg, void *arg) tlshd_accept_nl_policy); if (err < 0) { tlshd_log_nl_error("genlmsg_parse", err); - return NL_STOP; + return -NLE_FAILURE; } if (tb[HANDSHAKE_A_ACCEPT_SOCKFD]) { @@ -607,13 +702,13 @@ static int tlshd_genl_valid_handler(struct nl_msg *msg, void *arg) sap = (struct sockaddr *)&addr; if (getpeername(parms->sockfd, sap, &salen) == -1) { tlshd_log_perror("getpeername"); - return NL_STOP; + return -NLE_FAILURE; } err = getnameinfo(sap, salen, buf, sizeof(buf), NULL, 0, NI_NUMERICHOST); if (err) { tlshd_log_gai_error(err); - return NL_STOP; + return -NLE_FAILURE; } parms->peeraddr = strdup(buf); @@ -621,7 +716,7 @@ static int tlshd_genl_valid_handler(struct nl_msg *msg, void *arg) if (getsockopt(parms->sockfd, SOL_SOCKET, SO_PROTOCOL, &proto, &optlen) == -1) { tlshd_log_perror("getsockopt (SO_PROTOCOL)"); - return NL_STOP; + return -NLE_FAILURE; } parms->ip_proto = proto; } @@ -652,13 +747,13 @@ static int tlshd_genl_valid_handler(struct nl_msg *msg, void *arg) else if (sap) { char buf[NI_MAXHOST]; + /* A peer name is optional: leave it unset and proceed. */ err = getnameinfo(sap, salen, buf, sizeof(buf), NULL, 0, NI_NAMEREQD); - if (err) { + if (err) tlshd_log_gai_error(err); - return NL_STOP; - } - parms->peername = strdup(buf); + else + parms->peername = strdup(buf); } return NL_SKIP;