TCP之timestamps选项

TCPIP协议 专栏收录该内容
105 篇文章 6 订阅

默认情况下内核是开启timestamps选项的,如下tcp_sk_init函数中对sysctl_tcp_timestamps的初始化。

static int __net_init tcp_sk_init(struct net *net)
{
    net->ipv4.sysctl_tcp_timestamps = 1;

也可通过PROC文件tcp_timestamps控制选项行为,tcp_timestamps值为0时,表示关闭timestamps选项;值为1时,表示使能RFC1323(最新-RFC7323)中定义的timestamps选项,并且使用随机偏移值与timesstamp叠加,以抵御攻击。最后,当tcp_timestamps值为2时,仅开启timestamps选项,不使用偏移值。

$ cat /proc/sys/net/ipv4/tcp_timestamps
1

TSopt定义

如下所示,TSopt选项包含2个4字节的时间戳值,其中TSval为发送端的当前时间戳;而TSecr为最近接收到的对端所发送报文的TSopt选项中包含的TSval时间戳值,并且只有在TCP头部ACK标志置位时,携带的TSecr值才有效,否则,将此字段清零。接收端在收到ACK标志未设置的报文时,将忽略TSopt选项中的TSecr字段。

   TCP Timestamps option (TSopt):

   Kind: 8

   Length: 10 bytes

          +-------+-------+---------------------+---------------------+
          |Kind=8 |  10   |   TS Value (TSval)  |TS Echo Reply (TSecr)|
          +-------+-------+---------------------+---------------------+
              1       1              4                     4

TSopt选项的支持能力在连接建立阶段进行协商,如果SYN报文中携带了TSopt,并且,SYN+ACK报文中也携带了TSopt,才能成功完成TSopt的协商。如果SYN报文中未携带TSopt,服务端不能在SYN+ACK回复报文中携带TSopt。TSopt协商成功之后,所有的报文都必须携带TSopt选项数据,如果接收到一个未携带TSopt的报文,应将其丢弃。连接复位RST报文,不强制要求携带TSopt选项数据。

如果TSopt能力未协商成功,但在后续接收到了携带有TSopt的报文,应忽略TSopt选项数据,报文正常处理。

SYN报文中的TSopt

TCP客户端使用tcp_v4_connect函数发起连接建立过程,这里,由secure_tcp_ts_off函数根据源地址和目的地址,以及随机生成的秘钥进行hash计算得到一个偏移值。如上所述,如果sysctl_tcp_timestamps值不等于1,tsoffset偏移值为零。

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    if (likely(!tp->repair)) {
        tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
                         inet->inet_saddr, inet->inet_daddr);
    }
    
    err = tcp_connect(sk);

在函数tcp_connect中,使用tcp_mstamp_refresh函数,保存发送时间戳到tcp_mstamp(实际上其表示最近一次发送/接收的时间,单位时毫秒)。并且由变量retrans_stamp保存SYN报文的发送时间戳。

int tcp_connect(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    tcp_connect_init(sk);

    tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
    tcp_mstamp_refresh(tp);
    tp->retrans_stamp = tcp_time_stamp(tp);

如下为函数tcp_mstamp_refresh,注意,tcp_mstamp时间戳使用的是微秒单位,以上的retrans_stamp时间戳使用的是毫秒为单位,即TCP时钟。但是两者的时间戳是相同的。另外,在tcp_closk_cahe变量中缓存了此刻的纳秒为单位的时间戳。tcp_mstamp_refresh函数中的if判断,保证了时间戳的值是单向递增的。

变量tcp_mstamp主要用于RTT的估算过程。

void tcp_mstamp_refresh(struct tcp_sock *tp)
{      
    u64 val = tcp_clock_ns();

    if (val > tp->tcp_clock_cache)
        tp->tcp_clock_cache = val;

    val = div_u64(val, NSEC_PER_USEC);
    if (val > tp->tcp_mstamp)
        tp->tcp_mstamp = val; 
}
static inline u32 tcp_time_stamp(const struct tcp_sock *tp)
{
    return div_u64(tp->tcp_mstamp, USEC_PER_SEC / TCP_TS_HZ);
} 

在SYN报文发送处理函数__tcp_transmit_skb中,由于SYN报文为首个报文,tcp_wstamp_ns为零,skb_mstamp_ns的取值为tcp_closk_cache的值,即在以上tcp_mstamp_refresh函数中缓存的以纳秒表示的时间戳。在函数tcp_syn_options中,将使用此时间戳为TSopt的TSval赋值。

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
    prior_wstamp = tp->tcp_wstamp_ns;
    tp->tcp_wstamp_ns = max(tp->tcp_wstamp_ns, tp->tcp_clock_cache);

    skb->skb_mstamp_ns = tp->tcp_wstamp_ns;

    memset(&opts, 0, sizeof(opts));

    if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
        tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
		
    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);

    if (!err && oskb) {
        tcp_update_skb_after_send(sk, oskb, prior_wstamp);
        tcp_rate_skb_sent(sk, oskb);
    }

