抽了点时间研究了下KTLS,这源自于跟同事交流的一个问题,那就是现如今的HTTPS服务器以及scp命令传输本地文件的时候,无法使用sendfile系统调用!
这个话题让我想起了很多的老同事,为了不骚扰到他们,本文中我一律使用只有我们自己才知道的昵称。
我后悔当初为什么没有想到一个让HTTPS/scp支持sendfile/splice/tee调用族的方案,我表示后悔是因为这乃是比OpenVPN更加能体现业界影响力以及个人价值的途径,不管是对公司还是对个人,都将是价值无限的。并且,这是一个融合了系统调用,网络技术,加密解密技术于一体的方向。
我在2010年初经历了第二次迷茫之后的一年,苦苦寻求方向,在我一头扎进半瓶子的OpenVPN细节以及一知半解地搞定的原始版国标ECC加密算法与OpenSSL的融合之后,我选择了OpenVPN,之后我等于说再也没有在SSL协议方面有所突破,一去就是四五年!我本来可以做一个类似AF_KTLS的东西的,但是一次又一次的与之擦肩而过,直到现在,我已经离开了原来的公司,并且甚至都不再做任何与SSL/TLS以及PKI相关的事情,却突然让我知道了有一个来自Facebook现成的方案,这已经到了2016年!Facebook的方案来自2015年,其实我们可以更早的,但是没有,所以我表示非常悔恨!我之所以这么说,是因为几乎不用你仔细想就能Step by step跟上Dave Watson的思路,一切都显得那么自然和直接,就像KTLS是一个理所当然的方案一样,我曾经发表过《OpenVPN的linux内核版,鬼魅的残缺》这一系列文章,并且做了相同的事,但是差那么一点,如果我当时不是在优化VPN,而是在优化正向代理或者反向代理,或者在优化HTTPS服务器本身,那么我认为移进内核的应该就是TLS记录协议了。我摘录一句来自文章《 OpenVPN的Linux内核版,鬼魅的残缺 part IV:Normal Method 》的话:
1.内核模块:运行一个内核模式的TCP服务器,收到数据后按照数据包的第一个字节的值区分是控制通道数据还是数据通道数据,如果是控制通道数据,则通过一个字符设备路由给用户态的一个进程,如果是数据通道数据,则直接在内核处理;
那是在2014年!
另外一个插曲那就是,在2010年,我并不知道OpenVPN有自己的封装协议,我一直以为OpenVPN就是用SSL记录协议来封装数据的,这显然是最初对OpenVPN的错误认知,但如果我一直错下去并一直错到2014年往内核移植数据通道处理逻辑时,我依然也会把TLS记录协议扔进内核。虽然,我使用了Netfilter模块机制而不是socket机制...
正文之前,煽点情吧,温州皮鞋厂老板,王姐姐,小雨,小群群,木经理,主音吉他手,华叔...唉,九味杂陈啊...
另外,KTLS已经出来一年多了,网上的资源非常有限,除了google可以搜到的几个patch就是来自Dave Watson的github本身了,中文社区几乎没有讨论这个的,百度搜索的结果更是渺渺,所以我写了这篇文章,就像关于TCP BBR和TCP CDG的科普文章一般,我感觉我又一次占了沙发。
以上乃例行感慨,没有感慨是不成文的,每一篇文章都是情感的迸发,再加上半夜起来发现天有下雨,就不再睡了,我本人是比较喜欢下雨的。
-----------------------------------
我们知道,sendfile可以让文件系统直接和另一个文件系统通信,用通信网络的术语来讲,用户程序仅仅处在控制平面,数据平面完全在内核中进行,这样可以避免内存拷贝以及各种切换,从而大大提高性能。一般的WEB服务器或者文件服务器都是采用sendfile来将一个文件直接发送到一个socket的,但是对于HTTPS,这样不行,因为内核中无法处理加密,你总是要把数据拉到用户态,将其加密,然后再封装成TLS记录协议,然后再Write到socket...
且慢!不是有AF_ALG socket吗?不错,这正是本文的内容之一,现在还不是时候详解。
不光HTTPS,就连系统开发或者运维人员平日非常常用SSH套件中scp,也无法使用sendfile,理由同上!我们可以从下面的图中看出究竟。
同样的思路,请看一个关于在BSD系统优化HTTPS的介绍《 Optimizing TLS for High Bandwidth Applications in FreeBSD 》。但是本文无意详述sendfile/splice/tee这个系统调用族,这些只是一个引子,我想就这个引子展开针对内核态TLS的讨论。本文的结构大致如下:
0.例行的感慨
1.介绍KTLS的原理
2.介绍如何把Dave的例子跑通
3.对KTLS简单的进行评价
4.另外一种替代的方案
-----------------------------------
这次是Facebook带来了福音,参见《 TLS in the kernel 》,来自Facebook的Dave Watson引入了一个新型的socket,即AF_KTLS,它可以附着在一个TCP/UDP套接字上在内核态直接加密并且做TLS封装,其框架如下图所示:
按照作者所述,TLS握手逻辑还是在用户态完成,这个握手协议事实上是控制平面的事情。在AF_KTLS socket中,除了完成加密/解密以及记录协议封装/解封装之外,其它操作全部都由其附着于其上的TCP/UDP socket来完成,因此AF_KTLS socket暴露给用户态的接口就跟标准的AF_INET socket的TCP/UDP套接字一样,你当然可以通过sendfile给它发送文件!
原理就介绍到这里,非常简单!复杂的东西在于加密,解密,HMAC那些操作,这方面我不是专家,在PKI公司混迹了5年有余也只是略懂,所以说就不在这里装逼了。温州老板号称比我懂的多,但也只是号称,王姐姐那是真懂。
-----------------------------------
下面我们来看一下如何让Dave的例子跑起来。
1.KTLS源码下载
我们从KTLS的github上把源码Downlaod下来,并且编译,其地址如下: https://github.com/ktls/af_ktls
2.编译和加载
这个步骤我分为以下几个步骤,对于熟悉的人来讲,超级简单。
2.1.选择内核版本
我选择的是最新的Linux 4.9的内核,正好现在搞BBR算法,也是用的这个内核。估计忙完这一阵子,我要跟2.6.32内核告别了。
2.2.打补丁并重启到新内核
进入4.9内核根目录,直接执行 patch -p1 <$rfc5288.patch 的路径即可,然后例行地 make all -j20 && make modules_install && make install 即可。编译成功后重启到新内核,并且安排好新的4.9内核头文件路径,准备编译模块。
2.3.编译内核模块
此时开始编译af_ktls.c,直接make即可,skb_splice_bits这个函数的参数会报too many,按照4.9的内核fix掉即可。然后加载编译生成的af_ktls.ko。
3.KTLS实例下载
Dave顺便写了一个非常简单的使用KTLS进行记录协议封装的例子,基于OpenSSL的。这个例子的结构非常简单:
Server Thread:运行一个基于OpenSSL的服务端,在一个循环中调用SSL_write/SSL_read进行数据的读写,并输出结果;
注意:Dave的Demo就是使用的AF_ALG socket而不是AF_KTLS,起初我已经他搞错了,后来发现这是两个版本。
它内建了一个TLS服务端和一个TLS客户端,TLS客户端与TLS服务端成功完成TLS握手之后,客户端将用标准的OpenSSL调用SSL_read/SSL_write以及KTLS socket的send/recv/sendfile两种方式与服务端通信,旨在证明这两种方式的效果是等同的,从而就可以说明KTLS实现与OpenSSL的协议兼容性。
其github地址如下: https://github.com/djwatson/ktls 。照例我们应该把它下载到本地并编译运行。
-----------------------------------
然而,遗憾的是,这个例子并非针对上述AF_KTLS socket实现的,而是针对另一个基于AF_ALG socket的等效机制的(algif_tls patch,后面会谈)。我不知道Dave为什么重新基于AF_KTLS再Fork一个该Demo,虽然针对AF_KTLS基于OpenSSL的例子没有完成,但是Dave基于GUNTLS完成了一个更加正式的例子,它的地址如下: https://github.com/ktls/af_ktls-tool 。通过编译运行这个例子,你会发现KTLS确实是正确的且高效的。然而,我相信还是熟悉OpenSSL的比熟悉GNUTLS的要多。并且,ktls这个例子非常简单,就一个C文件,这种风格是我最喜欢的,简单清晰,不必陷入代码结构,语言以及工具的细节,所以说,我想完成上面那个基于OpenSSL的测试AF_KTLS的例子。
后来我花了点时间,终于把Dave的那个测试algif_tls的例子改成了测试AF_KTLS的例子并且调通了,并Fork了出来,地址在: https://github.com/marywangran/ktls/tree/patch-1 。
调试的过程非常乏味,主要就是“如何从OpenSSL的EVP_AES_GCM_CTX结构中取出Key,IV以及Salt问题”,这些都是在王姐姐的建议和指导下完成的。不得不说,调试加密算法是非常复杂的事情,这种事情我也干过,但时间不长,而王姐姐是这方面的老手!即便对于Dave Watson而言,可能真的就是因为加密算法,HAMC算法非常复杂才迫使其不得不先出一个仅仅支持GCM(aes) 128bit的简化版本的吧...不过听王姐姐说,RFC5288所描述的 AES Galois Counter Mode (GCM) Cipher Suites for TLS 要比CBC模式快很多,而安全性相当,并且不再需要独立计算HMAC,相信Dave也懂这个。所以说,Dave的所谓“简化版”KTLS并非因为这样实现简单。
关于加密算法就到此为止吧,这个周末抽点时间研究一下GCM的原理。
----------------
我的修改主要是把AF_ALG socket的操作改成了AF_KTLS socket操作:
.... // 等价的OpenSSL操作 /* Kernel TLS tests */ int tfmfd = socket(AF_KTLS, SOCK_STREAM, 0); if (tfmfd == -1) { perror("socket error:"); exit(-1); } struct sockaddr_ktls sa = { .sa_cipher = KTLS_CIPHER_AES_GCM_128, /* 指定cipher suit*/ .sa_socket = server, /* 指定附着的TCP socket */ .sa_version = KTLS_VERSION_1_2, }; if (bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa)) == -1) { perror("AF_ALG: bind failed"); close(tfmfd); exit(-1); } EVP_CIPHER_CTX * writeCtx = ssl->enc_write_ctx; EVP_CIPHER_CTX * readCtx = ssl->enc_read_ctx; EVP_AES_GCM_CTX* gcmWrite = (EVP_AES_GCM_CTX*)(writeCtx->cipher_data); EVP_AES_GCM_CTX* gcmRead = (EVP_AES_GCM_CTX*)(readCtx->cipher_data); unsigned char* writeKey = (unsigned char*)(gcmWrite->gcm.key); unsigned char* readKey = (unsigned char*)(gcmRead->gcm.key); unsigned char* writeIV = gcmWrite->iv; unsigned char* readIV = gcmRead->iv; char keyiv[20] = {0}; memcpy(keyiv, writeKey, 16); if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_KEY_SEND, keyiv, 16)) { perror("AF_ALG: set write key failed\n"); exit(-1); } memcpy(keyiv, writeIV, 4); if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_SALT_SEND, keyiv, 4)) { perror("AF_ALG: set write iv failed\n"); exit(-1); } uint64_t writeSeq; unsigned char* writeSeqNum = ssl->s3->write_sequence; memcpy(&writeSeq, writeSeqNum, sizeof(writeSeq)); if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_IV_SEND, (unsigned char*)&writeSeq, 8)) { perror("AF_ALG: set write salt failed\n"); exit(-1); } memcpy(keyiv, readKey, 16); if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_KEY_RECV, keyiv, 16)) { perror("AF_ALG: set read key failed\n"); exit(-1); } memcpy(keyiv, readIV, 4); if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_SALT_RECV, keyiv, 4)) { perror("AF_ALG: set read iv failed\n"); exit(-1); } uint64_t readSeq; unsigned char* readSeqNum = ssl->s3->read_sequence; memcpy(&readSeq, readSeqNum, sizeof(readSeq)); if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_IV_RECV, (unsigned char*)&readSeq, 8)) { perror("AF_ALG: set read salt failed\n"); exit(-1); } ....// 在tfmfd socket上的send/recv/sendfile,由tfmfd socket完成记录加密解密和协议的封装解封装,与其下的TCP/UDP socket交互。-----------------------------------
在继续另一个基于AF_ALG的KTLS方案之前,我这里插几句评价。
我个人觉得,KTLS并不仅仅是一个优化TLS的patch,而应该是一个名副其实的TLS,即传输层安全!在KTLS之前,基于OpenSSL等用户态库进行的封装都只能被成为“buffer安全”,并非真正的传输层安全,因为封装后的数据仅仅是一个socket的buffer。我个人比较关注整体架构,所以我才在这里咬文嚼字。
可能是因为Facebook没有Google那么大的影响力吧,加之Dave Watson这个人比较低调,所以KTLS才没有引来一阵吹捧甚至盲目跟风,相反,质疑的声音却不断,某种意义上这倒是好事,可以让人冷静地思考这么做是否真的有必要,真的有必要把TLS协议放在内核中实现吗?
诚然,Dave Watson本人也并不建议将整个TLS协议全部在内核层实现,而仅仅针对对称加密操作以及封装操作这种固定且稳定的操作。之所以将对称加密以及封装操作向下卸载,完全是因为这类操作是“固定的一套序列化的操作”。这就跟TCP将计算校验和的操作以及分段操作卸载到网卡而不把TCP三次握手以及拥塞控制卸载到网卡的道理一样,人们在希望通过向下卸载的手段达到优化的目的之前,必须权衡实现的复杂性以及效率,并在两者中间寻找一个平衡点。另外要考虑的就是,一旦卸载的部分在标准上发生了改变,升级操作的代价有多大。
TLS协议和TCP协议的发展非常类似,如果愿意,你也可以把TCP协议分为握手协议和记录协议,和TCP计算校验码以及分段一样,TLS的记录协议部分的操作非常固定,AES/DES这种对称加密算法非常稳定且固定,很久都不会发生改变,与之不同的是,TLS握手以及证书的处理却既复杂又多变。正如作者所希望的,KTLS希望乘着将对称操作从用户态程序库的剥离这种一鼓作气之风,最终推动网卡厂商来实现这个硬件卸载操作,即将对称加密和TLS记录协议封装的操作完全卸载到网卡!
不管怎样,KTLS可能还是不够火候,在文章《 TLS in the kernel 》的最后,有这么一段话:
Overall, it looks like it will take some more convincing arguments before putting TLS in the kernel will be seriously considered. For some specialized situations, it might make sense to do so, but even the limited version Watson posted adds more than 1200 lines of code to the kernel―for dubious gains. Over time, more and more crypto has been added to the kernel, though, so maybe TLS will eventually find its way in too.
我和温州皮鞋厂老板的对话就不贴了,温州老板特别想搞一个这样的项目制造点影响力,我表示我会全力支持。
----------------
我个人非常赞同Dave Watson的观点。但是,你可曾想到,假设这个KTLS被应用,那么将会有什么必须改变?!需要改变的是Apache,Nginx这种主流服务器的HTTPS处理逻辑,将所有调用SSL_read/write的地方改成基于KTLS的recv/send/sendfile,无疑,改变的代价非常巨大!
我称赞KTLS的朴素原因可能源自于我对OpenSSL的偏见,这属于个人情感的范畴,就不做煽动之辞了。
随着安全越来越重要,加密通信再也不是一个额外的组件了,它会内化成网络协议的一部分,这可以从HTTPS的逐渐风靡看出来,“buffer安全”还是“传输层安全”,哪个是你的追求呢?请继续管中窥豹吧!
-----------------------------------
现在该谈一下另一种方案了。
要实现KTLS,除了像上述那般直接实现一个AF_KTLS socket,还有一种方案,就是直接基于AF_ALG socket来实现,让AF_ALG socket可以做sendfile的fd_in参数!因此
事实上,这也是Dave Watson的那个algif_tls方案,关于该方案的patch以及讨论的patchwork地址是: https://patchwork.kernel.org/patch/7684551/
要想理解这种做法的合理性,我需要稍微花点篇幅来介绍一个Linux内核的AF_ALG机制。
AF_ALG是一种socket的类型,与AF_INET并列,该socket类型旨在导出一族用户接口,应用程序可以通过调用socket接口的方式来进行加密,解密操作。
关于AF_ALG的最初的patch介绍,请先看一下Lwn文章《 crypto: af_alg - User-space interface for Crypto API 》。如果你已经研究了代码,那么不难理出下图所示的关系:
首先,我们可以看出,最初通过socket建立的一个AF_ALG socket只是一个设置crypt机制的socket,最终需要调用accept来获取一个操作socket,这个过程非常类似TCP以及Linux的INIT进程!其次,我们注意到af_alg_type这个结构体非常重要,它存在的本意是让你可以实现不同的Crypto API,比如调用不同的加密算法,但它也使得自定义任何操作成了可能,你可以让一个AF_ALG socket在调用send的时候仅仅完成加密解密,然后在recv的时候将结果导出,也可以将一个AF_ALG OP socket附着在一个TCP socket或者UDP socket上,在调用send的时候,首先完成加密,然后将结果通过其附着的TCP socket或者UDP socket发出到网络,随便怎么做,取决于af_alg_type这个里面的回调你怎么实现。
AF_ALG Frame socket本身只是一个抽象的框架,具体的工作要通过将其与一个af_alg_type相关联,由其生成的一个OP socket来完成。这个原理为Dave实现algif_tls这个patch提供了依据,事实上,他就是实现了一个algif_tls结构体:
static struct proto_ops algif_tls_ops = { .family = PF_ALG, ..... // sendmsg/page 1.加密消息;2.通过TLS记录协议封装消息;3.发送到底层附着的TCP/UDP socket .sendmsg = tls_sendmsg, .sendpage = tls_sendpage, // recvmsg 1.获取底层附着的TCP/UDP socket的数据;2.解封装数据;3.解密数据后拷贝到用户态缓冲区 .recvmsg = tls_recvmsg, .poll = sock_no_poll, }; static const struct af_alg_type algif_type_tls = { // bing 创建底层crypto_aead基础设施 .bind = tls_bind, .release = tls_release, // setkey 在底层crypto_aead设施上设置密钥 .setkey = tls_setkey, .setauthsize = tls_setauthsize, // accept 获取一个OP socket用于实际的读写操作,并将以下的ops字段作为其操作回调集合 .accept = tls_accept_parent, // ops 实际的OP socket的回调集合 .ops = &algif_tls_ops, // name 在Frame socket进行bind的时候,可以通过这个name找到该algif_tls_ops,用于索引 .name = "tls", .owner = THIS_MODULE };我们来通过Dave的 tls.c 这个例子看一下到底如何操作:
/* Kernel TLS tests */ // 首先创建Frame socket int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0); // 创建一个索引结构体,用于查找已经注册进内核的af_alg_type struct sockaddr_alg sa = { .salg_family = AF_ALG, .salg_type = "tls", /* this selects the hash logic in the kernel */ .salg_name = "rfc5288(gcm(aes))" /* this is the cipher name */ }; // 根据上述的sa建立Frame socket与af_alg_type的关联 if (bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa)) == -1) // 获取一个操作实际IO的OP socket int opfd = accept(tfmfd, NULL, 0); ...// 以下OP socket的行为完全取决于af_alg_type中的ops字段指示的proto_ops回调集了。以上就是AF_ALG socket的框架以及Dave Watson在此框架上实现的不同于AF_KTLS的另一种KTLS方案。但是这并不是我要说的全部,既然AF_ALG的框架这么灵活,那为什么还要单独搞一个新的socket类型AF_KTLS呢?这可能是一个形而上的问题,我把 这篇文章 中的内容摘录如下,来自af_alg的作者Herbert Xu:
On Mon, Nov 23, 2015 at 09:43:02AM -0800, Dave Watson wrote:
> Userspace crypto interface for TLS. Currently supports gcm(aes) 128bit only,
> however the interface is the same as the rest of the SOCK_ALG interface, so it
> should be possible to add more without any user interface changes.
SOCK_ALG exists to export crypto algorithms to user-space. So if
we decided to support TLS as an algorithm then I guess this makes
sense.
However, I must say that it wouldn't have been my first pick. I'd
imagine a TLS socket to look more like a TCP socket, or perhaps a
于是,AF_KTLS难道就是一个更加合理的选择吗?
现在继续AF_KTLS和AF_ALG+自定义af_alg_type孰优孰劣的形而上讨论。个人认为还是AF_ALG+自定义af_alg_type更加灵活,你只需要自己实现一个af_alg_type并且注册进内核,就可以实现任何形式的封装,甚至实现一个完整的HTTP协议的处理过程。完全不像AF_KTLS那样自己实现一个完整的socket类型,那样会让你陷入类似ioctl分发类似的泥潭。诚然,sendfile将2.4内核的Kernel Web Server踢出了内核,但是AF_ALG+自定义af_alg_type则是更加通用的机制,它可以实现任何应用层协议!
-----------------------------------
还记得我在2014年到2015年间将OpenVPN的数据通道协议移植到了内核态,github地址在: https://github.com/marywangran/OpenVPN-Linux-kernel
这个工作与Dave Watson的工作非常类似,仔细研究了Dave的KTLS之后,我本来想花这个周末的时间重新再重构一下这个KOpenVPN了,但是显然感到无力,所以我退而求其次,我只是想基于AF_ALG+自定义af_alg_type来实现一个简单的HTTP协议,仅用来传输文件,大体逻辑非常简单,即通过sendfile将一个文件发送给一个AF_ALG OP socket,然后实现一个af_alg_type完成HTTP头的添加后再继续发给底层的INET socket。如果换一种方式,我也可以单独写一个AF_MYHTTP模块来用一个新型的socket更加直接地实现上述需求。
------------------------------------
在完成了这篇文章后,我的下一个挑战就是,2016年12月31日要在小小幼儿园的小区开阔场地开唱《新长征路上的摇滚》(这源自于几周前小小秋游时在路上我被疯子逼着唱了一首《假行僧》,然后幼儿园老师就非要让我在新年联欢会上再来一首摇滚...)...有点紧张,决定喝瓶酒再上!好在疯子可以陪我上去唱《One night in BeiJing》....有人陪我丢人,总比一个人丢人强吧...总之,加油吧!为了孩子,啥都可以付出,唱个歌丢一回人算个毛线啊!