PAWS检查

PAWS(Protection Against Wrapped Sequences)功能基于TCP的Timestamps选项实现,用于拒绝接收到的过期的重复报文。PAWS假设每个报文都携带有TSopt选项数据,其中的时间戳TSval保持单调递增,所有,当接收到一个报文其TSval值小于之前在此连接中接收到的时间戳(ts_recent),即认定此报文为过期报文,将其丢弃。

由于TSval时间戳为32-bit的值,按照RFC7323中的定义,如果时间戳t减去s的结果,大于零,并且小于2**31,即认为t大于s,如下所示:

    s < t  if 0 < (t - s) < 2^31,

连接建立阶段PAWS检验

对于服务端而言,在接收到ACK报文之后,函数tcp_check_req进行PAWS检查,前提是先由报文中提取出TSopt数据(tcp_parse_options),之后,将之前记录的ts_recent时间赋值到一个临时的选项结构中(tcp_options_received),并且,由于在创建请求套接口函数(tcp_openreq_init)中,在为ts_recent的赋值时,没有记录下时刻,此处,进行一下估算。即当前时刻减去重传花费的时间,变量num_timeout记录了SYNACK报文的重传次数,

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req, bool fastopen, bool *req_stolen)
{
    struct tcp_options_received tmp_opt;
    __be32 flg = tcp_flag_word(th) & (TCP_FLAG_RST|TCP_FLAG_SYN|TCP_FLAG_ACK);

    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);
        }
    }

在查看tcp_paws_reject函数之前,先看一下tcp_paws_check函数。如果ts_recent中记录的上次报文(SYN)的时间戳,小于当前报文的时间戳(TSval),表明paws检测通过。否则,内核检测一下当前系统时间,是否在上一次获得ts_recent时间戳的时刻的24天之后,为真表明已经有超过24天没有接收到对端的报文了,认为PAWS检测通过。

最后一种情况是,有些操作系统在SYN和SYNACK报文中携带的TSopt选项中的值都为零,即tsval=0 tsecr=0,这将导致ts_recent为零,也认为paws检测通过。以上条件都不成立的话,返回false,PAWS检测失败。

static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt, int paws_win)
{
    if ((s32)(rx_opt->ts_recent - rx_opt->rcv_tsval) <= paws_win)
        return true;
    if (unlikely(!time_before32(ktime_get_seconds(),
                    rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS)))
        return true;
    /*
     * Some OSes send SYN and SYNACK messages with tsval=0 tsecr=0,
     * then following tcp messages have valid values. Ignore 0 value,
     * or else 'negative' tsval might forbid us to accept their packets.
     */
    if (!rx_opt->ts_recent)
        return true;
    return false;
}

下面看一下paws检查函数tcp_paws_reject,其首先调用了以上的tcp_paws_check函数,如果PAWS检测通过,返回false,不进行reject操作。对于RESET报文,鉴于其执行的清理功能优先级高于时间戳,不建议对RST报文进行PAWS检测,原因是,如果对端设备重启,时钟可能混乱,导致本端half-open连接不能复位。内核将RST报文的PAWS检测进行了宽松处理,即tcp_paws_check检测不通过之后,再次确认当前时间是否位于上次获取的对端时间戳的60秒(TCP_PAWS_MSL)之后,则认为RST报文的PAWS检测通过。时长TCP_PAWS_MSL确保此RST报文不是对端设备重启之前的报文。

static inline bool tcp_paws_reject(const struct tcp_options_received *rx_opt, int rst)
{
    if (tcp_paws_check(rx_opt, 0))
        return false;

    /* RST segments are not recommended to carry timestamp,
       and, if they do, it is recommended to ignore PAWS because
       "their cleanup function should take precedence over timestamps."
       Certainly, it is mistake. It is necessary to understand the reasons
       of this constraint to relax it: if peer reboots, clock may go
       out-of-sync and half-open connections will not be reset.
       Actually, the problem would be not existing if all
       the implementations followed draft about maintaining clock
       via reboots. Linux-2.2 DOES NOT!

       However, we can relax time bounds for RST segments to MSL.
     */
    if (rst && !time_before32(ktime_get_seconds(),
                  rx_opt->ts_recent_stamp + TCP_PAWS_MSL))
        return false;
    return true;
}

连接建立后PAWS检验

如下函数tcp_validate_incoming所示,此处使用tcp_paws_discard函数进行paws检测,但是,对于RST报文,即便检测未通过,也不丢弃RST报文。

