精华内容
下载资源
问答
  • linux 原始套接字 发送
    更多相关内容
  • 使用C语言实现原始套接字从数据链路层到应用层的操作,Linux系统
  • 本文从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》

    展开全文
  • 主要介绍了Python 网络编程之UDP发送接收数据功能,结合实例形式分析了Python使用socket套接字实现基于UDP协议的数据发送端与接收端相关操作技巧,需要的朋友可以参考下
  • 原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据。区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有的数据帧...

    原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据。区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有的数据帧(数据包)。另外,必须在管理员权限下才能使用原始套接字。

    原始套接字的创建

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

    参数:

    family:协议族 这里写PF_PACKET

    type: 套接字类,这里写SOCK_RAW

    protocol:协议类别,指定可以接收或发送的数据包类型,不能写 “0”,取值如下,注意,传参时需要用htons() 进行字节序转换。

    ETH_P_IP:IPV4数据包

    ETH_P_ARP:ARP数据包

    ETH_P_ALL:任何协议类型的数据包

    返回值:

    成功( >0 ):套接字,这里为链路层的套接字

    失败( <0 ):出错

    实例如下:

    // 所需头文件
    #include <sys/socket.h>
    #include <netinet/ether.h>
    #include <stdio.h>  // perror
    
    int main(int argc,char *argv[])
    {
    	int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL) );
    
    	if(sock_raw_fd < 0){
    		perror("socket");
    		return -1;
    	}
    	
    	return 0;
    }
    

    获取链路层的数据包

    ssize_t recvfrom( int sockfd,

    void *buf,

    size_t nbytes,

    int flags,

    struct sockaddr *from,

    socklen_t *addrlen );

    参数:

    sockfd:原始套接字

    buf:接收数据缓冲区

    nbytes:接收数据缓冲区的大小

    flags:套接字标志(常为0)

    from:这里没有用,写 NULL

    addrlen:这里没有用,写 NULL

    返回值:

    成功:接收到的字符数

    失败:-1

    实例如下:

    #include <stdio.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <netinet/ether.h>
    
    int main(int argc,char *argv[])
    {
    	unsigned char buf[1024] = {0};
    	int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    
    	//获取链路层的数据包
    	int len = recvfrom(sock_raw_fd, buf, sizeof(buf), 0, NULL, NULL);
    	printf("len = %d
    ", len);
    
    	return 0;
    }
    

    混杂模式

    默认的情况下,我们接收数据,目的地址是本地地址,才会接收。有时候我们想接收所有经过网卡的所有数据流,而不论其目的地址是否是它,这时候我们需要设置网卡为混杂模式

    网卡的混杂模式一般在网络管理员分析网络数据作为网络故障诊断手段时用到,同时这个模式也被网络黑客利用来作为网络数据窃听的入口。在 Linux 操作系统中设置网卡混杂模式时需要管理员权限。在 Windows 操作系统和 Linux 操作系统中都有使用混杂模式的抓包工具,比如著名的开源软件 Wireshark。

    通过命令给 Linux 网卡设置混杂模式(需要管理员权限)

    设置混杂模式:ifconfig eth0 promisc

    取消混杂模式:ifconfig eth0 -promisc

    通过代码给 Linux网卡设置混杂模式

    代码如下:

    struct ifreq req;	//网络接口地址
    	
    strncpy(req.ifr_name, "eth0", IFNAMSIZ);			//指定网卡名称
    if(-1 == ioctl(sock_raw_fd, SIOCGIFINDEX, &req))	//获取网络接口
    {
    	perror("ioctl");
    	close(sock_raw_fd);
    	exit(-1);
    }
    
    req.ifr_flags |= IFF_PROMISC;
    if(-1 == ioctl(sock_raw_fd, SIOCSIFINDEX, &req))	//网卡设置混杂模式
    {
    	perror("ioctl");
    	close(sock_raw_fd);
    	exit(-1);
    }
    

    发送自定义的数据包:

    ssize_t sendto( int sockfd,

    const void *buf,

    size_t nbytes,int flags,

    const struct sockaddr *to,

    socklen_t addrlen );

    参数:

    sockfd:原始套接字

    buf:发送数据缓冲区

    nbytes:发送数据缓冲区的大小

    flags:一般为 0

    to:本机网络接口,指发送的数据应该从本机的哪个网卡出去,而不是以前的目的地址

    addrlen:to 所指向内容的长度

    返回值:

    成功:发送数据的字符数

    失败: -1

    本机网络接口的定义

    发送完整代码如下:

    struct sockaddr_ll sll;					//原始套接字地址结构
    struct ifreq req;					//网络接口地址
    
    strncpy(req.ifr_name, "eth0", IFNAMSIZ);			//指定网卡名称
    if(-1 == ioctl(sock_raw_fd, SIOCGIFINDEX, &req))	//获取网络接口
    {
    	perror("ioctl");
    	close(sock_raw_fd);
    	exit(-1);
    }
    
    /*将网络接口赋值给原始套接字地址结构*/
    bzero(&sll, sizeof(sll));
    sll.sll_ifindex = req.ifr_ifindex;
    
    // 发送数据
    // send_msg, msg_len 这里还没有定义,模拟一下
    int len = sendto(sock_raw_fd, send_msg, msg_len, 0 , (struct sockaddr *)&sll, sizeof(sll));
    if(len == -1)
    {
    	perror("sendto");
    }
    

    这里头文件情况如下:

    #include <net/if.h>// struct ifreq
    #include <sys/ioctl.h> // ioctl、SIOCGIFADDR
    #include <sys/socket.h> // socket
    #include <netinet/ether.h> // ETH_P_ALL
    #include <netpacket/packet.h> // struct sockaddr_ll
    
    展开全文
  • 基于原始套接字的网络嗅探器,主要就是把网卡设置为混杂模式,用vs可直接编译运行,必须使用管理员权限才能运行,抓到包,用c++写的
  • 原始套接字发送IP数据报

    千次阅读 2019-04-16 10:24:30
    原始套接字发送ARP数据包》 一样:构造套接字,填写地址数据结构,填充构造发送报文,调用sendto发送。  通过以下构造套接字: int sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP) );  由于我们想...

        IP是TCP/IP协议族中的核心协议。所有TCP、UDP、ICMP和IGMP数据都通过IP数据报传输。IP提供了一种尽力而为、无连接的数据报交付服务。

    IPv4头部

    图1 IPv4头部

        图1显示了IPv4数据报的头部,各个字段的如下:

    1. 版本:IP数据报的版本号。IPv4为4。
    2. Internet头部长度:IP头部的长度,以4字节为单位。
    3. 服务类型:指定了一个等效的通信类型。未被广泛使用。
    4. 总长度:IP数据报的总长度,以字节为单位。通过这个字段和Internet头部长度字段,可以知道数据报的数据部分从哪里开始。
    5. 标识:为了避免将一个数据报分片和其他数据报分片混淆,发送主机通常在每次发送数据报时都将一个内部计数器加1,并将该计数器值复制到IPv4标识字段。
    6. 标志:三个比特位。
      bit0:保留位。必须为0。
      bit1:DF位。为0表示可能分片。为1表示不能分片。
      bit2:MF位。为0表示最后一个分片。为1表示还有分片。
    7. 分片偏移:指示数据部分的偏移值,以8字节为单位。IP分片机制可以参考《IP分片》
    8. 生存周期:设置一个数据报可经过的路由器数量的上限。每台路由器在转发数据报时将该值减1。
    9. 协议:IP的上层协议字段。例如下文讨论的ICMP为1。
    10. Internet校验和:计算的是IPv4头部的校验和。校验算法为先将校验和字段置为0,然后将头部各字节以2字节为一组进行反码加码,即溢出的高位还要加到末位。最后将计算的结果取反存入校验和字段。
    	for (int i = 0; i <= count - 2; i += 2)
    	{
    		num = (HEADER[i] << 8) + HEADER[i+1];
    		checkSum += num;
    
    		checkSum = (checkSum & 0xffff) + (checkSum >> 16);
    	}
    
    	checkSum = (~checkSum) & 0xffff;
    
    1. 源IP地址。
    2. 目标IP地址。
    3. IP选项。

    ICMP头部

        由于自己构造IP数据报,需要一个上层协议进行验证,这里选取ICMP的回显服务。ICMP在IP数据报里的封装如图2所示。

    图2 ICMP封装

        一种最为常见的ICMP报文对就是回显请求和回显响应,如图3所示。在ICMPv4中,它们的类型分别为8和10。ICMP的回显请求报文大小是任意的。
        校验和字段的算法和IP的校验和算法一样,从ICMP首部开始到数据部分进行计算;标识符字段,使ping应用程序识别返回的应答,因为ICMP协议不像传输层协议那样有端口号;序列号字段,每发送一个回显请求报文便增加1。

    图3 ICMP Ping头部格式

    原始套接字发送IP数据报

        思路与《原始套接字发送ARP数据包》一样:构造套接字,填写地址数据结构,填充构造发送报文,调用sendto发送。
        通过以下构造套接字:

    int sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP) );
    

        由于我们想要自己构造IP头,自己计算校验和,所以第一个参数为PF_PACKET。对应的地址也要用sockaddr_ll结构。完整代码见附录1。

    实验

        将目的IP地址选为www.baidu.com的IP地址。打开wireshark进行抓包,运行程序发送echo请求报文,观察是否有应答。
        如图4所示,对方进行了应答。

    图4 Wireshark抓包

    附录1

    #include <sys/socket.h>
    #include <sys/ioctl.h>
    #include <netinet/in.h>
    #include <netinet/ip_icmp.h>
    #include <arpa/inet.h>
    #include <linux/if.h>
    #include <linux/if_ether.h>
    #include <linux/if_packet.h>
    #include <net/ethernet.h>
    
    #include <string.h>
    #include <stdio.h>
    
    int main()
    {
    	//套接字
    	int sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP) );
    	if (sockfd == -1)
    	{
    		printf("error at socket().\n");
    		return 0;
    	}
    
    	//地址
    	sockaddr_ll addr_ll;
    	memset(&addr_ll, 0, sizeof(sockaddr_ll) );
    	addr_ll.sll_family = PF_PACKET;
    
    	ifreq ifr;
    	strcpy(ifr.ifr_name, "ens33");
    	if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1)
    	{
    		printf("error ioctl SIOCGIFINDEX\n"); return 0;
    	}
    	addr_ll.sll_ifindex = ifr.ifr_ifindex; //接口索引
    
    	if (ioctl(sockfd, SIOCGIFADDR, &ifr) == -1)
    	{
    		printf("error ioctl SIOCGIFADDR\n"); return 0;
    	}
    	char* ipSrc = inet_ntoa(((struct sockaddr_in*)(&(ifr.ifr_addr)))->sin_addr);
    	printf("ip address : %s\n", ipSrc); //source ip
    
    	if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) == -1)
    	{
    		printf("error ioctl SIOCGIFHWADDR\n"); return 0;
    	}
    	unsigned char macDst[ETH_ALEN] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
    	unsigned char macSrc[ETH_ALEN];
    	memcpy(macSrc, ifr.ifr_hwaddr.sa_data, ETH_ALEN); //mac address
    	printf("mac address");
    	for (int i = 0; i < ETH_ALEN; i++)
    		printf(":%02x", macSrc[i]);
    	printf("\n");
    
    	//填充以太网头部
    	ethhdr ethheader;
    	memcpy(ethheader.h_source, macSrc, ETH_ALEN);
    	memcpy(ethheader.h_dest, macDst, ETH_ALEN);
    	ethheader.h_proto = htons(ETHERTYPE_IP);
    
    	//填充IP头部
    	iphdr ipheader;
    	ipheader.version = 0x4;
    	ipheader.ihl = 0x5;
    	ipheader.tos = 0x00;
    	ipheader.tot_len = htons(60);  //20 + 8 + 32
    	ipheader.id = 0x1000;
    	ipheader.frag_off = 0x0000;
    	ipheader.ttl = 128;
    	ipheader.protocol = 0x01;
    	ipheader.check = 0;
    	ipheader.saddr = inet_addr(ipSrc);
    	ipheader.daddr = inet_addr("115.239.211.112");
    
    	unsigned int checkSum = 0;
    	unsigned int num;
    	unsigned char* p = (unsigned char*)&ipheader;
    	int i;
    	for (i = 0; i <= 18; i += 2)
    	{
    		num = (p[i] << 8) + p[i + 1];
    		checkSum += num;
    		checkSum = (checkSum & 0xffff) + (checkSum >> 16);
    	}
    	checkSum = (~checkSum) & 0xffff;
    	ipheader.check = htons((unsigned short)checkSum);
    
    	//填充ICMP头部
    	icmphdr icmpheader;
    	icmpheader.type = ICMP_ECHO;
    	icmpheader.code = 0;
    	icmpheader.checksum = 0;
    	icmpheader.un.echo.id = 0x1000;
    	icmpheader.un.echo.sequence = 0x0001;
    
    	checkSum = 0;
    	p = (unsigned char*)&icmpheader;
    	for (i = 0; i <= 6; i += 2)
    	{
    		num = (p[i] << 8) + p[i + 1];
    		checkSum += num;
    		checkSum = (checkSum & 0xffff) + (checkSum >> 16);
    	}
    
    	//echo data
    	unsigned char echo[32];
    	for (i = 0; i < 32; i++)
    		echo[i] = (unsigned char)(0x10 + i);
    
    	for (i = 0; i <= 30; i += 2)
    	{
    		num = (echo[i] << 8) + echo[i + 1];
    		checkSum += num;
    		checkSum = (checkSum & 0xffff) + (checkSum >> 16);
    	}
    
    	checkSum = (~checkSum) & 0xffff;
    	icmpheader.checksum = htons((unsigned short)checkSum);
    
    	//发送
    	unsigned char sendBuf[sizeof(ethhdr) + 60];
    	memcpy(sendBuf, &ethheader, sizeof(ethhdr) );
    	memcpy(sendBuf + sizeof(ethhdr), &ipheader, sizeof(iphdr) );
    	memcpy(sendBuf + sizeof(ethhdr) + sizeof(iphdr), &icmpheader, 8);
    	memcpy(sendBuf + sizeof(ethhdr) + sizeof(iphdr) + 8, echo, 32);
    
    	int len = sendto(sockfd, sendBuf, sizeof(sendBuf), 0, (const sockaddr*)&addr_ll, sizeof(sockaddr_ll));
    	if (len > 0)
    	{
    		printf("send success.\n");
    	}
    
    	return 0;
    }
    

    参考

    1. RFC 791
    2. RFC 792
    3. TCP/IP详解 卷1
    展开全文
  • 原始socket发送自定义以太网帧,来开发属于你的协议吧
  • 这里是在 ubuntu 下通过原始套接字组一个 udp 数据包,给 PC 机的网络调试助手发送信息: #include #include #include #include <net/if.h> //struct ifreq #include <sys/ioctl.h> //ioctl、...
  • Linux 网络编程——原始套接字实例:发送 UDP 数据包,相关教程链接如下: http://blog.csdn.net/tennysonsky/article/details/44925057
  • 使用背景 今天继续介绍一下原始套接字的一种用法,就是用来模拟TCP的握手连接过程,不过作为客户端,用户可以直接使用connect函数来进行连接操作,那么为何还要用RawSocket来模拟呢?其实就涉及到一种使用场景,就是...
  • 网络编程,原始套接字编程实验...基于原始套接字的分片ICMP ECHO请求发送功能;基于原始套接字的ICMP ECHO响应接收功能;重叠分片的发送功能; 可以用来测试windows系统和Linux系统的重叠分片重组策略。 编程语言:C++
  • Linux网络通信之原始套接字SOCKET_RAW

    千次阅读 2022-05-04 14:23:31
    1.原始套接字与标准套接字传输区别 a.标准SOCKET APP--->STD_SOCKET--->TCP--->IP--->ETH APP--->STD_SOCKET--->UDP--->IP--->ETH b.原始SOCKET APP--->RAW_SOCKET--->IP---&...
  • Linux原始套接字抓取底层报文

    千次阅读 热门讨论 2018-12-16 00:08:09
    1.原始套接字使用场景  我们平常所用到的网络编程都是在应用层收发数据,每个程序只能收到发给自己的数据,即每个程序只能收到来自该程序绑定的端口的数据。收到的数据往往只包括应用层数据,原有的头部信息在传递...
  • linux原始套接字实战

    千次阅读 2017-12-22 16:04:05
    本文的主线是一个使用原始套接字发送数据的程序,工作在数据链路层,采用自定义以太网帧协议,靠MAC地址识别目标主机。所以不涉及到IP地址和端口号。程序主要用于互联的两台机器之间进行丢帧率计算。以下部分都是...
  • 发送端代码:#!/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,并设置 errno 。 domain 参数指定了通信域,常用值包括:AF_INET(IPv4)、AF_INET6(IPv6)。 type 参数指定了通信语义,常用值包括:SOCK_STREAM(字节流)、S
  • linux原始套接字详解

    千次阅读 2016-10-13 10:54:19
    一、原始套接字概述   协议栈的原始套接字从实现上可以分为“链路层原始套接...而网络层原始套接字可以直接用于接收和发送IP层的报文数据,在发送时需要自行构造IP报文头(取决是否设置IP_HDRINCL选项)。   1.1
  • 这里是在 ubuntu 下通过原始套接字组一个 udp 数据包,给 PC 机的网络调试助手发送信息: #include #include #include #include <net/if.h> //struct ifreq #include <sys/ioctl.h> //ioctl、SIOCGIFADDR...
  • linux下简单的原始套接字通信

    千次阅读 2016-12-20 15:21:02
    原始套接字编程分析 ...linux原始套接字(3)-构造IP_TCP发送与接收: http://www.cnblogs.com/yuuyuu/p/5169931.html linux原始套接字(4)-构造IP_UDP: http://www.cnblogs.com/yuuyuu/p/5170056.html
  • 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。...
  • 原始套接字概述

    2019-07-12 18:50:16
    原始套接字(SOCK_RAW)不同于流式套接字、数据报套接字。原始套接字是基于IP数据包的编程,流式套接字只能收发TCP协议的数据,数据报套接字只能收发UDP协议的数据。前面讲述的网络编程都是在应用层收发数据,每个...
  • 最近工作上有一项任务需要使用python在数据链路层上收发数据,遇到了很多坑,在此记录一下 注: 该脚本只能运行在 linux 系统上,windows 上无法使用原始套接字在数据链路层收发数据(socket.AF_PACKET在 windows ...
  • Linux网络编程:原始套接字简介

    千次阅读 2018-12-15 23:18:45
    Linux网络编程:原始套接字编程 一、原始套接字用途 通常情况下程序员接所接触到的套接字(Socket)为两类: 流式套接字(SOCK_STREAM):一种面向连接的Socket,针对于面向连接的TCP 服务应用; 数据报式套接字(SOCK...
  • 主要给大家介绍了关于golang如何利用原始套接字构造UDP包的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用golang具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
  • 当我们创建了一个TCP套接字的时候,我们只是负责把我们要发送的内容(buffer)传递给了系统。系统在收到我们的数据后,回自动的调用相应的模块给数据加上TCP头部,然后加上IP头部,再发送出去。而现在是我们自己创建各个...
  • 本文从IPV4协议栈原始套接字的分类入手,详细介绍了链路层和网络层原始套接字的特点及其内核实现细节。并结合原始套接字的实际应用,说明各类型原始套接字的适应范围,以及在实际使用时需要注意的问题。 ...
  • 原始套接字发送ARP数据包

    千次阅读 2019-04-14 10:42:29
    什么是ARP协议     ARP协议是Address Resolution Protocol(地址解析协议)的缩写...所谓“地址解析”就是主机在发送数据帧之前将目标IP地址转化成目标MAC地址的过程。ARP协议的基本功能就是通...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 27,176
精华内容 10,870
关键字:

linux 原始套接字 发送