TCP窗口擴張係數選項WSCALE

如下圖所示,TCP窗口擴張係數選項長度爲3個字節,在TCP握手階段的SYN報文和SYN+ACK報文中攜帶,其旨在通告對發送和接收窗口擴張功能的支持,以及通告擴張係數。注意前者意味着只有在SYN報文中通告了對WSopt選項的支持時,回覆的SYN+ACK報文才可攜帶WSopt選項,否則,SYN+ACK報文中不可攜帶WSopt。在SYN或者SYN+ACK報文中的通告的窗口值都是不能進行擴張的。

擴張係數shift.cnt表示窗口值左移的位數值。

+---------+---------+---------+
| Kind=3  |Length=3 |shift.cnt|
+---------+---------+---------+

x

擴張係數

擴張係數的最大值定義爲14。

/* Maximal number of window scale according to RFC1323 */
#define TCP_MAX_WSCALE      14U

內核中TCP使用32bit的變量snd_wnd保存窗口的值,故除去TCP頭部字段的16位窗口值後,擴張係數的最大值爲16。但是,如果整個窗口值爲32bit,即整個4G的空間,會導致任意序號的值都位於窗口空間內,而無法確定報文是重複的報文,還是新報文。即,假如下一個要接收的序號爲100,此時收到一個序號爲90的報文,其在窗口範圍內,將判斷其實之前的重複報文,還是新報文。

            100
             |
   0 |----------------------------| 4G
           |
           90

TCP在判斷一個報文的新舊時,依據此報文的序號是否位於窗口左側開始的231範圍(整個32bit空間的一半,2G)的空間內。所以發送端窗口的左側一定不能夠比接收端窗口右側大231,否則,發送的數據報文可能被當做重複報文而丟棄。同樣,發送端窗口的右側也不能夠比接收端窗口的左側大231,否則,如果最右側的報文先到達接收端,將判定爲重複報文而丟棄。故,TCP最大的窗口值應小於231的值,WSopt選項中的擴展係數最大爲14,即:

      must be less than 2**31, or

           max window < 2**30

發送WSopt

如下爲初始窗口選擇函數tcp_select_initial_window,窗口的鉗制值window_clamp最大爲U16_MAX << TCP_MAX_WSCALE,即0x3fffc000,爲1G-16K。

