diff --git a/include/net/handshake.h b/include/net/handshake.h
new file mode 100644
index 000000000000..ca401c08c541
--- /dev/null
+++ b/include/net/handshake.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Generic HANDSHAKE service.
+ *
+ * Author: Chuck Lever <chuck.lever@oracle.com>
+ *
+ * Copyright (c) 2023, Oracle and/or its affiliates.
+ */
+
+/*
+ * Data structures and functions that are visible only within the
+ * kernel are declared here.
+ */
+
+#ifndef _NET_HANDSHAKE_H
+#define _NET_HANDSHAKE_H
+
+struct handshake_req;
+
+/*
+ * Invariants for all handshake requests for one transport layer
+ * security protocol
+ */
+struct handshake_proto {
+	int			hp_protocol;
+	int			hp_mcgrp;
+	size_t			hp_privsize;
+
+	int			(*hp_accept)(struct handshake_req *req,
+					     struct genl_info *gi, int fd);
+	void			(*hp_done)(struct handshake_req *req,
+					   int status, struct nlattr *args);
+	void			(*hp_destroy)(struct handshake_req *req);
+};
+
+extern struct handshake_req *
+handshake_req_alloc(struct socket *sock, const struct handshake_proto *proto,
+		    gfp_t flags);
+extern void *handshake_req_private(struct handshake_req *req);
+extern int handshake_req_submit(struct handshake_req *req, gfp_t flags);
+extern void handshake_req_cancel(struct socket *sock);
+
+extern struct nlmsghdr *handshake_genl_put(struct sk_buff *msg,
+					   struct genl_info *gi);
+
+#endif /* _NET_HANDSHAKE_H */
diff --git a/include/net/net_namespace.h b/include/net/net_namespace.h
index 8c3587d5c308..a66309789560 100644
--- a/include/net/net_namespace.h
+++ b/include/net/net_namespace.h
@@ -186,6 +186,11 @@ struct net {
 #if IS_ENABLED(CONFIG_SMC)
 	struct netns_smc	smc;
 #endif
+
+	/* transport layer security handshake requests */
+	spinlock_t		hs_lock;
+	struct list_head	hs_requests;
+	int			hs_pending;
 } __randomize_layout;
 
 #include <linux/seq_file_net.h>