如下函数tcp_syn_options为SYN报文添加选项数据,如果sysctl_tcp_timestamps为真,将添加TSopt数据。其中,TSval的值为时间戳加上偏移值,这里tcp_skb_timestamp返回的时间戳单位为毫秒(TCP时钟);而TSecr等于零,但是对于此套接口非首次发起连接的情况,TSecr的值有可能不为零。

static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
                struct tcp_out_options *opts, struct tcp_md5sig_key **md5)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (likely(sock_net(sk)->ipv4.sysctl_tcp_timestamps && !*md5)) {
        opts->options |= OPTION_TS;
        opts->tsval = tcp_skb_timestamp(skb) + tp->tsoffset;
        opts->tsecr = tp->rx_opt.ts_recent;
        remaining -= TCPOLEN_TSTAMP_ALIGNED;
    }
static inline u32 tcp_skb_timestamp(const struct sk_buff *skb)
{  
    return div_u64(skb->skb_mstamp_ns, NSEC_PER_SEC / TCP_TS_HZ);
}

SYN报文TSopt处理和SYNACK报文的TSopt

在接收到SYN报文之后,函数tcp_conn_request处理此请求,其中与TSopt相关的有三个部分:一是解析报文TCP选项数据;二是request_sock结构初始化;最后是初始化本地TSopt的偏移值。

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)
{
    tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0, want_cookie ? NULL : &foc);

    tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
    tcp_openreq_init(req, &tmp_opt, skb, sk);

    if (tmp_opt.tstamp_ok)
        tcp_rsk(req)->ts_off = af_ops->init_ts_off(net, skb);

首先,看一下TSopt字段的解析,如下函数tcp_parse_options,在连接建立阶段,即estab为零时,如果本地的sysctl_tcp_timestamps设置为真,将对报文的TSopt进行解析,否则,本地不支持TSopt,无需解析对端的TSopt字段数据。

另外,在连接建立之后,根据tstamp_ok字段判断TSopt是否协商成功,参考以上函数tcp_conn_request,如果此处正确解析对端TSopt,将设置tstamp_ok标志。

void tcp_parse_options(const struct net *net, const struct sk_buff *skb,
               struct tcp_options_received *opt_rx, int estab, struct tcp_fastopen_cookie *foc)
{
     case TCPOPT_TIMESTAMP:
         if ((opsize == TCPOLEN_TIMESTAMP) && ((estab && opt_rx->tstamp_ok) ||
              (!estab && net->ipv4.sysctl_tcp_timestamps))) {
             opt_rx->saw_tstamp = 1;
             opt_rx->rcv_tsval = get_unaligned_be32(ptr);
             opt_rx->rcv_tsecr = get_unaligned_be32(ptr + 4);
         }
         break;

其次,在inet_request_sock初始化过程中,将在其成员tstamp_ok中保存TSopt协商结果。在ts_recent中保存对端报文中的TSval时间戳值,用于之后在TSopt的TSecr中返回给客户端。

static void tcp_openreq_init(struct request_sock *req, const struct tcp_options_received *rx_opt,
                 struct sk_buff *skb, const struct sock *sk)
{
    struct inet_request_sock *ireq = inet_rsk(req);

