套接字

可分为:

  • 标准套接字
  • 原始套接字
    套接字分类

rawsocket

原始套接字即socket 参数type为 SOCK_RAW的套接字, 必须管理员权限才可以使用

分两种:

  • 处理IP层及以上的数据,通过指定第一个参数为AF_INET来创建套接字
  • 处理数据链路层及其以上的数据, 通过指定第一个参数为AF_PACKET来创建这种套接字。

AF_INET 发送/接收IP数据包

socket(AF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)

  • 当接收包时,表示用户获得完整的包含IP报头的数据包,即数据从IP报头开始算起。

  • 当发送包时,用户只能发送包含TCP报头或UDP报头或包含其他传输协议的报文,IP报头以及以太网帧头则由内核自动加封。除非是设置了IP_HDRINCL的socket选项。

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

  • 如果第二个参数为SOCK_STREAM, SOCK_DGRAM,表示接收的数据直接为应用层数据

注意:

  • 如果IP_HDRINCL套接字选项未开启,那么由进程让内核发送的数据的起始地址是IP首部之后的第一个字节,因为内核将构造IP首部并把他置于来自进程的数据之前。内核把所构造的IPv4首部的协议字段设置成来自socket的第三个参数。

  • 如果IP_HDRINCL套接字选项已经开启,那么由进程让内核发送的数据的起始地址指的是IP首部的第一个字节。进程调用输出函数写出的数据量必须包含IP首部的大小。整个IP首部由进程构造,不过:IPv4标识字段可置为0,从而告知内核设置该值;IPv4首部校验和字段总是由内核计算并存储;IPv4选项字段是可选的。

  • 发送包时,如果走了IP层内核会对超出外出接口MTU的原始分组执行分片

  • 接收包时,如果存在分片,内核向raw socket交付的数据是已经重组好的IP包

注意:

  • 如果protocol是IPPROTO_RAW(255),这时候,这个socket只能用来发送IP包,而不能接收任何的数据。发送的数据需要自己填充IP包头,并且自己计算校验和。数据包传递出去的时候IP 层就不会参与运作,即如果数据包大于接口的MTU,那么不会进行分片而直接丢弃。
  • 错误的说法: 对于protocol为0(IPPROTO_IP)的raw socket。用于接收任何的IP数据包。其中的校验和和协议分析由程序自己完成。操作系统不允许
#include <stdio.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <stdlib.h>

/*
* 该程序用于验证AF_INET原始套接字协议为0或者255的情况
* IPPROTO_IP / 0 : linux提示错误
* IPPROTO_RAW / 255 : 只能发送,不能接收
* 在netinet/in.h头文件中定义了各种IPPROTO_
* IPPROTO_IP = 0
* IPPROTO_ICMP = 1
* IPPROTO_IGMP = 2
* IPPROTO_TCP = 6
* IPPROTO_UDP = 17
* IPPROTO_RAW = 255
*/

#define BUFSIZE 2048
int main(int argc, char *argv[]) {

    int sockfd;
    int n, iphrlen, packetlen, protype, ipproto;
    char buf[BUFSIZ];
    if (argc < 2) {
        printf("error format\n");
        return -1;
    }
    ipproto = atoi(argv[1]);

    if( -1 == (sockfd = socket(AF_INET, SOCK_RAW, ipproto))) {
        perror("socket");
        return -1;
    }

    // 循环接收信息
    while(1) {
        n = recvfrom(sockfd, &buf, BUFSIZE, 0, 0, 0);
        if (n < 0) {
            perror("recvfrom");
            return -1;
        }
        struct ip *ip = (struct ip *)buf;
        // 计算长度
        iphrlen = ip->ip_hl << 2;
        packetlen = ntohs(ip->ip_len);
        protype = ip->ip_p;

        printf("iphrlen = %d, packetlen= %d, protype = %d\n", iphrlen, packetlen, protype);
    }

    return 0;
}

运行结果为:

[root@iz2zecj7a5r32f2axsctb9z rawsockt]# ./inet
socket: Protocol not supported

PF_PACKET 发送/接收以太网数据帧

