Linux——原始套接字

    技术2023-03-19  54

     

    缅怀Stevens大师。

    本文只是,对UNPv1前几节的转载!!!并非原创!!!!!要想真的学会raw socket 把UNPv1 28章的3个程序从头到尾好好看看一下!!

    最好的参考资料:

    1.师从互联网。

    2.Linux man 命令:man  7 raw。

    3.UNP v1第28章 。

    4.http://www.cublog.cn/u2/62281/showart_1096746.html

    http://linux.chinaitlab.com/c/389513.html

     

    第一条:概述

    使用原始套接字(raw socket)可以发送和接收到主机网卡上的数据帧或者数据包,简而言之,可以编写基于IP协议的程序。

    man中指出:Raw  sockets 允许用户创建新的IPv4协议。原始套接口收发的原始数据报(raw  datagram)不包括链路层的头部。 

    UNPV1中:原始套接口提供了普通TCP和UDP socket不能提供的3个能力如下:

    1.进程使用raw socket 可以读写ICMPv4、IGMPv4、ICMPv6等分组。这个能力还使得使用ICMP或IGMP构造的应用程序能够完全作为用户进程处理,而不必往内核中添加额外代码。从下一条可以看出内核中是包括ICMP和IGMP协议的处理代码的。

     

    2.大多数内核只处理IPv4数据报中一个名为协议的8位字段的值为1(ICMP)、2(IGMP)、6(TCP)、17(UDP)四种情况。然而该字段的值还有许多其他值。进程使用raw socket 就可以读写那些内核不处理的IPv4数据报了。

    3.通过使用raw socket ,进程可以使用IP_HDRINCL套接口选项自行构造IPv4头部。这个能力可用于构造TCP或UDP分组等。//Head is include with data。

    本文中只涉及数据报相关的内容,至于以太网帧,留待下一篇文章

    第二条:创建Raw socket

    1.只有root用户才能创建raw socket,以防止普通用户向Internet输入他们自行构造的IP数据报。

    int sockfd=socket(AF_INET,SOCK_RAW,protocol);

    protocol参数其值如下:定义在netinet/in.h

    如果指定protocol为0时,原始套接字可以接收内核传递给原始套接字的任何IP数据包

        IPPROTO_IP = 0,   /* Dummy protocol for TCP.  *///这个协议的Dummy的意思是系统什么也不做。

     

        IPPROTO_HOPOPTS = 0,   /* IPv6 Hop-by-Hop options.  */

        IPPROTO_ICMP = 1,   /* Internet Control Message Protocol.  */

        IPPROTO_IGMP = 2,   /* Internet Group Management Protocol. */

        IPPROTO_IPIP = 4,   /* IPIP tunnels (older KA9Q tunnels use 94).  */

        IPPROTO_TCP = 6,   /* Transmission Control Protocol.  */

        IPPROTO_EGP = 8,   /* Exterior Gateway Protocol.  */

        IPPROTO_PUP = 12,   /* PUP protocol.  */

        IPPROTO_UDP = 17,   /* User Datagram Protocol.  */

        IPPROTO_IDP = 22,   /* XNS IDP protocol.  */

        IPPROTO_TP = 29,   /* SO Transport Protocol Class 4.  */

        IPPROTO_DCCP = 33,   /* Datagram Congestion Control Protocol.  */

        IPPROTO_IPV6 = 41,     /* IPv6 header.  */

        IPPROTO_ROUTING = 43,  /* IPv6 routing header.  */

        IPPROTO_FRAGMENT = 44, /* IPv6 fragmentation header.  */

        IPPROTO_RSVP = 46,   /* Reservation Protocol.  */

        IPPROTO_GRE = 47,   /* General Routing Encapsulation.  */

        IPPROTO_ESP = 50,      /* encapsulating security payload.  */

        IPPROTO_AH = 51,       /* authentication header.  */

        IPPROTO_ICMPV6 = 58,   /* ICMPv6.  */

        IPPROTO_NONE = 59,     /* IPv6 no next header.  */

        IPPROTO_DSTOPTS = 60,  /* IPv6 destination options.  */

        IPPROTO_MTP = 92,   /* Multicast Transport Protocol.  */

        IPPROTO_ENCAP = 98,   /* Encapsulation Header.  */

        IPPROTO_PIM = 103,   /* Protocol Independent Multicast.  */

        IPPROTO_COMP = 108,   /* Compression Header Protocol.  */

        IPPROTO_SCTP = 132,   /* Stream Control Transmission Protocol.  */

        IPPROTO_UDPLITE = 136, /* UDP-Lite protocol.  */

        IPPROTO_RAW = 255,   /* Raw IP packets.  */

    2.当需要编写自己的IP数据包首部时,在raw socket开启IP_HDRINCL套接口选项//Head is include with data:就是内核不会为这个数据报自动添加IP头部。用户告诉内核首部由我们自己构造,并且和数据放在一起,内核不用操心了~~~

     

    const int on=1;

    if(setsocketopt(scokfd,IPPROTO_IP,IP_HDRINCL ,&on,sizeof(on))<0)

                perror("setsocketopt error:");

     

    原始套接字直接使用IP协议的套接字,所以是非面向连接的。在这个套接字上可以调用connect和bind函数,分别执行绑定对方和本地地址。

    3.可以在这个原始套接口上调用bind函数,不过比较少见。bind函数仅仅设置本地地址,因为原始套接口不存在端口的概念。就输出而言,调用bind设置的是将用于从这个原始套接口发送的所有数据报的源IP地址(只有IP_HDRINCL套接口选项未开启的前提下)。如果不调用bind,内核就把源IP地址设置为外出接口的主IP地址。4.可以在这个原始套接口上调用connect函数,不过也比较少见。connect函数仅仅设置远地地址,同样因为原始套接口不存在端口号的概念。就输出而言,调用connect之后我们可以把sendto调用改为write调用,因为宿IP地址已经指定了。

    第三条:Raw socket 输出

    1    普通输出可以调用sendto或sendmsg并指定宿IP地址完成。如果在套接口已经连接(调用connect),那么可以调用write,writev或send。2    如果IP_HDRINCL选项未开启,那么由进程让内核发送的数据的起始地址指的是IP头部之后的第一个字节,因为内核将构造IP头部并把它置于来自进程的数据之前。内核把所构造IPv4头部的协议字段设置成来自socket调用的第三个参数。3    如果IP_HDRINCL选项已开启,那么由进程让内核发送的数据的起始地址指的是IP头部的第一个字节。进程调用输出函数写出的数据量必须包括IP头部的大小。整个IP头部由进程构造,不过:(a)IPv4标识字段可置为0,从而告知内核设置该值;(b)IPv4头部检验和总是由内核计算并存储;(c)IPv4选项字段是可选的。4    内核对于超出外出接口MTU的原始分组执行分片。5    对于IPv4,计算并设置IPv4头部之后所含的任何头部校验和总是由用户进程负责。也就是说,必须在调用sendto等之前计算数据的校验和并将其存入相应选项中。

    IPv6的差异:见(RFC3542)1    通过IPv6原始套接口发送和接收的协议头部中的所有字段均采用网络字节序。2    IPv6不存在与IPv4的IP_HDRINCL套接口类似的东西。通过IPv6原始套接口无法读入或写出完整的IPv6分组(包括IPv6头部和任何扩展头部)。IPv6头部的几乎所有字段以及所有扩展头部都可以通过套接口选项或辅助数据由应用进程指定或获取。如果应用进程需要读入或写出完整的IPv6数据报,那就必须使用数据链路访问。3    IPv6原始套接口的校验和处理存在差异,如下:IPV6_CHECKSUM套接口选项对于ICMPv6原始套接口,内核总是计算并存储ICMPv6头部中的校验和。这点不同于ICMPv4原始套接口,也就是说,ICMPv4头部中的校验和必须由应用进程自己计算并存储。ICMPv4和ICMPv6都要求计算校验和,但ICMPv6却在其校验和中包括一个伪头部(pseudoheader)。该伪头部中的字段之一是源IPv6地址,而应用进程通常让内核选择其值。与其让应用进程就为了计算校验和而不得不试图执行选择这个地址,还不如让内核计算校验和来的容易。对于其他IPv6原始套接口(不是以IPPROTO_ICMPV6为第三个参数调用socket创建的那些原始套接口),进程可以使用一个套接口选项告知内核是否计算并存储外出分组中的校验和,且验证接收分组中的校验和。该选项缺省是禁止的,不过把它的值设置为某个非负值就可以开启该选项。如:int offset = 2;if( setsockopt( sockfd, IPPROTO_IPV6, IPV6_CHECKSUM, &offset, sizeof(offset) ) < 0 )                    perror("setsocketopt error:");上面代码不止开启指定套接口上的校验和,而且告知内核这个16位的校验和字段的字节偏移量:本例中为 自 应用数据开始处起偏移2个字节。禁止该选项要求把这个偏移量设置为-1. 一旦开启内核将为在指定套接口上发送的外出分组计算并存储校验和,并且为在该套接口接收的外来分组验证校验和。

    第四条:Raw socket 输入

    就原始套接口的输入我们必须首先回答的问题是:内核把哪些收到的IP数据报传递到原始套接口?这儿遵循如下规则:1    接收到的UDP分组和TCP分组绝不传递到任何原始套接口。如果一个进程想要读取含有 UDP分组或TCP分组的IP数据报,它就必须在数据链路层读取这些分组。2    大多数ICMP分组在内核完成处理其中的ICMP消息后传递到原始套接口。源自Berkeley的实现把不同的回射请求,时间戳请求或地址掩码请求的所有接收到的ICMP分组传递给原始套接口。3    所有IGMP分组在内核中完成处理其中的IGMP消息后传递到原始套接口。4    内核不认识其协议字段的所有IP数据报传递到原始套接口。内核对这些分组执行的唯一处理是针对某些IP头部字段的最小验证:IP版本,IPv4头部校验和,头部长度以及宿IP地址。5    如果某个数据报以片段形式到达,那么在它的所有片段均达到且重组出该数据报之前,不传递任何分片分组到原始套接口。当内核有一个需要传递到原始套接口的IP数据报时,它将检查所有进程上的所有原始套接口,以寻找所匹配的套接口。每个匹配的套接口将被地送到该IP数据报的一个拷贝,内核对每个原始套接口均执行如下3个测试,只有这3个测试结果均为真,内核才把接收到的数据报递送到这个套接口:1    如果创建这个原始套接口时指定了非0的协议参数(socket的第三个参数),那么收到的数据报的协议字段必须匹配该值,否则该数据报不递送到这个套接口。2    如果这个原始套接口已经由bind绑定了某个本地IP,那么接收到的数据报的宿IP地址必须匹配这个绑定地址,否则该数据报不递送到这个套接口。3    如果这个原始套接口已经由connect调用了指定某个远地IP地址,那么接收到的数据报的源IP地址必须匹配这个已连接地址,否则该数据报不递送到该套接口。注意:如果一个原始套接口是以0值协议参数创建的,而且既未对它调用过bind,也未对它调用过connect,那么该套接口将被递送以可由内核传递到原始套接口的每个原始数据报的一个拷贝。无论何时往一个原始套接口递送一个接收到的数据报,传递到该套接口所在进程的都是包括IP头部在内的完整数据报。然而对于原始IPv6套接口,传递到套接口的只是扣除了IPv6头部和所有扩展头部的净荷(payload)。ICMPv6类型过滤原始ICMPv4套接口被递送以由内核接收的大多数ICMPv4消息。然而ICMPv6在功用上是ICMPv4的超集,把ARP和IGMP的功能也包括在里面。因此相比原始ICMP套接口,原始ICMPv6套接口有可能收取多得多的分组。可是使用原始套接口的应用程序大多数仅仅关注所有ICMP消息的某个小子集。为了缩减由内核通过原始ICMPv6套接口传递到应用进程的分组数量,应用进程可以自行提供一个过滤器。原始ICMPv6套接口上的过滤器使用定义在<netinet/icmp.h>头文件中的数据类型struct icmp6_filter声明,并使用level参数为IPPROTO_ICMPV6且optname参数为ICMP6_FILTER的setsockopt和getsockopt调用来设置和获取。一下6个宏用于操作icmp6_filter结构:#include<netinet/icmp6.h>void ICMP6_FILTER_SETPASSALL( struct icmp6_filter *filt );    //所有消息都传递到应用进程void ICMP6_FILTER_SETBLOCKALL( struct icmp6_filter *filt ); //不传递任何消息类型,创建//ICMP6原始套接口后,系统//缺省允许所有ICMP6消息传递void ICMP6_FILTER_SETPASS( int msgtype, struct icmp6_filter *filt ); //放行某个指定消息类型void ICMP6_FILTER_SETBLOCK( int msgtype, struct icmp6_filter *filt ); //阻止某个消息类型传递int ICMP6_FILTER_WILLPASS( int msgtype, const struct icmp6_filter *filt ); //如果指定消息类型//被过滤器放行返回1,否则返回0int ICMP6_FILTER_WILLBLOCK( int msgtype, const struct icmp6_filter *filt ); //如果指定消息类//型被阻止返回1,否则返回0返回值:1---若过滤器放行(或阻止)给定消息类型                 否则返回0                所以这些宏中的filt参数指向某个icmp6_filter变量的一个指针,其中前四个宏修改该变量,后2个宏查看该变量。msgtype参数在0-255之间取值,指定ICMP类型。示例如下(该例子为只想接收ICMPv6路由器通告消息的某个应用程序代码片段):    struct icmp6_filter myfilt;    fd = socket( AF_INET, SOCK_RAW, IPPROTO_ICMPV6 );    ICMPV6_FILTER_SETBLOCK( *myfilt );    ICMPV6_FILTER_SETPASS( ND_ROUTER_ADVERT, &myfilt );    setsockopt( fd, IPPROTO_ICMPV6, ICMP6_FILTER, &myfilt, sizeof(myfilt) );本例首先阻止所有消息类型的传递(因为缺省设置是传递所有消息类型),然后只放行路由器通告消息的传递。尽管如此设置了过滤器,该应用程序仍做好接收所有消息类型的准备,因为在socket和setsockopt这两个调用之间到达的任何ICMPv6消息将被添加到接收队列中。ICMP6_FILTER套接口选项仅仅是一个优化措施。

     

     

     

     

     

    最新回复(0)