Linux 4.13/4.14内核中带来的ULP(Upper Layer Protocol)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 4.13/4.14内核中带来的ULP(Upper Layer Protocol)相关的知识,希望对你有一定的参考价值。

过了一个很爽的国庆假期,跟小小的小男朋友家长一起回其老家尝到了潮汕美食,南澳岛捕鱼捕虾,海鲜撑到爆,回到深圳次日小小另一个小朋友家长又带我们到东莞长安尝到了正宗的恩施土家菜,几天下来喝了几顿爽酒,吃喝玩乐再没有比这更惬意的了,这多亏了小小拓展了我们的社交圈子…

??我想起了14年在上海跟邻居朋友一起从嘉定自驾到苏州,而后在一家金鸡湖边上的日料店作到杯盘狼藉而不思归,玩味了彻底淋漓江南烟雨中的感觉,想到了几年前的那些许一醉方休后的枝枝蔓蔓,加之假期后期看到摇滚君公众号关于唐朝乐队丁武的帖子,更加是热血沸腾,然后温州皮鞋厂老板又去了川西青藏高原旅游让我又忆起八个月前那难忘的高原之旅(只不过温州老板走的是入滇的线路),窦唯大仙人又出了新歌(为手游《重返魔域》而创作的主题曲)并有所争议…九味杂陈不一而足,我只能默默地看和想,这不,沉淀下来的是关于Linux 4.13/4.14内核ULP的一些内容,特意总结了一些乱语,以示自己活着的现实。

yet another KTLS

Linux 4.13/4.14内核带来了一个新玩意儿,它叫ULP(Upper Layer Protocol),这是一个框架,引入的初衷旨在支持KTLS(Kernel TLS Support),这就跟为了支持TCP BBR而对整个TCP实现进行重构一样。ULP完全就是为KTLS量身定制的,只是额外的,它恰到好处的可以做一些其它方面很漂亮的事情。本文后面将给出一个Dmeo。

??什么?KTLS?这难道不是早就有了么?

??是的,在Linux内核中支持TLS这个思路早就被Facebook提出来了,我之前也写过这方面的一篇文章:
《来自Facebook的KTLS(Kernel SSL/TLS)原理和实例》:http://blog.csdn.net/dog250/article/details/53868519
那么,Linux 4.14内核中的KTLS和Dave Watson的原始的KTLS实现又有何不同呢?答案是,4.14的KTLS利用ULP作为其底层支撑,妙处在于,ULP不但可以支撑KTLS,而且可以作为一个通用的框架来支撑其它的功能。

??而对于原始的KTLS,它基于一种新型的套接字类型来支持TLS记录协议,这种实现并不优雅,如果你要实现另外一个协议,就需要再来一个套接字类型,最终会形成像UNIX sockopt那样的噩梦。ULP不一般,它简直就是一个平铺在“传输层”之上的“任意上层”,从而完美映射了OSI模型的7层模型!

Linux协议栈

从协议分层模型上来讲,Linux仅仅实现了TCP/IP模型,因此在Linux内核协议栈里是看不到传输层以上的各层的,如果你想实现一个表示层或者会话层,你就必须使用某种用户态的库,比如作为会话层协议的SSL/TLS握手协议,以及作为表示层协议的SSL/TLS记录协议,你可以使用OpenSSL来实现(libssl,libcrypto),但是在内核协议栈中并不支持它们。

??另一方面,从BSD socket接口上来说,socket仅仅提供一个接口规范,并没有规定接口如何实现。我们知道,在Windows系统中,socket程序必须初始化SPI,即必须以WSAStartup作为起始,安装了特定供应商的socket实现的WinSock,socket的行为便可以和特定的WinSock服务供应商的实现绑定,我在很早以前写过这么一个Demo:
《Windows上的OpenVPN如何封装真实IP作为源地址》:http://blog.csdn.net/dog250/article/details/7988939
很早前玩TAP网卡时写的一篇,现在Linux也可以实现类似的功能了。

动机

如果你要问把一些编码,封装之类的功能放到内核里实现到底有什么好处,答案可不仅仅是这可以提高性能这么简单,很多人对待用户态实现和内核态实现之间的比较时,总喜欢从性能入手,这其实远不正确。内核支持比库支持更底层,可以让你远离库依赖导致的兼容性,并且可以让你做到不改一行用户态代码就可以用新的协议封装数据,
诱惑够大了吧!事实上,很多时候性能并非关注点,用户态的库实现性能如今都堪比甚至超越内核实现了,至于说用户态和内核态之间的数据拷贝开销,也有很多不同的解决之道。

