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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct tcp_skb_cb {
__u32 seq;
__u32 end_seq;
union {
__u32 tcp_tw_isn;
struct {
u16 tcp_gso_segs;
u16 tcp_gso_size;
};
};
.......
}

#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))

struct sk_buff {
......
char cb[48] __aligned(8);
......
unsigned int len,
data_len;
......
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
......
}

变量cb其实就是tcp_skb_cb结构体,可以通过该变量查看该数据包的seq和tcp_gso信息。

len表示的是该数据包中TCP数据的总长度,为线型数据的长度+非线形数据的长度,data_len表示的是非线形数据的长度。

data变量指向的是线性数据,tailend都是4字节数据,表示偏移。

head + tail => data
head + end => 非线性数据

线性数据指针是直接指向数据,但是非线性数据执行的是一个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define PAGE_SHIFT	12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#if (65536/PAGE_SIZE + 1) < 16
#define MAX_SKB_FRAGS 16UL
#else
#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 1)
#endif
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))

struct skb_shared_info {
unsigned char nr_frags;
......
skb_frag_t frags[MAX_SKB_FRAGS];
};

typedef struct skb_frag_struct skb_frag_t;

struct skb_frag_struct {
struct {
struct page *p;
} page;
#if (BITS_PER_LONG > 32) || (PAGE_SIZE >= 65536)
__u32 page_offset;
__u32 size;
#else
__u16 page_offset;
__u16 size;
#endif
};

在非线性数据中,储存的是内存页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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Normally R but no L won't result in plain S */
if (!dup_sack &&
(TCP_SKB_CB(skb)->sacked & (TCPCB_LOST|TCPCB_SACKED_RETRANS)) == TCPCB_SACKED_RETRANS)
goto fallback;
if (!skb_can_shift(skb))
goto fallback;
/* This frame is about to be dropped (was ACKed). */
if (!after(TCP_SKB_CB(skb)->end_seq, tp->snd_una))
goto fallback;
/* Can only happen with delayed DSACK + discard craziness */
if (unlikely(skb == tcp_write_queue_head(sk)))
goto fallback;
prev = tcp_write_queue_prev(sk, skb);

if ((TCP_SKB_CB(prev)->sacked & TCPCB_TAGBITS) != TCPCB_SACKED_ACKED)
goto fallback;
  1. 是DACK,或者没被设置为LOST状态(TCPCB_LOST)。
  2. skb_can_shift用来判断skb只存在非线性数据。
  3. tp->snd_una表示最后一个被确认接受的seq,表示已经被接受的包,不会被合并。
  4. 如果该skb处于发送队列的第一位,不会进行合并操作。
  5. 获取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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
SACK = TCP(sport=sport, dport=dport, flags='A', seq=SYNACK.ack, ack=SYNACK.seq+1, window=65535)
SACK.options = [("WScale", 14)]

SACK.ack = seq + 1
SACK.ack += 36 * (11 * 2 ** 0)
send(ip/SACK)

time.sleep(0.1)

SACK.ack += 36 * (11 * 2 ** 1)
send(ip/SACK)

time.sleep(0.5)

SACK.ack += 36 * (11 * 2 ** 2)
send(ip/SACK)

time.sleep(0.5)

SACK.ack += 36 * (11 * 2 ** 3)
send(ip/SACK)

time.sleep(0.5)

SACK.ack += 36 * (11 * 2 ** 4)
send(ip/SACK)

time.sleep(1)

SACK.ack += 36 * (11 * 2 ** 5)
send(ip/SACK)

time.sleep(1)

SACK.ack += 36 * (11 * 2 ** 6)
send(ip/SACK)

time.sleep(1)

SACK.ack += 36 * (11 * 2 ** 7)
send(ip/SACK)

time.sleep(1.5)

SACK.ack += 36 * (11 * 2 ** 8) - 36
send(ip/SACK)

time.sleep(2)

SACK.ack += 36 * (11 * 2 ** 9)
send(ip/SACK)

time.sleep(2)

SACK.ack += 36 * (11 * 2 ** 10)
send(ip/SACK)

拥塞窗口达到需要的值后,就是发送SACK对包进行合并了,但是发现没法把skb合并到17 * 32 * 1024,一个skb最大只能达到:17 * 4 * 1024 / 36 * 36,该大小收到了最初skb大小的限制。

TCP发送数据的两种方式

在对TCP的代码经过一系列的研究后发现,并没法合并17个32kb的数据包,所以开始研究数据到skb的过程。

sendpage

服务器我使用的是sendfile来发送大文件的,服务端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# -*- coding=utf-8 -*-

import socket

HOST = '0.0.0.0'
PORT = 8888

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print('Connected by', addr)
with open("test.bin", "rb") as f:
conn.sendfile(f, 0)
input()

tcp.c中的do_tcp_sendpages申请了最初的skb,并把page放入skb的非线性区域。这个时候page的size就已经是4k的大小了,所以开始回溯page是在什么时候生成的,该大小是怎么判断的。

最终追溯到了splice.c__generic_file_splice_read函数:

1
2
3
4
5
6
#define PAGE_CACHE_SIZE		PAGE_SIZE

/*
* this_len is the max we'll use from this page
*/
this_len = min_t(unsigned long, len, PAGE_CACHE_SIZE - loff);

变量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
2
3