1

net/handshake: Add a kernel API for requesting a TLSv1.3 handshake

To enable kernel consumers of TLS to request a TLS handshake, add
support to net/handshake/ to request a handshake upcall.

This patch also acts as a template for adding handshake upcall
support for other kernel transport layer security providers.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Chuck Lever 2023-04-17 10:32:33 -04:00 committed by Jakub Kicinski
parent 3b3009ea8a
commit 2fd5532044
10 changed files with 689 additions and 3 deletions

View File

@ -16,7 +16,7 @@ definitions:
type: enum
name: handler-class
value-start: 0
entries: [ none, max ]
entries: [ none, tlshd, max ]
-
type: enum
name: msg-type
@ -120,3 +120,5 @@ mcast-groups:
list:
-
name: none
-
name: tlshd

View File

@ -36,6 +36,7 @@ Contents:
scaling
tls
tls-offload
tls-handshake
nfc
6lowpan
6pack

View File

@ -0,0 +1,217 @@
.. SPDX-License-Identifier: GPL-2.0
=======================
In-Kernel TLS Handshake
=======================
Overview
========
Transport Layer Security (TLS) is a Upper Layer Protocol (ULP) that runs
over TCP. TLS provides end-to-end data integrity and confidentiality in
addition to peer authentication.
The kernel's kTLS implementation handles the TLS record subprotocol, but
does not handle the TLS handshake subprotocol which is used to establish
a TLS session. Kernel consumers can use the API described here to
request TLS session establishment.
There are several possible ways to provide a handshake service in the
kernel. The API described here is designed to hide the details of those
implementations so that in-kernel TLS consumers do not need to be
aware of how the handshake gets done.
User handshake agent
====================
As of this writing, there is no TLS handshake implementation in the
Linux kernel. To provide a handshake service, a handshake agent
(typically in user space) is started in each network namespace where a
kernel consumer might require a TLS handshake. Handshake agents listen
for events sent from the kernel that indicate a handshake request is
waiting.
An open socket is passed to a handshake agent via a netlink operation,
which creates a socket descriptor in the agent's file descriptor table.
If the handshake completes successfully, the handshake agent promotes
the socket to use the TLS ULP and sets the session information using the
SOL_TLS socket options. The handshake agent returns the socket to the
kernel via a second netlink operation.
Kernel Handshake API
====================
A kernel TLS consumer initiates a client-side TLS handshake on an open
socket by invoking one of the tls_client_hello() functions. First, it
fills in a structure that contains the parameters of the request:
.. code-block:: c
struct tls_handshake_args {
struct socket *ta_sock;
tls_done_func_t ta_done;
void *ta_data;
unsigned int ta_timeout_ms;
key_serial_t ta_keyring;
key_serial_t ta_my_cert;
key_serial_t ta_my_privkey;
unsigned int ta_num_peerids;
key_serial_t ta_my_peerids[5];
};
The @ta_sock field references an open and connected socket. The consumer
must hold a reference on the socket to prevent it from being destroyed
while the handshake is in progress. The consumer must also have
instantiated a struct file in sock->file.
@ta_done contains a callback function that is invoked when the handshake
has completed. Further explanation of this function is in the "Handshake
Completion" sesction below.
The consumer can fill in the @ta_timeout_ms field to force the servicing
handshake agent to exit after a number of milliseconds. This enables the
socket to be fully closed once both the kernel and the handshake agent
have closed their endpoints.
Authentication material such as x.509 certificates, private certificate
keys, and pre-shared keys are provided to the handshake agent in keys
that are instantiated by the consumer before making the handshake
request. The consumer can provide a private keyring that is linked into
the handshake agent's process keyring in the @ta_keyring field to prevent
access of those keys by other subsystems.
To request an x.509-authenticated TLS session, the consumer fills in
the @ta_my_cert and @ta_my_privkey fields with the serial numbers of
keys containing an x.509 certificate and the private key for that
certificate. Then, it invokes this function:
.. code-block:: c
ret = tls_client_hello_x509(args, gfp_flags);
The function returns zero when the handshake request is under way. A
zero return guarantees the callback function @ta_done will be invoked
for this socket. The function returns a negative errno if the handshake
could not be started. A negative errno guarantees the callback function
@ta_done will not be invoked on this socket.
To initiate a client-side TLS handshake with a pre-shared key, use:
.. code-block:: c
ret = tls_client_hello_psk(args, gfp_flags);
However, in this case, the consumer fills in the @ta_my_peerids array
with serial numbers of keys containing the peer identities it wishes
to offer, and the @ta_num_peerids field with the number of array
entries it has filled in. The other fields are filled in as above.
To initiate an anonymous client-side TLS handshake use:
.. code-block:: c
ret = tls_client_hello_anon(args, gfp_flags);
The handshake agent presents no peer identity information to the remote
during this type of handshake. Only server authentication (ie the client
verifies the server's identity) is performed during the handshake. Thus
the established session uses encryption only.
Consumers that are in-kernel servers use:
.. code-block:: c
ret = tls_server_hello_x509(args, gfp_flags);
or
.. code-block:: c
ret = tls_server_hello_psk(args, gfp_flags);
The argument structure is filled in as above.
If the consumer needs to cancel the handshake request, say, due to a ^C
or other exigent event, the consumer can invoke:
.. code-block:: c
bool tls_handshake_cancel(sock);
This function returns true if the handshake request associated with
@sock has been canceled. The consumer's handshake completion callback
will not be invoked. If this function returns false, then the consumer's
completion callback has already been invoked.
Handshake Completion
====================
When the handshake agent has completed processing, it notifies the
kernel that the socket may be used by the consumer again. At this point,
the consumer's handshake completion callback, provided in the @ta_done
field in the tls_handshake_args structure, is invoked.
The synopsis of this function is:
.. code-block:: c
typedef void (*tls_done_func_t)(void *data, int status,
key_serial_t peerid);
The consumer provides a cookie in the @ta_data field of the
tls_handshake_args structure that is returned in the @data parameter of
this callback. The consumer uses the cookie to match the callback to the
thread waiting for the handshake to complete.
The success status of the handshake is returned via the @status
parameter:
+------------+----------------------------------------------+
| status | meaning |
+============+==============================================+
| 0 | TLS session established successfully |
+------------+----------------------------------------------+
| -EACCESS | Remote peer rejected the handshake or |
| | authentication failed |
+------------+----------------------------------------------+
| -ENOMEM | Temporary resource allocation failure |
+------------+----------------------------------------------+
| -EINVAL | Consumer provided an invalid argument |
+------------+----------------------------------------------+
| -ENOKEY | Missing authentication material |
+------------+----------------------------------------------+
| -EIO | An unexpected fault occurred |
+------------+----------------------------------------------+
The @peerid parameter contains the serial number of a key containing the
remote peer's identity or the value TLS_NO_PEERID if the session is not
authenticated.
A best practice is to close and destroy the socket immediately if the
handshake failed.
Other considerations
--------------------
While a handshake is under way, the kernel consumer must alter the
socket's sk_data_ready callback function to ignore all incoming data.
Once the handshake completion callback function has been invoked, normal
receive operation can be resumed.
Once a TLS session is established, the consumer must provide a buffer
for and then examine the control message (CMSG) that is part of every
subsequent sock_recvmsg(). Each control message indicates whether the
received message data is TLS record data or session metadata.
See tls.rst for details on how a kTLS consumer recognizes incoming
(decrypted) application data, alerts, and handshake packets once the
socket has been promoted to use the TLS ULP.

View File

@ -8953,6 +8953,8 @@ L: kernel-tls-handshake@lists.linux.dev
L: netdev@vger.kernel.org
S: Maintained
F: Documentation/netlink/specs/handshake.yaml
F: Documentation/networking/tls-handshake.rst
F: include/net/handshake.h
F: include/trace/events/handshake.h
F: net/handshake/

43
include/net/handshake.h Normal file
View File

@ -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.
*/
#ifndef _NET_HANDSHAKE_H
#define _NET_HANDSHAKE_H
enum {
TLS_NO_KEYRING = 0,
TLS_NO_PEERID = 0,
TLS_NO_CERT = 0,
TLS_NO_PRIVKEY = 0,
};
typedef void (*tls_done_func_t)(void *data, int status,
key_serial_t peerid);
struct tls_handshake_args {
struct socket *ta_sock;
tls_done_func_t ta_done;
void *ta_data;
unsigned int ta_timeout_ms;
key_serial_t ta_keyring;
key_serial_t ta_my_cert;
key_serial_t ta_my_privkey;
unsigned int ta_num_peerids;
key_serial_t ta_my_peerids[5];
};
int tls_client_hello_anon(const struct tls_handshake_args *args, gfp_t flags);
int tls_client_hello_x509(const struct tls_handshake_args *args, gfp_t flags);
int tls_client_hello_psk(const struct tls_handshake_args *args, gfp_t flags);
int tls_server_hello_x509(const struct tls_handshake_args *args, gfp_t flags);
int tls_server_hello_psk(const struct tls_handshake_args *args, gfp_t flags);
bool tls_handshake_cancel(struct sock *sk);
#endif /* _NET_HANDSHAKE_H */

View File

@ -11,6 +11,7 @@
enum handshake_handler_class {
HANDSHAKE_HANDLER_CLASS_NONE,
HANDSHAKE_HANDLER_CLASS_TLSHD,
HANDSHAKE_HANDLER_CLASS_MAX,
};
@ -67,5 +68,6 @@ enum {
};
#define HANDSHAKE_MCGRP_NONE "none"
#define HANDSHAKE_MCGRP_TLSHD "tlshd"
#endif /* _UAPI_LINUX_HANDSHAKE_H */

View File

@ -8,4 +8,4 @@
#
obj-y += handshake.o
handshake-y := genl.o netlink.o request.o trace.o
handshake-y := genl.o netlink.o request.o tlshd.o trace.o

View File

@ -12,7 +12,7 @@
/* HANDSHAKE_CMD_ACCEPT - do */
static const struct nla_policy handshake_accept_nl_policy[HANDSHAKE_A_ACCEPT_HANDLER_CLASS + 1] = {
[HANDSHAKE_A_ACCEPT_HANDLER_CLASS] = NLA_POLICY_MAX(NLA_U32, 1),
[HANDSHAKE_A_ACCEPT_HANDLER_CLASS] = NLA_POLICY_MAX(NLA_U32, 2),
};
/* HANDSHAKE_CMD_DONE - do */
@ -42,6 +42,7 @@ static const struct genl_split_ops handshake_nl_ops[] = {
static const struct genl_multicast_group handshake_nl_mcgrps[] = {
[HANDSHAKE_NLGRP_NONE] = { "none", },
[HANDSHAKE_NLGRP_TLSHD] = { "tlshd", },
};
struct genl_family handshake_nl_family __ro_after_init = {

View File

@ -16,6 +16,7 @@ int handshake_nl_done_doit(struct sk_buff *skb, struct genl_info *info);
enum {
HANDSHAKE_NLGRP_NONE,
HANDSHAKE_NLGRP_TLSHD,
};
extern struct genl_family handshake_nl_family;

417
net/handshake/tlshd.c Normal file
View File

@ -0,0 +1,417 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Establish a TLS session for a kernel socket consumer
* using the tlshd user space handler.
*
* Author: Chuck Lever <chuck.lever@oracle.com>
*
* Copyright (c) 2021-2023, Oracle and/or its affiliates.
*/
#include <linux/types.h>
#include <linux/socket.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/key.h>
#include <net/sock.h>
#include <net/handshake.h>
#include <net/genetlink.h>
#include <uapi/linux/keyctl.h>
#include <uapi/linux/handshake.h>
#include "handshake.h"
struct tls_handshake_req {
void (*th_consumer_done)(void *data, int status,
key_serial_t peerid);
void *th_consumer_data;
int th_type;
unsigned int th_timeout_ms;
int th_auth_mode;
key_serial_t th_keyring;
key_serial_t th_certificate;
key_serial_t th_privkey;
unsigned int th_num_peerids;
key_serial_t th_peerid[5];
};
static struct tls_handshake_req *
tls_handshake_req_init(struct handshake_req *req,
const struct tls_handshake_args *args)
{
struct tls_handshake_req *treq = handshake_req_private(req);
treq->th_timeout_ms = args->ta_timeout_ms;
treq->th_consumer_done = args->ta_done;
treq->th_consumer_data = args->ta_data;
treq->th_keyring = args->ta_keyring;
treq->th_num_peerids = 0;
treq->th_certificate = TLS_NO_CERT;
treq->th_privkey = TLS_NO_PRIVKEY;
return treq;
}
static void tls_handshake_remote_peerids(struct tls_handshake_req *treq,
struct genl_info *info)
{
struct nlattr *head = nlmsg_attrdata(info->nlhdr, GENL_HDRLEN);
int rem, len = nlmsg_attrlen(info->nlhdr, GENL_HDRLEN);
struct nlattr *nla;
unsigned int i;
i = 0;
nla_for_each_attr(nla, head, len, rem) {
if (nla_type(nla) == HANDSHAKE_A_DONE_REMOTE_AUTH)
i++;
}
if (!i)
return;
treq->th_num_peerids = min_t(unsigned int, i,
ARRAY_SIZE(treq->th_peerid));
i = 0;
nla_for_each_attr(nla, head, len, rem) {
if (nla_type(nla) == HANDSHAKE_A_DONE_REMOTE_AUTH)
treq->th_peerid[i++] = nla_get_u32(nla);
if (i >= treq->th_num_peerids)
break;
}
}
/**
* tls_handshake_done - callback to handle a CMD_DONE request
* @req: socket on which the handshake was performed
* @status: session status code
* @info: full results of session establishment
*
*/
static void tls_handshake_done(struct handshake_req *req,
unsigned int status, struct genl_info *info)
{
struct tls_handshake_req *treq = handshake_req_private(req);
treq->th_peerid[0] = TLS_NO_PEERID;
if (info)
tls_handshake_remote_peerids(treq, info);
treq->th_consumer_done(treq->th_consumer_data, -status,
treq->th_peerid[0]);
}
#if IS_ENABLED(CONFIG_KEYS)
static int tls_handshake_private_keyring(struct tls_handshake_req *treq)
{
key_ref_t process_keyring_ref, keyring_ref;
int ret;
if (treq->th_keyring == TLS_NO_KEYRING)
return 0;
process_keyring_ref = lookup_user_key(KEY_SPEC_PROCESS_KEYRING,
KEY_LOOKUP_CREATE,
KEY_NEED_WRITE);
if (IS_ERR(process_keyring_ref)) {
ret = PTR_ERR(process_keyring_ref);
goto out;
}
keyring_ref = lookup_user_key(treq->th_keyring, KEY_LOOKUP_CREATE,
KEY_NEED_LINK);
if (IS_ERR(keyring_ref)) {
ret = PTR_ERR(keyring_ref);
goto out_put_key;
}
ret = key_link(key_ref_to_ptr(process_keyring_ref),
key_ref_to_ptr(keyring_ref));
key_ref_put(keyring_ref);
out_put_key:
key_ref_put(process_keyring_ref);
out:
return ret;
}
#else
static int tls_handshake_private_keyring(struct tls_handshake_req *treq)
{
return 0;
}
#endif
static int tls_handshake_put_peer_identity(struct sk_buff *msg,
struct tls_handshake_req *treq)
{
unsigned int i;
for (i = 0; i < treq->th_num_peerids; i++)
if (nla_put_u32(msg, HANDSHAKE_A_ACCEPT_PEER_IDENTITY,
treq->th_peerid[i]) < 0)
return -EMSGSIZE;
return 0;
}
static int tls_handshake_put_certificate(struct sk_buff *msg,
struct tls_handshake_req *treq)
{
struct nlattr *entry_attr;
if (treq->th_certificate == TLS_NO_CERT &&
treq->th_privkey == TLS_NO_PRIVKEY)
return 0;
entry_attr = nla_nest_start(msg, HANDSHAKE_A_ACCEPT_CERTIFICATE);
if (!entry_attr)
return -EMSGSIZE;
if (nla_put_u32(msg, HANDSHAKE_A_X509_CERT,
treq->th_certificate) ||
nla_put_u32(msg, HANDSHAKE_A_X509_PRIVKEY,
treq->th_privkey)) {
nla_nest_cancel(msg, entry_attr);
return -EMSGSIZE;
}
nla_nest_end(msg, entry_attr);
return 0;
}
/**
* tls_handshake_accept - callback to construct a CMD_ACCEPT response
* @req: handshake parameters to return
* @info: generic netlink message context
* @fd: file descriptor to be returned
*
* Returns zero on success, or a negative errno on failure.
*/
static int tls_handshake_accept(struct handshake_req *req,
struct genl_info *info, int fd)
{
struct tls_handshake_req *treq = handshake_req_private(req);
struct nlmsghdr *hdr;
struct sk_buff *msg;
int ret;
ret = tls_handshake_private_keyring(treq);
if (ret < 0)
goto out;
ret = -ENOMEM;
msg = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL);
if (!msg)
goto out;
hdr = handshake_genl_put(msg, info);
if (!hdr)
goto out_cancel;
ret = -EMSGSIZE;
ret = nla_put_u32(msg, HANDSHAKE_A_ACCEPT_SOCKFD, fd);
if (ret < 0)
goto out_cancel;
ret = nla_put_u32(msg, HANDSHAKE_A_ACCEPT_MESSAGE_TYPE, treq->th_type);
if (ret < 0)
goto out_cancel;
if (treq->th_timeout_ms) {
ret = nla_put_u32(msg, HANDSHAKE_A_ACCEPT_TIMEOUT, treq->th_timeout_ms);
if (ret < 0)
goto out_cancel;
}
ret = nla_put_u32(msg, HANDSHAKE_A_ACCEPT_AUTH_MODE,
treq->th_auth_mode);
if (ret < 0)
goto out_cancel;
switch (treq->th_auth_mode) {
case HANDSHAKE_AUTH_PSK:
ret = tls_handshake_put_peer_identity(msg, treq);
if (ret < 0)
goto out_cancel;
break;
case HANDSHAKE_AUTH_X509:
ret = tls_handshake_put_certificate(msg, treq);
if (ret < 0)
goto out_cancel;
break;
}
genlmsg_end(msg, hdr);
return genlmsg_reply(msg, info);
out_cancel:
genlmsg_cancel(msg, hdr);
out:
return ret;
}
static const struct handshake_proto tls_handshake_proto = {
.hp_handler_class = HANDSHAKE_HANDLER_CLASS_TLSHD,
.hp_privsize = sizeof(struct tls_handshake_req),
.hp_accept = tls_handshake_accept,
.hp_done = tls_handshake_done,
};
/**
* tls_client_hello_anon - request an anonymous TLS handshake on a socket
* @args: socket and handshake parameters for this request
* @flags: memory allocation control flags
*
* Return values:
* %0: Handshake request enqueue; ->done will be called when complete
* %-ESRCH: No user agent is available
* %-ENOMEM: Memory allocation failed
*/
int tls_client_hello_anon(const struct tls_handshake_args *args, gfp_t flags)
{
struct tls_handshake_req *treq;
struct handshake_req *req;
req = handshake_req_alloc(&tls_handshake_proto, flags);
if (!req)
return -ENOMEM;
treq = tls_handshake_req_init(req, args);
treq->th_type = HANDSHAKE_MSG_TYPE_CLIENTHELLO;
treq->th_auth_mode = HANDSHAKE_AUTH_UNAUTH;
return handshake_req_submit(args->ta_sock, req, flags);
}
EXPORT_SYMBOL(tls_client_hello_anon);
/**
* tls_client_hello_x509 - request an x.509-based TLS handshake on a socket
* @args: socket and handshake parameters for this request
* @flags: memory allocation control flags
*
* Return values:
* %0: Handshake request enqueue; ->done will be called when complete
* %-ESRCH: No user agent is available
* %-ENOMEM: Memory allocation failed
*/
int tls_client_hello_x509(const struct tls_handshake_args *args, gfp_t flags)
{
struct tls_handshake_req *treq;
struct handshake_req *req;
req = handshake_req_alloc(&tls_handshake_proto, flags);
if (!req)
return -ENOMEM;
treq = tls_handshake_req_init(req, args);
treq->th_type = HANDSHAKE_MSG_TYPE_CLIENTHELLO;
treq->th_auth_mode = HANDSHAKE_AUTH_X509;
treq->th_certificate = args->ta_my_cert;
treq->th_privkey = args->ta_my_privkey;
return handshake_req_submit(args->ta_sock, req, flags);
}
EXPORT_SYMBOL(tls_client_hello_x509);
/**
* tls_client_hello_psk - request a PSK-based TLS handshake on a socket
* @args: socket and handshake parameters for this request
* @flags: memory allocation control flags
*
* Return values:
* %0: Handshake request enqueue; ->done will be called when complete
* %-EINVAL: Wrong number of local peer IDs
* %-ESRCH: No user agent is available
* %-ENOMEM: Memory allocation failed
*/
int tls_client_hello_psk(const struct tls_handshake_args *args, gfp_t flags)
{
struct tls_handshake_req *treq;
struct handshake_req *req;
unsigned int i;
if (!args->ta_num_peerids ||
args->ta_num_peerids > ARRAY_SIZE(treq->th_peerid))
return -EINVAL;
req = handshake_req_alloc(&tls_handshake_proto, flags);
if (!req)
return -ENOMEM;
treq = tls_handshake_req_init(req, args);
treq->th_type = HANDSHAKE_MSG_TYPE_CLIENTHELLO;
treq->th_auth_mode = HANDSHAKE_AUTH_PSK;
treq->th_num_peerids = args->ta_num_peerids;
for (i = 0; i < args->ta_num_peerids; i++)
treq->th_peerid[i] = args->ta_my_peerids[i];
return handshake_req_submit(args->ta_sock, req, flags);
}
EXPORT_SYMBOL(tls_client_hello_psk);
/**
* tls_server_hello_x509 - request a server TLS handshake on a socket
* @args: socket and handshake parameters for this request
* @flags: memory allocation control flags
*
* Return values:
* %0: Handshake request enqueue; ->done will be called when complete
* %-ESRCH: No user agent is available
* %-ENOMEM: Memory allocation failed
*/
int tls_server_hello_x509(const struct tls_handshake_args *args, gfp_t flags)
{
struct tls_handshake_req *treq;
struct handshake_req *req;
req = handshake_req_alloc(&tls_handshake_proto, flags);
if (!req)
return -ENOMEM;
treq = tls_handshake_req_init(req, args);
treq->th_type = HANDSHAKE_MSG_TYPE_SERVERHELLO;
treq->th_auth_mode = HANDSHAKE_AUTH_X509;
treq->th_certificate = args->ta_my_cert;
treq->th_privkey = args->ta_my_privkey;
return handshake_req_submit(args->ta_sock, req, flags);
}
EXPORT_SYMBOL(tls_server_hello_x509);
/**
* tls_server_hello_psk - request a server TLS handshake on a socket
* @args: socket and handshake parameters for this request
* @flags: memory allocation control flags
*
* Return values:
* %0: Handshake request enqueue; ->done will be called when complete
* %-ESRCH: No user agent is available
* %-ENOMEM: Memory allocation failed
*/
int tls_server_hello_psk(const struct tls_handshake_args *args, gfp_t flags)
{
struct tls_handshake_req *treq;
struct handshake_req *req;
req = handshake_req_alloc(&tls_handshake_proto, flags);
if (!req)
return -ENOMEM;
treq = tls_handshake_req_init(req, args);
treq->th_type = HANDSHAKE_MSG_TYPE_SERVERHELLO;
treq->th_auth_mode = HANDSHAKE_AUTH_PSK;
treq->th_num_peerids = 1;
treq->th_peerid[0] = args->ta_my_peerids[0];
return handshake_req_submit(args->ta_sock, req, flags);
}
EXPORT_SYMBOL(tls_server_hello_psk);
/**
* tls_handshake_cancel - cancel a pending handshake
* @sk: socket on which there is an ongoing handshake
*
* Request cancellation races with request completion. To determine
* who won, callers examine the return value from this function.
*
* Return values:
* %true - Uncompleted handshake request was canceled
* %false - Handshake request already completed or not found
*/
bool tls_handshake_cancel(struct sock *sk)
{
return handshake_req_cancel(sk);
}
EXPORT_SYMBOL(tls_handshake_cancel);