    req->ts_recent = rx_opt->saw_tstamp ? rx_opt->rcv_tsval : 0;
    ireq->tstamp_ok = rx_opt->tstamp_ok;

最后,如果TSopt协商成功,对于IPv4而言,函数tcp_v4_init_ts_off将初始化服务端的TS偏移值,但是如果sysctl_tcp_timestamps不等于1,偏移值赋值为零。

static u32 tcp_v4_init_ts_off(const struct net *net, const struct sk_buff *skb)
{
    return secure_tcp_ts_off(net, ip_hdr(skb)->daddr, ip_hdr(skb)->saddr);
}
u32 secure_tcp_ts_off(const struct net *net, __be32 saddr, __be32 daddr)
{                  
    if (net->ipv4.sysctl_tcp_timestamps != 1)
        return 0;  

    ts_secret_init();
    return siphash_2u32((__force u32)saddr, (__force u32)daddr, &ts_secret);
}

在回复SYN+ACK报文时,函数tcp_make_synack使用tcp_clock_ns获取到当前时刻的纳秒值,保存于skb_mstamp_ns变量中。之后,由函数tcp_synack_options初始化选项字段。

struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
                struct request_sock *req,...)
{
#ifdef CONFIG_SYN_COOKIES
    if (unlikely(req->cookie_ts))
        skb->skb_mstamp_ns = cookie_init_timestamp(req);
    else
#endif
        skb->skb_mstamp_ns = tcp_clock_ns();

    tcp_header_size = tcp_synack_options(sk, req, mss, skb, &opts, md5, foc) + sizeof(*th);

此时,根据新创建的inet_request_sock结构中标示TSopt协商成功的标志tstamp_ok,将TSecr赋值为以上tcp_openreq_init函数中赋值的ts_recent变量的值,而本地的TSval值为tcp_skb_timestamp的返回值和ts_off的和。

static unsigned int tcp_synack_options(const struct sock *sk, struct request_sock *req,
                       unsigned int mss, struct sk_buff *skb,
                       struct tcp_out_options *opts,...)
{
    struct inet_request_sock *ireq = inet_rsk(req);

    if (likely(ireq->tstamp_ok)) {
        opts->options |= OPTION_TS;
        opts->tsval = tcp_skb_timestamp(skb) + tcp_rsk(req)->ts_off;
        opts->tsecr = req->ts_recent;
        remaining -= TCPOLEN_TSTAMP_ALIGNED;
    }

如下函数tcp_skb_timestamp,将skb_mstamp_ns中的纳秒值转换为timestamps的毫秒值(TCP时钟)。

#define TCP_TS_HZ   1000

static inline u32 tcp_skb_timestamp(const struct sk_buff *skb)
{   
    return div_u64(skb->skb_mstamp_ns, NSEC_PER_SEC / TCP_TS_HZ);
} 

SYNACK报文TSopt处理

TCP客户端在接收到SYNACK报文之后,由函数tcp_rcv_synsent_state_process进行处理,tcp_parse_options负责解析选项信息,如果TSopt选项解析完成,saw_tstamp为真,并且服务端返回的TSecr值不为零,减去偏移值tsoffset,即得到本端原始的时间戳值。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
    ...
    tcp_parse_options(sock_net(sk), skb, &tp->rx_opt, 0, &foc);
    if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr)
        tp->rx_opt.rcv_tsecr -= tp->tsoffset;

    if (th->ack) {
        if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
            !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp, tcp_time_stamp(tp))) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSACTIVEREJECTED);
            goto reset_and_undo;
        }
        if (tp->rx_opt.saw_tstamp) {
            tp->rx_opt.tstamp_ok       = 1;
            tp->tcp_header_len = sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
            tp->advmss      -= TCPOLEN_TSTAMP_ALIGNED;
            tcp_store_ts_recent(tp);
        }

