原始套接字构建 TCP三次握手 及相关问题

863 阅读7分钟

分析TCP、IP头部

头部字段

IP头部结构 在这里插入图片描述 TCP头部结构 在这里插入图片描述 熟悉计算机网络的同学对这些都已经很熟悉了,就不复述了。 Tip: 为了结构清晰 分开的代码可能有一定逻辑性问题,请参照源码

构建TCP伪首部结构体

用于计算校验和

struct udp_front  //tcp(udp)伪首部结构体
{
    uint32_t srcip;				//源IP 
    uint32_t desip;				//目的IP 
    u_int8_t zero;				
    u_int8_t protocol;			//协议类型 
    u_int16_t len;				//协议长度 
};

填充TCP伪首部

//...
struct udp_front front;
    front.srcip = ip->saddr;
    front.desip = ip->daddr;
    front.len = htons(20 + strlen(message));
    front.protocol = 6;
    front.zero = 0;

填充IP头部

    ip = (struct iphdr *)sendbuf;
    ip->ihl = sizeof(struct iphdr) >> 2; //首部长度
    ip->version = 4;   //ip协议版本
    ip->tos = 0;   //服务类型字段
    ip->tot_len = 0;   //总长度
    ip->id = htons(my_seq);   //id值
    ip->frag_off = 0;
    ip->ttl = 128;
    ip->protocol = IPPROTO_TCP;
    ip->check = 0;  //内核会算相应的效验和
//    ip->saddr = src_ip;
	//将一个点分十进制的IP转换成一个长整数型数 
	ip->saddr = inet_addr(argv[1]);
    ip->daddr = inet_addr(argv[3]);
    //...
    ip->tot_len = (20 + 20 + strlen(message));   //IP头长度+TCP头部长度+数据长度 = 总长度
    printf("ip->tot_len:%d\n",ip->tot_len);
    ip->check = in_chksum((unsigned short *)sendbuf, 20);

填充TCP头部

TCP首部的syn、ack、fin等位 必须按照三次握手的要求构建,第一次请求syn位为1,响应ack位则为1...

 struct tcphdr *tcp;
    tcp = (struct tcphdr *)(sendbuf + sizeof(struct iphdr));
    bzero(tcp, sizeof(struct tcphdr *));
    //-------------------------------------------------------------------
    //将短整型变量从主机字节顺序转变成网络字节顺序
    //htonl 长  
	//atoi 把字符串转换成整型数 
    tcp->source = htons(atoi(argv[2]));  //源端口
    tcp->dest = htons(atoi(argv[4]));    //目的端口
    tcp->seq = htonl(100000000);   
    tcp->ack_seq = htonl(ack_seq); 

    tcp->doff = 5;  //数据偏移(TCP头部字节长度/4)
    tcp->res1 = 0;  //保留字段(4位)
    tcp->fin = 0;       //..用来释放一个连接
    tcp->syn = 1;         //..表示这是一个连接请求
    tcp->rst = 0;         //..用来表示tcp连接是否出现严重差错
    tcp->psh = 0;         //..推送
    tcp->ack = 0;         //..表示是一个连接请求
    tcp->urg = 0;         //..紧急数据标志
    tcp->res2 = 0;  //保留字段(2位)
    tcp->window = htons(65535);  //初始窗口值设置

    tcp->check = 0;
    tcp->urg_ptr = 0; 
    tcp->check = 0;   //效验和,效验整个tcp数据报
    strcpy((sendbuf+20+20), message);  //把message存入IP+TCP头部位之后 

    tcp->check = tcp_check((sendbuf+20), 20+strlen(message), front);

计算校验和

想要详细了解的请 参考 博客

//计算tcp(udp)效验和
unsigned short tcp_check(char *sendbuf, int len, const struct udp_front front)
{
    char str[MAXLINE];
    bzero(&str, MAXLINE);
    bcopy(&front, str, sizeof(front));
    bcopy(sendbuf, str+sizeof(front), len);
    struct udp_front *ptr;
    ptr = (struct udp_front *)str;
    char *s;
    s = (str+20);
    return in_chksum((unsigned short *)str, sizeof(front)+len);
}

