C/C++ TCP/IP 协议簇校验合计算方法(tcpip checksum)

本文介绍了一种实用的TCP/IP校验和计算方法,并详细解释了如何正确地计算TCP/IP包的校验和,包括TCP头部、负载及伪头部的处理方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    本文将提供真正可用的 TCP/IP 协议校验合合计算,即它不是众人理论上的 TCPIP 校验合计算方法,虽然我曾为此被坑的有点小小的头疼(开个小玩笑)

    此代码摘要自本人的一个实验工程(astar-tun)故名思意这是一个 “tun/tap” 虚拟网卡层面(链路层)的一个小工程,它内部实现了一个super simple 专用于 “forwarding” 的 tcpip 协议栈(proto net stack),完整的协议栈;诸如:lwIP、uc/IP、TinyTCP、BSD / linux,win32。

   下面为 tcpip 协议头的定义的,一般情况下 tcpip 头为 20 个字节,人们总会说 tcpip 至少 20 个字节,否则就不是 tcpip 协议,本人在此纠正这句话,tcpip 协议头是变长的,它可以是 0 个字节(虽然这不可能)这取决于协议头中 “th_off” 字段的值( “th_off * 4 = nTcpHeaderCount”)

#ifndef tcp_seq
#define tcp_seq tcp_seq
typedef volatile uint32_t tcp_seq;
#endif

/*
* TCP header.
* Per RFC 793, September, 1981.
*/
typedef struct _tcphdr
{
    uint16_t th_sport;     /* source port */
    uint16_t th_dport;     /* destination port */
    tcp_seq  th_seq;       /* sequence number */
    tcp_seq  th_ack;       /* acknowledgement number */
    uint8_t  th_x2 : 4;    /* (unused) */
    uint8_t  th_off : 4;   /* data offset */
    uint8_t  th_flags;

    uint16_t th_win;       /* window */
    uint16_t th_sum;       /* checksum */
    uint16_t th_urp;       /* urgent pointer */
} tcphdr;

    上述代码是 “RFC 793” 定义的 “tcpip” 标准协议头,为 20 字节( “th_off” = 0x5),那么回到本文重心 tcpip 协议校验合究是连带着 payload(其实就是 tcp_segments) 一起 check,还是与 ip 协议相同只需要 check ip 协议头?tcpip 协议的设计者为了保证数据在“互联网”中传递的 payload 数据“正确与一致性”,电原子信号在“电缆或光缆”传输过程中可能会发生错误【光缆错误率会低很多】,或者中途的“交换机与路由器”出现一些问题,促使了 tcp 携带的 payload 数据出现故障,而这个问题几乎无时无刻不在发生。

int tcp_checksum(uint8_t* tcphdr, int tcplen, uint32_t* srcaddr, uint32_t* dstaddr)
{
    uint8_t pseudoheader[12];
    uint16_t checksum = 0;

    if (tcphdr != NULL && srcaddr && dstaddr)
    {
        memcpy(&pseudoheader[0], srcaddr, 4);
        memcpy(&pseudoheader[4], dstaddr, 4);
        pseudoheader[8] = 0; /* fill zeors */
        pseudoheader[9] = IPPROTO_TCP;
        memcpy(&pseudoheader[10], &tcplen, 2);

        uint8_t n = pseudoheader[10];
        pseudoheader[10] = pseudoheader[11];
        pseudoheader[11] = n;
        checksum = ~tcpip_chksum(tcpip_chksum(0, pseudoheader, sizeof(pseudoheader)), tcphdr, tcplen);
    }
    return checksum;
}

