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
4
skb = sk_stream_alloc_skb(sk,
select_size(sk, sg),
sk->sk_allocation,
skb_queue_empty(&sk->sk_write_queue));

但是却有一个问题,sk_stream_alloc_skb函数的第二个参数为线性数据的长度,在sendpage函数中为0,所以在该函数中只存在非线性数据。

但是在sendmsg中就有设置线性数据了,该值由select_size函数确定,不过我觉得这个问题不大,因为该值最大也就一两千,只要sack的skb中不包含线性数据就好了,虽然麻烦点,但也不是不行。

所以重要的还是看非线性数据区域:

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
bool merge = true;
int i = skb_shinfo(skb)->nr_frags;
struct page_frag *pfrag = sk_page_frag(sk);

if (!sk_page_frag_refill(sk, pfrag))
goto wait_for_memory;

if (!skb_can_coalesce(skb, i, pfrag->page,
pfrag->offset)) {
if (i == MAX_SKB_FRAGS || !sg) {
tcp_mark_push(tp, skb);
goto new_segment;
}
merge = false;
}

copy = min_t(int, copy, pfrag->size - pfrag->offset);

if (!sk_wmem_schedule(sk, copy))
goto wait_for_memory;

err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
pfrag->page,
pfrag->offset,
copy);

page是通过sk_page_frag_refill -> skb_page_frag_refill进行分配的:

1
2
3
4
5
6
7
8
9
/* On 32bit arches, an skb frag is limited to 2^15 */
#define SKB_FRAG_PAGE_ORDER get_order(32768)

if (SKB_FRAG_PAGE_ORDER) {
/* Avoid direct reclaim but allow kswapd to wake */
pfrag->page = alloc_pages((gfp & ~__GFP_DIRECT_RECLAIM) |
__GFP_COMP | __GFP_NOWARN |
__GFP_NORETRY,
SKB_FRAG_PAGE_ORDER);

这么一看,frags数组中,一页的最大值的确是32k。但是在继续研究的过程中发现:

1
2
3
4
5
6
mss_now = tcp_send_mss(sk, &size_goal, flags);

int max = size_goal;

if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
continue;

有一个size_goal在限制着skb->len的长度,该值是在tcp_send_mss函数中被设置的:

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
static unsigned int tcp_xmit_size_goal(struct sock *sk, u32 mss_now,
int large_allowed)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 new_size_goal, size_goal;

if (!large_allowed || !sk_can_gso(sk))
return mss_now;

/* Note : tcp_tso_autosize() will eventually split this later */
new_size_goal = sk->sk_gso_max_size - 1 - MAX_TCP_HEADER;
new_size_goal = tcp_bound_to_half_wnd(tp, new_size_goal);

/* We try hard to avoid divides here */
size_goal = tp->gso_segs * mss_now;
if (unlikely(new_size_goal < size_goal ||
new_size_goal >= size_goal + mss_now)) {
tp->gso_segs = min_t(u16, new_size_goal / mss_now,
sk->sk_gso_max_segs);
size_goal = tp->gso_segs * mss_now;
}

return max(size_goal, mss_now);
}

static int tcp_send_mss(struct sock *sk, int *size_goal, int flags)
{
int mss_now;

mss_now = tcp_current_mss(sk);
*size_goal = tcp_xmit_size_goal(sk, mss_now, !(flags & MSG_OOB));

return 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

文章目录
  1. 1. Linux内核中关于TCP的数据结构
  2. 2. Linux内核中关于TCP的函数
    1. 2.1. 1. tcp_output.c中的tcp_write_xmit
    2. 2.2. 2. tcp_output.c中的tso_fragment
    3. 2.3. 3. route.c中的__ip_rt_update_pmtu
    4. 2.4. 4. tcp_input.c中的tcp_sacktag_walk
    5. 2.5. 5. tcp_input.c中的tcp_shift_skb_data
    6. 2.6. 6. skbuff.c中的skb_shift
    7. 2.7. 7. tcp_input.c中的tcp_shifted_skb
  3. 3. 总结1
  4. 4. TCP发送数据的两种方式
    1. 4.1. sendpage
    2. 4.2. sendmsg
  5. 5. 总计