L2TP¶
第二层隧道协议 (L2TP) 允许将 L2 帧通过 IP 网络进行隧道传输。
本文档涵盖内核的 L2TP 子系统。它记录了想要使用 L2TP 子系统的应用程序开发人员的内核 API,并提供了一些关于内部实现的技术细节,这些细节可能对内核开发人员和维护人员有用。
概述¶
内核的 L2TP 子系统实现了 L2TPv2 和 L2TPv3 的数据路径。 L2TPv2 通过 UDP 传输。 L2TPv3 通过 UDP 或直接通过 IP(协议 115)传输。
L2TP RFC 定义了两种基本类型的 L2TP 数据包:控制数据包(“控制平面”)和数据数据包(“数据平面”)。内核只处理数据数据包。更复杂的控制数据包由用户空间处理。
一个 L2TP 隧道携带一个或多个 L2TP 会话。每个隧道都与一个套接字关联。每个会话都与一个虚拟网络设备相关联,例如 pppN
, l2tpethN
,数据帧通过该设备传入/传出 L2TP。 L2TP 标头中的字段标识隧道或会话,以及它是控制数据包还是数据数据包。 当使用 Linux 内核 API 设置隧道和会话时,我们只是设置 L2TP 数据路径。控制协议的所有方面都由用户空间处理。
这种职责划分导致了建立隧道和会话时操作的自然顺序。该过程如下所示
创建一个隧道套接字。通过该套接字与对等方交换 L2TP 控制协议消息,以建立隧道。
在内核中创建一个隧道上下文,使用从对等方通过控制协议消息获得的信息。
通过隧道套接字与对等方交换 L2TP 控制协议消息,以建立会话。
在内核中创建一个会话上下文,使用从对等方通过控制协议消息获得的信息。
L2TP API¶
本节记录 L2TP 子系统的每个用户空间 API。
隧道套接字¶
L2TPv2 总是使用 UDP。 L2TPv3 可以使用 UDP 或 IP 封装。
要创建 L2TP 使用的隧道套接字,可以使用标准的 POSIX 套接字 API。
例如,对于使用 IPv4 地址和 UDP 封装的隧道
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
或者对于使用 IPv6 地址和 IP 封装的隧道
int sockfd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_L2TP);
这里不需要介绍 UDP 套接字编程。
IPPROTO_L2TP 是内核 L2TP 子系统实现的 IP 协议类型。 L2TPIP 套接字地址在 include/uapi/linux/l2tp.h 中的 struct sockaddr_l2tpip 和 struct sockaddr_l2tpip6 中定义。该地址包括 L2TP 隧道(连接)ID。 要使用 L2TP IP 封装,L2TPv3 应用程序应使用本地分配的隧道 ID 绑定 L2TPIP 套接字。 当对等方的隧道 ID 和 IP 地址已知时,必须执行连接。
如果 L2TP 应用程序需要处理来自使用 L2TPIP 的对等方的 L2TPv3 隧道设置请求,它必须打开一个专用的 L2TPIP 套接字来侦听这些请求,并使用隧道 ID 0 绑定套接字,因为隧道设置请求的地址是隧道 ID 0。
当隧道套接字关闭时,L2TP 隧道及其所有会话都会自动关闭。
Netlink API¶
L2TP 应用程序使用 netlink 来管理内核中的 L2TP 隧道和会话实例。 L2TP netlink API 在 include/uapi/linux/l2tp.h 中定义。
L2TP 使用 通用 Netlink (GENL)。 定义了几个命令:针对隧道和会话实例的创建、删除、修改和获取,例如 L2TP_CMD_TUNNEL_CREATE
。 API 标头列出了每个命令可以使用的 netlink 属性类型。
隧道和会话实例由本地唯一的 32 位 ID 标识。 L2TP 隧道 ID 由 L2TP_ATTR_CONN_ID
和 L2TP_ATTR_PEER_CONN_ID
属性给出,L2TP 会话 ID 由 L2TP_ATTR_SESSION_ID
和 L2TP_ATTR_PEER_SESSION_ID
属性给出。 如果使用 netlink 来管理 L2TPv2 隧道和会话实例,则 L2TPv2 16 位隧道/会话 ID 将在此类属性中强制转换为 32 位值。
在 L2TP_CMD_TUNNEL_CREATE
命令中,L2TP_ATTR_FD
告诉内核正在使用的隧道套接字 fd。 如果未指定,内核会为隧道创建一个内核套接字,使用在 L2TP_ATTR_IP[6]_SADDR
、L2TP_ATTR_IP[6]_DADDR
、L2TP_ATTR_UDP_SPORT
、L2TP_ATTR_UDP_DPORT
属性中设置的 IP 参数。 内核套接字用于实现非托管 L2TPv3 隧道(iproute2 的“ip l2tp”命令)。 如果给定了 L2TP_ATTR_FD
,则它必须是已绑定和连接的套接字 fd。 本文档后面会提供有关非托管隧道的更多信息。
L2TP_CMD_TUNNEL_CREATE
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
是 |
设置隧道(连接)ID。 |
PEER_CONN_ID |
是 |
设置对等隧道(连接)ID。 |
PROTO_VERSION |
是 |
协议版本。 2 或 3。 |
ENCAP_TYPE |
是 |
封装类型:UDP 或 IP。 |
FD |
否 |
隧道套接字文件描述符。 |
UDP_CSUM |
否 |
启用 IPv4 UDP 校验和。 仅当未设置 FD 时使用。 |
UDP_ZERO_CSUM6_TX |
否 |
传输时将 IPv6 UDP 校验和归零。 仅当未设置 FD 时使用。 |
UDP_ZERO_CSUM6_RX |
否 |
接收时将 IPv6 UDP 校验和归零。 仅当未设置 FD 时使用。 |
IP_SADDR |
否 |
IPv4 源地址。 仅当未设置 FD 时使用。 |
IP_DADDR |
否 |
IPv4 目标地址。 仅当未设置 FD 时使用。 |
UDP_SPORT |
否 |
UDP 源端口。 仅当未设置 FD 时使用。 |
UDP_DPORT |
否 |
UDP 目标端口。 仅当未设置 FD 时使用。 |
IP6_SADDR |
否 |
IPv6 源地址。 仅当未设置 FD 时使用。 |
IP6_DADDR |
否 |
IPv6 目标地址。 仅当未设置 FD 时使用。 |
DEBUG |
否 |
调试标志。 |
L2TP_CMD_TUNNEL_DESTROY
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
是 |
标识要销毁的隧道 ID。 |
L2TP_CMD_TUNNEL_MODIFY
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
是 |
标识要修改的隧道 ID。 |
DEBUG |
否 |
调试标志。 |
L2TP_CMD_TUNNEL_GET
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
否 |
标识要查询的隧道 ID。 在 DUMP 请求中忽略。 |
L2TP_CMD_SESSION_CREATE
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
是 |
父隧道 ID。 |
SESSION_ID |
是 |
设置会话 ID。 |
PEER_SESSION_ID |
是 |
设置父会话 ID。 |
PW_TYPE |
是 |
设置伪线类型。 |
DEBUG |
否 |
调试标志。 |
RECV_SEQ |
否 |
启用 rx 数据序列号。 |
SEND_SEQ |
否 |
启用 tx 数据序列号。 |
LNS_MODE |
否 |
启用 LNS 模式(自动启用数据序列号)。 |
RECV_TIMEOUT |
否 |
重新排序接收到的数据包时等待的超时。 |
L2SPEC_TYPE |
否 |
设置第 2 层特定子层类型(仅限 L2TPv3)。 |
COOKIE |
否 |
设置可选 Cookie(仅限 L2TPv3)。 |
PEER_COOKIE |
否 |
设置可选的对等 Cookie(仅限 L2TPv3)。 |
IFNAME |
否 |
设置接口名称(仅限 L2TPv3)。 |
对于以太网会话类型,这将创建一个 l2tpeth 虚拟接口,然后可以根据需要对其进行配置。 对于 PPP 会话类型,还必须打开并连接 PPPoL2TP 套接字,将其映射到新会话。 这将在后面的“PPPoL2TP 套接字”中介绍。
L2TP_CMD_SESSION_DESTROY
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
是 |
标识要销毁的会话的父隧道 ID。 |
SESSION_ID |
是 |
标识要销毁的会话 ID。 |
IFNAME |
否 |
按接口名称标识会话。 如果设置,这将覆盖任何 CONN_ID 和 SESSION_ID 属性。 目前仅支持 L2TPv3 以太网会话。 |
L2TP_CMD_SESSION_MODIFY
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
是 |
标识要修改的会话的父隧道 ID。 |
SESSION_ID |
是 |
标识要修改的会话 ID。 |
IFNAME |
否 |
按接口名称标识会话。 如果设置,这将覆盖任何 CONN_ID 和 SESSION_ID 属性。 目前仅支持 L2TPv3 以太网会话。 |
DEBUG |
否 |
调试标志。 |
RECV_SEQ |
否 |
启用 rx 数据序列号。 |
SEND_SEQ |
否 |
启用 tx 数据序列号。 |
LNS_MODE |
否 |
启用 LNS 模式(自动启用数据序列号)。 |
RECV_TIMEOUT |
否 |
重新排序接收到的数据包时等待的超时。 |
L2TP_CMD_SESSION_GET
属性:-
属性 |
必需 |
用途 |
---|---|---|
CONN_ID |
否 |
标识要查询的隧道 ID。 对于 DUMP 请求,此项被忽略。 |
SESSION_ID |
否 |
标识要查询的会话 ID。 对于 DUMP 请求,此项被忽略。 |
IFNAME |
否 |
按接口名称标识会话。 如果设置,这将覆盖任何 CONN_ID 和 SESSION_ID 属性。 对于 DUMP 请求,此项被忽略。 目前仅支持 L2TPv3 以太网会话。 |
应用程序开发人员应参阅 include/uapi/linux/l2tp.h 以获取 netlink 命令和属性定义。
使用 libmnl 的用户空间代码示例
打开 L2TP netlink 套接字
struct nl_sock *nl_sock; int l2tp_nl_family_id; nl_sock = nl_socket_alloc(); genl_connect(nl_sock); genl_id = genl_ctrl_resolve(nl_sock, L2TP_GENL_NAME);创建隧道
struct nlmsghdr *nlh; struct genlmsghdr *gnlh; nlh = mnl_nlmsg_put_header(buf); nlh->nlmsg_type = genl_id; /* assigned to genl socket */ nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_seq = seq; gnlh = mnl_nlmsg_put_extra_header(nlh, sizeof(*gnlh)); gnlh->cmd = L2TP_CMD_TUNNEL_CREATE; gnlh->version = L2TP_GENL_VERSION; gnlh->reserved = 0; mnl_attr_put_u32(nlh, L2TP_ATTR_FD, tunl_sock_fd); mnl_attr_put_u32(nlh, L2TP_ATTR_CONN_ID, tid); mnl_attr_put_u32(nlh, L2TP_ATTR_PEER_CONN_ID, peer_tid); mnl_attr_put_u8(nlh, L2TP_ATTR_PROTO_VERSION, protocol_version); mnl_attr_put_u16(nlh, L2TP_ATTR_ENCAP_TYPE, encap);创建会话
struct nlmsghdr *nlh; struct genlmsghdr *gnlh; nlh = mnl_nlmsg_put_header(buf); nlh->nlmsg_type = genl_id; /* assigned to genl socket */ nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_seq = seq; gnlh = mnl_nlmsg_put_extra_header(nlh, sizeof(*gnlh)); gnlh->cmd = L2TP_CMD_SESSION_CREATE; gnlh->version = L2TP_GENL_VERSION; gnlh->reserved = 0; mnl_attr_put_u32(nlh, L2TP_ATTR_CONN_ID, tid); mnl_attr_put_u32(nlh, L2TP_ATTR_PEER_CONN_ID, peer_tid); mnl_attr_put_u32(nlh, L2TP_ATTR_SESSION_ID, sid); mnl_attr_put_u32(nlh, L2TP_ATTR_PEER_SESSION_ID, peer_sid); mnl_attr_put_u16(nlh, L2TP_ATTR_PW_TYPE, pwtype); /* there are other session options which can be set using netlink * attributes during session creation -- see l2tp.h */删除会话
struct nlmsghdr *nlh; struct genlmsghdr *gnlh; nlh = mnl_nlmsg_put_header(buf); nlh->nlmsg_type = genl_id; /* assigned to genl socket */ nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_seq = seq; gnlh = mnl_nlmsg_put_extra_header(nlh, sizeof(*gnlh)); gnlh->cmd = L2TP_CMD_SESSION_DELETE; gnlh->version = L2TP_GENL_VERSION; gnlh->reserved = 0; mnl_attr_put_u32(nlh, L2TP_ATTR_CONN_ID, tid); mnl_attr_put_u32(nlh, L2TP_ATTR_SESSION_ID, sid);删除隧道及其所有会话(如果有)
struct nlmsghdr *nlh; struct genlmsghdr *gnlh; nlh = mnl_nlmsg_put_header(buf); nlh->nlmsg_type = genl_id; /* assigned to genl socket */ nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_seq = seq; gnlh = mnl_nlmsg_put_extra_header(nlh, sizeof(*gnlh)); gnlh->cmd = L2TP_CMD_TUNNEL_DELETE; gnlh->version = L2TP_GENL_VERSION; gnlh->reserved = 0; mnl_attr_put_u32(nlh, L2TP_ATTR_CONN_ID, tid);
PPPoL2TP 会话套接字 API¶
对于 PPP 会话类型,必须打开 PPPoL2TP 套接字并将其连接到 L2TP 会话。
创建 PPPoL2TP 套接字时,应用程序在套接字 connect() 调用中向内核提供有关隧道和会话的信息。 提供源隧道 ID 和目标隧道 ID 以及会话 ID,以及 UDP 或 L2TPIP 套接字的文件描述符。 请参阅 include/linux/if_pppol2tp.h 中的 struct pppol2tp_addr。 由于历史原因,对于 L2TPv2/L2TPv3 IPv4/IPv6 隧道,地址结构略有不同,并且用户空间必须使用与隧道套接字类型匹配的适当结构。
用户空间可以使用 setsockopt 和 ioctl 在 PPPoX 套接字上控制隧道或会话的行为。 支持以下套接字选项:-
DEBUG |
调试消息类别的位掩码。 请参见下文。 |
SENDSEQ |
|
RECVSEQ |
|
LNSMODE |
|
REORDERTO |
重新排序超时(以毫秒为单位)。 如果为 0,则不要尝试重新排序。 |
除了标准的 PPP ioctl 之外,还提供 PPPIOCGL2TPSTATS,以使用相应隧道或会话的 PPPoX 套接字从内核检索隧道和会话统计信息。
用户空间代码示例
创建会话 PPPoX 数据套接字
/* Input: the L2TP tunnel UDP socket `tunnel_fd`, which needs to be * bound already (both sockname and peername), otherwise it will not be * ready. */ struct sockaddr_pppol2tp sax; int session_fd; int ret; session_fd = socket(AF_PPPOX, SOCK_DGRAM, PX_PROTO_OL2TP); if (session_fd < 0) return -errno; sax.sa_family = AF_PPPOX; sax.sa_protocol = PX_PROTO_OL2TP; sax.pppol2tp.fd = tunnel_fd; sax.pppol2tp.addr.sin_addr.s_addr = addr->sin_addr.s_addr; sax.pppol2tp.addr.sin_port = addr->sin_port; sax.pppol2tp.addr.sin_family = AF_INET; sax.pppol2tp.s_tunnel = tunnel_id; sax.pppol2tp.s_session = session_id; sax.pppol2tp.d_tunnel = peer_tunnel_id; sax.pppol2tp.d_session = peer_session_id; /* session_fd is the fd of the session's PPPoL2TP socket. * tunnel_fd is the fd of the tunnel UDP / L2TPIP socket. */ ret = connect(session_fd, (struct sockaddr *)&sax, sizeof(sax)); if (ret < 0 ) { close(session_fd); return -errno; } return session_fd;
L2TP 控制数据包仍可在 tunnel_fd 上读取。
创建 PPP 通道
/* Input: the session PPPoX data socket `session_fd` which was created * as described above. */ int ppp_chan_fd; int chindx; int ret; ret = ioctl(session_fd, PPPIOCGCHAN, &chindx); if (ret < 0) return -errno; ppp_chan_fd = open("/dev/ppp", O_RDWR); if (ppp_chan_fd < 0) return -errno; ret = ioctl(ppp_chan_fd, PPPIOCATTCHAN, &chindx); if (ret < 0) { close(ppp_chan_fd); return -errno; } return ppp_chan_fd;
LCP PPP 帧可在 ppp_chan_fd 上读取。
创建 PPP 接口
/* Input: the PPP channel `ppp_chan_fd` which was created as described * above. */ int ifunit = -1; int ppp_if_fd; int ret; ppp_if_fd = open("/dev/ppp", O_RDWR); if (ppp_if_fd < 0) return -errno; ret = ioctl(ppp_if_fd, PPPIOCNEWUNIT, &ifunit); if (ret < 0) { close(ppp_if_fd); return -errno; } ret = ioctl(ppp_chan_fd, PPPIOCCONNECT, &ifunit); if (ret < 0) { close(ppp_if_fd); return -errno; } return ppp_if_fd;
IPCP/IPv6CP PPP 帧可在 ppp_if_fd 上读取。
然后可以使用 netlink 的 RTM_NEWLINK、RTM_NEWADDR、RTM_NEWROUTE 或 ioctl 的 SIOCSIFMTU、SIOCSIFADDR、SIOCSIFDSTADDR、SIOCSIFNETMASK、SIOCSIFFLAGS,或使用 ip 命令,像往常一样配置 ppp<ifunit> 接口。
通过桥接要桥接的两个 L2TP 会话的 PPP 通道,支持桥接具有 PPP 伪线类型的 L2TP 会话(这也称为 L2TP 隧道交换或 L2TP 多跳)
/* Input: the session PPPoX data sockets `session_fd1` and `session_fd2` * which were created as described further above. */ int ppp_chan_fd; int chindx1; int chindx2; int ret; ret = ioctl(session_fd1, PPPIOCGCHAN, &chindx1); if (ret < 0) return -errno; ret = ioctl(session_fd2, PPPIOCGCHAN, &chindx2); if (ret < 0) return -errno; ppp_chan_fd = open("/dev/ppp", O_RDWR); if (ppp_chan_fd < 0) return -errno; ret = ioctl(ppp_chan_fd, PPPIOCATTCHAN, &chindx1); if (ret < 0) { close(ppp_chan_fd); return -errno; } ret = ioctl(ppp_chan_fd, PPPIOCBRIDGECHAN, &chindx2); close(ppp_chan_fd); if (ret < 0) return -errno; return 0;
可以注意到,在桥接 PPP 通道时,PPP 会话不会在本地终止,并且不会创建本地 PPP 接口。 在一个通道上到达的 PPP 帧直接传递到另一个通道,反之亦然。
不需要保持 PPP 通道打开。 只需要保持会话 PPPoX 数据套接字打开。
更一般地,也可以以相同的方式将 PPPoL2TP PPP 通道与其他类型的 PPP 通道(例如 PPPoE)桥接。
有关 PPP 端的更多详细信息,请参见 PPP 通用驱动程序和通道接口。
旧的仅限 L2TPv2 的 API¶
当 L2TP 首次在 2.6.23 版本中添加到 Linux 内核时,它仅实现了 L2TPv2,并且不包含 netlink API。 而是仅使用 PPPoL2TP 套接字直接管理内核中的隧道和会话实例。 PPPoL2TP 套接字的使用方式与“PPPoL2TP 会话套接字 API”部分中描述的方式相同,但隧道和会话实例是在套接字的 connect() 上自动创建的,而不是通过单独的 netlink 请求创建的
隧道是使用隧道管理套接字管理的,该套接字是专用的 PPPoL2TP 套接字,已连接到(无效的)会话 ID 0。当连接 PPPoL2TP 隧道管理套接字时,会创建 L2TP 隧道实例,并在关闭套接字时销毁。
当 PPPoL2TP 套接字连接到非零会话 ID 时,将在内核中创建会话实例。 会话参数是使用 setsockopt 设置的。 当关闭套接字时,L2TP 会话实例会被销毁。
此 API 仍受支持,但不鼓励使用。 而是,新的 L2TPv2 应用程序应首先使用 netlink 创建隧道和会话,然后为会话创建 PPPoL2TP 套接字。
非托管 L2TPv3 隧道¶
内核 L2TP 子系统还支持静态(非托管)L2TPv3 隧道。 非托管隧道没有用户空间隧道套接字,并且不与对等方交换控制消息来设置隧道;隧道是在隧道的每一端手动配置的。 所有配置都是使用 netlink 完成的。 在这种情况下,不需要 L2TP 用户空间应用程序 - 隧道套接字由内核创建,并使用在 L2TP_CMD_TUNNEL_CREATE
netlink 请求中发送的参数进行配置。 iproute2
的 ip
实用程序具有用于管理静态 L2TPv3 隧道的命令; 执行 ip l2tp help
以获取更多信息。
调试¶
L2TP 子系统通过 debugfs 文件系统提供了一系列调试接口。
要访问这些接口,必须首先挂载 debugfs 文件系统
# mount -t debugfs debugfs /debug
然后可以访问 l2tp 目录下的文件,从而提供内核中存在的当前隧道和会话上下文的摘要
# cat /debug/l2tp/tunnels
应用程序不应使用 debugfs 文件来获取 L2TP 状态信息,因为文件格式可能会更改。 它的实现目的是提供额外的调试信息来帮助诊断问题。 应用程序应改为使用 netlink API。
此外,L2TP 子系统使用标准内核事件跟踪 API 实现跟踪点。 可用的 L2TP 事件可以按如下方式查看
# find /debug/tracing/events/l2tp
最后,为了向后兼容原始 pppol2tp 代码,还提供了 /proc/net/pppol2tp。 它仅列出有关 L2TPv2 隧道和会话的信息。 不鼓励使用它。
内部实现¶
本节适用于内核开发人员和维护人员。
套接字¶
UDP 套接字由网络核心实现。 当使用 UDP 套接字创建 L2TP 隧道时,通过在 UDP 套接字上设置 encap_rcv 和 encap_destroy 回调,将套接字设置为封装的 UDP 套接字。 当在套接字上收到数据包时,将调用 l2tp_udp_encap_recv。 当用户空间关闭套接字时,将调用 l2tp_udp_encap_destroy。
L2TPIP 套接字在 net/l2tp/l2tp_ip.c 和 net/l2tp/l2tp_ip6.c 中实现。
隧道¶
内核为每个 L2TP 隧道保留一个 struct l2tp_tunnel 上下文。 l2tp_tunnel 始终与 UDP 或 L2TP/IP 套接字相关联,并保留隧道中会话的列表。 当隧道首次在 L2TP 核心中注册时,套接字上的引用计数会增加。 这确保了在 L2TP 的数据结构引用套接字时无法删除套接字。
隧道由唯一的隧道 ID 标识。 对于 L2TPv2,ID 为 16 位,对于 L2TPv3,ID 为 32 位。 在内部,ID 存储为 32 位值。
隧道保存在按隧道 ID 索引的每个网络列表中。 L2TPv2 和 L2TPv3 共享隧道 ID 命名空间。
处理隧道套接字关闭可能是 L2TP 实现中最棘手的部分。 如果用户空间关闭隧道套接字,则必须关闭并销毁 L2TP 隧道及其所有会话。 由于隧道上下文保存对隧道套接字的引用,因此在隧道 sock_put 其套接字之前,不会调用套接字的 sk_destruct。 对于 UDP 套接字,当用户空间关闭隧道套接字时,将调用套接字的 encap_destroy 处理程序,L2TP 使用该处理程序启动其隧道关闭操作。 对于 L2TPIP 套接字,套接字的关闭处理程序会启动相同的隧道关闭操作。 首先关闭所有会话。 每个会话都会删除其隧道引用。 当隧道引用达到零时,隧道会删除其套接字引用。
会话¶
内核为每个会话保留一个 struct l2tp_session 上下文。 每个会话都有专用数据,这些数据用于特定于会话类型的数据。 对于 L2TPv2,会话始终携带 PPP 流量。 对于 L2TPv3,会话可以携带以太网帧(以太网伪线)或其他数据类型,例如 PPP、ATM、HDLC 或帧中继。 Linux 目前仅实现以太网和 PPP 会话类型。
某些 L2TP 会话类型也具有套接字(PPP 伪线),而其他类型没有套接字(以太网伪线)。
与隧道一样,L2TP 会话由唯一的会话 ID 标识。 与隧道 ID 一样,对于 L2TPv2,会话 ID 为 16 位,对于 L2TPv3,会话 ID 为 32 位。 在内部,ID 存储为 32 位值。
会话保留对其父隧道的引用,以确保在有一个或多个会话引用隧道时隧道保持存在。
会话保存在每个网络列表中。 L2TPv2 会话和 L2TPv3 会话存储在单独的列表中。 L2TPv2 会话的键由 32 位密钥组成,该密钥由 16 位隧道 ID 和 16 位会话 ID 组成。 L2TPv3 会话的键由 32 位会话 ID 组成,因为 L2TPv3 会话 ID 在所有隧道中都是唯一的。
尽管 L2TPv3 RFC 规定 L2TPv3 会话 ID 不受隧道限制,但 Linux 实现历来允许这样做。 使用按 sk 和会话 ID 键控的每个网络哈希表来支持此类会话 ID 冲突。 查找 L2TPv3 会话时,列表条目可能会链接到具有该会话 ID 的多个会话,在这种情况下,将使用与给定 sk(隧道)匹配的会话。
PPP¶
net/l2tp/l2tp_ppp.c 实现了 PPPoL2TP 套接字系列。 每个 PPP 会话都有一个 PPPoL2TP 套接字。
PPPoL2TP 套接字的 sk_user_data 引用 l2tp_session。
用户空间使用 PPPoL2TP 套接字通过 L2TP 发送和接收 PPP 数据包。 只有 PPP 控制帧通过此套接字传递:PPP 数据数据包完全由内核处理,并在 L2TP 会话及其关联的 pppN
网络设备之间通过内核 PPP 子系统的 PPP 通道接口传递。
L2TP PPP 实现通过关闭其相应的 L2TP 会话来处理 PPPoL2TP 套接字的关闭。 这很复杂,因为它必须考虑与 netlink 会话创建/销毁请求和尝试重新连接到正在关闭的会话的 pppol2tp_connect 竞争。 PPP 会话保留对其关联套接字的引用,以便在会话引用它时套接字保持存在。
以太网¶
net/l2tp/l2tp_eth.c 实现了 L2TPv3 以太网伪线。 它为每个会话管理一个网络设备。
L2TP 以太网会话通过 netlink 请求创建和销毁,或者在隧道被销毁时销毁。与 PPP 会话不同,以太网会话没有关联的套接字。
其他¶
RFC¶
内核代码实现了以下 RFC 中指定的数据路径功能
RFC2661 |
L2TPv2 |
|
RFC3931 |
L2TPv3 |
|
RFC4719 |
L2TPv3 以太网 |
实现¶
许多开源应用程序使用 L2TP 内核子系统
iproute2 |
|
go-l2tp |
|
tunneldigger |
|
xl2tpd |
局限性¶
当前的实现存在一些局限性
与 openvswitch 的接口尚未实现。将 OVS 以太网和 VLAN 端口映射到 L2TPv3 隧道可能很有用。
VLAN 伪线使用配置了 VLAN 子接口的
l2tpethN
接口实现。由于 L2TPv3 VLAN 伪线仅携带一个 VLAN,因此最好使用单个 netdevice,而不是每个 VLAN 会话使用一个l2tpethN
和l2tpethN
:M 对。已为此添加了 netlink 属性L2TP_ATTR_VLAN_ID
,但从未实现。
测试¶
内核内置的自检测试对非托管 L2TPv3 以太网功能进行测试。请参阅 tools/testing/selftests/net/l2tp.sh。
另一个测试套件 l2tp-ktest 涵盖了所有 L2TP API 和隧道/会话类型。将来可能会将其集成到内核的内置 L2TP 自检测试中。