Linux raw socket

Linux raw socket

==========================================

1. 为什么要详细了解raw socket呢?

?其实很早以前就对原始套接字有了一定的了解,那时候还做过一个小的抓包程序,当时以为对原始套接字很熟悉了,但是最近在看nmap的时候被其中的一句话给整迷糊了。
?在《Nmap Network Discovery III》的SYN scan章节中文中提到通过raw socket构造一个SYN包发给目标端口以后,如果目标端口发回了SYN/ACK包,那么此时nmap直接发送一个RST包来快速关闭此次连接,接着来了一句”其实你都不需要自己发送RST包,因为发送的SYN包是nmap手动构造的,所以kernel收到了不在期望内的数据包,kernel会直接发送RST包”。
?卧槽!这句话当时思考了半小时也没有搞明白了,这跟我之前对raw socket的了解太不一样了,难道raw socket就不经过协议栈啦?难道raw socket也就能发送一个包?
?觉得有必要把这个问题搞清楚了,所以就开始网上、书本开始重新了解raw socket。

2. 原始套接字的protocol

?我们知道PF_INET协议族支持的protocol参数有很多个,在一般的socket编程中我们习惯使用socket(PF_INET, SOCK_STREAM, 0)来新建TCP的套接字,这个问题在前面的文章《socket的三个参数(family、type、protocol)解析》中已经提到过了,那么在新建原始套接字的时候可以用socket(PF_INET, SOCK_RAW, 0)吗?
?答案是不能!如果socket(PF_INET, SOCK_RAW, 0)来新建原始套接字会返回错误”- EPROTONOSUPPORT”,我们从内核新建套接字函数inet_create中可以找到答案:

    list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
        err = 0;
        /* Check the non-wild match. */
        if (protocol == answer->protocol) {
            if (protocol != IPPROTO_IP)
                break;
        } else {
            /* Check for the two wild cases. */
            if (IPPROTO_IP == protocol) {
                protocol = answer->protocol;
                break;
            }
            if (IPPROTO_IP == answer->protocol)
                break;
        }
        err = -EPROTONOSUPPORT;
    }
    //type等于SOCK_RAW的inet_protosw
    {
        .type =       SOCK_RAW,
        .protocol =   IPPROTO_IP,   /* wild card */
        .prot =       &raw_prot,
        .ops =        &inet_sockraw_ops,
        .flags =      INET_PROTOSW_REUSE,
    }

?上面代码的下半部分是原始套接字的inet_protosw,其中protocol的值为IPPROTO_IP(等于0),那么上半部分的代码逻辑走到protocol == answer->protocol,而两者的值都为IPPROTO_IP,所以if (protocol != IPPROTO_IP)逻辑不满足,最后err就变成了-EPROTONOSUPPORT,新建socket失败。
?Protocol等于IPPROTO_RAW的原始套接字能接受数据包吗?这个问题待到后面的原始套接字接受部分我们来回答。

3. 原始套接字的权限检查

?新建raw socket的时候需要有新建原始套接字的权限(一般都会要求是特权用户),在内核新建socket的函数inet_create对raw socket的用户权限检查如下:

if (sock->type == SOCK_RAW && !kern &&
    !ns_capable(net->user_ns, CAP_NET_RAW))
    goto out_rcu_unlock;

4. IP_HDRINCL

?原始套接字需要应用自己构造传输层(TCP, UDP)头部,但是如果连IP头部都应用自己来构造的话就得通过setsocketopt中的IP_HDRINCL(Header Include)选项来设置socket,这样协议栈就不会自动填充IP header了。
?在Linux中新建raw socket的时候协议指定为IPPROTO_RAW那么我们不再需要显示调用setsocketopt的IP_HDRINCL选项了:

if (SOCK_RAW == sock->type) {
    inet->inet_num = protocol;
    if (IPPROTO_RAW == protocol)
        inet->hdrincl = 1;      //setsocketopt(...,IP_HDRINCL)
}

?不过为了代码的可读性和可移植性还是建议设置IP_HDRINCL选项。

5. 原始套接字的发送

?在前面的《socket的三个参数(family、type、protocol)解析》一文中已经提到了内核中socket有三个重要的结构体inet_protosw、proto_ops、proto,分别代表了socket传输层的操作集合、某一种类型的套接字的操作集合、具体协议的操作集合。原始套接字对应的这三个结构体为:

static struct inet_protosw inetsw_array[] =
{
    ……
    {
        .type =       SOCK_RAW,
        .protocol =   IPPROTO_IP,   /* wild card */
        .prot =       &raw_prot,
        .ops =        &inet_sockraw_ops,
        .flags =      INET_PROTOSW_REUSE,
    }
}

/*
 * For SOCK_RAW sockets; should be the same as inet_dgram_ops but without
 * udp_poll
 */
static const struct proto_ops inet_sockraw_ops = {
    .family        = PF_INET,
    ... 
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,
    ... 
};

struct proto raw_prot = {
    .name          = "RAW",
     ... 
    .sendmsg       = raw_sendmsg,
    .recvmsg       = raw_recvmsg,
    ... 
};

?原始套接字的发送流程是:SYS_CALL(sendto)->inet_sendmsg->raw_sendmsg,raw_sendmsg是原始套接字的主要发送函数:

static int raw_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
               size_t len)
{
    struct inet_sock *inet = inet_sk(sk);
    struct ipcm_cookie ipc;
    struct rtable *rt = NULL;
    struct flowi4 fl4;

    ...
    if (inet->hdrincl)
        /* XXX: stripping const */
        err = raw_send_hdrinc(sk, &fl4, (struct iovec *)msg->msg_iter.iov, len,
                      &rt, msg->msg_flags);

     else {
         ...
        err = ip_append_data(sk, &fl4, raw_getfrag,
                     &rfv, len, 0,
                     &ipc, &rt, msg->msg_flags);
         ...
     }
     ...
}

?假如我们为原始套接字设置了IP_HDRINCL(或者是protocol为IPPROTO_RAW)那么raw_send_hdrinc函数会被调用:

static int raw_send_hdrinc(struct sock *sk, struct flowi4 *fl4,
               void *from, size_t length,
               struct rtable **rtp,
               unsigned int flags)
{
    struct inet_sock *inet = inet_sk(sk);
    struct net *net = sock_net(sk);
    ...
    /*
     * We don‘t want to modify the ip header, but we do need to
     * be sure that it won‘t cause problems later along the network
     * stack.  Specifically we want to make sure that iph->ihl is a
     * sane value.  If ihl points beyond the length of the buffer passed
     * in, reject the frame as invalid
     */
    err = -EINVAL;
    if (iphlen > length)
        goto error_free;

    if (iphlen >= sizeof(*iph)) {
        if (!iph->saddr)
            iph->saddr = fl4->saddr;
        iph->check   = 0;
        iph->tot_len = htons(length);
        if (!iph->id)
            ip_select_ident(skb, NULL);

        iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
    }
    if (iph->protocol == IPPROTO_ICMP)
        icmp_out_count(net, ((struct icmphdr *)
            skb_transport_header(skb))->type);

    err = NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
              rt->dst.dev, dst_output);
    ...
}

?从上面的代码看到如果没有设置源地址那么kernel会给头部加上源地址,IP序列号也是一样的。对于IP校验码和IP长度字段kernel总是会设置。

当设置了IP_HDRINCL时IP header中字段如何设置
IP Checksum(IP报头校验码) 总是由内核填充
Source Address(源地址) 当为0时由内核填充
Packet ID(数据包ID) 当为0时由内核填充
Total Length(数据包长度) 总是由内核填充

?从上面梳理的原始套接字的发送流程(指定了IP_HDRINCL)还可以知道:

  • ?新建socket时候指定的protocol(如:IPPROTO_TCP、IPPROTO_UDP)在发送时没有用到,并不会用指定的protocol来填充IP头部,因为我们设置了IP_HDRINCL来指明由我们自己来填充IP头部。
  • ?发送的数据没有再经过IP层,最后直接调用dst_output发送数据报了,所以如果发送的数据报长度超过了MTU那么不会有IP分片产生,发送失败,返回EMSGSIZE错误码。
    ?相反的如果没有设置IP_HDRINCL来由我们自己定义IP头部,那么流程会走到ip_append_data函数进行IP分片。
  • ?发送的数据报没有经过TCP层,这也就为我最开始迷惑的那个kernel自动发送RST数据报的问题埋下了”伏笔”。

6. 原始套接字的接收

?网卡驱动收到报文后在软中断上下文由netif_receive_skb()处理,对于IP报文且目的地址为本机的会由ip_rcv()最终调用ip_local_deliver_finish()函数。ip_local_deliver_finish()中对每一个数据包都会调用raw_local_deliver(),raw_local_deliver()就是原始套接字接收的入口。
?原始套接字的接收流程主要是:先根据报文的L4层协议类型hash值在raw_v4_htable表中查找是否有匹配的sock。如果有匹配的sock结构,就进一步调用raw_v4_input()处理网络层原始套接字,在raw_v4_input()中进一步调用__raw_v4_lookup()函数进行匹配,原始套接字的源IP、目的IP、绑定的接口eth等是否匹配,最后调用raw_rcv()。
技术分享
?不管是否有原始套接字要处理,该报文都会走后续的协议栈处理流程。即会继续匹配inet_protos[]数组,根据L4层协议类型走TCP、UDP、ICMP等不同处理流程。
?了解了原始套接字的接收过程以后我们来回答上文中提到的两个问题:
?Q:通过raw socket发送了一个SYN数据包以后,为什么kernel在接收到SYN+ACK的回包以后会发送RST给对端?
?A:从第5节我们知道raw socket在发送数据报的时候没有经过L3的IP层和L4的TCP/UDP层,所以kernel也就完全不知道你自己”偷偷”的发送了一个SYN数据包。等待对端给我们发回来SYN+ACK的数据包的时候我们从第6节内容得知无论是否有对应的原始套接字处理,该报文都会走后续的协议栈处理流程,此时协议栈发现你发过来的这个数据包我根本不认识,我自己没有发过对应的SYN报文,毫无疑问协议栈肯定就会认为这是一个异常的报文,直接给回送了RST。
?Q:protocol等于IPPROTO_RAW的套接字能否接收到数据包?
?A:从上面的原始套接字的接收流程得知,对数据包进行的第一次筛选就是根据数据包的protocol生成的hash从raw_v4_htable中查找对应的raw socket,我们从网卡接收回来的数据包的L4层protocol肯定是TCP、UDP、ICMP等有效的值,我们没有听过说哪个数据包的protocol是IPPROTO_RAW吧,那么我们用protocol等于IPPROTO_RAW来新建原始套接字最后生成的hash值也就不会匹配到任何的数据包了。也就是说用IPPROTO_RAW新建的套接字只适合发送,不能接收数据包。
参考链接:http://sock-raw.org/papers/sock_raw(基本看完这篇paper对raw socket的了解也就足够了)

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。