Linux 4.14的KTLS

Linux 4.14内核内置了KTLS的支持,然而如果你使用它的话,请务必注意,目前的KTLS并不是一个完整的TLS实现,它仅仅实现记录协议的一部分(即对称加密,目前依然没有实现对称解密操作)并完全没有实现握手协议,相关的资料链接列如下:
KTLS支撑-ULP:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=734942cc4ea6478eed125af258da1bdbb4afe578
ULP说明:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=99c195fb4eea405160ade58f74f62aed19b1822c
KTLS实现:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?www.zzktv.cn id=3c4d7559159bfe1e3b94df3a657b2cda3a34e218
仔细看下这个实现,就会发现,它虽然没有完整实现整个TLS,但是搭建了一个好用的架子,要比Dave Watson的原始版本规整很多,在ULP基础上,所有的功能都可以很容易实现,让实现者更多的关注于业务逻辑而不是平台相关的细节,这是多么的OO啊!

UTP Demo-Caesar cipher

好了,以下才是本文我想着重表达的东西,即正文。

??本文中,我想给出一个Demo,实现一个无缝的凯撒加密传输,所谓的无缝,就是说交互双方并不了解自己的数据被凯撒加密算法加密了,应用程序依然使用send/recv这类socket接口,然而仅仅通过一个显式的setsockopt或者一个被劫持的setsockopt调用,数据就被加密了,待数据到达接收端的时候,它被自然解密而还原。秘密何在,我来揭开!

??首先看一下传输双方的代码,我以一个典型的基于TCP协议的C/S程序为例,首先我给出C和S的代码:

服务端代码(编译成server)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/ www.rbuluoyl.c types.h>
#include <netinet/tcp.h>
#include <unistd.h>

int port = 1234;
// 两边的key必须一致
int key = 12354;

#define TCP_ULP 31
#define TCP_NONE 100

int main()
{
int sockfd;
int ret;
char buf[256];
struct sockaddr_in serveraddr;
struct sockaddr_in clientaddr;
int addr_len = sizeof(clientaddr);
int clientfd;

bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
perror("socket error_1");
return 1;
}

ret = bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1){
perror("bind error_1");
close(sockfd);
return 1;
}

ret = listen(sockfd, 5);
if (ret < 0) {
perror("listen");
close(sockfd);
return 1;
}

while(1){
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, (socklen_t*)&addr_len);
if(clientfd < 0) {
perror("accept");
continue;
}
// 设置使用支持凯撒加密的ULP
setsockopt(clientfd, SOL_TCP, TCP_ULP, "Caesar cipher", sizeof("Caesar cipher"));
perror("ulp init");
// 设置加密算法的key
setsockopt(clientfd, SOL_TCP, TCP_NONE, &key, sizeof(int));
perror("cipher init");

ret = recv(clientfd, www.gouyifl.cn/ buf, sizeof(buf), 0);
if (ret < 0) {
perror("recv error");
close(clientfd);
close(sockfd);
return 1;
}
buf[ret] = 0;
printf("%s\n", buf);
close(clientfd);
}
close(sockfd);
return 0;
客户端代码(编译成client)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/www.thd580.com in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/types.h>
#include <netinet/tcp.h>
#include <unistd.h>

int port = 1234;
// 两边的key必须一致
int key = 12354;

#define TCP_ULP 31
#define TCP_NONE 100

