Linux内核TCP部分代码分析
这段时间为了分析TCP SACK的洞,所以研究了一下TCP在Linux内核中的实现。
Linux内核中关于TCP的数据结构
在sock.h
中定义了一个struct sock
结构体,用来存储每个socket的数据,在内核代码中该结构的变量名一般都是sk
。
在该结构体中,有一个成员变量struct sk_buff_head sk_write_queue;
为发送队列的链表,通过该链表来查询需要发送的数据包。
数据包的数据结构在skbuff.h
中定义:sk_buff
,下面来讲讲该数据结构中比较重要的几个变量:
1 | struct tcp_skb_cb { |
变量cb
其实就是tcp_skb_cb
结构体,可以通过该变量查看该数据包的seq和tcp_gso信息。
len
表示的是该数据包中TCP数据的总长度,为线型数据的长度+非线形数据的长度,data_len
表示的是非线形数据的长度。
data
变量指向的是线性数据,tail
和end
都是4字节数据,表示偏移。
head + tail => data
head + end => 非线性数据
线性数据指针是直接指向数据,但是非线性数据执行的是一个结构体:
1 |
|
在非线性数据中,储存的是内存页page
数据结构,存储在frags
数组中,长度为MAX_SKB_FRAGS
,在SACK漏洞中,要求该长度大于等于17, 所以要求:
- 65536/PAGE_SIZE >= 16
- PAGE_SIZE <= 4k = 4096
默认情况下PAGE_SIZE = 1<<12 = 4k
,从这里就产生了一个问题,从MAX_SKB_FRAGS
的宏定义我猜测,最初定义该结构体的时候,应该是设定非线性区域的最大长度64k,所以才有了#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 1)
,默认情况下,一页的大小是4k,非线性区域一定可以存储17页,那么怎么来的17 * 32 * 1024
?这个疑问一直没得到解决,暂时放下,继续看看数据结构。
怎么通过page
结构体获取到存储数据的地址?一般是通过page_to_virt
宏定义,这个比较复杂,根据不同情况有不同的算法,在ubuntu18.04的内核中,公式为:
- page地址最低4位<<6 + 内核基地址
Linux内核中关于TCP的函数
说完了几个比较重要的数据结构,接下来就说说几个比较重要的函数
1. tcp_output.c中的tcp_write_xmit
在内核打包完要发送的数据后,通过该函数把skb按照MSS来进行分片,发往下一层。我们可以在while ((skb = tcp_send_head(sk)))
内核该代码处下断点,来查看发送的数据包,这个时候发现,每个skb的最大大长度不会超过64k。
2. tcp_output.c中的tso_fragment
根据mss进行分片,假设skb包的长度为17 * 32 * 1024,mss被设置成了8,那么在该函数调用tcp_set_skb_tso_segs
就会导致溢出。
3. route.c中的__ip_rt_update_pmtu
可以利用ICMP的frag_needed(PMTU)在发包的过程中修改MSS,但是却不是默认设置:
1 | static int ip_rt_min_pmtu __read_mostly = 512 + 20 + 20; |
默认情况下最小只能把MTU设置到552,但是可以通过修改/proc/sys/net/ipv4/route/min_pmtu
文件,把MTU设置成48,因为不是默认设置,所以后续的研究就没再关注PMTU了。
4. tcp_input.c中的tcp_sacktag_walk
当TCP接收到SACK,就会根据值进入到该函数
5. tcp_input.c中的tcp_shift_skb_data
在tcp_sacktag_walk
中找到对应的skb,传入到该函数中,判断是否能把数据进行合并。有如下要求:
1 | /* Normally R but no L won't result in plain S */ |
- 是DACK,或者没被设置为LOST状态(TCPCB_LOST)。
- skb_can_shift用来判断skb只存在非线性数据。
- tp->snd_una表示最后一个被确认接受的seq,表示已经被接受的包,不会被合并。
- 如果该skb处于发送队列的第一位,不会进行合并操作。
- 获取skb前一个数据包,并且该数据包也是被SACK要求重发的,skb才能被合并进prev中
6. skbuff.c中的skb_shift
经过一系列判断后,prev和skb将会进入skb_shift函数,把skb中的非线性数据储存到prev中。这里又有一个问题,合并操作只是移动一下skb_frag_struct
结构体中的长度和偏移,skb中本身page的长度是0x1000,并不会通过该函数变成一个page的长度0x2000。所以一开始发包page的长度限制也限制了合并操作,一开始发包一个skb的最大长度不超过0xffff,所以合并操作也没办法让skb的非线性区域最大长度超过0xffff。
7. tcp_input.c中的tcp_shifted_skb
在tso_fragment中溢出后,如果执行到该函数,就会触发内核crash。
1 | BUG_ON(tcp_skb_pcount(skb) < pcount); |
总结1
当MSS设置为48时,需要通过发送至少4个数据包才能把MSS设置成8,但是这个时候大的skb数据包已经根据mss被拆分成了小的skb数据包了,所以之后需要通过SACK再让skb进行合并。
因为不能合并已经被确认的数据包,并且有拥塞窗口rwnd,内核一次只会发送rwnd数量的数据包,直到收到ACK确认回复,才会继续发送。所以这个时候需要增加拥塞窗口,让内核一次性发的包除以8能溢出。经过我的测试,这个并不难实现,不过中间遇到一个问题,TCP中滑动窗口为2字节,最大也就65535,所以需要使用TCP的options: (“WScale”, 14)来扩窗,简单的测试代码如下:
1 | SACK = TCP(sport=sport, dport=dport, flags='A', seq=SYNACK.ack, ack=SYNACK.seq+1, window=65535) |
拥塞窗口达到需要的值后,就是发送SACK对包进行合并了,但是发现没法把skb合并到17 * 32 * 1024
,一个skb最大只能达到:17 * 4 * 1024 / 36 * 36
,该大小收到了最初skb大小的限制。
TCP发送数据的两种方式
在对TCP的代码经过一系列的研究后发现,并没法合并17个32kb的数据包,所以开始研究数据到skb的过程。
sendpage
服务器我使用的是sendfile来发送大文件的,服务端代码如下:
1 | #!/usr/bin/env python3 |
在tcp.c
中的do_tcp_sendpages
申请了最初的skb,并把page放入skb的非线性区域。这个时候page的size就已经是4k的大小了,所以开始回溯page是在什么时候生成的,该大小是怎么判断的。
最终追溯到了splice.c
的__generic_file_splice_read
函数:
1 | #define PAGE_CACHE_SIZE PAGE_SIZE |
变量this_len
为page的最大长度,该变量的最大值为PAGE_CACHE_SIZE = PAGE_SIZE = 1<<12
,代码分析到这里,就感觉没法把page的大小设置为32k啊。
sendmsg
随后继续分析,TCP发送数据除了用sendpage,还有一个叫sendmsg。使用socket的send
, sendmsg
函数发送数据后,都会调用该函数。
在tcp.c
中的tcp_sendmsg
函数中进行skb的初始化:
1 | skb = sk_stream_alloc_skb(sk, |
但是却有一个问题,sk_stream_alloc_skb
函数的第二个参数为线性数据的长度,在sendpage
函数中为0,所以在该函数中只存在非线性数据。
但是在sendmsg
中就有设置线性数据了,该值由select_size
函数确定,不过我觉得这个问题不大,因为该值最大也就一两千,只要sack的skb中不包含线性数据就好了,虽然麻烦点,但也不是不行。
所以重要的还是看非线性数据区域:
1 | bool merge = true; |
page是通过sk_page_frag_refill -> skb_page_frag_refill
进行分配的:
1 | /* On 32bit arches, an skb frag is limited to 2^15 */ |
这么一看,frags数组中,一页的最大值的确是32k。但是在继续研究的过程中发现:
1 | mss_now = tcp_send_mss(sk, &size_goal, flags); |
有一个size_goal
在限制着skb->len
的长度,该值是在tcp_send_mss
函数中被设置的:
1 | static unsigned int tcp_xmit_size_goal(struct sock *sk, u32 mss_now, |
该值受sk_gso_max_size
变量的影响,但是在回溯该值的时候,发现该值默认情况是65535,这又导致了没法让初始的skb长度达到17 * 32k
。
能把page的长度设置为32k,但是skb的长度只有不到65535,能通过sack的合并操作把skb设置成17 * 32k
吗?
经过研究发现是不行的,因为skb有线性数据,先不考虑mss的问题,假设线性数据的长度是2000,skb->len = 65535,则frags的长度为63535,通过sack,能让这部分的skb合并成一个。但是合并成功后,下一个skb就是线性数据,不能进行合并,这样就导致了再下一块skb是和当前的线性数据的skb进行合并,不会和前面63535长度的skb进行合并。
总计
目前的研究进展还是卡在了17 * 32 * 1024
的skb长度上,如果使用sendpage发送数据,page的长度没法达到32k,如果使用sendmsg发送数据,又有线性数据的干扰。
但是已经有人录了个利用的视频:https://blog.csdn.net/dog250/article/details/95387061
还有他的分析文章:https://blog.csdn.net/dog250/article/details/95252740
但是很疑惑的是,他的分析从头到位就没提过32k的问题,而且他服务端应该用的也是sendfile进行发送数据。感觉page能等于32k是一件很自然,本应该的事,这让我感到很疑惑。是系统的问题?还是特殊配置?内核编译选项?
我测试过内核4.15的ubuntu18.04和内核4.4的ubuntu16.04的,在这两款内核下,默认情况我无法让skb的长度达到17 * 32kb
。
Linux内核TCP部分代码分析