void tcp_select_initial_window(const struct sock *sk, int __space, __u32 mss,
                   __u32 *rcv_wnd, __u32 *window_clamp,
                   int wscale_ok, __u8 *rcv_wscale,  __u32 init_rcv_wnd)
{   
    unsigned int space = (__space < 0 ? 0 : __space);
    
    if (*window_clamp == 0)       /* If no clamp set the clamp to the max possible scaled window */
        (*window_clamp) = (U16_MAX << TCP_MAX_WSCALE);
    space = min(*window_clamp, space);
    
    /* Quantize space offering to a multiple of mss if possible. */
    if (space > mss)
        space = rounddown(space, mss);
    
    if (sock_net(sk)->ipv4.sysctl_tcp_workaround_signed_windows)
        (*rcv_wnd) = min(space, MAX_TCP_WINDOW);
    else
        (*rcv_wnd) = min_t(u32, space, U16_MAX);
    
    if (init_rcv_wnd)
        *rcv_wnd = min(*rcv_wnd, init_rcv_wnd * mss);

以上可見TCP標準頭部的窗口字段rcv_wnd最大值爲U16_MAX,更大的窗口值就需要WSopt選項來實現。前提是如果對端支持WSopt選項,即wscale_ok爲真,首先計算最大可用的窗口空間,取值space、sysctl_tcp_rmem[2]和sysctl_rmem_max三者之間的最大值,之後,確定此值不超過窗口鉗制值,否則使用鉗制值。

    *rcv_wscale = 0;
    if (wscale_ok) {
        /* Set window scaling on max possible window */
        space = max_t(u32, space, sock_net(sk)->ipv4.sysctl_tcp_rmem[2]);
        space = max_t(u32, space, sysctl_rmem_max);
        space = min_t(u32, space, *window_clamp); 
        *rcv_wscale = clamp_t(int, ilog2(space) - 15, 0, TCP_MAX_WSCALE);

如下發送函數__tcp_transmit_skb可見,對於設置有SYN標誌的TCP報文,窗口值不做擴展。但是對於設置有SYN標誌的報文,使用tcp_syn_options添加TCP選項字段,否則,由函數tcp_established_options添加選項,在前者中將添加WSopt選項;而在後者連接建立狀態中,可添加MD5、timestamp和sack選項。

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                  int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
    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_options_write((__be32 *)(th + 1), tp, &opts);
    skb_shinfo(skb)->gso_type = sk->sk_gso_type;
    if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
        th->window      = htons(tcp_select_window(sk));
        tcp_ecn_send(sk, skb, th, tcp_header_size);
    } else {
        /* RFC1323: The window in SYN & SYN/ACK segments is never scaled.
         */
        th->window  = htons(min(tp->rcv_wnd, 65535U));
    }

對於未設置SYN標誌的報文,使用函數tcp_select_window選擇窗口值,如下所示,其將選擇的窗口值右移rcv_wscale位。

static u16 tcp_select_window(struct sock *sk)
{
    /* RFC1323 scaling applied */
    new_win >>= tp->rx_opt.rcv_wscale;

如下tcp_syn_options函數,如果系統開啓了TCP窗口擴張功能,發送WSopt選項數據,擴張係數爲在以上函數tcp_select_initial_window中確定的值。

static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
                struct tcp_out_options *opts, struct tcp_md5sig_key **md5)
{
    if (likely(sock_net(sk)->ipv4.sysctl_tcp_window_scaling)) {
        opts->ws = tp->rx_opt.rcv_wscale;
        opts->options |= OPTION_WSCALE;
        remaining -= TCPOLEN_WSCALE_ALIGNED;
    }

接收WSopt

如果接收到的TCP報文頭部長度值大於TCP頭部結構tcphdr的長度,表明包含TCP選項數據。如下函數tcp_parse_options負責解析選項,這裏看一下WSopt數據,只有在報文的TCP頭部設置了SYN標誌,並且當前狀態不是連接建立狀態,而且本地開啓了窗口擴張功能(sysctl_tcp_window_scaling默認爲1)的情況下,才需要解析WSopt數據。

void tcp_parse_options(const struct net *net, const struct sk_buff *skb,
               struct tcp_options_received *opt_rx, int estab,...)
{
    const struct tcphdr *th = tcp_hdr(skb);
    int length = (th->doff * 4) - sizeof(struct tcphdr);

    while (length > 0) {
        switch (opcode) {
        default:
            switch (opcode) {
            case TCPOPT_WINDOW:
                if (opsize == TCPOLEN_WINDOW && th->syn &&
                    !estab && net->ipv4.sysctl_tcp_window_scaling) {
                    __u8 snd_wscale = *(__u8 *)ptr;
                    opt_rx->wscale_ok = 1;
                    if (snd_wscale > TCP_MAX_WSCALE) {
                        net_info_ratelimited("%s: Illegal window scaling value %d > %u received\n",
                                     __func__, snd_wscale, TCP_MAX_WSCALE);
                        snd_wscale = TCP_MAX_WSCALE;
                    }
                    opt_rx->snd_wscale = snd_wscale;
                }
                break;

如果其中通告的擴張係數值大於最大值TCP_MAX_WSCALE(14),打印警告信息,並將擴張係數限定在最大值TCP_MAX_WSCALE。另外,這裏將wscale_ok設置爲真,表明對端支持WSopt選項。

對於TCP的客戶端,如果接收到的回覆報文SYN+ACK中沒有包含WSopt選項數據,表明服務端不支持窗口擴張,此情況下,本地也將禁止窗口擴張功能,將snd_wscale和rcv_wscale清零。另外,RFC1323中規定SYN與SYNACK報文中的窗口值不擴張,snd_wnd直接取值TCP頭部中的16bit窗口值。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
                     const struct tcphdr *th)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);

    tcp_parse_options(sock_net(sk), skb, &tp->rx_opt, 0, &foc);

    if (th->ack) {
        ...
        /* RFC1323: The window in SYN & SYN/ACK segments is never scaled.
         */
        tp->snd_wnd = ntohs(th->window);
        if (!tp->rx_opt.wscale_ok) {
            tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0;
            tp->window_clamp = min(tp->window_clamp, 65535U);
        }

在服務端創建子套接口時,如果客戶端不支持窗口擴張,將子套接口的snd_wscale和rcv_wscale清零,如下tcp_create_openreq_child函數所示。TCP發送窗口snd_wnd的計算爲TCP頭部的16bit的窗口值左移擴張係數snd_wscale。

struct sock *tcp_create_openreq_child(const struct sock *sk, struct request_sock *req, struct sk_buff *skb)
{
    newtp->rx_opt.wscale_ok = ireq->wscale_ok;
    if (newtp->rx_opt.wscale_ok) {
        newtp->rx_opt.snd_wscale = ireq->snd_wscale;
        newtp->rx_opt.rcv_wscale = ireq->rcv_wscale;
    } else {
        newtp->rx_opt.snd_wscale = newtp->rx_opt.rcv_wscale = 0;
        newtp->window_clamp = min(newtp->window_clamp, 65535U);
    }
    newtp->snd_wnd = ntohs(tcp_hdr(skb)->window) << newtp->rx_opt.snd_wscale;
    newtp->max_window = newtp->snd_wnd;

內核版本 5.0

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