int main()
{
int sockfd;
int i = 0;
int ret;
struct sockaddr_in serveraddr;
char *buf = "abcdefg";

bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

sockfd = socket(AF_www.tianhengyl1.com INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket error!");
return 1;
}
// 设置使用凯撒加密的ULP
setsockopt(sockfd, SOL_TCP, TCP_ULP, "Caesar cipher", sizeof("Caesar cipher"));
perror("ulp init");
// 设置加密算法的key
setsockopt(sockfd, SOL_TCP, TCP_NONE, &key, sizeof(int));
perror("cipher init");

ret = connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (ret < 0) {
perror("connect");
close(sockfd);
return 1;
}

ret = send(sockfd, buf, sizeof(buf), 0);
if (ret < 0) {
perror("recvfrom error");
perror("connect");
return 1;
}

close(sockfd);
return 0;
先不要看代码里的那些setsockopt,只管看send/recv这些,可以看到和往常并没有什么不一样,客户端发送给服务端一串字符串“abcdefg”,服务端如实打印出来的就是“abcdefg”,毫不失真,然而,如果你抓包,就会看到下面的包:

这里写图片描述

显然,数据被加密了!

??没有隧道,没有任何额外的外部配置,仅仅一个setsockopt就能让数据被加密?匪夷所思吧…这就是ULP实现的表示层,我们马上就来看看这是怎么实现的。

非常简单,内核中的ULP框架实际上做了两件事:

ULP支持框架做的事
替换setsockopt为自己的setsockopt回调,默认调用原始的setsockopt。

具体ULP实现做的事
实现setsockopt回调,在setsockopt回调中替换proto结构体,择情况重新实现proto结构体中的sendmsg,sendpage,recvmsg等回调函数。

实现有点简陋,但先不评鉴,先实现了一个Demo再说,我接下来马上就给出实现,即凯撒加密的实现,代码如下(编译成Caesar_cipher.ko):

#include <linux/module.h>
#include <net/tcp.h>

static struct proto Caesar_cipher_base_prot;
static struct proto ulp_Caesar_cipher_prot;

#define TCP_NONE 100

struct Caesar_cipher_context {
// 简陋的密钥
int key;
// 保留的原始的proto结构的setsockopt函数
int (*setsockopt)(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen);
// 保留的原始的proto结构的getsockopt函数
int (*getsockopt)(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen);
// 保留的原始的proto结构的sendmsg函数
int (*sendmsg)(struct sock *sk, struct msghdr *msg, size_t len);
// 保留的原始的proto结构的recvmsg函数
int (*recvmsg)(struct sock *sk, struct msghdr *msg, size_t len, int noblock, int flags, int *addr_len);
// 保留的原始的proto结构的close函数
void (*close)(struct sock *sk, long timeout);
};

static inline struct Caesar_cipher_context *Caesar_cipher_get_ctx(const struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
return icsk->icsk_ulp_data;
}

// 感觉实现这个没有什么意义,暂不实现
static int Caesar_cipher_getsockopt(struct sock *sk, int level, int optname,
char __user *optval, int __user *optlen)
{
struct Caesar_cipher_context *ctx = Caesar_cipher_get_ctx(sk);
return ctx->getsockopt(sk, level, optname, optval, optlen);
}

static int Caesar_cipher_setsockopt(struct sock *sk, int level, int optname,
char __user *optval, unsigned int optlen)
{
struct Caesar_cipher_context *ctx = Caesar_cipher_get_ctx(sk);
struct proto *prot = NULL;
int val;

if (level != TCP_NONE) {
return ctx->setsockopt(sk, level, optname, optval, optlen);
}

if (get_user(val, (int __user *)optval)) {
ctx->key = 0;
} else {
ctx->key = val;
}

// 替换socket的proto并保留原始的close回调
prot = &ulp_Caesar_cipher_prot;
ctx->close = sk->sk_prot->close;
sk->sk_prot = prot;

return 0;
}

int Caesar_cipher_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
struct Caesar_cipher_context *ctx = Caesar_cipher_get_ctx(sk);
char *userbuf;
struct msghdr newmsg;
struct iovec iov[1];
int err;

mm_segment_t oldfs = get_fs();

// 分配一个新的buffer,这是为了用户空间和内核空间冲突
userbuf = vmalloc(size);
if (!userbuf) {
return -ENOMEM;
}

iov[0].iov_base = userbuf;
iov[0].iov_len = size;
err = memcpy_from_msg(userbuf, msg, size);
if (err) {
vfree(userbuf);
return -EINVAL;
}

// inline encrypt 凯撒加密操作
{
int i = 0;
for (i = 0; i < size; i++) {
userbuf[i] = userbuf[i] + ctx->key;
}
}

iov_iter_init(&newmsg.msg_iter, WRITE, iov, 1, size);
newmsg.msg_name = NULL;
newmsg.msg_namelen = 0;
newmsg.msg_control = NULL;
newmsg.msg_controllen = 0;
newmsg.msg_flags = msg->msg_flags;

// 申明现在是在内核态发送数据
set_fs(KERNEL_DS);
// 调用原始的sendmsg发送新分配的buffer
err = ctx->sendmsg(sk, &newmsg, size);
set_fs(oldfs);

vfree(userbuf);

return err;
}