//效验和算法
uint16_t in_chksum(uint16_t *addr, int len)
{
    int nleft = len;
    uint32_t sum = 0;
    uint16_t *w = addr;
    uint16_t answer = 0;
    //把ICMP报头二进制数据以2字节为单位累加起来
    while (nleft > 1)
    {
        sum += *w++;
        nleft -= 2;
    }
    if (nleft == 1)
    {
        *(unsigned char *)(&answer) = *(unsigned char *)w;
        sum += answer;
    }
    sum = (sum>>16) + (sum&0xffff);
    sum += (sum>>16);
    answer = ~sum;
    return answer;
}

握手过程细节

TCP需要三次握手建立连接之后才能传输数据,所以使用TCP原始套接字传输数据需要先完成三次握手: 第一次握手:客户端TCP头部 SYN位至为1 给定seq位的值 第二次握手:服务端返回SYN+ACK(0x012),ack_seq 为客户端第一次握手的seq位+1,并且会随机一个seq位值 第三次握手:客户端需要获取到服务端响应seq位的值,将其+1,存入TCP头部ack_seq位中,seq位同样+1

Tips:

  1. recvfrom获取的数据中,需要根据TCP首部位和IP首部位去判断所需字段在rev[]中的位置,已给出相关注释
  2. ipheadlen = 4 * ipheadlen; 这句代码 是因为IP首部的IP报头域的计算单位是4bytes ,所以需要将获取的值乘4才能得出最终结果20
//接收SYN + ACK报文
    printf("receive SYN_ACK start:---------------------------------------\n");  //将接受的IP数据报输出
    unsigned char rec[1024];
	int n = recvfrom(raw_sockfd, rec, 1024, 0, NULL, NULL); 
	printf("receive %d bytes:\n", n);  //将接受的IP数据报输出
    for(int i=0; i<n; i++){
        if(i % 16 == 0)
            printf("\n");
        printf("%02x ", rec[i]);
    }
    printf("\n\nreceive SYN_ACK end:---------------------------------------\n");
    unsigned char ipheadlen = rec[0]; //取出IP数据包的长度
    ipheadlen = (ipheadlen & 0x0f);   //IP首部长度字段只占该字节后四位   将前四位全部至为0
	printf("ipheadlen: %d\n",ipheadlen);
    ipheadlen = 4 * ipheadlen; //IP报头域的数值表示整个IP报头的长度,单位是4bytes 
    //版本 4 +  头部长度 4 +  服务类型 8 
    unsigned short iplength = ntohs(*((unsigned short *)(rec+2))); //获取IP数据报总长度
    printf("iplengthAll: %d\n",iplength);
    unsigned short tcplength = iplength - ipheadlen;  //计算TCP数据报长度
    printf("TCPLen: %d\n",tcplength);
    //ip头部 + 4(端口) + 4 (SEQ) ntohl 从网络字节顺序 转换为主机字节顺序 
    // SEQ 
    unsigned int seq = ntohs(*((signed int*)(rec+ipheadlen+4)));
    printf("SEQ:%d\n",seq);
    // 确认应答号 
    unsigned int ack = ntohs(*((signed int*)(rec+ipheadlen+8))); //获取TCP首部的确认号
    printf("ACK:%d, %d\n", ack, my_seq);
    //flag 位位置无法确定 
    unsigned char flag = rec[ipheadlen + 14]; //获取标志字段
    printf("flag:%02x\n", flag);
    //--------------------------------------------------------------------- 
    // SYN + ACK 0x012
    flag = (flag & 0x12);  //只需要ACK和SYN标志的值 其他位都清0 
    if(flag != 0x12){  //判断是否为SYN+ACK包
	    printf("ACK+SYN\n");   
    }else{
        printf("OK ACK+SYN\n");
        //发送 ACK包
        unsigned int ack_seq = ack; 
		int mesg_len_a = make_message_ACK(send_message, MAXLINE, argv, ack_seq); 
    	sendto(raw_sockfd,send_message, mesg_len_a, 0, (struct sockaddr *)&server_address, sizeof(server_address));
    }

实现效果

在这里插入图片描述 在这里插入图片描述

相关问题

  • 使用原始套接字发送握手请求之后即使接收到了服务端的响应,Linux操作系统还是会因为TCP数据包未通过内核封装而直接发送RST中断连接

