精华内容
下载资源
问答
  • 原始套接字

    2021-03-10 06:08:05
    原始套接字:是一种对原始网络报文进行处理的套接字,主要用途有:l 发送自定义的IP数据包l 发送ICMP数据包l 网卡的侦听模式,监听网络上的数据包l 伪装IP地址l 自定位协议的实现原始套接字主要应用于底层...

    标准套接字分为:

    l  流式套接字(SOCK_STREAM):面向连接的套接字,应用于TCP应用程序。

    l  数据包套接字(SOCK_DGRAM):无连接的套接字,应用于UDP应用程序。

    原始套接字:是一种对原始网络报文进行处理的套接字,主要用途有:

    l  发送自定义的IP数据包

    l  发送ICMP数据包

    l  网卡的侦听模式,监听网络上的数据包

    l  伪装IP地址

    l  自定位协议的实现

    原始套接字主要应用于底层网络编程,原始套接字与标准套接字之间的关系如下:

    fb4d1fe0d4cf7453564b2dc249e4ae8f.png

    原始套接字的创建:

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

    常见的协议类型如下:

    l  IPPROTO_IP:            IP协议,接受或者发送IP数据包,包含IP头部

    l  IPPROTO_ICMP: ICMP协议,接受或者发送ICMP的数据包,IP的头部不需要处理

    l  IPPROTO_TCP:  TCP协议,接受或者发送TCP数据包

    l  IPPROTO_UDP: UDP协议,接受或者UDP数据包

    l  IPPROTO_RAW: 原始IP包。

    链路层原始套接字

    可以直接用于接收和发送链路层MAC帧,在发送时需要由调用者自行构造和封装MAC首部。而网络层原始套接字可以直接用于接收和发送IP层的报文数据,在发送时,需要自行构造IP报文头(取决是否设置IP_HEADER选项),另外必须在管理员权限下才能使用原始套接字。

    原始套接口提供了普通TCP和UDP的socket不能提供的三种能力:

    l  进程使用raw socket可以读写ICMP、IGMP等分组,这个功能还使得使用ICMP或IGMP构造的应用程序能够完全作为用户进程处理,而不必往内核中添加额外代码。

    l  大多数内核只处理IPV4数据包中的一个名为协议的8字段的值1(ICMP)、2(IGMP),6(TCP)、17(UDP)四种情况,然而该字段还有其他值。进程使用raw socket就可以读写那些内核不处理IPv数据包。因此,可以使用原始套接字定义自己的协议格式。

    l  通过使用raw socket,进程可以使用IP_HADINCL套接口选项自行定义IP头部,这个功能可用于构造特定类型的TCP或UDP分组等。

    链路层原始套接字调用socket()函数创建,第一个 参数指定协议族类型为PF_PACKET,第二个type可以设置为SOCK_RAW或SOCK_DGRAM,第三个参数时协议类型(只对报文接收由意义)。协议类型protocol不同取值的意义如下:

    socket(PF_PACKET,type,htons(protocol))

    l  参数type设置为SOCK_RAW时:套接字接收和发送数据都是从MAC首部开始的早发送时需要由调用者从MAC首部开始构造函数和封装报文数据。该种情况是用于某些项目需要用到自定义的二层报文socket(PF_PACKET,SOCK_RAW,htons(protocol))

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

    protocol不同取值:

    protocol

    作用

    ETH_P_ALL

    0x0003

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

    ETH_P_IP

    0x0008

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

    ETH_P_ARP

    0x0806

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

    ETH_P_RARP

    0x8035

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

    自定义协议

    比如0x0810

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

    不指定

    0

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

    网络层原始套接字:

    创建面向连接的TCP和创建面向无连接的UDP套接字,在接受和发送时只能操作数据部分,而不能对IP首部或TCP或UDP首部进行操作。如果想要操作IP首部或传输层协议首部,就需要调用如下socket()函数创建网络层原始套接字。

    第一个参数指定协议族的类型为PF_INET

    第二个参数为SOCK_RAW

    第三个参数protocol为协议类型。

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

    l  发送报文       网络层原始套接字发送的报文数据,在默认情况下是从IP首部之后开始的,即需要由调用者自行构造和封装TCP/UDP等协议首部。这种套接字也提供了发送时从IP首部开始构造数据的功能。通过setsocketopt()个套接字设置上IP_HDRINCL选项,就需要在发送时自行构造IP首部。

    protocol

    作用

    IPPROTO_TCP

    6

    接收TCP类型的报文

    IPPROTO_UDP

    17

    接收UDP类型报文

    IPPROTO_ICMP

    1

    接收ICMP类型报文

    IPPROTO_IGMP

    2

    接收IGMP类型报文

    IPPROTO_RAW

    255

    不能接收报文,只能发送(需要构造数据包首部)

    IPPROTO_OSPF

    89

    接收协议号为89的报文

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

    之所以要转这篇文章,是因为这篇文章是我看到的同类博客中写得最好的,但非常可惜,这篇博客中只有一篇文章,没有什么收藏价值,故将其原文转载,以供今后学习查阅。

    本文从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))

    10a5fd1b6f7bb08b11f769fa3e476b32.png

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

    socket(PF_PACKET, SOCK_DGRAM, htons(protocol))

    09f28d43fc58bafda428c52c89023451.png

    表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等首部,以及数据部分。

    453b8c8caab84b843e9918488cd0ef4f.png

    b)发送报文

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

    e4394841a5778873d1a23d30a8206dc7.png

    这种套接字也提供了发送时从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原始套接字报文收发流程

    a526759d1158a77169fd9b17bd17a341.png

    图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所示,这里创建了两个链路层原始套接字。

    5372443cef4f5b8beb763617025b3b8d.png

    图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类型的报文。

    f92c5d6841790c2ebd8af73442f52c08.png

    图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结构又将传输层映射到网络层。

    89998cb4ea7c5c616d007e97a944bbf7.png

    图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。

    b0d3a14c55e491b3144da93e21b9c47b.png

    图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[]数组中没有相应的项,内核会将其丢弃掉,这种报文只能提供网络层原始套接字接收到用户态来处理。

    acc8b04aabdd627c192cd4d117a2fddd.png

    图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主机

    参考资料

    1.《Linux内核源码剖析——TCP/IP实现》

    2.《深入理解Linux网络内幕》

    3.《UNIX网络编程 第1卷:套接口API》

    展开全文
  • 原始套接字编程实例

    2021-05-16 16:05:20
    原始套接字编程实例#include #include #include #include #include #include #include #include #include #include #include //#include #include #define DESTPORT 80 /* 要***的端口 */#define LOCALPORT 8888 /*...

    原始套接字编程实例

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    //#include

    #include

    #define DESTPORT 80 /* 要***的端口 */

    #define LOCALPORT 8888 /*管理员打开的端口*/

    void send_tcp(int sockfd,struct sockaddr_in *addr);

    unsigned short check_sum(unsigned short *addr,int len);

    int main(int argc,char **argv)

    {

    int sockfd;

    struct sockaddr_in addr;

    struct hostent *host;

    int on=1;

    if(argc!=2){

    fprintf(stderr,"Usage:%s hostname\n\a",argv[0]);

    exit(1);

    }

    bzero(&addr,sizeof(struct sockaddr_in));

    addr.sin_family=AF_INET;

    addr.sin_port=htons(DESTPORT);

    if(inet_aton(argv[1],&addr.sin_addr)==0){

    host=gethostbyname(argv[1]);

    if(host==NULL) {

    fprintf(stderr,"HostName Error:%s\n\a",hstrerror(h_errno));

    exit(1);

    }

    addr.sin_addr=*(struct in_addr *)(host->h_addr_list[0]);

    }

    /**** 使用IPPROTO_TCP创建一个TCP的原始套接字 ****/

    sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP);

    if(sockfd<0){

    fprintf(stderr,"Socket Error:%s\n\a",strerror(errno));

    exit(1);

    }

    /******** 设置IP数据包格式,告诉系统内核模块IP数据包由我们自己来填写 ***/

    setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on));

    /**** 没有办法,只用超级护用户才可以使用原始套接字 *********/

    setuid(getpid());

    /********* 发送×××了!!!! ****/

    send_tcp(sockfd,&addr);

    }

    /******* 发送×××的实现 *********/

    void send_tcp(int sockfd,struct sockaddr_in *addr)

    {

    char buffer[100];

    /**** 用来放置我们的数据包 ****/

    struct ip *ip;

    struct tcphdr *tcp;

    int head_len;

    /******* 我们的数据包实际上没有任何内容,所以长度就是两个结构的长度 ***/

    head_len=sizeof(struct ip)+sizeof(struct tcphdr);

    bzero(buffer,100);

    /******** 填充IP数据包的头部,还记得IP的头格式吗? ******/

    ip=(struct ip*)buffer;

    ip->ip_v=4; /** IPVERSION版本一般的是 4 **/

    ip->ip_hl=sizeof(struct ip)>>2; /** IP数据包的头部长度 **/

    ip->ip_tos=0; /** 服务类型 **/

    ip->ip_len=htons(head_len); /** IP数据包的长度 **/

    ip->ip_id=0; /** 让系统去填写吧 **/

    ip->ip_off=0; /** 和上面一样,省点时间 **/

    ip->ip_ttl=255; /**MAXTTL 最长的时间 255 **/

    ip->ip_p=IPPROTO_TCP; /** 我们要发的是 TCP包 **/

    ip->ip_sum=0; /** 校验和让系统去做 **/

    ip->ip_dst=addr->sin_addr; /** 我们***的对象 **/

    /******* 开始填写TCP数据包 *****/

    tcp=(struct tcphdr *)(buffer +sizeof(struct ip));

    tcp->source=htons(LOCALPORT);

    tcp->dest=addr->sin_port; /** 目的端口 **/

    tcp->seq=random();//这里就是seq的数值了。

    tcp->ack_seq=0;//这里是ack_seq

    tcp->doff=5;

    tcp->syn=1;

    /** 我要建立连接 **/

    tcp->check=0;//这里可以用while之类的循环来达到穷举seq的目的。

    ip->ip_src.s_addr=inet_addr("192.168.1.200");//这里是管理员的IP

    /** 什么都让系统做了,也没有多大的意思,还是让我们自己来校验头部吧,节约时间的话把校验合去掉 */

    tcp->check=check_sum((unsigned short *)tcp,sizeof(struct tcphdr));

    //sendto(sockfd,buffer,head_len,0,(const struct sockaddr *)addr, sizeof(struct sockaddr_in));

    sendto(sockfd,buffer,head_len,0,(const struct sockaddr *)addr, sizeof(struct sockaddr_in));

    }

    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);

    }

    /*

    hostent的定义如下:

    struct hostent {

    char *h_name;

    char **h_aliases;

    int h_addrtype;

    int h_length;

    char **h_addr_list;

    #define h_addr h_addr_list[0]

    };

    struct hostent

    h_name – 地址的正式名称。

    h_aliases – 空字节-地址的预备名称的指针。

    h_addrtype –地址类型; 通常是AF_INET。

    h_length – 地址的比特长度。

    h_addr_list – 零字节-主机网络地址指针。网络字节顺序。

    h_addr - h_addr_list中的第一地址。

    展开全文
  • 原始套接字:是一种对原始网络报文进行处理的套接字,主要用途有:l 发送自定义的IP数据包l 发送ICMP数据包l 网卡的侦听模式,监听网络上的数据包l 伪装IP地址l 自定位协议的实现原始套接字主要应用于底层...

    标准套接字分为:

    l  流式套接字(SOCK_STREAM):面向连接的套接字,应用于TCP应用程序。

    l  数据包套接字(SOCK_DGRAM):无连接的套接字,应用于UDP应用程序。

    原始套接字:是一种对原始网络报文进行处理的套接字,主要用途有:

    l  发送自定义的IP数据包

    l  发送ICMP数据包

    l  网卡的侦听模式,监听网络上的数据包

    l  伪装IP地址

    l  自定位协议的实现

    原始套接字主要应用于底层网络编程,原始套接字与标准套接字之间的关系如下:

    20191011164426237188.png

    原始套接字的创建:

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

    常见的协议类型如下:

    l  IPPROTO_IP:            IP协议,接受或者发送IP数据包,包含IP头部

    l  IPPROTO_ICMP: ICMP协议,接受或者发送ICMP的数据包,IP的头部不需要处理

    l  IPPROTO_TCP:  TCP协议,接受或者发送TCP数据包

    l  IPPROTO_UDP: UDP协议,接受或者UDP数据包

    l  IPPROTO_RAW: 原始IP包。

    链路层原始套接字

    可以直接用于接收和发送链路层MAC帧,在发送时需要由调用者自行构造和封装MAC首部。而网络层原始套接字可以直接用于接收和发送IP层的报文数据,在发送时,需要自行构造IP报文头(取决是否设置IP_HEADER选项),另外必须在管理员权限下才能使用原始套接字。

    原始套接口提供了普通TCP和UDP的socket不能提供的三种能力:

    l  进程使用raw socket可以读写ICMP、IGMP等分组,这个功能还使得使用ICMP或IGMP构造的应用程序能够完全作为用户进程处理,而不必往内核中添加额外代码。

    l  大多数内核只处理IPV4数据包中的一个名为协议的8字段的值1(ICMP)、2(IGMP),6(TCP)、17(UDP)四种情况,然而该字段还有其他值。进程使用raw socket就可以读写那些内核不处理IPv数据包。因此,可以使用原始套接字定义自己的协议格式。

    l  通过使用raw socket,进程可以使用IP_HADINCL套接口选项自行定义IP头部,这个功能可用于构造特定类型的TCP或UDP分组等。

    链路层原始套接字调用socket()函数创建,第一个 参数指定协议族类型为PF_PACKET,第二个type可以设置为SOCK_RAW或SOCK_DGRAM,第三个参数时协议类型(只对报文接收由意义)。协议类型protocol不同取值的意义如下:

    socket(PF_PACKET,type,htons(protocol))

    l  参数type设置为SOCK_RAW时:套接字接收和发送数据都是从MAC首部开始的早发送时需要由调用者从MAC首部开始构造函数和封装报文数据。该种情况是用于某些项目需要用到自定义的二层报文socket(PF_PACKET,SOCK_RAW,htons(protocol))

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

    protocol不同取值:

    protocol

    作用

    ETH_P_ALL

    0x0003

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

    ETH_P_IP

    0x0008

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

    ETH_P_ARP

    0x0806

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

    ETH_P_RARP

    0x8035

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

    自定义协议

    比如0x0810

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

    不指定

    0

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

    网络层原始套接字:

    创建面向连接的TCP和创建面向无连接的UDP套接字,在接受和发送时只能操作数据部分,而不能对IP首部或TCP或UDP首部进行操作。如果想要操作IP首部或传输层协议首部,就需要调用如下socket()函数创建网络层原始套接字。

    第一个参数指定协议族的类型为PF_INET

    第二个参数为SOCK_RAW

    第三个参数protocol为协议类型。

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

    l  发送报文       网络层原始套接字发送的报文数据,在默认情况下是从IP首部之后开始的,即需要由调用者自行构造和封装TCP/UDP等协议首部。这种套接字也提供了发送时从IP首部开始构造数据的功能。通过setsocketopt()个套接字设置上IP_HDRINCL选项,就需要在发送时自行构造IP首部。

    protocol

    作用

    IPPROTO_TCP

    6

    接收TCP类型的报文

    IPPROTO_UDP

    17

    接收UDP类型报文

    IPPROTO_ICMP

    1

    接收ICMP类型报文

    IPPROTO_IGMP

    2

    接收IGMP类型报文

    IPPROTO_RAW

    255

    不能接收报文,只能发送(需要构造数据包首部)

    IPPROTO_OSPF

    89

    接收协议号为89的报文

    展开全文
  • 原始套接字 IP_HDRINCL

    2021-03-22 16:45:13
    原始套接字可以访问ICMP和ICMP等协议包,可以读写内核不处理的IP数据包。可以创建自定义的IP数据包首部。一句话,使用原始套接字可以编写基于IP协议的通讯程序。1.创建原始套接字具体格式如下:int sockfd;sockfd =...
  • 1 原始套接字概述 1.1链路层原始套接字 1.2网络层原始套接字 1.2.1 接收报文 1.2.2 发送报文 2 原始套接字实现 2.1原始套接字报文收发流程 2.2链路层原始套接字的实现 2.2.1套接字创建 2.2.2报文接收 ...
  • 这确实不多,甚至可能不是真正的原始套接字,只是一个协议。 对于其他类型的原始通信,我建议通过一些基于JNI的包装器使用Berkley sockets under Linux(需要root权限)。 Java-通过套接字发送指向BufferedImage的对象...
  • 我想通过监听原始套接字通过代码从网站读取响应,但迄今为止,我只能读取由我的计算机发送的传出请求,而不是我真正感兴趣的即将到来的回复。 我该如何去阅读传入的回复?无法使用原始套接字读取传入响应编辑:使用...
  • 原始套接字编程

    2021-09-29 17:46:14
    实验四 原始套接字编程 一、 实验目的 1.了解Winsock原始套接字编程功能和特点。 2.掌握Winsock原始套接字编程基本方法和步骤。 3.理解ICMP协议在网络中的具体应用及其实现原理。 二、实验内容 1.利用原始套接字编程...
  • 这篇文章主要介绍了Python原始套接字编程实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM...
  • 原始套接字编写Sniffer失败??希望有过原始套接字编程经验的朋友帮忙看一下:#include #include #include #pragma comment (lib,"ws2_32.lib")//#define SIO_RCVALL 0x98000001#define SIO_RCVALL _WSAIOW(IOC_VENDOR,...
  • } if(closesocket(rawSocket)==SOCKET_ERROR) ReportError("关闭套接字"); } int SendEchoRequest(SOCKET s,LPSOCKADDR_IN lpstToAddr) { static ECHOREQUEST echoReq; static nId=1; static nSeq=1; static nRet; ...
  • 如下图所示MAC帧地址,获得的包为从目的MAC地址到CRC之前(不包括CRC) 以如下方式使用原始套接字 (sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) )//使用原始套接字,ETH_P_ALL可以获取所有经过本机的...
  • 关于IPv6的原始套接字

    2021-05-16 17:02:31
    关于IPv6的原始套接字发布时间:2008-09-16 11:01:16来源:红联作者:njngypp想利用原始套接字编写一个可以在发送UDP数据包时更改源地址的程序,v4下已经成功,但在v6下出现问题,没找到对应v4中setsockopt(sockfd, ...
  • 处于一些目的,有时需要...但是还有一个很少用的叫SOCK_RAW,原始套接字,使用它你可以捕获网卡上的所有网络数据,当然这需要超级用户权限。贴个列子吧,网上摘的,具体出处忘了#include #include #include #include ...
  • 先简单复习一下TCP报文的格式 原始套接字还提供了一个非常有用的参数IP_HDRINCL: 1、当开启该参数时:我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0时)和...
  • 10:原始套接字

    2021-01-16 22:02:05
    - 原始套接字创建 创建原始套接字需要超级用户权限 int sockfd = socket(AF_INET, SOCK_RAW, protocol); 1.原始套接字不存在端口号概念 对其调bind,会设置从其发出数据报源地址设为设置值 对其调connect,会...
  • 下面开始构造HTTP数据包,IP层和TCP层使用python的Impacket库,http内容自行填写。代码如下:#!/usr/bin/env python#-------------------------------------------------------------------------------# Name: raw_...
  • 原始套接字构建 TCP三次握手 及相关问题分析TCP、IP头部头部字段构建TCP伪首部结构体填充IP头部填充TCP头部计算校验和握手过程细节实现效果相关问题源码 分析TCP、IP头部 头部字段 IP头部结构 TCP头部结构 熟悉...
  • } /*将网络接口赋值给原始套接字地址结构*/ bzero(&sll, sizeof(sll)); sll.sll_ifindex = ethreq.ifr_ifindex; // 发送数据 int len = sendto(sock_raw_fd, buf, 14, 0 , (struct sockaddr *)&sll, sizeof(sll)); ...
  • 原始套接字数据包过滤 ebpf 支持原始套接字过滤功能,本文参考 《Linux 内核观测技术 BPF》第 6 章的示例进行描述,并深挖隐藏在 epbf 程序背后的一些技术细节。 ebpf 程序示例代码 bfp_program 源码如下: #include...
  • 深入解析MMAP模式下的原始套接字 一、前言 对于原始套接字大家都不陌生,即PF_PACKET类型的套接字,我们平时使用的抓包程序就是基于这种套接字实现的。下图中就是我们平时使用到的套接字的分类,可以看到原始套接字...
  • }//建立一个原始socket句柄 int Open_Raw_Socket(void) {intsock;if ((sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) ) { perror("raw socket error\n"); exit(1); }returnsock; }//设置eth0为混杂模式 int Set_...
  • 利用原始套接字模拟ping
  • 关于Linux下原始套接字发送分片包的问题?如题,在Linux环境下,使用原始套接字发送数据包,如果是普通的小于1500字节的TCP,UDP,ICMP数据包都没有问题,但是如果是大于1500字节,程序就会分片IP包,可是问题来了IP...
  • 13.由于原始套接字提供管理下层传输的能力,它们可能会被恶意利用,这是一个安全问题,因此只有具有管理员权限的用户才能创建原始套接字,否则在bind()函数调用时会失败,错误码为WSAEACCES,这么左可以防止普通用户...
  • 我想使用套接字来传输我的数据包不变,所以我尝试使用这样的原始套接字。使用Linux原始套接字与vconfig接口static int raw_sock = 0;static struct sockaddr_ll saddr;static struct ifreq ifr;static int ifindex;...
  • 第13章节原始套接字第13章 原始套接字 通常情况下程序设计人员接触的网络知识限于如下两类: 流式套接字(SOCK_STREAM),它是一种面向连接的套接字,对应于TCP应用程序。 数据报套接字(SOCK_DGRAM),它是一种无连接的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 108,445
精华内容 43,378
关键字:

原始套接字