// 本回调函数的实现和sendmsg思路无异
int Caesar_cipher_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
struct Caesar_cipher_context *ctx = Caesar_cipher_get_ctx(sk);
int ret, err;

char *userbuf;
struct msghdr newmsg;
struct iovec iov[1];

mm_segment_t oldfs = get_fs();

userbuf = vmalloc(len);
if (!userbuf) {
return -ENOMEM;
}

iov[0].iov_base = userbuf;
iov[0].iov_len = len;

iov_iter_init(&newmsg.msg_iter, READ, iov, 1, len);
newmsg.msg_name = NULL;
newmsg.msg_namelen = 0;
newmsg.msg_control = NULL;
newmsg.msg_controllen = 0;
newmsg.msg_flags = msg->msg_flags;

set_fs(KERNEL_DS);
ret = ctx->recvmsg(sk, &newmsg, len, nonblock, flags, addr_len);
set_fs(oldfs);

// inline decrypt 凯撒解密操作
{
int i = 0;
for (i = 0; i < ret; i++) {
userbuf[i] = userbuf[i] - ctx->key;
}
}

err = memcpy_to_msg(msg, userbuf, ret);
if (err) {
ret = -EINVAL;
}

vfree(userbuf);
return ret;
}

static int Caesar_cipher_init(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct Caesar_cipher_context *ctx;
int rc = 0;

ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx) {
rc = -ENOMEM;
goto out;
}

icsk->icsk_ulp_data = ctx;

// 保留set/getsockopt的核心操作
ctx->setsockopt = sk->sk_prot->setsockopt;
ctx->getsockopt = sk->sk_prot->getsockopt;
// 保留send/recvmsg的核心操作
ctx->sendmsg = sk->sk_prot->sendmsg;
ctx->recvmsg = sk->sk_prot->recvmsg;

// 整体替换socket的proto操作回调,但事实上就是复制了原始的proto操作回调,真正替换操作在setsockopt中进行
sk->sk_prot = &Caesar_cipher_base_prot;
out:
return rc;
}

static void Caesar_cipher_close(struct sock *sk, long timeout)
{
struct Caesar_cipher_context *ctx = Caesar_cipher_get_ctx(sk);
kfree (ctx);
ctx->close(sk, timeout);
}

static struct tcp_ulp_ops tcp_Caesar_cipher_ulp_ops __read_mostly = {
.name = "Caesar cipher",
.owner = THIS_MODULE,
.init = Caesar_cipher_init,
};

static int __init Caesar_cipher_register(void)
{
Caesar_cipher_base_prot = tcp_prot;
Caesar_cipher_base_prot.setsockopt = Caesar_cipher_setsockopt;
Caesar_cipher_base_prot.getsockopt = Caesar_cipher_getsockopt;

// 替换个别的回调函数
ulp_Caesar_cipher_prot = Caesar_cipher_base_prot;
ulp_Caesar_cipher_prot.sendmsg = Caesar_cipher_sendmsg;
ulp_Caesar_cipher_prot.recvmsg = Caesar_cipher_recvmsg;
ulp_Caesar_cipher_prot.close = Caesar_cipher_close;

// 这个注册不再解释,无非就是把一个结构体链入一个链表,等到setsockopt执行ULP绑定时再查找而已
tcp_register_ulp(&tcp_Caesar_cipher_ulp_ops);

return 0;
}

static void __exit Caesar_cipher_unregister(void)
{
tcp_unregister_ulp(&tcp_Caesar_cipher_ulp_ops);
}

module_init(Caesar_cipher_register);
module_exit(Caesar_cipher_unregister);

MODULE_AUTHOR("Zhao Ya <[email protected]>");
MODULE_DESCRIPTION("Linux 4.14 ULP Demo");
MODULE_LICENSE("GPL");
根据以上的描述和源码,这就很容易理解为什么需要两个setsockopt来完成注册和绑定了。第一个setsockopt只是替换了setsockopt函数,第二个setsockopt替换了部分的proto操作回调并且设置了加密密钥,然后新的proto操作就可以自由挥洒了,仅此而已。

