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

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