天天看点

linux kernel raw packet的接收与发送

Q:如果我要在linux上写一个程序,程序的功能是接收网络数据包,根据接收到的包再决定发送网络数据包,但这里的网络数据包并非TCP/UDP类型的数据包,而是仅包含以太头的原始数据包raw packet,那么这个的程序应该怎样编写呢?

为了使贴上来的程序完整,不至于只包含一部分代码,我先将各文件的完整代码附上,之后再针对各个需要注意的点逐一介绍。

先定义一个原始包处理的类,类的接口包括发送与接收、socket的创建与关闭、混杂模式的打开与关闭。头文件如下

#ifndef __RAWPKT_PROCESSOR_H__
#define __RAWPKT_PROCESSOR_H__

#include <iostream>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <sys/ioctl.h>

class RawPktProcessor
{
public:
    RawPktProcessor()=default;
    ~RawPktProcessor()=default;
    
    static void initSocket();
    static void closeSocket();
    static int32_t ifnameToIndex();
    static void setPromiscMode(bool enable);
    static int32_t send(const std::vector<uint8_t> &data);
    static int32_t recv(std::vector<uint8_t> &data);
private:
    static std::string s_ifName;
    static struct sockaddr_ll s_srcSaddr, s_dstSaddr;
    static int32_t s_sfd;
};

#endif
           

源文件如下

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "rawpkt_processor.h"

/* change the s_ifName to your own NIC's name */
std::string RawPktProcessor::s_ifName = std::string("eth0");
struct sockaddr_ll RawPktProcessor::s_srcSaddr;
struct sockaddr_ll RawPktProcessor::s_dstSaddr;
int32_t RawPktProcessor::s_sfd = -1;

void RawPktProcessor::initSocket()
{
    s_sfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (-1 == s_sfd)
    {
        std::cout << "create socket failed" << std::endl;
        return;
    }
    
    int ifIndex = ifnameToIndex();
    if (ifIndex <= 0)
    {
        std::cout << "get ifIndex failed" << std::endl;
        return;
    }
    
    memset(&s_srcSaddr, 0, sizeof(s_srcSaddr));
    memset(&s_dstSaddr, 0, sizeof(s_dstSaddr));
    
    s_srcSaddr.sll_family = AF_PACKET;
    s_srcSaddr.sll_protocol = htons(ETH_P_ALL);
    s_srcSaddr.sll_ifindex = ifIndex;
    s_srcSaddr.sll_pkttype = PACKET_OUTGOING;
    s_srcSaddr.sll_halen = ETH_ALEN;
    
    s_dstSaddr.sll_family = AF_PACKET;
    s_dstSaddr.sll_protocol = htons(ETH_P_ALL);
    s_dstSaddr.sll_ifindex = ifIndex;
    s_dstSaddr.sll_pkttype = PACKET_HOST;
    s_dstSaddr.sll_halen = ETH_ALEN;
    if (bind(s_sfd, (struct sockaddr *)&s_dstSaddr, sizeof(s_dstSaddr)) == -1) {
        std::cout << "bind failed" << std::endl;
    }
}

int32_t RawPktProcessor::ifnameToIndex()
{
    if (s_sfd == -1)
    {
        std::cout << "there is no socket to be create" << std::endl;
        return -1;
    }
    struct ifreq req;
    memset(&req, 0, sizeof(req));
    strncpy(req.ifr_name, s_ifName.c_str(), s_ifName.size());
    if (ioctl(s_sfd, SIOCGIFINDEX, &req) < 0)
    {
        std::cout << "get ifindex failed" << std::endl;
        return -1;
    }
    return req.ifr_ifindex;
}

