UNP——原始套接字

1.原始套接字的用处

  使用原始套接字可以构造或读取网际层及其以上报文。

  具体来说,可以构造 ICMP, IGMP 协议报文,通过开启 IP_HDRINCL 套接字选项,进而自定义 IPv4首部。

 

2. 创建原始套接字

2.1 使用 SOCK_RAW 创建原始套接字

sockfd = socket(AF_INET, SOCK_RAW,  protocol);

  protocol 定义在 <netinet/in.h>,如 IPPROTO_xxx。

2.2 初始化原始套接字的常用步骤

2.2.1 IP_HDRINCL

int on = 1;
setsockopt(sockfd, IPPOTO_IP, IP_HDRINCL, &on, sizeof(on));

2.2.2 bind, connect

  原始套接字不存在端口的概念(TCP报文只是原始套接字报文的数据部分,而非首部),所以调用 bind, connect,分别设置 报文的源地址和目标地址。

  对应bind而言,没有启用 IP_HDRINCL 选项,则bind会设置套接字源地址,若没有进行bind,内核会 根据数据包出口设置源地址。

  对于connect,若进行connect,则设置套接字对端地址,从而可以用send/write而非sendto。

 

3. 原始套接字的输出

3.1 sendto 和 send

  如果套接字已经设置了对端地址,则可以使用send

3.2 内核对发送数据的处理

(1)没启动 IP_HDRINCL,内核构造IPv4首部,应用进程传来的数据会被当成 IPv4数据部分,内核会将自己构造的首部和应用传来的数据并接,其中IPv4协议号按照 socket 第三个参数即 IPPROTO_XXX 设置。

(2)开启了 IP_HDRINCL,IPv4首部由应用程序构造, 内核会将应用进程传来的数据当成包含IPv4首部和IPv4数据部分的完整IPv4报文,但 IPv4首部的校验和字段总是由内核计算并存储。

  即,根据IPROTO_XXX是否启用,传递给原始套接字的数据报可能不完整(不包含IP首部)。

3.3 内核会对超出 MTU 的分组进行分片。

3.4 内核总会对IPv4首部求校验并存储,而对于其他校验如ICMP,TCP,需要自己求的。

 

4. 原始套接字的输入

  原始套接字的输入来自于内核,内核会将哪些数据传给原始套接字呢?

(1)内核不会将TCP和UDP分组传给原始套接字,如果希望获得TCP和UDP分组,必须使用链路层套接字。

(2)大多数的ICMP消息和所有的IGMP消息在内核处理完其中信息后,会传递给原始套接字

(3)内核不认识的协议字段的所有IP数据报将传给原始套接字

(4)如果数据包以分片形式到达,则在数据包完整前,内核不会传给原始套接字

  当IP数据报通过上面的筛选,内核会找到匹配该数据包的原始套接字,并将数据包拷贝给该套接字,那么匹配的规则是什么呢?

(1)数据包的协议和套接字的协议匹配

(2)数据包的目的地址和套接字的源地址匹配

(3)数据包的源地址和套接字的目的地址匹配

(4)若套接字的协议和目的地址和源地址都是 void的,即协议设置为0,没有调用 connect 设置目的地址,没有调用bind设置 源地址。则该套接字会接受所有的IP数据包。

  从原始套接字,收到的数据报,一定是包含IP首部的完整数据包。

 

5.实际测试

5.1 ping程序

实际发现 connect 没有。

#include <stdlib.h>
#include <stdio.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include "wrap_common.h"
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/ip_icmp.h>

void proc_v4(char *ptr, ssize_t len, struct timeval *tvrecv);

uint16_t in_cksum(uint16_t *addr, int len)
{
    int                     nleft = len;
    uint32_t                sum = 0;
    uint16_t                *w = addr;
    uint16_t                answer = 0;

    while (nleft > 1)  {
        sum += *w++;
        nleft -= 2;

    }

    if (nleft == 1) {
        *(unsigned char *)(&answer) = *(unsigned char *)w ;
        sum += answer;

    }

    /* 4add back carry outs from top 16 bits to low 16 bits */
    sum = (sum >> 16) + (sum & 0xffff);     /* add hi 16 to low 16 */
    sum += (sum >> 16);                     /* add carry */
    answer = ~sum;                          /* truncate to 16 bits */
    return(answer);
}