??如果你仔细看KTLS的实现,它也仅此而已。妙处就在ULP而别无他。然而,然而我不是说可以无缝进行应用程序的数据加密吗?如果应用程序不能改,也就说说不能分别加入那两个setsockopt,那可怎么办?简单!用Linux的LD_PRELOAD来劫持系统调用即可,仅以服务端为例。请把上述的服务端的C代码中的setsockopt分别注释掉,然后编译以下的代码:

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include<errno.h>
#include <netinet/tcp.h>

#define TCP_ULP 31
#define TCP_NONE 100

int key = 12354;

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
{
int clisd;
typeof(accept) *_accept;

_accept = dlsym(RTLD_NEXT, "accept");
clisd = (*_accept)(sockfd, addr, addrlen);
setsockopt(clisd, SOL_TCP, TCP_ULP, "Caesar cipher", sizeof("Caesar cipher"));
perror("client ulp init");
setsockopt(clisd, SOL_TCP, TCP_NONE, &key, sizeof(int));
perror("client cipher init");

return clisd;
编译方法如下:

gcc -shared -fPIC preload_ulp.c -o preload_ulp.so
1
然后再运行服务端前引用一个环境变量:

LD_PRELOAD=./preload_ulp.so ./server
1
即可!

??好啦,现在可以统一运行它们了。首先加载ULP内核模块:

insmod ./Caesar_cipher.ko
1
然后执行server:

LD_PRELOAD=./preload_ulp.so ./server
1
最后执行client:

./client
1
OK,服务端完美打印出了“abcdefg”。结束了吗?结束了!

现在要评鉴它了,ULP确实有点Low,基于它的Low,我也只能实现一个凯撒加密,更高大上的实现我止步于那些复杂的数据结构解析,当然这可能是因为我术业不精所致,但这难道不也印证了ULP的接口不友好吗?一个友好的接口可以让人们编程像是烧锅炉一样,就像吃饭一样,当一个接口调用像是做引体向上一样的时候,那就悲哀了!

??我的感触是,并且我的愿望是,实现一个iptables的target,可以为任意一个既有的连接在端到端实现无缝的凯撒加密,比如这样:

iptables -A INPUT -s 1.1.1.1 -p tcp --sport 123 -d 2.2.2.2 --dport 234 -j GaiusDecrypt --key 12354
iptables -A OUTPUT -s 1.1.1.1 -p tcp --sport 123 -d 2.2.2.2 --dport 234 -j GaiusDecrypt --key 12354
1
2
然后对应五元组TCP-1.1.1.1:123/2.2.2.2:234流量就被密钥为12354给加密了,这比较帅了,所需要做的只是在target处理函数中查找一下socket然后调用一下setsockopt而已,或者更加简单的,找到conntrack,然后将key设置到conntrack表项。然而这只是一个非常简单的思路,更加复杂的玩法尽在想象中,完全不受限制。

过程

编写这个Caesar cipher内核模块是艰辛的,这倒不是说ULP原理上有多难,代码多么难写,事实上代码是很简单的,问题出在环境的搭建上。

??我的手头有Debian 9的开发机,它内置了4.9的内核,已经够新的了吧,但是很抱歉,ULP以及KTLS只在4.13+上被支持,这个从下面的链接可以看到:
Linux 4.13#Networking:https://kernelnewbies.org/Linux_4.13
所以我选择了4.14版本进行Demo的开发。我为源码编译分配了4G的磁盘空间,使用Debian 9自带的config文件进行编译,然而没过多久就提示空间不足,我只能全部重来,这个时候我把空间加大了一倍,到达8G的样子,于是我就睡觉了,次日醒来,几个提示空间不足的大字扎瞎了我的眼睛…由于我看到已经到最后一步了,所以我把空间加大到了9G,开启脚本后就去上班去了,晚上下班到家,终于搞定了….这才开始写这个Demo,空间不足问题足足耗了我一天一夜的时间!

??我在想什么时候编译一个内核都需要这么大的磁盘空间了,难道诸发行版真的是越来越臃肿了吗?这还怎么玩啊!都胖成这样了,为何还不减肥…










































































































































































































































































































以上是关于Linux 4.13/4.14内核中带来的ULP(Upper Layer Protocol)的主要内容,如果未能解决你的问题,请参考以下文章

Java:添加/减去Math.ulp()与Math.nextAfter()

Linux内核启动

深入分析Linux内核链表

MicroPython ESP32超低功耗协处理器(ULP):睡眠模式示例详解

Linux内核分析(第七周)

Linux内核分析07