解决方式:

  1. 想办法过滤掉RST请求,让它无法发送出去,参考网上的解决方案
  2. 修改Linux源码(做不到,只剩下第一种方法)

源码

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/ip_icmp.h>
#include <netinet/udp.h>
#include <netinet/tcp.h>

#define MAXLINE 1024

#define LOCAL_IP "192.168.1.136" //本主机IP
#define LOCAL_PORT 8081           //本主机定义端口
#define DEST_IP "172.27.174.245"      //要测试的目的ip
#define DEST_PORT 8080                   //要测试的目的端口

struct udp_front  //tcp(udp)伪首部结构体
{
    uint32_t srcip;				//源IP 
    uint32_t desip;				//目的IP 
    u_int8_t zero;				
    u_int8_t protocol;			//协议类型 
    u_int16_t len;				//协议长度 
};

u_int16_t in_chksum(u_int16_t *addr, int len);
u_int16_t tcp_check(char *sendbuf, int len, const struct udp_front front);
int make_message(char *sendbuf, int send_buf_len, char **argv);
int make_message_ACK(char *sendbuf, int send_buf_len, char **argv, unsigned int ack_seq);
int my_seq = 100; //TCP序号

int main(int argc, char **argv)
{
	if (argc != 5) {
        printf("Useage:SendTcp source_ip source_port dest_ip dest_port");
        return -1;
    }
    int raw_sockfd;
    int size = 1024;
    char send_message[MAXLINE];
    struct sockaddr_in server_address;
    //创建原始套接字
    raw_sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
    //创建套接字地址
    bzero(&server_address,sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = inet_addr(DEST_IP);
    //设置套接字为随数据包含IP首部(设置这个选项后需要我们手动写入IP头)
    setsockopt(raw_sockfd, IPPROTO_IP, IP_HDRINCL, &size, sizeof(size));

    bzero(&send_message, sizeof(send_message));
    //拼接完整的TCP数据包(IP头+TCP头+数据)
    int mesg_len = make_message(send_message, MAXLINE, argv);
    printf("mesg_len:%d\n",mesg_len);
    //for(int i = 40; i < 60; i++){
    //	printf("%c-",send_message[i]);
    //}
    //将IP数据包发送出去 
    sendto(raw_sockfd,send_message, mesg_len, 0, (struct sockaddr *)&server_address, sizeof(server_address));
    
    //接收SYN + ACK报文
    printf("receive SYN_ACK start:---------------------------------------\n");  //将接受的IP数据报输出
    unsigned char rec[1024];
	int n = recvfrom(raw_sockfd, rec, 1024, 0, NULL, NULL); 
	printf("receive %d bytes:\n", n);  //将接受的IP数据报输出
    for(int i=0; i<n; i++){
        if(i % 16 == 0)
            printf("\n");
        printf("%02x ", rec[i]);
    }
    printf("\n\nreceive SYN_ACK end:---------------------------------------\n");
    unsigned char ipheadlen = rec[0]; //取出IP数据包的长度
    ipheadlen = (ipheadlen & 0x0f);   //IP首部长度字段只占该字节后四位   将前四位全部至为0
	printf("ipheadlen: %d\n",ipheadlen);
    ipheadlen = 4 * ipheadlen; //IP报头域的数值表示整个IP报头的长度,单位是4bytes 
    //版本 4 +  头部长度 4 +  服务类型 8 
    unsigned short iplength = ntohs(*((unsigned short *)(rec+2))); //获取IP数据报总长度
    printf("iplengthAll: %d\n",iplength);
    unsigned short tcplength = iplength - ipheadlen;  //计算TCP数据报长度
    printf("TCPLen: %d\n",tcplength);
    //ip头部 + 4(端口) + 4 (SEQ) ntohl 从网络字节顺序 转换为主机字节顺序 
    // SEQ 
    unsigned int seq = ntohs(*((signed int*)(rec+ipheadlen+4)));
    printf("SEQ:%d\n",seq);
    // 确认应答号 
    unsigned int ack = ntohs(*((signed int*)(rec+ipheadlen+8))); //获取TCP首部的确认号
    printf("ACK:%d, %d\n", ack, my_seq);
    //flag 位位置无法确定 
    unsigned char flag = rec[ipheadlen + 14]; //获取标志字段
    printf("flag:%02x\n", flag);
    //--------------------------------------------------------------------- 
    // SYN + ACK 0x012
    flag = (flag & 0x12);  //只需要ACK和SYN标志的值 其他位都清0 
    if(flag != 0x12){  //判断是否为SYN+ACK包
	    printf("ACK+SYN\n");   
    }else{
        printf("OK ACK+SYN\n");
        //发送 ACK包
        unsigned int ack_seq = ack; 
		int mesg_len_a = make_message_ACK(send_message, MAXLINE, argv, ack_seq); 
    	sendto(raw_sockfd,send_message, mesg_len_a, 0, (struct sockaddr *)&server_address, sizeof(server_address));
    }
    //... 
    unsigned int ack_seq = ack; 
	int mesg_len_a = make_message_ACK(send_message, MAXLINE, argv, ack_seq); 
	sendto(raw_sockfd,send_message, mesg_len_a, 0, (struct sockaddr *)&server_address, sizeof(server_address));
    close(raw_sockfd);
    return 0;
}


//拼接IP数据报  回应ACK 
int make_message_ACK(char *sendbuf, int send_buf_len, char **argv, unsigned int ack_seq)
{
	//建立握手过程中无法传输数据 
    char message[]={"This is my homework of network of network,I am happy!"};   
    //bzero(message, sizeof(message));
    //strcpy(message, "hello,world!");
    printf("Data OK:->%s\n",message);
    struct iphdr *ip;
    ip = (struct iphdr *)sendbuf;
    ip->ihl = sizeof(struct iphdr) >> 2; //首部长度
    ip->version = 4;   //ip协议版本
    ip->tos = 0;   //服务类型字段
    ip->tot_len = 0;   //总长度
    ip->id = htons(my_seq + 1);   //id值
    ip->frag_off = 0;
    ip->ttl = 128;
    ip->protocol = IPPROTO_TCP;
    ip->check = 0;  //内核会算相应的效验和
//    ip->saddr = src_ip;
	//将一个点分十进制的IP转换成一个长整数型数 
	ip->saddr = inet_addr(argv[1]);
    ip->daddr = inet_addr(argv[3]);
	
    struct udp_front front;
    front.srcip = ip->saddr;
    front.desip = ip->daddr;
    front.len = htons(20 + strlen(message));
    printf("____________________________________DATA:%d_____________________________",strlen(message));
    front.protocol = 6;
    front.zero = 0;

    struct tcphdr *tcp;
    tcp = (struct tcphdr *)(sendbuf + sizeof(struct iphdr));
    bzero(tcp, sizeof(struct tcphdr *));
    //-------------------------------------------------------------------
    //将短整型变量从主机字节顺序转变成网络字节顺序
    //htonl 长  
	//atoi 把字符串转换成整型数 
    tcp->source = htons(atoi(argv[2]));  //源端口
    tcp->dest = htons(atoi(argv[4]));    //目的端口
    tcp->seq = htonl(100000001);   
    tcp->ack_seq = htonl(ack_seq + 1); 

    tcp->doff = 5;  //数据偏移(TCP头部字节长度/4)
    tcp->res1 = 0;  //保留字段(4位)
    tcp->fin = 0;       //..用来释放一个连接
    tcp->syn = 0;         //..表示这是一个连接请求
    tcp->rst = 0;         //..用来表示tcp连接是否出现严重差错
    tcp->psh = 0;         //..推送
    tcp->ack = 1;         //..表示是一个连接请求
    tcp->urg = 0;         //..紧急数据标志
    tcp->res2 = 0;  //保留字段(2位)
    tcp->window = htons(65535);  //初始窗口值设置

    tcp->check = 0;
    tcp->urg_ptr = 0; 
    tcp->check = 0;   //效验和,效验整个tcp数据报
    strcpy((sendbuf+20+20), message);  //把message存入IP+TCP头部位之后 

    tcp->check = tcp_check((sendbuf+20), 20+strlen(message), front);

    ip->tot_len = (20 + 20 + strlen(message));   //IP头长度+TCP头部长度+数据长度 = 总长度
    printf("ip->tot_len:%d\n",ip->tot_len);
    ip->check = in_chksum((unsigned short *)sendbuf, 20);

    return (ip->tot_len);
}

//拼接IP数据报  SYN第一次握手 
int make_message(char *sendbuf, int send_buf_len, char **argv)
{
	//建立握手过程中无法传输数据 
    char message[]={""};   
    //bzero(message, sizeof(message));
    //strcpy(message, "hello,world!");
    struct iphdr *ip;
    ip = (struct iphdr *)sendbuf;
    ip->ihl = sizeof(struct iphdr) >> 2; //首部长度
    ip->version = 4;   //ip协议版本
    ip->tos = 0;   //服务类型字段
    ip->tot_len = 0;   //总长度
    ip->id = htons(my_seq);   //id值
    ip->frag_off = 0;
    ip->ttl = 128;
    ip->protocol = IPPROTO_TCP;
    ip->check = 0;  //内核会算相应的效验和
//    ip->saddr = src_ip;
	//将一个点分十进制的IP转换成一个长整数型数 
	ip->saddr = inet_addr(argv[1]);
    ip->daddr = inet_addr(argv[3]);
	
    struct udp_front front;
    front.srcip = ip->saddr;
    front.desip = ip->daddr;
    front.len = htons(20 + strlen(message));
    printf("____________________________________DATA:%d_____________________________",strlen(message));
    front.protocol = 6;
    front.zero = 0;

    struct tcphdr *tcp;
    tcp = (struct tcphdr *)(sendbuf + sizeof(struct iphdr));
    bzero(tcp, sizeof(struct tcphdr *));
    //-------------------------------------------------------------------
    //				TCP头部 
    //-------------------------------------------------------------------
    //将短整型变量从主机字节顺序转变成网络字节顺序
    //htonl 长  
	//atoi 把字符串转换成整型数 
    tcp->source = htons(atoi(argv[2]));  //源端口
    tcp->dest = htons(atoi(argv[4]));    //目的端口
    tcp->seq = htonl(100000000);   					
    //tcp->ack_seq = 0;  //当ack置0的时候,ack_seq无所谓

    tcp->doff = 5;  //数据偏移(TCP头部字节长度/4)
    tcp->res1 = 0;  //保留字段(4位)
    tcp->fin = 0;       //..用来释放一个连接
    tcp->syn = 1;         //..表示这是一个连接请求
    tcp->rst = 0;         //..用来表示tcp连接是否出现严重差错
    tcp->psh = 0;         //..推送
    tcp->ack = 0;         //..表示是一个连接请求
    tcp->urg = 0;         //..紧急数据标志
    tcp->res2 = 0;  //保留字段(2位)
    tcp->window = htons(65535);  //初始窗口值设置

    tcp->check = 0;
    tcp->urg_ptr = 0; 
    tcp->check = 0;   //效验和,效验整个tcp数据报
    strcpy((sendbuf+20+20), message);  //把message存入IP+TCP头部位之后 

    tcp->check = tcp_check((sendbuf+20), 20+strlen(message), front);

    ip->tot_len = (20 + 20 + strlen(message));   //IP头长度+TCP头部长度+数据长度 = 总长度
    printf("ip->tot_len:%d\n",ip->tot_len);
    ip->check = in_chksum((unsigned short *)sendbuf, 20);

    return (ip->tot_len);
}

//计算tcp(udp)效验和
unsigned short tcp_check(char *sendbuf, int len, const struct udp_front front)
{
    char str[MAXLINE];
    bzero(&str, MAXLINE);
    bcopy(&front, str, sizeof(front));
    bcopy(sendbuf, str+sizeof(front), len);
    struct udp_front *ptr;
    ptr = (struct udp_front *)str;
    char *s;
    s = (str+20);
    return in_chksum((unsigned short *)str, sizeof(front)+len);
}

//效验和算法
uint16_t in_chksum(uint16_t *addr, int len)
{
    int nleft = len;
    uint32_t sum = 0;
    uint16_t *w = addr;
    uint16_t answer = 0;
    //把ICMP报头二进制数据以2字节为单位累加起来
    while (nleft > 1)
    {
        sum += *w++;
        nleft -= 2;
    }
    if (nleft == 1)
    {
        *(unsigned char *)(&answer) = *(unsigned char *)w;
        sum += answer;
    }
    sum = (sum>>16) + (sum&0xffff);
    sum += (sum>>16);
    answer = ~sum;
    return answer;
}