tcplen = tcphdr + payload  

   tcp_checksum 除计算头部与载荷的值,还需要加上一个占“12”字节的 “tcp_psehdr” 伪头部,才可以(这个头里面其实没什么东西,就是标识了 srcaddr、dstaddr、prop、zeros(rsv)、tcplen 几个字段。

我们先把 pseudoheader 的 checksum 计算出来,然后在与 “tcphdr + payload” 的值,连接在一起最后“位取反(~)”就可以获取到正确的这个 tcpip packet 真正的 checksum。

int process_tcppacket_datapacket_ack(tcppcb* pcb, uint32_t cseq, uint32_t sseq)
{
    if (pcb == NULL)
    {
        return 0;
    }
    uint32_t dstaddr = pcb->dstaddr;
    uint32_t srcaddr = pcb->srcaddr;
    uint16_t dstport = pcb->dstport;
    uint16_t srcport = pcb->srcport;

    tcphdr tcpo;
    tcpo.th_ack = __htonl(cseq);
    tcpo.th_seq = __htonl(sseq);
    tcpo.th_urp = 0;
    tcpo.th_sum = 0;
    tcpo.th_x2 = 0;
    tcpo.th_flags = 0x10; // SETACK(tcpo.th_flags);
    tcpo.th_off = 5;
    tcpo.th_win = __htons(365); 
    tcpo.th_dport = __htons(srcport);
    tcpo.th_sport = __htons(dstport);

    int packetlen = 20; // tcpo.th_off * 4
    tcpo.th_sum = __htons(tcp_checksum((uint8_t*)&tcpo, packetlen, &dstaddr, &srcaddr));

    uint8_t* packet = ipv4_build_packet(dstaddr, srcaddr, 0x00, IPPROTO_TCP, (uint8_t*)&tcpo, &packetlen);
    if (packet != NULL)
    {
        tun_write(tuntap, packet, packetlen);
        __free(packet);
    }
    return 0;
}

    上述代码是一从 “astar-tun” 中摘要的一段代码,它的作用主要用于返回 ack tcpip-packet 到 “tun/tap” 网卡中,上述代码直接展示了,你应当如何正确的调用 “tcp_checksum” 函数,下方列出 “tcp_checksum”  函数依赖的 “tcpip_chksum” 函数。

unsigned short tcpip_chksum(unsigned short initcksum, unsigned char* data, int datalen)
{
    unsigned int checksum = initcksum;
    bool odd = (datalen & 1) != 0 ? 1 : 0;
    int index = 0;
    if (odd)
    {
        datalen -= odd;
    }
    for (index = 0; index < datalen; index += 2)
    {
        checksum += ((unsigned long)data[index] << 8) + ((unsigned long)data[index + 1]);
    }
    if (odd)
    {
        checksum += ((unsigned long)data[index] << 8);
    }
    while (checksum >> 16)
    {
        checksum = (checksum & 0xFFFF) + (checksum >> 16);
    }
    return checksum;
}

* 程序名称:路由追踪(Tracert)程序 实现原理:Tracert 程序关键是对 IP 头部生存时间(time to live)TTL 字段的使用,程序实现时是向目 地主机发送一个 ICMP 回显请求消息,初始时 TTL 等于 1,这样当该数据报抵达途中的第一个路由器 时,TTL 的值就被减为 0,导致发生超时错误,因此该路由生成一份 ICMP 超时差错报文返回给源主 机。随后,主机将数据报的 TTL 值递增 1,以便 IP 报能传送到下一个路由器,并由下一个路由器生成 ICMP 超时差错报文返回给源主机。不断重复这个过程,直到数据报达到最终的目地主机,此时目地 主机将返回 ICMP 回显应答消息。这样,源主机只需对返回的每一份 ICMP 报文进行解析处理,就可 以掌握数据报从源主机到达目地主机途中所经过的路由信息。 */ #include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include<map> #include<vector> using namespace std; #pragma comment(lib, "Ws2_32.lib") //IP 报头 typedef struct { unsigned char hdr_len:4; //4 位头部长度 unsigned char version:4; //4 位版本号 unsigned char tos; //8 位服务类型 unsigned short total_len; //16 位总长度 unsigned short identifier; //16 位标识符 unsigned short frag_and_flags; //3 位标志加 13 位片偏移 unsigned char ttl; //8 位生存时间 unsigned char protocol; //8 位上层协议号 unsigned short checksum; //16 位校验和 unsigned long sourceIP; //32 位源 IP 地址 unsigned long destIP; //32 位目的 IP 地址 } IP_HEADER; //ICMP 报头 typedef struct { BYTE type; //8 位类型字段 BYTE code; //8 位代码字段 USHORT cksum; //16 位校验和 USHORT id; //16 位标识符 USHORT seq; //16 位序列号 } ICMP_HEADER; //报文解码结构 typedef struct { USHORT usSeqNo; //序列号 DWORD dwRoundTripTime; //往返时间 in_addr dwIPaddr; //返回报文的 IP 地址 } DECODE_RESULT; vector< pair<string,string> > IpAddressStatus1; //计算网际校验和函数 USHORT checksum(USHORT *pBuf,int iSize) { unsigned long cksum=0; while(iSize>1) { cksum+=*pBuf++; iSize-=sizeof(USHORT); } if(iSize) { cksum+=*(UCHAR *)pBuf; } cksum=(cksum>>16)+(cksum&0xffff); cksum+=(cksum>>16); return (USHORT)(~cksum); } //对数据包进行解码 BOOL DecodeIcmpResponse(char * pBuf,int iPacketSize,DECODE_RESULT &DecodeResult,BYTE ICMP_ECHO_REPLY,BYTE ICMP_TIMEOUT) { //检查数据报大小的法性 IP_HEADER* pIpHdr = (IP_HEADER*)pBuf; int iIpHdrLen = pIpHdr->hdr_len * 4; if (iPacketSize < (int)(iIpHdrLen+sizeof(ICMP_HEADER))) return FALSE; //根据 ICMP 报文类型提取 ID 字段和序列号字段 ICMP_HEADER *pIcmpHdr=(ICMP_HEADER *)(pBuf+iIpHdrLen); USHORT usID,usSquNo; if(pIcmpHdr->type==ICMP_ECHO_REPLY) { //ICMP 回显应答报文 usID=pIcmpHdr->id; //报文 ID usSquNo=pIcmpHdr->seq; //报文序列号 } else if(pIcmpHdr->type==ICMP_TIMEOUT) { //ICMP 超时差错报文 char * pInnerIpHdr=pBuf+iIpHdrLen+sizeof(ICMP_HEADER); //载荷中的 IP 头 int iInnerIPHdrLen=((IP_HEADER *)pInnerIpHdr)->hdr_len*4; //载荷中的 IP 头长 ICMP_HEADER * pInnerIcmpHdr=(ICMP_HEADER *)(pInnerIpHdr+iInnerIPHdrLen);//载荷中的 ICMP 头 usID=pInnerIcmpHdr->id; //报文 ID usSquNo=pInnerIcmpHdr->seq; //序列号 } else { return false; } //检查 ID 和序列号以确定收到期待数据报 if(usID!=(USHORT)GetCurrentProcessId()||usSquNo!=DecodeResult.usSeqNo) { return false; } // cout<<" pIpHdrLen="<<htons(pIpHdr->total_len); //填充序列号<<" "; cout<<" bytes="<<(int)iPacketSize-iIpHdrLen-8<<" "; cout<<"ttl="<<(int)pIpHdr->ttl<<" "; // cout<<"Protocol:"<<(int)pIpHdr->protocol<<"\n"; //记录 IP 地址并计算往返时间 DecodeResult.dwIPaddr.s_addr=pIpHdr->sourceIP; DecodeResult.dwRoundTripTime=GetTickCount()-DecodeResult.dwRoundTripTime; //处理正确收到的 ICMP 数据报 if (pIcmpHdr->type == ICMP_ECHO_REPLY ||pIcmpHdr->type == ICMP_TIMEOUT) { //输出往返时间信息 if(DecodeResult.dwRoundTripTime) cout<<" 时间="<<DecodeResult.dwRoundTripTime<<"ms"<<flush; else cout<<" "<<"时间<1ms"<<flush; } return true; } int main(void) { //初始化 Windows sockets 网络环境 WSADATA wsa; WSAStartup(MAKEWORD(2,2),&wsa); char IpAddress[255]; map<string,string> IpAddressStatus; int ip1,ip2,ip3,ip4,ip5; int cnt = 255; int maxHops = 20; int maxTimeout=1000; cout<<"请输入一个 IP 地址范围(如192.168.142.119-255,只需要输入192 168 142 119 255):"; cin>>ip1>>ip2>>ip3>>ip4>>ip5; cnt=ip5-ip4; while(ip1>255||ip2>255||ip3>255||ip4>255||cnt<0) { cout<<"输入的 IP 地址范围无效!请重新输入:"<<"\n"; cin>>ip1>>ip2>>ip3>>ip4>>ip5; cnt=ip5-ip4; } cout<<"请输入超时时间(ms):"; cin>>maxTimeout; cout<<"最大路由跳数:"; cin>>maxHops; while(cnt>=0) { // if(ip1>255||ip2>255||ip3>255||ip4>255||) { // cout<<"输入的 IP 地址范围无效!请重新输入:"<<"\n"; // cin>>ip1>>ip2>>ip3>>ip4>>ip5; // cnt=ip5-ip4; // } sprintf(IpAddress,"%d.%d.%d.%d",ip1,ip2,ip3,ip5-cnt); cnt--; //得到 IP 地址 u_long ulDestIP=inet_addr(IpAddress); cout<<"\n正在 ping 的 ip 地址:"<<IpAddress<<"\n"; //转换不成功时按域名解析 if(ulDestIP==INADDR_NONE) { hostent * pHostent=gethostbyname(IpAddress); if(pHostent) { ulDestIP=(*(in_addr*)pHostent->h_addr).s_addr; } else { cout<<"输入的 IP 地址或域名无效!"<<endl; WSACleanup(); return 0; } } // cout<<"Tracing route to "<<IpAddress<<" with a maximum of "<<maxHops<<" hops.\n"<<endl; //填充目地端 socket 地址 sockaddr_in destSockAddr; ZeroMemory(&destSockAddr,sizeof(sockaddr_in)); destSockAddr.sin_family=AF_INET; destSockAddr.sin_addr.s_addr=ulDestIP; //创建原始套接字 SOCKET sockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0, WSA_FLAG_OVERLAPPED); //超时时间 int iTimeout=maxTimeout; //接收超时 setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char *)&iTimeout,sizeof(iTimeout)); //发送超时 // setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char *)&iTimeout,sizeof(iTimeout)); //构造 ICMP 回显请求消息,并以 TTL 递增的顺序发送报文 //ICMP 类型字段 const BYTE ICMP_ECHO_REQUEST=8; //请求回显 const BYTE ICMP_ECHO_REPLY=0; //回显应答 const BYTE ICMP_TIMEOUT=11; //传输超时 //其他常量定义 const int DEF_ICMP_DATA_SIZE=32; //ICMP 报文默认数据字段长度 const int MAX_ICMP_PACKET_SIZE=1024;//ICMP 报文最大长度(包括报头) const DWORD DEF_ICMP_TIMEOUT=maxTimeout; //回显应答超时时间 // const int DEF_MAX_HOP=30; //最大跳站数 const int DEF_MAX_HOP=maxHops; //最大跳站数 //填充 ICMP 报文中每次发送时不变的字段 char IcmpSendBuf[sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE];//发送缓冲区 memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf)); //初始化发送缓冲区 char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; //接收缓冲区 memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收缓冲区 ICMP_HEADER * pIcmpHeader=(ICMP_HEADER*)IcmpSendBuf; pIcmpHeader->type=ICMP_ECHO_REQUEST; //类型为请求回显 pIcmpHeader->code=0; //代码字段为 0 pIcmpHeader->id=(USHORT)GetCurrentProcessId(); //ID 字段为当前进程号 memset(IcmpSendBuf+sizeof(ICMP_HEADER),'E',DEF_ICMP_DATA_SIZE);//数据字段 USHORT usSeqNo=0; //ICMP 报文序列号 int iTTL=1; //TTL 初始值为 1 BOOL bReachDestHost=FALSE; //循环退出标志 int iMaxHot=DEF_MAX_HOP; //循环的最大次数 DECODE_RESULT DecodeResult; //传递给报文解码函数的结构化参数 int flag=0; int ping_ttl=4; while(!bReachDestHost&&ping_ttl--) { //设置 IP 报头的 TTL 字段 // setsockopt(sockRaw,IPPROTO_IP,IP_TTL,(char *)&iTTL,sizeof(iTTL)); // cout<<iTTL<<flush; //输出当前序号 cout<<4-ping_ttl; //填充 ICMP 报文中每次发送变化的字段 ((ICMP_HEADER *)IcmpSendBuf)->cksum=0; //校验和先置为 0 ((ICMP_HEADER *)IcmpSendBuf)->seq=htons(usSeqNo++); //填充序列号 ((ICMP_HEADER *)IcmpSendBuf)->cksum=checksum((USHORT *)IcmpSendBuf, sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE); //计算校验和 //记录序列号和当前时间 DecodeResult.usSeqNo=((ICMP_HEADER*)IcmpSendBuf)->seq; //当前序号 DecodeResult.dwRoundTripTime=GetTickCount(); //当前时间 //发送 TCP 回显请求信息 sendto(sockRaw,IcmpSendBuf,sizeof(IcmpSendBuf),0,(sockaddr*)&destSockAddr,sizeof(destSockAddr)); //接收 ICMP 差错报文并进行解析处理 sockaddr_in from; //对端 socket 地址 int iFromLen=sizeof(from); //地址结构大小 int iReadDataLen; //接收数据长度 while(1) { //接收数据 iReadDataLen=recvfrom(sockRaw,IcmpRecvBuf,MAX_ICMP_PACKET_SIZE,0,(sockaddr*)&from,&iFromLen); // cout<<"iReadDataLen:"<<iReadDataLen<<"\n"; // cout<<"IcmpRecvBuf:"<<IcmpRecvBuf<<"\n"; if(iReadDataLen!=SOCKET_ERROR) { //有数据到达 //对数据包进行解码 if(DecodeIcmpResponse(IcmpRecvBuf,iReadDataLen,DecodeResult,ICMP_ECHO_REPLY,ICMP_TIMEOUT)) { //到达目的地,退出循环 if(DecodeResult.dwIPaddr.s_addr==destSockAddr.sin_addr.s_addr) // bReachDestHost=true; flag=1; //输出 IP 地址 cout<<'\t'<<inet_ntoa(DecodeResult.dwIPaddr)<<"\n"; // IpAddressStatus[IpAddress] = "在线"; // IpAddressStatus1.push_back(make_pair(IpAddress,"在线")); break; } // else{ // IpAddressStatus1.push_back(make_pair(IpAddress,"在线")); // break; // } } else if(WSAGetLastError()==WSAETIMEDOUT) { //接收超时,输出*号 cout<<" *"<<'\t'<<"Request timed out."<<endl; // IpAddressStatus1.push_back(make_pair(IpAddress,"不可达")); iTTL++; if(iTTL>6)break; break; } else { cout<<"错误\n"; break; } } // iTTL++; //递增 TTL 值 } if(flag) { IpAddressStatus1.push_back(make_pair(IpAddress,"在线")); } else { IpAddressStatus1.push_back(make_pair(IpAddress,"不可达")); } // if(iMaxHot <= 0) { // //cout<<"地址不可达\n"; IpAddressStatus[IpAddress] = "不可达"; // IpAddressStatus1.push_back(make_pair(IpAddress,"不可达")); // } } //迭代 cout<<"----------------------------------------------------------\n"; cout<<"ip 地址范围 "<<ip1<<"."<<ip2<<"."<<ip3<<"."<<ip4<<"-"<<ip5<<" 的 ping 情况:\n"; for(vector< pair<string,string> > ::iterator it = IpAddressStatus1.begin(); it != IpAddressStatus1.end(); it++) cout<<(*it).first<<":\t\t"<<(*it).second<<"\n";//输出key 和value值 } 优化该代码 且符ICMP报文结构 实现Ping的基本操作 发送ICMP回显请求报文,用于测试主机与主机之间的连通情况 使用c++语言
最新发布
05-29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值