在函数tcp_connect中使用retrans_stamp保存了SYN报文的时间戳,而tcp_time_stamp返回的为套接口最近发送或接收报文的时间戳,如在函数tcp_rcv_state_process中对此时间戳的更新(tcp_mstamp_refresh),因此,TSecr应当位于retrans_stamp和tcp_mstamp两者之间。此三个时间戳值的单位都是毫秒。函数tcp_mstamp_refresh也将更新tcp_clock_cache缓存值。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    switch (sk->sk_state) {
    case TCP_SYN_SENT:
        tp->rx_opt.saw_tstamp = 0;
        tcp_mstamp_refresh(tp);
        queued = tcp_rcv_synsent_state_process(sk, skb, th);

在处理函数tcp_rcv_synsent_state_process中,TSopt选项协商完成,设置标志位tstamp_ok,并且,在函数tcp_store_ts_recent中,将服务端SYNACK报文中的TSval值保存在ts_recent变量中,记录下ts_recent更新时刻的时间戳(ts_recent_stamp)。

static void tcp_store_ts_recent(struct tcp_sock *tp)
{   
    tp->rx_opt.ts_recent = tp->rx_opt.rcv_tsval;
    tp->rx_opt.ts_recent_stamp = ktime_get_seconds();
} 

TCP客户端回复的ACK报文(第三个握手报文)以SYN报文一样,由函数__tcp_transmit_skb负责发送。通常情况下skb_mstamp_ns的值由tcp_clock_cache决定,稍后介绍例外情况。由于不是SYN报文,此时使用函数tcp_established_options设置TCP选项数据。

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                  int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);

    tp = tcp_sk(sk);

    prior_wstamp = tp->tcp_wstamp_ns;
    tp->tcp_wstamp_ns = max(tp->tcp_wstamp_ns, tp->tcp_clock_cache);
    skb->skb_mstamp_ns = tp->tcp_wstamp_ns;

    memset(&opts, 0, sizeof(opts));

    if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
        tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
    else
        tcp_options_size = tcp_established_options(sk, skb, &opts, &md5);

以下函数tcp_established_options,首先判断TSopt是否协商成功;其次,将记录在ts_recent中的服务端时间戳赋值到TSecr字段,返回给服务端;之后,本端的时间戳值TSval设置为当前时间加上偏移值tsoffset。之前介绍了tcp_skb_timestamp函数的作用是将skb_mstamp_ns变量表示的纳秒值转换为毫秒值。

static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb,
                    struct tcp_out_options *opts, struct tcp_md5sig_key **md5)
{       
    struct tcp_sock *tp = tcp_sk(sk);

    if (likely(tp->rx_opt.tstamp_ok)) {
        opts->options |= OPTION_TS;
        opts->tsval = skb ? tcp_skb_timestamp(skb) + tp->tsoffset : 0;
        opts->tsecr = tp->rx_opt.ts_recent;
        size += TCPOLEN_TSTAMP_ALIGNED;
    }

ACK报文TSopt选项

函数tcp_check_req首先对ACK报文进行相应的检查,这里仅关注TSopt相关的部分,如果ACK报文中携带有TSopt选项(saw_tstamp为真),将request_sock中保存的上次客户端通过的时间戳ts_recent赋值到tcp_options_received结构的tmp_opt变量的成员ts_recent中,随后的PAWS检查函数,将比较此时间戳与当前ACK报文中时间戳的大小,以便判断是否是重复的报文。

另外,在设置ts_recent的同时,设置了时间戳ts_recent_stamp,这里其单位为秒。num_timeout为SYNACK超时重传的次数。之后,根据报文中TSopt字段的TSval值更新ts_recent值,记录下客户端ACK报文中的时间戳。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req, bool fastopen, bool *req_stolen)
{
    tmp_opt.saw_tstamp = 0;
    if (th->doff > (sizeof(struct tcphdr)>>2)) {
        tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0, NULL);

        if (tmp_opt.saw_tstamp) {
            tmp_opt.ts_recent = req->ts_recent;
            if (tmp_opt.rcv_tsecr)
                tmp_opt.rcv_tsecr -= tcp_rsk(req)->ts_off;
            /* We do not store true stamp, but it is not required,
             * it can be estimated (approximately) from another data.
             */
            tmp_opt.ts_recent_stamp = ktime_get_seconds() - ((TCP_TIMEOUT_INIT/HZ)<<req->num_timeout);
            paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
        }
    }
	...
    /* In sequence, PAWS is OK. */

    if (tmp_opt.saw_tstamp && !after(TCP_SKB_CB(skb)->seq, tcp_rsk(req)->rcv_nxt))
        req->ts_recent = tmp_opt.rcv_tsval;
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);

在tcp_v4_syn_recv_sock函数中,将使用tcp_create_openreq_child创建子套接口,将TSopt相关的数据保存到新套接口中。比如,TSopt协商标志tstamp_ok,客户端的时间戳ts_recent,以及本端的偏移值tsoffset。