void RawPktProcessor::setPromiscMode(bool enable)
{
    if (s_sfd == -1)
    {
        std::cout << "there is no socket to be create" << std::endl;
        return;
    }
    #ifdef GLOBAL_PROMISC
    struct ifreq req;
    memset(&req, 0, sizeof(req));
    strncpy(req.ifr_name, s_ifName.c_str(), s_ifName.size());
    if (ioctl(s_sfd, SIOCGIFFLAGS, &req) < 0)
    {
        std::cout << "get IFFLAGS failed" << std::endl;
        return;
    }
    if (enable) {
        req.ifr_flags |= IFF_PROMISC;
    }else {
        req.ifr_flags &= ~IFF_PROMISC;
    }
    
    if (ioctl(s_sfd, SIOCSIFFLAGS, &req) < 0)
    {
        std::cout << "set ifflags error" << std::endl;
    }
    #else
    struct packet_mreq mreq = {0};
    int action;
    
    mreq.mr_ifindex = ifnameToIndex();
    mreq.mr_type = PACKET_MR_PROMISC;
    
    if (mreq.mr_ifindex <= 0)
    {
        std::cout << "unable to get ifindex" << std::endl;
        return;
    }
    
    if (enable) {
        action = PACKET_ADD_MEMBERSHIP;
    }else {
        action = PACKET_DROP_MEMBERSHIP;
    }
    
    if (setsockopt(s_sfd, SOL_PACKET, action, &mreq, sizeof(mreq)) != 0) {
        std::cout << "unable to enter promiscuous mode" << std::endl;
    }
    #endif
}

int32_t RawPktProcessor::send(const std::vector<uint8_t> &data)
{
    return sendto(s_sfd, data.data(), data.size(), 0, (const struct sockaddr *)&s_srcSaddr, sizeof(s_srcSaddr));
}

int32_t RawPktProcessor::recv(std::vector<uint8_t> &data)
{
    return ::recv(s_sfd, data.data(), data.size(), MSG_DONTWAIT);
}

void RawPktProcessor::closeSocket()
{
    setPromiscMode(false);
    close(s_sfd);
}
           

主程序文件如下:

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <sstream>
#include <vector>
#include <memory>
#include <functional>
#include <arpa/inet.h>
#include "rawpkt_processor.h"

using namespace std;
bool g_terminated = false;

std::vector<uint8_t> broadcastPkt = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x08, 0x00
};

std::vector<uint8_t> unicastPkt = {
0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x08, 0x00
};

std::vector<uint8_t> testPkt = {
0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x08, 0x00, 0x11, 0x11
};


void recvResponseThreadEntry()
{
    std::vector<uint8_t> rcvBuf(1518, 0);
    while (!g_terminated)
    {
        int realLen = RawPktProcessor::recv(rcvBuf);
        if (realLen <= 0)
        {
            continue;
        }
        
        if (!memcmp(rcvBuf.data(), broadcastPkt.data(), broadcastPkt.size()))
        {
            cout << "I had received " << realLen << " bytes broadcast packet" << endl;
        }
        else if (!memcmp(rcvBuf.data(), unicastPkt.data(), unicastPkt.size()))
        {
            cout << "I had received " << realLen << " bytes unicast packet" << endl;
        }
    }
}

int main()
{
    RawPktProcessor::initSocket();
    auto runnerPtr = std::make_shared<std::thread>(std::bind(recvResponseThreadEntry));
    std::string cmd;
    while(!g_terminated)
    {
        std::getline(std::cin, cmd);
        if (!cmd.empty())
        {
            istringstream ss(cmd);
            std::string cmdName, param1, param2, param3;
            ss >> cmdName >> param1 >> param2 >> param3;
            if (cmdName == "quit" || cmdName == "exit")
            {
                g_terminated = true;
            }
            else if (cmdName == send)
            {
                RawPktProcessor::send(testPkt);
            }
        }
    }
    runnerPtr->join();
    return 0;
}

           

Q1: 之前我在网上搜索到的发送raw packet时,很多时候都使用多个socket,比如当获取网卡ifindex时创建了一次socket,再调用ioctl获取ifindex之后又close socket的文件描述符,上面的代码全程仅使用了一个socket,会不会有问题?

没有问题,比如为了获取ifindex,有的做法是直接创建一个socket,然后将socket传递到ioctl的参数中,用完之后就关闭,这种属于更通用的做法,不依赖与具体哪个socket,我上面的函数ifnameToIndex就必须依赖于s_sfd已创建的前提下才能使用这个函数,调用顺序有一定依赖,而我这里因为没有其他的函数需要调用函数ifnameToIndex,且为了简单,所以就没有单独创建socket,而是采用已创建的socket(s_sfd)。

