您的位置:首页 > 运维架构 > Linux

【Linux 驱动】Netfilter/iptables (六) 内核协议栈编程(发送skb)

2015-12-25 10:33 477 查看
内核态基于 Netfilter 构造skb数据包

前面第四篇介绍了 Netfilter Hook 机制,我们知道了数据包在协议栈中传递时会经过不同的HOOK点,而每个HOOK点上又被Netfilter预先注册了一系列hook回调函数,每个数据包经过时都会经过这些hook函数的处理。

在hook回调函数中,你可以进行任何操作,这些操作你都可以自定义,然后以模块的形式加载进去。

这里我们给出一个实例:每收到5个TCP报文就向指定的IP地址发送一个UDP报文,这个功能的开发涉及到内核协议栈编程,而不是我们之前学的用户空间的 Socket 网络编程。

看代码实现:

//linux kernel 3.13.0-43
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>//for ip header
#include <linux/types.h>
#include <linux/skbuff.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/tcp.h>
#include <net/udp.h>
#include <net/icmp.h>
#include <linux/inet.h>

MODULE_LICENSE("Dual BSD/GPL");

#define ETH "wlan0"//用的本机无线网卡设备
#define SIP "192.168.1.27"//随意
#define DIP "118.6.24.132"//
#define SPORT 39804
#define DPORT 6980

#define NIPQUAD(addr)\
((unsigned char *)&addr)[0],\
((unsigned char *)&addr)[1],\
((unsigned char *)&addr)[2],\
((unsigned char *)&addr)[3]

unsigned char SMAC[ETH_ALEN] =
{0x1c, 0x4b, 0xd6, 0x7a, 0x55, 0x96};//本机无线网卡硬件地址
unsigned char DMAC[ETH_ALEN] =
{0xe0, 0xcb, 0x4e, 0xb0, 0xed, 0xd8};//本机网卡硬件地址

static struct nf_hook_ops nfho;

//构建一个udp报文并发送
static int build_and_xmit_udp(char *eth, u_char *smac, u_char *dmac,
u_char *pkt, int pkt_len, u_long sip, u_long dip,
u_short sport, u_short dport)
{
struct sk_buff *skb = NULL;
struct net_device *dev = NULL;
struct udphdr *udph = NULL;
struct iphdr *iph = NULL;
struct ethhdr *ethdr = NULL;
u_char *pdata = NULL;
int nret = 1;

if(NULL == smac || NULL == dmac)
goto out;

//根据设备名获得设备指针
//这里调用的函数高版本做了修改,多了个参数struct net*
if(NULL == (dev = dev_get_by_name(&init_net, eth)))
goto out;

//创建一个skb
skb = alloc_skb(pkt_len + sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof(struct ethhdr), GFP_ATOMIC);
if(NULL == skb)
goto out;

//为skb预留空间,方便后面skb_buff协议封装
skb_reserve(skb, pkt_len + sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof(struct ethhdr));

//skb字节填充
skb->dev = dev;
skb->pkt_type = PACKET_OTHERHOST;
skb->protocol = __constant_htons(ETH_P_IP);
skb->ip_summed = CHECKSUM_NONE;
skb->priority = 0;

//数据包封装
//分别压入应用层,传输层,网络层,链路层栈帧
//skb_push由后面往前面,与skb_put不同
pdata = skb_push(skb, pkt_len);
udph = (struct udphdr*)skb_push(skb, sizeof(struct udphdr));
iph = (struct iphdr*)skb_push(skb, sizeof(struct iphdr));
ethdr = (struct ethhdr*)skb_push(skb, sizeof(struct ethhdr));

//应用层数据填充
memcpy(pdata, pkt, pkt_len);

//传输层udp数据填充
memset(udph, 0, sizeof(struct udphdr));
udph->source = sport;
udph->dest = dport;
udph->len = htons(sizeof(struct udphdr) + pkt_len);//主机字节序转网络字节序
udph->check = 0;//skb_checksum之前必须置0.协议规定

//网络层数据填充
iph->version = 4;
iph->ihl = sizeof(struct iphdr) >> 2;
iph->frag_off = 0;
iph->protocol = IPPROTO_UDP;
iph->tos = 0;
iph->daddr = dip;
iph->saddr = sip;
iph->ttl = 0x40;
iph->tot_len = __constant_htons(skb->len);
iph->check = 0;
iph->check = ip_fast_csum((unsigned char*)iph, iph->ihl);//计算校验和

skb->csum = skb_checksum(skb, iph->ihl*4, skb->len - iph->ihl*4, 0);//skb校验和计算
udph->check = csum_tcpudp_magic(sip, dip, skb->len - iph->ihl*4, IPPROTO_UDP, skb->csum);//dup和tcp伪首部校验和

//链路层数据填充
memcpy(ethdr->h_dest, dmac, ETH_ALEN);
memcpy(ethdr->h_source, smac, ETH_ALEN);
ethdr->h_proto = __constant_htons(ETH_P_IP);

//调用内核协议栈函数,发送数据包
if(dev_queue_xmit(skb) < 0)
{
printk("dev_queue_xmit error\n");
goto out;
}
nret = 0;//这里是必须的
printk("dev_queue_xmit correct\n");
//出错处理
out:
/*下面的0!=nret是必须的,前面即使不执行goto out,下面的语句程序也会执行,
如果不加0!=nret语句,那么前面dev_queue_xmit返回之后(已经kfree_skb一次了),
再进入下面的语句第二次执行kfree_skb,就会导致系统死机*/
//关键在于知道dev_queue_xmit内部调用成功后,会kfree_skb,以及goto语句的作用

if(0 != nret && NULL != skb)//这里前面的nret判断是必须的,不然必定死机
{
dev_put(dev);//减少设备的引用计数
kfree_skb(skb);//销毁数据包
}

return nret;//F_ACCEPT;
}