在函数inet_csk_clone_lock中,将子套接口的状态设置为了TCP_SYN_RECV,稍后将用到此状态。

struct sock *tcp_create_openreq_child(const struct sock *sk, struct request_sock *req, struct sk_buff *skb)
{
    struct sock *newsk = inet_csk_clone_lock(sk, req, GFP_ATOMIC);
    const struct inet_request_sock *ireq = inet_rsk(req);
    struct tcp_request_sock *treq = tcp_rsk(req);

    newtp->rx_opt.tstamp_ok = ireq->tstamp_ok;

    if (newtp->rx_opt.tstamp_ok) {
        newtp->rx_opt.ts_recent = req->ts_recent;
        newtp->rx_opt.ts_recent_stamp = ktime_get_seconds();
        newtp->tcp_header_len = sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
    } else {
        newtp->rx_opt.ts_recent_stamp = 0;
        newtp->tcp_header_len = sizeof(struct tcphdr);
    }
    newtp->tsoffset = treq->ts_off;

在接收状态机函数tcp_rcv_state_process中,首先由函数tcp_mstamp_refresh更新套接口中的缓存时间戳tcp_clock_cache,之后由tcp_ack函数处理ACK报文,最后子套接口的状态已经设置为了TCP_SYN_RECV,此处将其更改为TCP_ESTABLISHED。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    tcp_mstamp_refresh(tp);
    tp->rx_opt.saw_tstamp = 0;

    if (!th->ack && !th->rst && !th->syn)
        goto discard;
    if (!tcp_validate_incoming(sk, skb, th, 0))
        return 0;

    /* step 5: check the ACK field */
    acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH |
                      FLAG_UPDATE_TS_RECENT | FLAG_NO_CHALLENGE_ACK) > 0;

    switch (sk->sk_state) {
    case TCP_SYN_RECV:
        ...
        tcp_set_state(sk, TCP_ESTABLISHED);
    }

由于在调用tcp_ack函数时,指定了FLAG_UPDATE_TS_RECENT选项,调用函数tcp_replace_ts_recent对其进行更新。

static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
    /* ts_recent update must be made after we are sure that the packet is in window.
     */
    if (flag & FLAG_UPDATE_TS_RECENT)
        tcp_replace_ts_recent(tp, TCP_SKB_CB(skb)->seq);

最终,函数tcp_store_ts_recent使用选项结构rx_opt中保存的TSval值更新ts_recent时间戳。

static void tcp_store_ts_recent(struct tcp_sock *tp)
{
    tp->rx_opt.ts_recent = tp->rx_opt.rcv_tsval;
    tp->rx_opt.ts_recent_stamp = ktime_get_seconds();
}
static void tcp_replace_ts_recent(struct tcp_sock *tp, u32 seq)
{
    if (tp->rx_opt.saw_tstamp && !after(seq, tp->rcv_wup)) {
        /* PAWS bug workaround wrt. ACK frames, the PAWS discard
         * extra check below makes sure this can only happen for pure ACK frames.  -DaveM
         * Not only, also it occurs for expired timestamps.
         */
        if (tcp_paws_check(&tp->rx_opt, 0))
            tcp_store_ts_recent(tp);
    }
}

TSval取值

在连接建立之后,函数tcp_established_options负责为TSopt选项赋值,以下可见TSval的值由skb_mstamp_ns和tsoffset偏移值组成。

static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb,
                    struct tcp_out_options *opts,...)
{
    if (likely(tp->rx_opt.tstamp_ok)) {
        opts->options |= OPTION_TS;
        opts->tsval = skb ? tcp_skb_timestamp(skb) + tp->tsoffset : 0;
        opts->tsecr = tp->rx_opt.ts_recent;
        size += TCPOLEN_TSTAMP_ALIGNED;
    }
static inline u32 tcp_skb_timestamp(const struct sk_buff *skb)
{   
    return div_u64(skb->skb_mstamp_ns, NSEC_PER_SEC / TCP_TS_HZ);
} 

在发送函数__tcp_transmit_skb中,skb_mstamp_ns的值等于tcp_wstamp_ns和tcp_clock_cache两者中的最大值。

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
    prior_wstamp = tp->tcp_wstamp_ns;
    tp->tcp_wstamp_ns = max(tp->tcp_wstamp_ns, tp->tcp_clock_cache);