如何理解socket呢,实际上我们可以把内核各网卡收发数据包看成是家里的电路一样,如果你要想获取电流就必须买一个插座与家里的主线路接通,然后你的电子设备才能用电,这里内核也一样,要想接收或者发送数据包到各网卡,那么必须创建一个插头socket,有了socket之后,网卡驱动收到报文送入到协议栈,协议栈再转发至每一个socket的队列里边,然后应用层通过recv函数从接收队列中取数据包。

Q2:同一个函数中多次调用ioctl引发的问题可能被你轻易忽略,但这样做很可能会让你无法接收报文

让我重新展示一下我遇到的连续调用ioctl引发问题的代码段

struct ifreq req;
memset(&req, 0, sizeof(req));
strncpy(req.ifr_name, s_ifName.c_str(), s_ifName.size());
if (ioctl(s_sfd, SIOCGIFINDEX, &req) < 0)
{
    std::cout << "get ifindex failed" << std::endl;
    return -1;
}
if (ioctl(s_sfd, SIOCGIFFLAGS, &req))
{
    std::cout << "get ifflags failed" << std::endl;
    return -1;
}

s_srcSaddr.sll_ifindex = req.ifr_ifindex;
s_dstSaddr.sll_ifindex = req.ifr_ifindex;
           

不知道你有没有从上面的代码中发现问题,这段代码第一个ioctl主要是为了获取网卡的接口编号,第二个是为了获取网卡的一些标志位。如果你对这个ioctl没怎么使用过,也对内核不熟悉,那你很可能也跟我当初一样看不出任何问题,这个的问题是第一个ioctl的调用与第二个ioctl调用时使用的同一个req参数,这种连续调用且使用相同参数的就会出问题,这里并不是我们想象的第一个获取ifr_ifindex之后在此基础上又从内核中把ifr_flags读出来赋值给req,如果你去看ioctl的内核源码时就会发现,每一次ioctl调用,内核都会把整个req参数重新覆盖,所以当你第一个ioctl调用完成之后,ifr_ifindex被赋值了真实的值,其他的成员值都是空值,第二个ioctl调用时,内核又重新将内核态下的ifreq赋值到用户态,同样也只有ifr_flags是真实的值,其他的也是空值,如果你两次ioctl使用相同的参数req,那么之前已获得的ifr_ifindex就会被第二次调用覆盖清空,导致后续使用ifr_ifindex这个值时是一个错误的值,导致收发包异常。这个问题很隐晦,新手也很容易犯错误。为了解决这个问题,你如果两次操作ioctl针对的是不同的成员变量,则使用独立的参数,不要混用。我之所以说是不同的成员变量是因为针对以下的两次ioctl的连续调用是不存在上述问题的

struct ifreq req;
memset(&req, 0, sizeof(req));
strncpy(req.ifr_name, s_ifName.c_str(), s_ifName.size());
if (ioctl(s_sfd, SIOCGIFFLAGS, &req) < 0)
{
    std::cout << "get IFFLAGS failed" << std::endl;
    return;
}
if (enable) {
    req.ifr_flags |= IFF_PROMISC;
}else {
    req.ifr_flags &= ~IFF_PROMISC;
}

if (ioctl(s_sfd, SIOCSIFFLAGS, &req) < 0)
{
    std::cout << "set ifflags error" << std::endl;
}
           

这是因为两次ioctl操作都是针对ifr_flags这个成员变量的,即使其他成员变量被赋值为空也不影响针对本成员变量的操作。

Q3:接收那些目的MAC为非本机的报文时是否需要开启混杂模式,如果开启的话是全局开启还是其他的方式开启,哪种更可靠?推荐做法是什么?