atomic_t pktcnt = ATOMIC_INIT(0);//定义并初始化
//钩子函数,注意参数格式与开发环境源码树保持一致
static unsigned int hook_func(const struct nf_hook_ops *ops,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct iphdr *iph = ip_hdr(skb);
int ret = NF_ACCEPT;
unsigned char *pdata = "hello kernel";

printk("hook function processing\n");

if(iph->protocol == IPPROTO_TCP)
{
atomic_inc(&pktcnt);
if(atomic_read(&pktcnt) % 5 == 0)
{
printk(KERN_INFO "Sending the %d udp packet\n", atomic_read(&pktcnt)/5);
ret = build_and_xmit_udp(ETH, SMAC, DMAC, pdata, strlen(pdata), in_aton(SIP), in_aton(DIP), htons(SPORT), htons(DPORT));
}
}
return ret;
}

static int __init hook_init(void)
{
nfho.hook = hook_func;//关联对应处理函数
nfho.hooknum = NF_INET_LOCAL_OUT;//ipv4的本地出口处
nfho.pf = PF_INET;//ipv4,所以用这个
nfho.priority = NF_IP_PRI_FIRST;//优先级,第一顺位

nf_register_hook(&nfho);//注册

return 0;
}

static void __exit hook_exit(void)
{
nf_unregister_hook(&nfho);//注销
}

module_init(hook_init);
module_exit(hook_exit);


注释已经大致说明了。需要注意的是你所写的内核程序接口以及数据结构等等必须和你的开发源码树保持一直,3.13版本对比2.6版本有较大改动,这里主要体现在sk_buff上。

这里是在hook函数中创建一个skb,然后手动填充各个协议栈的数据,最后调用内核协议栈函数dev_queue_xmit函数发送,这个函数是物理层的,一个驱动函数,所以我们构造的skb数据包需要填充应用层,传输层,网络层,以及链路层的数据。

这段程序一开始时,只要运行(加载模块),就会立马死机,为此不知道强制关机重启了多少遍,最后经调试以及查阅资料,发现是kfree_skb这个环节出错,后来注释掉这行代码,程序运行无误,测试也没问题,但是总有bug,就是创建的数据包最终没有销毁。

后来查阅得知,因为存在两次kfree_skb,在dev_queue_xmit调用成功后,已经kfree_skb一次了,返回后,后面的out:语句还会继续执行,不加 0!=nret 判断语句的话,还会再次调用 kree_skb ,导致死机。

这跟goto语句用的少(几乎不用),不清楚goto语句的作用,现在应该这么理解 goto out; 是该语句到 out: 之间的语句跳过,其余依旧怎样怎样…

测试:make(本系列代码的make文件是一样的),insmod wqlkp.ko

这里我们用 wireshark抓包(root权限)

1. 打开一个视频网站,然后dmesg

[ 3523.489826] Sending the 237 udp packet
[ 3523.489850] dev_queue_xmit correct
[ 3523.489922] hook function processing
[ 3523.602361] hook function processing
[ 3523.602532] hook function processing
[ 3523.606425] hook function processing
[ 3523.688857] hook function processing
[ 3523.688864] Sending the 238 udp packet
[ 3523.688899] dev_queue_xmit correct
[ 3523.691320] hook function processing


利用wireshark抓包



可以看到,我们发送了一个数据内容为“hello kernel”的UDP报文。

关于协议栈编程以及上面的skb封装,我们后面有时间再跟上。

本门还算一枚菜鸟,纰漏和不对之处还望指正。

参考资料:

sk_buff封装和解封装网络数据包的过程详解
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息