介绍一下对TCP初始序列号(Initial Sequence Numbers)的理解。
TCP客户端ISN
TCP客户端首次发起连接,在函数tcp_v4_connect中,调用secure_tcp_seq函数初始化其ISN序号,在稍后一节进行介绍。
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
if (likely(!tp->repair)) {
if (!tp->write_seq)
tp->write_seq = secure_tcp_seq(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port);
TCP服务端ISN
函数tcp_conn_request处理接收到的SYN报文,在客户端初次连接时,并且没有启用syncookie的情况下,初始序列号由协议栈函数init_seq获得。
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb)
{
__u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
if (!want_cookie && !isn) {
...
isn = af_ops->init_seq(skb);
}
tcp_ecn_create_request(req, skb, sk, dst);
...
tcp_rsk(req)->snt_isn = isn;
对于IPv4协议,init_seq指针对应tcp_v4_init_seq函数,而对于IPv6协议,其对应函数tcp_v6_init_seq。
static const struct tcp_request_sock_ops tcp_request_sock_ipv4_ops = {
....
.init_seq = tcp_v4_init_seq,
函数tcp_v4_init_seq是对函数secure_tcp_seq的一个封装,可见序列号的计算与报文的源/目的地址,以及源和目的端口号相关。
static u32 tcp_v4_init_seq(const struct sk_buff *skb)
{
return secure_tcp_seq(ip_hdr(skb)->daddr,
ip_hdr(skb)->saddr,
tcp_hdr(skb)->dest,
tcp_hdr(skb)->source);
}
如下secure_tcp_seq函数,序列号的计算使用SipHash算法(一种Keyed Hash算法实现)函数siphash_3u32完成,此函数对三个u32类型的值计算hash,这里为源和目的地址,以及源和目的端口组成的一个32位数,此hash函数的最后一个参数为秘钥值,为由函数net_secret_init获得的随机数,长度为128比特。SipHash计算的hash值为64bit的值,这里将其转换为32比特。
注意如果是同一个连接,在计算初始序列号时,hash值将是相同的,这就需要之后的seq_scale函数的处理了。
/* secure_tcp_seq_and_tsoff(a, b, 0, d) == secure_ipv4_port_ephemeral(a, b, d),
* but fortunately, `sport' cannot be 0 in any circumstances. If this changes,
* it would be easy enough to have the former function use siphash_4u32, passing
* the arguments as separate u32.
*/
u32 secure_tcp_seq(__be32 saddr, __be32 daddr, __be16 sport, __be16 dport)
{
u32 hash;
net_secret_init();
hash = siphash_3u32((__force u32)saddr, (__force u32)daddr,
(__force u32)sport << 16 | (__force u32)dport,
&net_secret);
return seq_scale(hash);
}
按照RFC793上的说法,对于250KHz的序号时钟,每个序号对应的时长为1/250KHz,大约为4微秒,那么得出整个序号空间(0xFFFF FFFF)占用的时长大约为4.55小时,如下公式:
0xFFFF FFFF * 1 / 250K / 60 / 60 = 整个序号空间占用的小时数值。
不知道什么问题,我计算出来的时长为大约4.66小时,与RFC上的4.55不相同。TCP的MSL值为2分钟,远远小于4.55小时的翻转时间,所以初始序号ISN应当是唯一的。RFC793的计算前提是在一个2Mb/s的网络环境下,对于10Mb/s的网络,序列号时钟应当为250*(10/2) = 1250KHz。 以此类推,对于10Gb/s的网络,序列号时钟应为:250*(10 000/2) = 1 250 000KHz,如果选取1GHz的序号时钟(时长大约1ns),那么整个序号空间占用的时间为大约3.28秒钟,小于MSL时长,这就可能造成初始序号与当前网络中上一次连接传输的报文序列号冲突。
0xFFFF FFFF * 1/250K * 2M/10G = 大约3.28秒钟
所以,如下函数seq_scale,将序号时钟时长提高了64倍,这样序号空间的整个时间拉长为3.28*64,大约为229秒钟,这样就大于了MSL的时长,避免了序号冲突的发生。但是在函数seq_scale的注释中,讲到序号整个空间的时长为274秒,不是229秒,可能哪里计算有问题吧。
static u32 seq_scale(u32 seq)
{
/*
* As close as possible to RFC 793, which suggests using a 250 kHz clock.
* Further reading shows this assumes 2 Mb/s networks.
* For 10 Mb/s Ethernet, a 1 MHz clock is appropriate.
* For 10 Gb/s Ethernet, a 1 GHz clock should be ok, but
* we also need to limit the resolution so that the u32 seq
* overlaps less than one time per MSL (2 minutes).
* Choosing a clock of 64 ns period is OK. (period of 274 s)
*/
return seq + (ktime_get_real_ns() >> 6);
}
连接重新开启的ISN
在上节的函数tcp_conn_request中,看到连接的初始ISN也可能有tcp_tw_isn变量中获得。如下TIMEWAIT状态处理函数tcp_timewait_state_process,如果在此状态接收到SYN报文,在确保其不是之前老的重复报文的情况下,将重新打开此连接。条件如下:
1) SYN报文的序号在当前要接收序号(tw_rcv_nxt)之后;
2) 或者,SYN报文中的时间戳大于最近接收到的时间戳;
满足以上的一个条件即可,本地的新序列号,使用当前的发送序号tw_snd_nxt,加上65535 + 2获得,不知何意?猜测是增加一个64K窗口的大小。但是,如果此SYN报文确认为被延时的报文,在客户端接收到本地回复的SYN-ACK报文之后,客户端将回复Reset报文,此时本机将套接口重置为TIMEWAIT状态。
enum tcp_tw_status tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb, const struct tcphdr *th)
{
/* Out of window segment.
All the segments are ACKed immediately.
The only exception is new SYN. We accept it, if it is not old duplicate and we are not in danger to be killed
by delayed old duplicates. RFC check is that it has newer sequence number works at rates <40Mbit/sec.
However, if paws works, it is reliable AND even more, we even may relax silly seq space cutoff.
RED-PEN: we violate main RFC requirement, if this SYN will appear old duplicate (i.e. we receive RST in reply to SYN-ACK),
we must return socket to time-wait state. It is not good, but not fatal yet.
*/
if (th->syn && !th->rst && !th->ack && !paws_reject &&
(after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||
(tmp_opt.saw_tstamp && (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) {
u32 isn = tcptw->tw_snd_nxt + 65535 + 2;
if (isn == 0) isn++;
TCP_SKB_CB(skb)->tcp_tw_isn = isn;
return TCP_TW_SYN;
}
内核版本 5.0