struct icmp* get_icmp(int seq)
{
    static struct icmp icmp_buf;
    static int datalen = 56;
    int len;

    icmp_buf.icmp_type = ICMP_ECHO;
    icmp_buf.icmp_code = 0;
    icmp_buf.icmp_id = getpid();
    icmp_buf.icmp_seq = seq;
    memset(icmp_buf.icmp_data, 0xa5, datalen);

    gettimeofday((struct timeval *)icmp_buf.icmp_data, NULL);
    len = 8 + datalen;
    icmp_buf.icmp_cksum = 0;
    icmp_buf.icmp_cksum = in_cksum((unsigned short *)&icmp_buf, len);

    return &icmp_buf;
}

int main(int argc, char **argv)
{
    int sockfd;
    const struct sockaddr_in dst_addr = {0}, addr = {0}, src_addr = {0};
    char *dst_ip;
    struct icmp *icmp;
    struct timeval tval;
    int recv_len, i;
    char buf[128];

    dst_ip = argv[1];

    sockfd = Socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

    Inet_pton(AF_INET, dst_ip, (void *)&dst_addr.sin_addr);

    for (i = 0; ; i++) {
        icmp = get_icmp(i);

        if (sendto(sockfd, icmp, 8+56,0, (struct sockaddr *)&dst_addr, sizeof(struct sockaddr_in)) < 0)
            perror("sendto");
        if ((recv_len = recv(sockfd, &buf, sizeof(buf), 0)) < 0)
            perror("recv");

        gettimeofday(&tval, NULL);
        proc_v4(buf, recv_len, &tval);

        sleep(1);
    }

    return 0;
}

void tv_sub(struct timeval *out, struct timeval *in)
{
    if ( (out->tv_usec -= in->tv_usec) < 0 ) {       /* out -= in */
        --out->tv_sec;  
        out->tv_usec += 1000000;
    }
    out->tv_sec -= in->tv_sec;
}


void proc_v4(char *ptr, ssize_t len, struct timeval *tvrecv)
{
    int                     hlen1, icmplen;
    double                  rtt;
    struct ip               *ip;
    struct icmp             *icmp;
    struct timeval  *tvsend;

    ip = (struct ip *) ptr;         /* start of IP header */
    hlen1 = ip->ip_hl << 2;         /* length of IP header */
    if (ip->ip_p != IPPROTO_ICMP)
        return;                         /* not ICMP */

    icmp = (struct icmp *) (ptr + hlen1);   /* start of ICMP header */
    if ( (icmplen = len - hlen1) < 8 )
        return;                         /* malformed packet */

    if (icmp->icmp_type == ICMP_ECHOREPLY) {

        if (icmp->icmp_id != getpid()) {
            printf("icmp id error : %d != %d \n", icmp->icmp_id, getpid());
            return;                 /* not a response to our ECHO_REQUEST */

        }

        if (icmplen < 16)
            return;                 /* not enough data to use */

        tvsend = (struct timeval *) icmp->icmp_data;
        tv_sub(tvrecv, tvsend);
        rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;

        printf("from %s ", inet_ntoa(ip->ip_src));
        printf("%d bytes, seq=%u, ttl=%d, rtt=%.3f ms\n", icmplen, icmp->icmp_seq, ip->ip_ttl, rtt);
    }
}

5.2 使用多进程,和多线程测试

发现只要一个套接字发包,所有套接字都会收到回复,所以在读包时应该将所有包读出。

                while ((recv_len = recv(sockfd, &buf, sizeof(buf), MSG_DONTWAIT)) >= 0) {
                        gettimeofday(&tval, NULL);
                        proc_v4(buf, recv_len, &tval);
                }

 

5.3 ping程序的recv必须即使处理,才知道接受包时的准确时间。所以应该用多路转接IO,或者专门用个线程来阻塞接受。

posted on 2020-11-18 14:19  开心种树  阅读(427)  评论(0编辑  收藏  举报