    skb->skb_mstamp_ns = tp->tcp_wstamp_ns;

    if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
    else
        tcp_options_size = tcp_established_options(sk, skb, &opts, &md5);

    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);

    if (!err && oskb) {
        tcp_update_skb_after_send(sk, oskb, prior_wstamp);
        tcp_rate_skb_sent(sk, oskb);
    }

由函数tcp_write_xmit可知,在发送之前,其使用tcp_mstamp_refresh函数,将tcp_clock_cache时间戳值更新到了当前时刻。所以,通过情况下,tcp_clock_cache的值大于tcp_wstamp_ns。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)
{
    tcp_mstamp_refresh(tp);

    while ((skb = tcp_send_head(sk))) {
        ...
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;

如下函数tcp_update_skb_after_send,其在报文发送之后被调用,以确定下一个报文的发送时间戳。由于Pacing的存在,报文并不一定在tcp_write_xmit调用的时刻被发送出去。而函数tcp_update_skb_after_send根据Pacing速率和报文长度,估算出下一个报文的发送时刻,保存在tcp_wstamp_ns中,此种情况下,其有可能大于tcp_clock_cache的值。所以TSval的值取两者之间较大值。

static void tcp_update_skb_after_send(struct sock *sk, struct sk_buff *skb, u64 prior_wstamp)
{
    struct tcp_sock *tp = tcp_sk(sk);

    skb->skb_mstamp_ns = tp->tcp_wstamp_ns;
    if (sk->sk_pacing_status != SK_PACING_NONE) {
        unsigned long rate = sk->sk_pacing_rate;

        /* Original sch_fq does not pace first 10 MSS Note that tp->data_segs_out overflows after 2^32 packets,
         * this is a minor annoyance.
         */
        if (rate != ~0UL && rate && tp->data_segs_out >= 10) {
            u64 len_ns = div64_ul((u64)skb->len * NSEC_PER_SEC, rate);
            u64 credit = tp->tcp_wstamp_ns - prior_wstamp;

            /* take into account OS jitter */
            len_ns -= min_t(u64, len_ns / 2, credit);
            tp->tcp_wstamp_ns += len_ns;
        }
    }
    list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);

TSecr值的选取

由以上的介绍可知,TSecr的值取自ts_recent变量。RFC7323中规定ts_recent的更新需满足以下公式,即接收报文中的TSval大于等于本地上次记录的ts_recent值,并且,接收报文的序号小于等于本地最近ACK字段请求的序号:

            SEG.TSval >= TS.Recent and SEG.SEQ <= Last.ACK.sent

        then SEG.TSval is copied to TS.Recent; otherwise, it is ignored.

在quickack模式下,一个报文对应一个ACK,必然满足以上的条件。在延迟ACK模式下,例如接收到两个数据报文之后,回复一个ACK,只有第一个数据报文满足以上条件,第二个数据报文的序号将大于Last.ACK.sent。另外,当接收到一个乱序报文时,其序号也将大于Last.ACK.sent的值,乱序报文的TSval将不用做更新ts_recent。

变量ts_recent的值通常情况下由函数tcp_store_ts_recent进行更新,并且记录下更新时间(ts_recent_stamp),更新时间由PAWS检查时使用。

static void tcp_store_ts_recent(struct tcp_sock *tp)
{
    tp->rx_opt.ts_recent = tp->rx_opt.rcv_tsval;
    tp->rx_opt.ts_recent_stamp = ktime_get_seconds();
}

在连接建立之后,函数tcp_rcv_established处理接收报文,首先看一下在TCP快速接收路径中ts_recent时间戳的更新。前提是报文中的TSval值必须大于当前记录的ts_recent值,再者看一下第二个条件SEG.SEQ <= Last.ACK.sent的判断,在内核中对应seq<=rcv_wup,而在此快速路径中,seq是等于rcv_nxt的,即rcv_nxt<=rcv_wup,最后rcv_wup变量表示的是上一次窗口更新时的rcv_nxt的值,必然有rcv_nxt>=rcv_wup,所以,仅需要判断rcv_nxt是否等于rcv_wup。

另外,对于报文长度len,小于tcp_header_len指定长度(其中包含TCPOLEN_TSTAMP_ALIGNED长度)的报文,说明其中未携带TSopt字段数据,将其丢弃。

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
    if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
        TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
        !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
		
        if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
            if (!tcp_parse_aligned_timestamp(tp, th)) /* No? Slow path! */
                goto slow_path;

            /* If PAWS failed, check it more carefully in slow path */
            if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
                goto slow_path;
        }
		
        if (len <= tcp_header_len) { /* Bulk data transfer: sender */
            if (len == tcp_header_len) {
                /* Predicted packet is in window by definition.
                 * seq == rcv_nxt and rcv_wup <= rcv_nxt.
                 * Hence, check seq<=rcv_wup reduces to:
                 */
                if (tcp_header_len == (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
                    tp->rcv_nxt == tp->rcv_wup)
                    tcp_store_ts_recent(tp);
            } else { /* Header too small */
                TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
                goto discard;
            }

对于TCP慢速路径,将在tcp_ack中更新ts_recent时间戳,以上也将进行了接收,其参数FLAG_UPDATE_TS_RECENT指明了这一点。

slow_path:
    if (len < (th->doff << 2) || tcp_checksum_complete(skb))
        goto csum_error;

    if (!th->ack && !th->rst && !th->syn)
        goto discard;

    /*  Standard slow path. */
    if (!tcp_validate_incoming(sk, skb, th, 1))
        return;
step5:
    if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0)
        goto discard;

tcp_ack使用函数tcp_replace_ts_recent更新ts_recent的值,以下函数实现可见,两个if语句确保负荷RFC7323中的规定。

static void tcp_replace_ts_recent(struct tcp_sock *tp, u32 seq)
{
    if (tp->rx_opt.saw_tstamp && !after(seq, tp->rcv_wup)) {
        /* PAWS bug workaround wrt. ACK frames, the PAWS discard
         * extra check below makes sure this can only happen for pure ACK frames.  -DaveM
         *
         * Not only, also it occurs for expired timestamps.
         */

        if (tcp_paws_check(&tp->rx_opt, 0))
            tcp_store_ts_recent(tp);
    }

RFC7323中对ts_recent的更新定义,目的再有帮助对端能够更好的计算连接RTT值。例如,对于延迟ACK的情况,ts_recent使用的是最早的未确认报文中携带的TSval的值,这将使得对端计算的RTT值变大,降低对端进行重传的风险。反之,如果使用最近一个接收到的报文中的TSval值,对端计算的RTT值将减小,进一步导致RTO值减小,容易造成对较早发送报文进行不必要的重传。

TIMEWAIT套接口TSopt

套接口在进入TIMEWAIT状态(子状态可能为TCP_FIN_WAIT2)之后,分配新的inet_timewait_sock结构套接口,随将TSopt相关记录:ts_recent、ts_recent_stamp和tsoffset赋值到新的套接口中。

void tcp_time_wait(struct sock *sk, int state, int timeo)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    struct inet_timewait_sock *tw;

    tw = inet_twsk_alloc(sk, tcp_death_row, state);
    if (tw) {
        struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
        const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);
        struct inet_sock *inet = inet_sk(sk);

        tcptw->tw_ts_recent = tp->rx_opt.ts_recent;
        tcptw->tw_ts_recent_stamp = tp->rx_opt.ts_recent_stamp;
        tcptw->tw_ts_offset = tp->tsoffset;

如下所示为TIMEWAIT套接口状态机处理函数tcp_timewait_state_process,其中对TSopt数据的处理与以上介绍的一致。

enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb, const struct tcphdr *th)
{
    struct tcp_options_received tmp_opt;
    struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
    bool paws_reject = false;

    tmp_opt.saw_tstamp = 0;
    if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) {
        tcp_parse_options(twsk_net(tw), skb, &tmp_opt, 0, NULL);

        if (tmp_opt.saw_tstamp) {
            if (tmp_opt.rcv_tsecr)
                tmp_opt.rcv_tsecr -= tcptw->tw_ts_offset;
            tmp_opt.ts_recent   = tcptw->tw_ts_recent;
            tmp_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
            paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
        }
    }

内核版本 5.0

  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值