static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, int syn_inerr)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* RFC1323: H1. Apply PAWS check first. */
    if (tcp_fast_parse_options(sock_net(sk), skb, th, tp) &&
        tp->rx_opt.saw_tstamp &&
        tcp_paws_discard(sk, skb)) {
        if (!th->rst) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
            if (!tcp_oow_rate_limited(sock_net(sk), skb,
                          LINUX_MIB_TCPACKSKIPPEDPAWS, &tp->last_oow_ack_time))
                tcp_send_dupack(sk, skb);
            goto discard;
        }
        /* Reset is accepted even if it did not pass PAWS. */
    }

以下tcp_paws_discard函数,其调用了之前介绍的tcp_paws_check函数和一个新的tcp_disordered_ack函数,来判定是否丢弃当前报文。对于前一个函数,其第二个参数paws_win不同于上一节使用的零值,这里使用的为TCP_PAWS_WINDOW(1),即新报文的时间戳比上一次记录的时间戳早TCP_PAWS_WINDOW(对端的1个时间戳刻度)以内,也认为PAWS检查通过,这样乱序的重复ACK报文也可进行接收但是,也可能接收到重复的数据报文。

static inline bool tcp_paws_discard(const struct sock *sk, const struct sk_buff *skb)
{
    const struct tcp_sock *tp = tcp_sk(sk);

    return !tcp_paws_check(&tp->rx_opt, TCP_PAWS_WINDOW) &&
           !tcp_disordered_ack(sk, skb);
}
static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt, int paws_win)
{
    if ((s32)(rx_opt->ts_recent - rx_opt->rcv_tsval) <= paws_win)
        return true;

以下函数tcp_disordered_ack,对于单纯的ACK报文,如果其并不更改套接口关键的状态信息(seqs,window),可将其提交到协议栈,用作拥塞避免或者快速重传处理。

  • 设置了ACK标志,并且报文中没有数据,pure-ACK报文。
  • 此ACK报文为重复的ACK报文,由于snd_una表示当前发送的首个没有收到确认的字节数据,表明对端之前已经请求过此数据,ack序号又等于snd_una即表明其为重复ACK。
  • 此ACK报文不更新发送窗口,参见以下函数tcp_may_update_window。
  • 位于重放窗口内(~RTO),大概率为用于快速重传的ACK报文。
static int tcp_disordered_ack(const struct sock *sk, const struct sk_buff *skb)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    const struct tcphdr *th = tcp_hdr(skb);
    u32 seq = TCP_SKB_CB(skb)->seq;
    u32 ack = TCP_SKB_CB(skb)->ack_seq;

    return (/* 1. Pure ACK with correct sequence number. */
        (th->ack && seq == TCP_SKB_CB(skb)->end_seq && seq == tp->rcv_nxt) &&

        /* 2. ... and duplicate ACK. */
        ack == tp->snd_una &&

        /* 3. ... and does not update window. */
        !tcp_may_update_window(tp, ack, seq, ntohs(th->window) << tp->rx_opt.snd_wscale) &&

        /* 4. ... and sits in replay window. */
        (s32)(tp->rx_opt.ts_recent - tp->rx_opt.rcv_tsval) <= (inet_csk(sk)->icsk_rto * 1024) / HZ);

以下函数tcp_may_update_window判断是否需要更新发送窗口,以下三个条件满足其一即可返回真:

  • 报文ACK序号在第一个未确认字节序号之后,表明对端接收了新数据,本地的发送窗口可能要减小;
  • 报文序号在上一次引起窗口变化的报文的序号(snd_wl1)之后。
  • 报文序号等于上一次引起窗口变化的报文的序号(snd_wl1),并且报文中通告的窗口大于当前的发送窗口。
/* Check that window update is acceptable.
 * The function assumes that snd_una<=ack<=snd_next.
 */    
static inline bool tcp_may_update_window(const struct tcp_sock *tp,
                    const u32 ack, const u32 ack_seq,
                    const u32 nwin)
{         
    return  after(ack, tp->snd_una) ||
        after(ack_seq, tp->snd_wl1) ||
        (ack_seq == tp->snd_wl1 && nwin > tp->snd_wnd);

PAWS与TIMESTAMP

对于PAWS而言,

Timestamps的翻转时间不能小于报文最大生存期(MSL),否则,网络中可能同时存在两个TSval时间戳相同的报文,一旦发生乱序,接收端将无法判断先后顺序。TSval字段长度为32bit,因此,按照最大255秒的MSL计算,timestamps的时钟最快为59.37纳秒。

    255s / 2**32 = 59.37ns

对于内核中timestamps的时钟为1毫秒的情况,32bit的timestamps的一半时长将是24.85天。参见以上的函数tcp_paws_check,其中如果TCP连接空闲了TCP_PAWS_24DAYS长的时间之后,再次接收到对端的报文,即使报文中的时间戳TSval不能通过PAWS检查,内核也认为是合法报文。

    1ms * 2**31 = 24.85 days

内核版本 5.0

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页