假如你需要接收目的mac为非本机的mac时,你的程序到底要不要开启混杂模式呢?这里有两种情况,如果你要接收的报文的目的mac不是你本机mac,且不会频繁变化,只存在一个固定的mac值,那么可以不用开启混杂模式,如果你要接收的报文的目的mac不固定,随时有可能动态变化,那么你就必须要将接收的网卡开启混杂模式,只有开启了混杂模式,AF_PACKET模块采用正常接收到报文。下面来介绍一下这两种情况的做法

  • 不开启混杂模式,使用setsockopt函数去指定接收目的mac,代码如下:
struct packet_mreq mreq = {0};
int action;

mreq.mr_ifindex = ifnameToIndex();
mreq.mr_type = PACKET_MR_UNICAST;
mreq.mr_alen = ETHER_ALEN;
memset(mreq.mr_address, 0x0a, ETHER_ALEN); 

if (mreq.mr_ifindex <= 0)
{
    std::cout << "unable to get ifindex" << std::endl;
    return;
}

if (setsockopt(s_sfd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) != 0) {
    std::cout << "setsockopt failed" << std::endl;
}
           

上面的代码就是针对指定目的mac 0a:0a:0a:0a:0a:0a 进行接收的,setsockopt可以针对向要接收的类型与目的mac在数据链路层进行设置。以此过滤想要接收的报文,如果不设置,则只能接收目的mac属于自己或者广播报文。

当前这个mr_type开始可以设置为组播,如果是组播的话,地址就填期望的组播地址

  • 开启混杂模式
  1. 全局混杂模式:开启混杂模式的代码位于上面源程序文件的RawPktProcessor::setPromiscMode函数中,这个函数里还有一个宏GLOBAL_PROMISC,表示全局混杂的意思,如果把这个宏打开,这设置为全局混杂模式,上面是全局混杂模式呢,就一个网卡一个混杂模式的全局计数,当一个进程通过ioctl的方式全局打开混杂模式时,全局混杂计数器就加一,第一个进程去打开混杂模式时,全局混杂计数器就从0变为1,这时混杂模式就打开了,当第二个进程再次打开混杂模式时,全局计数器再加一,如果有进程设置网卡退出全局模式,那么那个网卡的全局混杂计数器就减一,如果全局混杂计数器的值减到0,内核就将混杂模式关闭。从上面的描述可以知道,如果一个进程打开了混杂模式,但由于意外退出导致没有正常调用关闭混杂模式,就会导致对应网卡的全局混杂计数器的值始终大于1,导致无法关闭混杂模式。所以一般不推荐通过ioctl的方式去设置全局混杂模式。
  2. 局部混杂模式:通过调用setsockopt函数去设置的混杂模式则称为局部混杂,为什么被称为局部混杂,是因为当一个进程启动时打开了局部混杂模式,而这个进程意外退出时,系统会自动关掉局部混杂,这就不需要人为去保证。进程创建时打开,进程退出时自动关闭局部混杂,就不会因异常退出而导致网卡一致保持混杂模式。tcpdump程序也是采用这种局部混杂的方式。所以推荐局部混杂

Q4:网上针对raw packet的接收,有些例子使用了bind函数,有些例子又没使用bind函数,直接调用recvfrom,这两种方式有什么区别?我应该怎么做?

针对这个问题我做过实验,一个是不使用bind函数,而是直接使用recvfrom函数去接收,一个是使用bind函数去接收,实验证明最好是使用bind函数绑定之后再调用recv函数去接收,因为调用bind函数之后会将你的指定网卡进行绑定,绑定之后只有指定网卡接收到的包recv才会被接收到,而如果不调用bind函数,直接使用recvfrom,所有的网卡的报文都会被recvfrom接收到,且无论你的目的地址如何设置都不起任何作用。当然我不清楚最新的内核版本会不会同样有此问题,我在4.1.35上进行测试,有这个问题。因为bind函数在内核会重新调用dev_add_pack,重新注册dev以及协议相关的hookfun。基于此我还是推荐使用bind函数来提升接收效率。