socket(PF_PACKET, SOCK_RAW|SOCK_DGRAM,htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))

socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP)):表示获得IPV4的数据链路层帧,即数据包含以太网帧头。14+20+(8:udp 或 20:tcp) 第三那个参数对应于MAC帧首部类型字段

  • ETH_P_IP: 在 < linux/if_ether.h > 中定义
  • SOCK_RAW, SOCK_DGRAM两个参数都可以使用,区别在于使用SOCK_DGRAM收到的数据不包括数据链路层协议头。

第三个参数说明:

  • 0 不能用于接收,只能用于发送
  • ETH_P_IP 0x800 只接收发往本机mac的ip类型的数据帧
  • ETH_P_ARP 0x806 只接受发往本机mac的arp类型的数据帧
  • ETH_P_RARP 0x8035 只接受发往本机mac的rarp类型的数据帧
  • ETH_P_ALL 0x3 接收发往本机mac的所有类型ip arp rarp的数据帧, 接收从本机发出的所有类型的数据帧.(混杂模式打开的情况下,会接收到非发往本地mac的数据帧)

linux/if_ether.h 中关于ETH的定义 一般引入 net/ethernet.h 头文件

#define ETH_ALEN        6               /* 以太网地址长度  */
#define ETH_HLEN        14              /* 以太网帧头部长度       */
#define ETH_ZLEN        60              /* 以太网帧的最小长度 */
#define ETH_DATA_LEN    1500            /* payload的最大长度 */
#define ETH_FRAME_LEN   1514            /* FCS最大的长度 */
#define ETH_FCS_LEN     4               /* FCS的长度*/

#define ETH_MIN_MTU     68              /* IPv4 MTU的最小值      */
#define ETH_MAX_MTU     0xFFFFU         /* MTU最大值    */

/*
 *      These are the defined Ethernet Protocol ID's.
 */

#define ETH_P_LOOP      0x0060          /* 回环包    */
#define ETH_P_IP        0x0800          /* ip包     */
#define ETH_P_ARP       0x0806          /* arp包    */
#define ETH_P_RARP      0x8035          /* rarp包      */
#define ETH_P_ALL       0x0003          /* 所有包*/
...

原始套接字的实现

收发流程

收发流程

AF_INET原始套接字输入

  • 接收到的UDP分组和TCP分组绝不能传递给任何原始套接字。如果一个进程想要读取含有UDP分组或TCP分组的IP数据报,就必须在数据链路层读取这些分组。

  • 大多数ICMP分组在内核处理完其中的ICMP消息后传递到原始套接字。源自Berkeley的实现 把 不是 回射请求、时间戳请求 或 地址掩码请求(这三类完全由内核处理) 的所有接收到的ICMP分组传递给原始套接字

  • 所有ICMP分组在内核完成处理其中的IGMP消息后传递到原始套接字

  • 内核不认识其协议字段的所有IP数据报传递给原始套接字。

  • 如果某个数据报以片段形式到达,那在它的所有片段均到达且重组出该数据之前,不传递任何片段分组到原始套接字

当内核有一个需要传递到原始套接字的IP数据报时,它将检查所有进程上的原始套接字,以寻找所有匹配的套接字。每个匹配到的套接字将被递送以该iP数据报的一个副本。(事实证明,如果进程过多,匹配的套接字过多,内核会忙于数据报的软过滤和分发,而实际的套接字却空闲,导致性能下降)
内核对每个原始套接字均执行如下3个测试,只有这三个测试为真,内核才把接收到的数据报递送到这个套接字。

  1. 如果创建这个套接字时制订了非0的协议参数(socket的第三个参数),那么接受到的数据报的协议字段必须匹配该值,否则数据报不递送到这个套接字
  2. 如果这个原始套接字已由bind调用绑定了某个本地IP地址,那么接受到的数据报的目的IP地址必须匹配这个绑定的地址,否则该数据报不递送到这个套接字。
  3. 如果这个原始套接字已由connect调用指定了某个外地IP地址,那么接受到的数据报的源IP地址必须匹配这个已连接地址,否则该数据报不递送到这个套接字。
文档更新时间: 2021-02-05 02:37   作者:周国强