TCP校验值的伪头以及校验值计算

tcp层的校验值难道还需要ip层的元数据也就是ip头吗?如果一切都是理想的显然不需要,因为这违背了分层隔离的原则,下层一定不能依赖上层,但是上层可以访问下层,还好tcp使用ip信息正是这一点。按照封包原则,封装到TCP层的时候,ip信息还没有封装上去,但是校验值却需要马上进行计算,所以必须手工构造一个伪头部来表示ip层的信息,怎么构造呢?在数据到tcp层的时候其实用户肯定知道数据发往何处,源地址和目的地址都有,只不过是还没有封装到数据上罢了,简单的例子就是在socket程序中,connect或者accept或者recvfrom以及sendto都会保留有地址信息,另外伪头中还将保留有传输层协议信息,所有这一切都是为了使得通信更加安全和缜密,试想如果一个中间人截获了一个icmp包,然后改为了udp包或发生什么,该udp不是随意的而是精心构造的,但是加入了伪头部如此之行为得逞就困难多了,因为伪头部中有协议字段,除此之外,任何错误的投递,错误的数据长度以及错误的协议都会被检测到。看一下伪头吧:

struct psd_head

{

__u32 saddr; // 源网络层地址

__u32 daddr; // 目的网络层地址

__u8 mbz; //赋0

__u8 ptcl; // 传输层协议

__u16 tcpudpl; //传输层长度

};

以下是一个简单的校验和校验码的计算函数

void tcpv4_check_addr( __u16 * ppkgdata )

{

char * indata;

__u16 ippktlen, udppktlen,tcppktlen,wd;

__u32 ipheadlen;

__u32 sum,i,pl,el;

struct psd_head psd;

struct iphdr * ipd;

struct tcphdr * tcpd;

struct udphdr * udpd;

__u16 * databegin;

indata = (char *)ppkgdata; //从MAC开始的整个帧

ipheadlen = 14 + (indata[14]&0x0f)*4 ; //MAC和ip头的长度和

databegin = (__u16 *)(indata + ipheadlen); //ip数据

ipd = (struct iphdr *)(indata + 14); //MAC数据

tcpd = (struct tcphdr *)(indata + ipheadlen); //ip数据

ippktlen = htons(ipd->tot_len); //ip头和ip数据的总长度

if(ipd->protocol == 0x6){

tcppktlen = ippktlen +14 - ipheadlen; //tcp头和tcp数据的总长度

tcpd->check = 0;

psd.saddr = ipd->saddr; //构造伪头部

psd.daddr = ipd->daddr;

psd.mbz=0;

psd.ptcl = 0x06; //ip的下一个头

psd.tcpudpl = htons(tcppktlen);

sum = 0;

wd = tcppktlen/2; //每次数据前移16位而不是一个字节

for(i=0;i<wd><p>sum += *databegin;</p> <p>databegin++;</p> <p>}</p> <p>el = tcppktlen - wd*2;</p> <p>if(el != 0)</p> <p>sum += (*databegin&amp;0xff);</p> <p>wd = sizeof(struct psd_head)/2;</p> <p>databegin = (__u16 *) &amp;psd.saddr;</p> <p>for(i=0;i<wd><p>sum += *databegin;</p> <p>databegin++;</p> <p>}//下面这个表达式就是高低16分别相加,sum/65536就是高16位:sum </p> <p>pl = (sum + sum/65536)&amp;0xffff;</p> <p>sum = 0xffff^pl; </p> <p>tcpd-&gt;check = (__u16)sum; //检验和的计算很简单,就是将数据相加并且回卷之后取反</p> <p>}</p> <p>return;</p> <p>}</p> <p>以上的算法再清晰不过了,甚至将tcp,ip头部的偏移怎么计算都表达了出来,但是这个函数并不适用于实际情况,因为在高负载网络环境下,特别是NAT或者数据过滤网关环境下,校验和的计算是一个很频繁的过程,因此上述函数的c语言本质将很影响效率,取而代之的是用汇编实现,正如linux内核中的那样:</p> <p>static inline __sum16 csum_fold(__wsum sum)</p> <p>{</p> <p>__asm__(</p> <p>"addl %1, %0 ;/n"</p> <p>"adcl $0xffff, %0 ;/n"</p> <p>: "=r" (sum)</p> <p>: "r" ((__force u32)sum </p> <p>"" ((__force u32)sum &amp; 0xffff0000)</p> <p>);</p> <p>return (__force __sum16)(~(__force u32)sum &gt;&gt; 16);</p> <p>}</p> <p>static inline __wsum csum_tcpudp_nofold(__be32 saddr, __be32 daddr, unsigned short len, unsigned short proto, __wsum sum)</p> <p>{</p> <p>__asm__(</p> <p>"addl %1, %0 ;/n"</p> <p>"adcl %2, %0 ;/n"</p> <p>"adcl %3, %0 ;/n"</p> <p>"adcl $0, %0 ;/n"</p> <p>: "=r" (sum)</p> <p>: "g" (daddr), "g"(saddr), "g"((len + proto) </p> <p>return sum;</p> <p>}</p> <p>static inline __sum16 csum_tcpudp_magic(__be32 saddr, __be32 daddr, unsigned short len, unsigned short proto, __wsum sum)</p> <p>{</p> <p>return csum_fold(csum_tcpudp_nofold(saddr,daddr,len,proto,sum));</p> <p>}</p> <p>别看一个小小的计算校验和,它本质上影响了网络传输的效率,如果用tcpv4_check_addr这个函数计算校验和,效率慢了10倍之多,但是用汇编取而代之的话,效率虽然由于额外吸收而有所下降,但是数量级并没有改变。</p> </wd></p></wd>

你可能感兴趣的:(tcp)