Q5:当网卡驱动收到报文然后创建skb之后将skb送入协议栈的接收队列中,协议栈的接收队列包含了来至各个网卡的skb,用户空间创建一个socket之后,如果这个socket没有使用bind函数绑定指定的网卡,那么这个socket就会接收来至所有网卡的报文,如果这个socket使用了bind函数绑定指定的网卡,则这个socket就只接收指定网卡的skb,基于这一点,内核如何实现的?在哪一个位置实现了网卡的过滤?

我这里还是以我上面的例子为背景进行展开,首先来看一下创建socket的流程

函数名称 文件名 做了什么
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) net/socket.c 系统调用socket入口
sock_create net/socket.c 准备创建sock
__sock_create net/socket.c 分配sock,调用family的create函数
pf->create net/socket.c 调用family的create函数
packet_create net/packet/af_packet.c AF_PACKET socket的create入口

有了上面的调用栈之后接下来我们看一下packet_create函数的关于注册ptype的部分代码

/*
	 *	Attach a protocol block
	 */

	spin_lock_init(&po->bind_lock);
	mutex_init(&po->pg_vec_lock);
	po->prot_hook.func = packet_rcv;

	if (sock->type == SOCK_PACKET)
		po->prot_hook.func = packet_rcv_spkt;

	po->prot_hook.af_packet_priv = sk;

	if (proto) {
		po->prot_hook.type = proto;
		register_prot_hook(sk);
	}
           

上面的代码主要意思就是判断创建socket时传入的type是不是SOCK_PACKET,如果不是则使用接收函数为packet_rcv,而我们在文章开头的代码socket创建时传入的type为SOCK_RAW,所以po->prot_hook.func应该是packet_rcv,之后把协议类型赋值给prot_hook.type之后就开始向/net/core/dev.c文件发起注册了。这里值得注意的是,初始创建socket时,po->prot_hook.dev并没有赋值,而这个po->prot_hook.dev还比较重要。这是后话,我们后面再说。

接下来我们再看看register_prot_hook函数是如何注册的,这个函数基本上就是调用的函数dev_add_pack函数,所以我们直接看一下dev_add_pack函数的内容

void dev_add_pack(struct packet_type *pt)
{
	struct list_head *head = ptype_head(pt);

	spin_lock(&ptype_lock);
	list_add_rcu(&pt->list, head);
	spin_unlock(&ptype_lock);
}
EXPORT_SYMBOL(dev_add_pack);
           

通过上面的代码,我们可以大致知道注册ptype就相当于把prot_hook加入到一个hash列表中。那么到底是什么样的hash列表呢,我们需要看一下ptype_head函数的内容

