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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章