diff --git a/include/net/sock.h b/include/net/sock.h
index e0517ecc6531..e16e63ff61f2 100644
--- a/include/net/sock.h
+++ b/include/net/sock.h
@@ -515,6 +515,7 @@ struct sock {
 
 	struct socket		*sk_socket;
 	void			*sk_user_data;
+	void			*sk_handshake_req;
 #ifdef CONFIG_SECURITY
 	void			*sk_security;
 #endif
diff --git a/include/uapi/linux/handshake.h b/include/uapi/linux/handshake.h
new file mode 100644
index 000000000000..9544edeb181f
--- /dev/null
+++ b/include/uapi/linux/handshake.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * GENL HANDSHAKE service.
+ *
+ * Author: Chuck Lever <chuck.lever@oracle.com>
+ *
+ * Copyright (c) 2023, Oracle and/or its affiliates.
+ */
+
+/*
+ * Data structures and functions that are visible to user space are
+ * declared here. This file constitutes an API contract between the
+ * Linux kernel and user space.
+ */
+
+#ifndef _UAPI_LINUX_HANDSHAKE_H
+#define _UAPI_LINUX_HANDSHAKE_H
+
+#define HANDSHAKE_GENL_NAME	"handshake"
+#define HANDSHAKE_GENL_VERSION	0x01
+
+enum handshake_genl_mcgrps {
+	HANDSHAKE_GENL_MCGRP_NONE = 0,
+};
+
+#define HANDSHAKE_GENL_MCGRP_NONE_NAME	"none"
+
+enum handshake_genl_cmds {
+	HANDSHAKE_GENL_CMD_UNSPEC = 0,
+	HANDSHAKE_GENL_CMD_READY,
+	HANDSHAKE_GENL_CMD_ACCEPT,
+	HANDSHAKE_GENL_CMD_DONE,
+
+	__HANDSHAKE_GENL_CMD_MAX
+};
+#define HANDSHAKE_GENL_CMD_MAX	(__HANDSHAKE_GENL_CMD_MAX - 1)
+
+enum handshake_genl_attrs {
+	HANDSHAKE_GENL_ATTR_UNSPEC = 0,
+	HANDSHAKE_GENL_ATTR_MSG_STATUS,
+	HANDSHAKE_GENL_ATTR_SESS_STATUS,
+	HANDSHAKE_GENL_ATTR_SOCKFD,
+	HANDSHAKE_GENL_ATTR_PROTOCOL,
+
+	HANDSHAKE_GENL_ATTR_ACCEPT,
+	HANDSHAKE_GENL_ATTR_DONE,
+
+	__HANDSHAKE_GENL_ATTR_MAX
+};
+#define HANDSHAKE_GENL_ATTR_MAX	(__HANDSHAKE_GENL_ATTR_MAX - 1)
+
+enum handshake_genl_protocol {
+	HANDSHAKE_GENL_PROTO_UNSPEC = 0,
+};
+
+#endif /* _UAPI_LINUX_HANDSHAKE_H */
diff --git a/net/Makefile b/net/Makefile
index 6a62e5b27378..c1bb53f00486 100644
--- a/net/Makefile
+++ b/net/Makefile
@@ -78,3 +78,4 @@ obj-$(CONFIG_NET_NCSI)		+= ncsi/
 obj-$(CONFIG_XDP_SOCKETS)	+= xdp/
 obj-$(CONFIG_MPTCP)		+= mptcp/
 obj-$(CONFIG_MCTP)		+= mctp/
+obj-y				+= handshake/
diff --git a/net/handshake/Makefile b/net/handshake/Makefile
new file mode 100644
index 000000000000..824e08c626af
--- /dev/null
+++ b/net/handshake/Makefile
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for the Generic HANDSHAKE service
+#
+# Author: Chuck Lever <chuck.lever@oracle.com>
+#
+# Copyright (c) 2023, Oracle and/or its affiliates.
+#
+
+obj-y += handshake.o
+handshake-y := netlink.o request.o
diff --git a/net/handshake/handshake.h b/net/handshake/handshake.h
new file mode 100644
index 000000000000..1cbcfc632a24
--- /dev/null
+++ b/net/handshake/handshake.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Generic netlink handshake service
+ *
+ * Author: Chuck Lever <chuck.lever@oracle.com>
+ *
+ * Copyright (c) 2023, Oracle and/or its affiliates.
+ */
+
+/*
+ * Data structures and functions that are visible only within the
+ * handshake module are declared here.
+ */
+
+#ifndef _INTERNAL_HANDSHAKE_H
+#define _INTERNAL_HANDSHAKE_H
+
+/*
+ * One handshake request
+ */
+struct handshake_req {
+	refcount_t			hr_ref;
+	struct list_head		hr_list;
+	unsigned long			hr_flags;
+	const struct handshake_proto	*hr_proto;
+	struct socket			*hr_sock;
+	int				hr_fd;
+};
+
+#define HANDSHAKE_F_COMPLETED	BIT(0)
+
+int handshake_genl_notify(struct net *net, struct handshake_req *req,
+			  gfp_t flags);
+void handshake_complete(struct handshake_req *req, int status,
+			struct nlattr *args);
+
+struct handshake_req *handshake_req_get(struct handshake_req *req);
+void handshake_req_put(struct handshake_req *req);
+
+void add_pending_locked(struct net *net, struct handshake_req *req);
+bool handshake_remove_pending(struct net *net, struct handshake_req *req);
+
+#endif /* _INTERNAL_HANDSHAKE_H */
diff --git a/net/handshake/netlink.c b/net/handshake/netlink.c
new file mode 100644
index 000000000000..8d0bf11396a7
--- /dev/null
+++ b/net/handshake/netlink.c
@@ -0,0 +1,370 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Generic netlink handshake service
+ *
+ * Author: Chuck Lever <chuck.lever@oracle.com>
+ *
+ * Copyright (c) 2023, Oracle and/or its affiliates.
+ */
+
+#include <linux/types.h>
+#include <linux/socket.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/skbuff.h>
+#include <linux/inet.h>
+
+#include <net/sock.h>
+#include <net/genetlink.h>
+#include <net/handshake.h>
+
+#include <uapi/linux/handshake.h>
+#include "handshake.h"
+
+static struct genl_family __ro_after_init handshake_genl_family;
+
+void add_pending_locked(struct net *net, struct handshake_req *req)
+{
+	net->hs_pending++;
+	list_add_tail(&req->hr_list, &net->hs_requests);
+}
+
+static void remove_pending_locked(struct net *net, struct handshake_req *req)
+{
+	net->hs_pending--;
+	list_del_init(&req->hr_list);
+}
+
+/*
+ * Returns true if this req was on the pending list.
+ */
+bool handshake_remove_pending(struct net *net, struct handshake_req *req)
+{
+	struct sock *sk = req->hr_sock->sk;
+	bool ret;
+
+	ret = false;
+
+	spin_lock(&net->hs_lock);
+	if (!list_empty(&req->hr_list)) {
+		remove_pending_locked(net, req);
+		ret = true;
+	}
+	sk->sk_handshake_req = NULL;
+	spin_unlock(&net->hs_lock);
+
+	return ret;
+}
+
+void handshake_complete(struct handshake_req *req, int status,
+			struct nlattr *args)
+{
+	if (!test_and_set_bit(HANDSHAKE_F_COMPLETED, &req->hr_flags)) {
+		req->hr_proto->hp_done(req, status, args);
+		req->hr_sock->sk->sk_handshake_req = NULL;
+	}
+	handshake_req_put(req);
+}
+
+int handshake_genl_notify(struct net *net, struct handshake_req *req,
+			  gfp_t flags)
+{
+	struct sk_buff *skb;
+	void *hdr;
+
+	if (!genl_has_listeners(&handshake_genl_family, net,
+				req->hr_proto->hp_mcgrp))
+		return -ESRCH;
+
+	skb = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL);
+	if (!skb)
+		return -ENOMEM;
+
+	hdr = genlmsg_put(skb, 0, 0, &handshake_genl_family, 0,
+			  HANDSHAKE_GENL_CMD_READY);
+	if (!hdr) {
+		nlmsg_free(skb);
+		return -EMSGSIZE;
+	}
+
+	genlmsg_end(skb, hdr);
+	return genlmsg_multicast(&handshake_genl_family, skb, 0,
+				 req->hr_proto->hp_mcgrp, flags);
+}
+
+static int handshake_accept(struct handshake_req *req)
+{
+	struct socket *sock = req->hr_sock;
+	int flags = O_CLOEXEC;
+	struct file *file;
+	int fd;
+
+	fd = get_unused_fd_flags(flags);
+	if (fd < 0)
+		return fd;
+	file = sock_alloc_file(sock, flags, sock->sk->sk_prot_creator->name);
+	if (IS_ERR(file)) {
+		put_unused_fd(fd);
+		return PTR_ERR(file);
+	}
+
+	req->hr_fd = fd;
+	fd_install(fd, file);
+	return 0;
+}
+
+static const struct nla_policy
+handshake_genl_policy[HANDSHAKE_GENL_ATTR_MAX + 1] = {
+	[HANDSHAKE_GENL_ATTR_MSG_STATUS] = {
+		.type = NLA_U32
+	},
+	[HANDSHAKE_GENL_ATTR_SESS_STATUS] = {
+		.type = NLA_U32
+	},
+	[HANDSHAKE_GENL_ATTR_SOCKFD] = {
+		.type = NLA_U32
+	},
+	[HANDSHAKE_GENL_ATTR_PROTOCOL] = {
+		.type = NLA_U32
+	},
+
+	[HANDSHAKE_GENL_ATTR_ACCEPT] = {
+		.type = NLA_NESTED,
+	},
+	[HANDSHAKE_GENL_ATTR_DONE] = {
+		.type = NLA_NESTED,
+	},
+};
+
+/**
+ * handshake_genl_put - Create a generic netlink message header
+ * @msg: buffer in which to create the header
+ * @gi: generic netlink message context
+ *
+ * Returns a ready-to-use header, or NULL.
+ */
+struct nlmsghdr *handshake_genl_put(struct sk_buff *msg, struct genl_info *gi)
+{
+	return genlmsg_put(msg, gi->snd_portid, gi->snd_seq,
+			   &handshake_genl_family, 0, gi->genlhdr->cmd);
+}
+EXPORT_SYMBOL(handshake_genl_put);
+
+static int handshake_genl_status_reply(struct sk_buff *skb,
+				       struct genl_info *gi, int status)
+{
+	struct nlmsghdr *hdr;
+	struct sk_buff *msg;
+	int ret;
+
+	ret = -ENOMEM;
+	msg = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL);
+	if (!msg)
+		goto out;
+	hdr = handshake_genl_put(msg, gi);
+	if (!hdr)
+		goto out_free;
+
+	ret = -EMSGSIZE;
+	ret = nla_put_u32(msg, HANDSHAKE_GENL_ATTR_MSG_STATUS, status);
+	if (ret < 0)
+		goto out_free;
+
+	genlmsg_end(msg, hdr);
+	return genlmsg_reply(msg, gi);
+
+out_free:
+	genlmsg_cancel(msg, hdr);
+out:
+	return ret;
+}
+
+static int handshake_genl_cmd_accept(struct sk_buff *skb, struct genl_info *gi)
+{
+	struct nlattr *tb[HANDSHAKE_GENL_ATTR_MAX + 1];
+	struct net *net = sock_net(skb->sk);
+	struct handshake_req *pos, *req;
+	int err;
+
+	err = genlmsg_parse(nlmsg_hdr(skb), &handshake_genl_family, tb,
+			    HANDSHAKE_GENL_ATTR_MAX, handshake_genl_policy,
+			    NULL);
+	if (err) {
+		pr_err_ratelimited("%s: genlmsg_parse() returned %d\n",
+				   __func__, err);
+		return err;
+	}
+
+	if (!tb[HANDSHAKE_GENL_ATTR_PROTOCOL])
+		return handshake_genl_status_reply(skb, gi, -EINVAL);
+
+	req = NULL;
+	spin_lock(&net->hs_lock);
+	list_for_each_entry(pos, &net->hs_requests, hr_list) {
+		if (pos->hr_proto->hp_protocol !=
+		    nla_get_u32(tb[HANDSHAKE_GENL_ATTR_PROTOCOL]))
+			continue;
+		remove_pending_locked(net, pos);
+		req = handshake_req_get(pos);
+		break;
+	}
+	spin_unlock(&net->hs_lock);
+	if (!req)
+		return handshake_genl_status_reply(skb, gi, -EAGAIN);
+
+	err = handshake_accept(req);
+	if (err < 0) {
+		handshake_complete(req, -EIO, NULL);
+		handshake_req_put(req);
+		return handshake_genl_status_reply(skb, gi, err);
+	}
+	err = req->hr_proto->hp_accept(req, gi, req->hr_fd);
+	if (err) {
+		put_unused_fd(req->hr_fd);
+		handshake_complete(req, -EIO, NULL);
+		handshake_req_put(req);
+		return err;
+	}
+	return 0;
+}
+
+/*
+ * This function is careful to not close the socket. It merely removes
+ * it from the file descriptor table so that it is no longer visible
+ * to the calling process.
+ */
+static int handshake_genl_cmd_done(struct sk_buff *skb, struct genl_info *gi)
+{
+	struct nlattr *tb[HANDSHAKE_GENL_ATTR_MAX + 1];
+	struct handshake_req *req;
+	struct socket *sock;
+	int fd, status, err;
+
+	err = genlmsg_parse(nlmsg_hdr(skb), &handshake_genl_family, tb,
+			    HANDSHAKE_GENL_ATTR_MAX, handshake_genl_policy,
+			    NULL);
+	if (err) {
+		pr_err_ratelimited("%s: genlmsg_parse() returned %d\n",
+				   __func__, err);
+		return err;
+	}
+
+	if (!tb[HANDSHAKE_GENL_ATTR_SOCKFD])
+		return handshake_genl_status_reply(skb, gi, -EINVAL);
+	err = 0;
+	fd = nla_get_u32(tb[HANDSHAKE_GENL_ATTR_SOCKFD]);
+	sock = sockfd_lookup(fd, &err);
+	if (err)
+		return handshake_genl_status_reply(skb, gi, -EBADF);
+
+	req = sock->sk->sk_handshake_req;
+	if (req->hr_fd != fd)	/* sanity */
+		return handshake_genl_status_reply(skb, gi, -EBADF);
+
+	status = -EIO;
+	if (tb[HANDSHAKE_GENL_ATTR_SESS_STATUS])
+		status = nla_get_u32(tb[HANDSHAKE_GENL_ATTR_SESS_STATUS]);
+
+	put_unused_fd(req->hr_fd);
+	handshake_complete(req, status, tb[HANDSHAKE_GENL_ATTR_DONE]);
+	handshake_req_put(req);
+	return 0;
+}
+
+static const struct genl_ops handshake_genl_ops[] = {
+	{
+		.cmd		= HANDSHAKE_GENL_CMD_ACCEPT,
+		.doit		= handshake_genl_cmd_accept,
+		.flags		= GENL_ADMIN_PERM,
+	},
+	{
+		.cmd		= HANDSHAKE_GENL_CMD_DONE,
+		.doit		= handshake_genl_cmd_done,
+		.flags		= GENL_ADMIN_PERM,
+	},
+};
+
+static const struct genl_multicast_group handshake_genl_mcgrps[] = {
+	[HANDSHAKE_GENL_MCGRP_NONE] = {
+		.name		= HANDSHAKE_GENL_MCGRP_NONE_NAME,
+	},
+};
+
+static struct genl_family __ro_after_init handshake_genl_family = {
+	.hdrsize		= 0,
+	.name			= HANDSHAKE_GENL_NAME,
+	.version		= HANDSHAKE_GENL_VERSION,
+	.maxattr		= HANDSHAKE_GENL_ATTR_MAX,
+	.netnsok		= true,
+	.n_mcgrps		= ARRAY_SIZE(handshake_genl_mcgrps),
+	.n_ops			= ARRAY_SIZE(handshake_genl_ops),
+	.resv_start_op		= HANDSHAKE_GENL_CMD_MAX,
+	.policy			= handshake_genl_policy,
+	.ops			= handshake_genl_ops,
+	.mcgrps			= handshake_genl_mcgrps,
+	.module			= THIS_MODULE,
+};
+
+static int __net_init handshake_net_init(struct net *net)
+{
+	spin_lock_init(&net->hs_lock);
+	INIT_LIST_HEAD(&net->hs_requests);
+	net->hs_pending	= 0;
+	return 0;
+}
+
+static void __net_exit handshake_net_exit(struct net *net)
+{
+	struct handshake_req *req;
+	LIST_HEAD(requests);
+
+	/*
+	 * XXX: This drains the net's pending list, but does
+	 *	nothing about requests that have been accepted
+	 *	and are in progress.
+	 */
+	spin_lock(&net->hs_lock);
+	list_splice_init(&requests, &net->hs_requests);
+	spin_unlock(&net->hs_lock);
+
+	while (!list_empty(&requests)) {
+		req = list_first_entry(&requests, struct handshake_req, hr_list);
+		list_del(&req->hr_list);
+
+		/*
+		 * Requests on this list have not yet been
+		 * accepted, so they do not have an fd to put.
+		 */
+
+		handshake_complete(req, -ETIMEDOUT, NULL);
+	}
+}
+
+static struct pernet_operations handshake_genl_net_ops = {
+	.init		= handshake_net_init,
+	.exit		= handshake_net_exit,
+};
+
+static int __init handshake_init(void)
+{
+	int ret;
+
+	ret = genl_register_family(&handshake_genl_family);
+	if (ret)
+		return ret;
+
+	ret = register_pernet_subsys(&handshake_genl_net_ops);
+	if (ret)
+		genl_unregister_family(&handshake_genl_family);
+
+	return ret;
+}
+
+static void __exit handshake_exit(void)
+{
+	unregister_pernet_subsys(&handshake_genl_net_ops);
+	genl_unregister_family(&handshake_genl_family);
+}
+
+module_init(handshake_init);
+module_exit(handshake_exit);
diff --git a/net/handshake/request.c b/net/handshake/request.c
new file mode 100644
index 000000000000..bf56703ea1f5
--- /dev/null
+++ b/net/handshake/request.c
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Handshake request lifetime events
+ *
+ * Author: Chuck Lever <chuck.lever@oracle.com>
+ *
+ * Copyright (c) 2023, Oracle and/or its affiliates.
+ */
+
+#include <linux/types.h>
+#include <linux/socket.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/skbuff.h>
+#include <linux/inet.h>
+#include <linux/fdtable.h>
+
+#include <net/sock.h>
+#include <net/genetlink.h>
+#include <net/handshake.h>
+
+#include <uapi/linux/handshake.h>
+#include "handshake.h"
+
+/*
+ * This limit is to prevent slow remotes from causing denial of service.
+ * A ulimit-style tunable might be used instead.
+ */
+#define HANDSHAKE_PENDING_MAX (10)
+
+struct handshake_req *handshake_req_get(struct handshake_req *req)
+{
+	return likely(refcount_inc_not_zero(&req->hr_ref)) ? req : NULL;
+}
+
+static void handshake_req_destroy(struct handshake_req *req)
+{
+	__sock_put(req->hr_sock->sk);
+	req->hr_proto->hp_destroy(req);
+	kfree(req);
+}
+
+void handshake_req_put(struct handshake_req *req)
+{
+	if (refcount_dec_and_test(&req->hr_ref))
+		handshake_req_destroy(req);
+}
+
+/**
+ * handshake_req_alloc - consumer API to allocate a request
+ * @sock: open socket on which to perform a handshake
+ * @proto: security protocol
+ * @flags: memory allocation flags
+ *
+ * Returns an initialized handshake_req or NULL.
+ */
+struct handshake_req *handshake_req_alloc(struct socket *sock,
+					  const struct handshake_proto *proto,
+					  gfp_t flags)
+{
+	struct handshake_req *req;
+
+	req = kzalloc(sizeof(*req) + proto->hp_privsize, flags);
+	if (!req)
+		return NULL;
+
+	sock_hold(sock->sk);
+
+	refcount_set(&req->hr_ref, 1);
+	INIT_LIST_HEAD(&req->hr_list);
+	req->hr_sock = sock;
+	req->hr_proto = proto;
+	return req;
+}
+EXPORT_SYMBOL(handshake_req_alloc);
+
+/**
+ * handshake_req_private - consumer API to return per-handshake private data
+ * @req: handshake arguments
+ *
+ */
+void *handshake_req_private(struct handshake_req *req)
+{
+	return (void *)(req + 1);
+}
+EXPORT_SYMBOL(handshake_req_private);
+
+/**
+ * handshake_req_submit - consumer API to submit a handshake request
+ * @req: handshake arguments
+ * @flags: memory allocation flags
+ *
+ * Return values:
+ *   %0: Request queued
+ *   %-EBUSY: A handshake is already under way for this socket
+ *   %-ESRCH: No handshake agent is available
+ *   %-EAGAIN: Too many pending handshake requests
+ *   %-ENOMEM: Failed to allocate memory
+ *   %-EMSGSIZE: Failed to construct notification message
+ *
+ * A zero return value from handshake_request() means that
+ * exactly one subsequent completion callback is guaranteed.
+ *
+ * A negative return value from handshake_request() means that
+ * no completion callback will be done and that @req is
+ * destroyed.
+ */
+int handshake_req_submit(struct handshake_req *req, gfp_t flags)
+{
+	struct sock *sk = req->hr_sock->sk;
+	struct net *net = sock_net(sk);
+	int ret;
+
+	ret = -EAGAIN;
+	if (READ_ONCE(net->hs_pending) >= HANDSHAKE_PENDING_MAX)
+		goto out_err;
+
+	ret = -EBUSY;
+	spin_lock(&net->hs_lock);
+	if (sk->sk_handshake_req || !list_empty(&req->hr_list)) {
+		spin_unlock(&net->hs_lock);
+		goto out_err;
+	}
+
+	add_pending_locked(net, req);
+	sk->sk_handshake_req = req;
+	spin_unlock(&net->hs_lock);
+
+	ret = handshake_genl_notify(net, req, flags);
+	if (ret)
+		if (handshake_remove_pending(net, req))
+			goto out_err;
+
+	return 0;
+
+out_err:
+	handshake_req_put(req);
+	return ret;
+}
+EXPORT_SYMBOL(handshake_req_submit);
+
+/**
+ * handshake_req_cancel - consumer API to cancel an in-progress handshake
+ * @sock: socket on which there is an ongoing handshake
+ *
+ * The consumer must discard @sock immediately when this function
+ * returns.
+ */
+void handshake_req_cancel(struct socket *sock)
+{
+	struct sock *sk = sock->sk;
+	struct handshake_req *req = sk->sk_handshake_req;
+
+	if (!req)
+		return;
+
+	handshake_remove_pending(sock_net(sk), req);
+	handshake_complete(req, -ERESTARTSYS, NULL);
+}
+EXPORT_SYMBOL(handshake_req_cancel);