static inline struct list_head *ptype_head(const struct packet_type *pt)
{
	if (pt->type == htons(ETH_P_ALL))
		return pt->dev ? &pt->dev->ptype_all : &ptype_all;
	else
		return pt->dev ? &pt->dev->ptype_specific :
				 &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
           

上面的代码比较关键,仔细看看,它的逻辑为:如果协议类型为ETH_P_ALL,且pt->dev不为空,则返回pt->dev->ptype_all,如果协议类型为ETH_P_ALL,但是pt->dev为空,则返回全局变量的ptype_all。再说得通俗一点就是,如果你注册的po->prot_hook->dev为空,协议类型为ETH_P_ALL, 那么就将你的po->prot_hook插入到全局hash列表ptype_all中,如果po->prot_hook->dev不为空,协议类型为ETH_P_ALL, 那么就将你的po->prot_hook插入到po->prot_hook->dev设备的ptype_all上

有了上面的分析,我们再来看我们不使用bind函数时,socket在创建时调用packet_create函数中并没有给po->prot_hook->dev赋值,所以默认会被注册到全局变量ptype_all中。而当应用层调用了bind函数之后,bind函数最终会调用到/net/packet/af_packet.c文件中的packet_do_bind函数,接下来我们看看这个函数的内容

static int packet_do_bind(struct sock *sk, const char *name, int ifindex,
			  __be16 proto)
{
	struct packet_sock *po = pkt_sk(sk);
	struct net_device *dev_curr;
	__be16 proto_curr;
	bool need_rehook;
	struct net_device *dev = NULL;
	int ret = 0;
	bool unlisted = false;

	if (po->fanout)
		return -EINVAL;

	lock_sock(sk);
	spin_lock(&po->bind_lock);
	rcu_read_lock();

	if (name) {
		dev = dev_get_by_name_rcu(sock_net(sk), name);
		if (!dev) {
			ret = -ENODEV;
			goto out_unlock;
		}
	} else if (ifindex) {
		dev = dev_get_by_index_rcu(sock_net(sk), ifindex);
		if (!dev) {
			ret = -ENODEV;
			goto out_unlock;
		}
	}

	if (dev)
		dev_hold(dev);

	proto_curr = po->prot_hook.type;
	dev_curr = po->prot_hook.dev;

	need_rehook = proto_curr != proto || dev_curr != dev;

	if (need_rehook) {
		if (po->running) {
			rcu_read_unlock();
			__unregister_prot_hook(sk, true);
			rcu_read_lock();
			dev_curr = po->prot_hook.dev;
			if (dev)
				unlisted = !dev_get_by_index_rcu(sock_net(sk),
								 dev->ifindex);
		}

		po->num = proto;
		po->prot_hook.type = proto;

		if (unlikely(unlisted)) {
			dev_put(dev);
			po->prot_hook.dev = NULL;
			po->ifindex = -1;
			packet_cached_dev_reset(po);
		} else {
			po->prot_hook.dev = dev;
			po->ifindex = dev ? dev->ifindex : 0;
			packet_cached_dev_assign(po, dev);
		}
	}
	if (dev_curr)
		dev_put(dev_curr);

	if (proto == 0 || !need_rehook)
		goto out_unlock;

	if (!unlisted && (!dev || (dev->flags & IFF_UP))) {
		register_prot_hook(sk);
	} else {
		sk->sk_err = ENETDOWN;
		if (!sock_flag(sk, SOCK_DEAD))
			sk->sk_error_report(sk);
	}

out_unlock:
	rcu_read_unlock();
	spin_unlock(&po->bind_lock);
	release_sock(sk);
	return ret;
}
           

这个函数就会根据用户态传递的ifindex或者网卡名词来查找到真实的网卡设备,之后将这个socket已注册的prot_hook去注册,把新的绑定信息刷新

po->prot_hook.dev = dev;
po->ifindex = dev ? dev->ifindex : 0;
packet_cached_dev_assign(po, dev);
           

然后调用注册函数register_prot_hook(sk)重新注册

接下来再回到内核协议栈入口函数__netif_receive_skb_core,该文件目录为/net/core/dev.c

list_for_each_entry_rcu(ptype, &ptype_all, list) {
	if (pt_prev)
		ret = deliver_skb(skb, pt_prev, orig_dev);
	pt_prev = ptype;
}

list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
	if (pt_prev)
		ret = deliver_skb(skb, pt_prev, orig_dev);
	pt_prev = ptype;
}
           

函数__netif_receive_skb_core每收到一个skb都会被调用一次,这里skb与socket与dev三者之间筛选的关键代码如上所示,一开始会从全局hash列表中遍历,这里的意思是如果有要接收所有协议类型的包,且不针对具体某一张网卡进行接收的socket,skb都会分发到这个socket的rx队列里去,所以当使用bind函数的socket,就是在这里将所有网卡收上来的skb都拷贝到socket的rx队列里。之后通过当前的skb所属dev上面注册的ptype_all进行遍历。意思是如果要接收所有协议类型的包,且只针对skb所属网卡进行接收的socket,skb才会分发到socket上。而使用了bind函数指定网卡就属于下面这类。

总结: 当用户空间创建一个socket后,这个socket默认是接收来至所有网卡的数据包,所以这个socket被划分到了全局组,协议栈任何一个skb都会被转发至全局组下任何一个socket的接收队列中,如果使用bind函数指定了网卡之后,这个socket就会被移出全局组进入指定网卡组,当协议栈的skb到达时,只有skb的dev与socket的dev一致,才会将这个skb分发到这个socket的rx队列,从而保证了指定网卡设备的接收。

继续阅读