Chinaunix首页 | 论坛 | 博客
  • 博客访问: 745629
  • 博文数量: 141
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1115
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-17 14:32
个人简介

小公司研发总监,既当司令也当兵!

文章分类

全部博文(141)

分类: LINUX

2016-12-26 23:25:19

背景

网络程序员对于“最大传输单元”--MTU应该都不陌生。对于网络传输而已,一条链路上的负载大小通常都是有限制的。比如,对于以太网,MTU通常被设置为1500字节(ip报文最大长度)。对于网络传输中最常用TCP协议而言,其架设与IP协议之上:TCP是基于流,它的数据需要通过划分成一个一个的块,然后组装成一个一个tcp报文,再交由ip协议封装、并发送。通常,为了更好的使用效率,TCP最好确认其数据块的大小,使得一个TCP报文能够顺利的装入一个IP报文中,而且不超过MTU的限制(相反,如果TCP报文过大,那么在IP层发送时,需要将报文分割为更小的多个报文发送,在接收端重组,效率很低)。TCP为了很好的完成这个任务,其通过扩展选项,通过服务端和客户端进行友好协商,选择一个合适的分片大小(MSS),保证双方在数据传输过程中,不需要进行IP分片。

经过上面TCP协商处理后,通常是没有问题的。但后来运营商在网络接入时引入了PPPoE后,情况就不同了!

PPPoE协议是架设在以太网的ppp协议,它会在IP与Ethernet之间添加一个PPPoE头部(包含pppoe头部和ppp头部,共8字节),这样其实变形减小了链路的MTU;但问题在于,对于TCP/IP层面而已,PPPoE是不可见,即TCP在协商MSS时,所看到的MTU依然是Ethernet的MTU,并没有排除PPPoE头部长度。
见下面一个典型拓扑,client 通过PPPoE接入intenet,并试图访问站点SERVER的资源:
(CLINET)-------(PPPoE cli) ------- (PPPoE serv) ------(INTERNET) ------(SERVER)

step1:client向server端发起TCP连接请求,同时申明它支持的最大分片长度为1460字节(MTU - TCP_HEADER_LEN - IP_HEADER_LEN);
step2:server回复client,同时申明自己支持最大分片长度为1460字节;
step3:client发起读取资源请求;
step4-1:server把资源按最大1460字节切片,然封装为TCP报文,进一步封装IP报文,经以太网成帧后发往client。
step4-2:报文从server向client过程中,流经PPPoE serv,PPPoE serv需要向报文添加PPPoE头部,此时发现添加PPPoE头部后报文超过MTU限制了!(怎么办?怎么办?怎么办?)
step4-3: (i)SERVER在发送报文时,明确这个报文不允许分片,那么PPPoE serv只能丢弃该报文;(ii)PPPoE serv检查报文已经超长,那么久默默丢弃,当什么事情也没发生;(iii)PPPoE serv对报文进行分片,逐个发生到CLINET。

从上面可以看到,当PPPoE serv接受到一个“超长”报文时,其对待的态度是不一定的;同理,当CLIENT发生一个“超长”报文时,PPPoE cli对待态度也是不一定的。很遗憾,绝大多数PPPoE cli不会对报文进行分片,并且PPPoE serv也不是总是会执行分片操作。由此引发的问题是,通常CLIENT可以和SERVER建立连接,但进行大数据传输时却失败了!

解决方案

问题原因知悉后,解决就不困难了。在上述拓扑中,PPPoE cli (或者PPPoE serv)监听连接过程,CLIENT与SERVER进行MSS协商时,主动参与,修正MSS:
(CLINET)-------(PPPoE cli) ------- (PPPoE serv) ------(INTERNET) ------(SERVER)

step1:client向server端发起TCP连接请求,同时申明它支持的最大分片长度为1460字节;
step1-1:PPPoE cli 捕获TCP MSS协商,修正为1412字节;
step2:SERVER确认连接请求,知悉CLIENT最大支持1412字节;申明其支持1412字节没有问题;
step2-1:PPPoE cli 捕获TCP MSS 协商,判断其值不大于1412,OK没有问题
step2-2:ClIENT 了解到SERVER最大支持1412
字节的分片长度;
step3:CLIENT请求资源;
step4:SERVER按最大1412字节分片资源,组装发生给CLIENT;
step5:ClIENT 收到报文,与以ACK确认;
......

经过上面分步说明,这个问题基本阐释清楚了。在路由器上,如果采用PPPoE接入,通常需要执行TCP MSS clamp,下面是内核pppoe模块添加TCP MSS clamp的代码:

点击(此处)折叠或打开

  1. static uint16_t tcp_checksum(uint8_t *piphdr, uint8_t *ptcphdr)
  2. {
  3.     uint32_t sum = 0;
  4.     uint16_t count;
  5.     uint16_t tmp;

  6.     uint8_t *addr;
  7.     uint8_t pseudo_header[12];
  8.     int i;

  9.     /* Count number of bytes in TCP header and data: IP total length - IP header length */
  10.     count = piphdr[2] * 256 + piphdr[3];
  11.     count -= (piphdr[0] & 0x0F) * 4;

  12.     /*ip src addr, dest addr, protocl, payload length*/
  13.     memcpy(pseudo_header, piphdr+12, 8);
  14.     pseudo_header[8] = 0;
  15.     pseudo_header[9] = piphdr[9];
  16.     pseudo_header[10] = (count >> 8) & 0xFF;
  17.     pseudo_header[11] = (count & 0xFF);

  18.     /* Checksum the pseudo-header */
  19.     for (i = 0; i < 12; i += 2)
  20.     {
  21.         sum += *(uint16_t *)(pseudo_header + i);
  22.     }

  23.     /* Checksum the TCP header and data */
  24.     addr = ptcphdr;
  25.     while (count > 1)
  26.     {
  27.         memcpy(&tmp, addr, sizeof(tmp));
  28.         sum += (uint32_t) tmp;
  29.         addr += sizeof(tmp);
  30.         count -= sizeof(tmp);
  31.     }

  32.     if (count > 0)
  33.     {
  34.         sum += (uint8_t) *addr;
  35.     }

  36.     while (sum >> 16)
  37.     {
  38.         sum = (sum & 0xffff) + (sum >> 16);
  39.     }
  40.     return (uint16_t) ((~sum) & 0xFFFF);
  41. }



  42. /**
  43. * detect SYN of tcp, clamp MSSs
  44. */
  45. static void clamp_mss(struct sk_buff* skb, int clamp_mss)
  46. {
  47.     struct tcphdr* ptcphdr;
  48.     struct iphdr* piphdr;
  49.     uint8_t* pppphdr;
  50.     struct pppoe_hdr *ppppoehdr;
  51.     int len;
  52.     int minlen;
  53.     int optlen;

  54.     uint16_t csum;
  55.     uint16_t mss = 0;
  56.     uint8_t* opt;
  57.     uint8_t* mssopt;

  58.     ppppoehdr = pppoe_hdr(skb);

  59.     pppphdr = (uint8_t*)ppppoehdr + sizeof(struct pppoe_hdr);

  60.     /* check PPP protocol type */
  61.     if (pppphdr[0] & 0x01)
  62.     {
  63.         /* may be 8 bit protocol type ? */
  64.         if (pppphdr[0] != 0x21)
  65.         {
  66.             return;
  67.         }

  68.         piphdr = (struct iphdr*)(pppphdr + 1);
  69.         minlen = 41; // tcp header len + ip header len + ppp header len
  70.     }
  71.     else
  72.     {
  73.         /* 16 bit protocol type, upper layer is IP, and the protocol value is 0x0021*/
  74.         if (pppphdr[0] != 0x00 || pppphdr[1] != 0x21)
  75.         {
  76.             return;
  77.         }
  78.         piphdr = (struct iphdr*)(pppphdr + 2);
  79.         minlen = 42;
  80.     }

  81.     /* Is it too short? */
  82.     len = (int)ntohs(ppppoehdr->length);
  83.     if (len < minlen)
  84.     {
  85.         return;
  86.     }

  87.     /* Verify once more that it's IPv4 */
  88.     if (piphdr->version != 4)
  89.     {
  90.         return;
  91.     }

  92.     /* Is it a fragment that's not at the beginning of the packet? */
  93.     if ( ntohs(piphdr->frag_off) & 0x1FFF)
  94.     {
  95.         return;
  96.     }

  97.     /* Is it TCP? */
  98.     if (piphdr->protocol != 0x06)
  99.     {
  100.         return;
  101.     }

  102.     /* Get start of TCP header */
  103.     ptcphdr = (struct tcphdr*)((uint8_t*)piphdr + (piphdr->ihl) * 4);

  104.     /* Is SYN set? */
  105.     if (!ptcphdr->syn)
  106.     {
  107.         return;
  108.     }

  109.     /* Compute and verify TCP checksum -- do not touch a packet with a bad checksum */
  110.     csum = tcp_checksum((uint8_t*)piphdr, (uint8_t*)ptcphdr);
  111.     if (csum)
  112.     {
  113.         return;
  114.     }

  115.     /* Look for existing MSS option */
  116.     optlen = ntohs(ptcphdr->doff) * 4 - 20;

  117.     if (optlen <= 0)
  118.     {
  119.         return;
  120.     }

  121.     opt = (uint8_t*)ptcphdr + 20;

  122.     while (optlen > 0)
  123.     {
  124.         switch (*opt)
  125.         {
  126.         case 0:    // end of options
  127.         case 1:    // empty option, always use for pad
  128.             len = 1;
  129.             break;
  130.         case 2:    // MSS option
  131.             if (opt[1] != 4)
  132.             {
  133.                 return;
  134.             }

  135.             len = 4;
  136.             mss = opt[2] * 256 + opt[3];
  137.             mssopt = opt;
  138.             break;
  139.         case 3:
  140.         case 4:
  141.         case 5:
  142.         case 8:
  143.             len = (int)opt[1];
  144.             break;
  145.         default:
  146.             return;

  147.         }

  148.         if (mss > 0)
  149.         {
  150.             break;
  151.         }

  152.         optlen -= len;
  153.         opt += len;

  154.     }

  155.     /* If MSS Not exists or it's low enough, do nothing */
  156.     if (!mss || mss <= clamp_mss)
  157.     {
  158.         return;
  159.     }

  160.     mssopt[2] = (((unsigned) clamp_mss) >> 8) & 0xFF;
  161.     mssopt[3] = ((unsigned) clamp_mss) & 0xFF;

  162.     /* Recompute TCP checksum */
  163.     ptcphdr->check = 0;

  164.     csum = tcp_checksum((uint8_t*)piphdr, (uint8_t*)ptcphdr);
  165.     ptcphdr->check = csum;
  166. }

阅读(3694) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~