记得在大学时研究 Linux Kernel 下的网络编程时,写的一个小程序,用于理解 UDP 包的构建和发送原理。这篇文章的内容是我在 ChinaUnix 论坛上面发的一个帖子。由于现在经常在用 CSDN 博客,所以将该帖子的内容转过来 CSDN 博客中。
介绍
这篇文章主要的工作是:在一台计算机 C1(AMD Athlon 64*2 Dual Core Processor 4600+, Ubuntu10.04 desktop)通过加载模块 sendUDPWithKernelModule.ko
发送一个 udp packet(data是”hello world”)。另一台计算机 C2(Intel Atom N450, Ubuntu10.04 netbook)通过加载模块 packetCaptureWithNetfilter.ko
接收 C1 发过来的 packet,并打印出 ip header, udp header 和 data 的内容(两台计算机的内核都是: 2.6.32.33)。
发送模块
sendUDPWithKernelModule.c
/***********************************************************************
* File: sendUDPWithkernelModule.c
* Abstract Description:
* To send a UDP packet to another in kernel space.
*
*------------------------Revision History------------------------
* No. Date Revised By Description
* 1 2011/7/28 Sam make the program work on my machines
* IP address: [10.14.1.122] -> [10.14.1.21]
* MAC address:
* 00:e0:4d:8b:3c:d7 -> 20:cf:30:57:1a:18
* 2 2011/8/7 Sam To correct the checksum.(fail)
* 3 2011/8/9 Sam To correct the checksum.(success)
***********************************************************************/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/workqueue.h>
#include <linux/in.h>
#include <linux/inet.h>
#include <linux/socket.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <net/sock.h>
#define IF_NAME "eth0"
#define DIP "10.14.1.20"
#define SIP "10.14.1.122"
#define SPORT 319
#define DPORT 319
#define SRC_MAC {0x00, 0xe0, 0x4d, 0x8b, 0x3c, 0xd7}
#define DST_MAC {0x20, 0xcf, 0x30, 0x57, 0x1a, 0x18}
struct socket *sock;
static void sock_init()
{
/* "struct ifreq" is defined in linux/if.h.
* Interface request structure used for socket
* ioctl's. All interface ioctl's must have parameter
* definitions which begin with ifr_name. The
* remainder may be interface specific. */
struct ifreq ifr;
/* sock_create_kern():创建socket结构.
* SIOCSIFNAME is defined in include/linux/sockios.h.
* It is used to set interface name. */
sock_create_kern(PF_INET, SOCK_DGRAM, 0, &sock);
// copy the interface name to the ifrn_name.
strcpy(ifr.ifr_ifrn.ifrn_name, IF_NAME);
kernel_sock_ioctl(sock, SIOCSIFNAME, (unsigned long) &ifr);
}
static void send_by_skb()
{
struct net_device *netdev = NULL;
struct net *net = NULL;
struct sk_buff *skb = NULL;
struct ethhdr *eth_header = NULL;
struct iphdr *ip_header = NULL;
struct udphdr *udp_header = NULL;
__be32 dip = in_aton(DIP);
__be32 sip = in_aton(SIP);
u8 buf[16] = {"hello world"};
u16 data_len = sizeof(buf);
//u16 expand_len = 16; /* for skb align */
u8 *pdata = NULL;
u32 skb_len;
u8 dst_mac[ETH_ALEN] = DST_MAC; /* dst MAC */
u8 src_mac[ETH_ALEN] = SRC_MAC; /* src MAC */
/* construct skb
* sock_net() is defined in include/net/sock.h
* dev_get_by_name()函数用来取得设备指针,使用该函数
* 后一定要使用dev_put()函数取消设备引用. */
sock_init();
net = sock_net((const struct sock *) sock->sk);
netdev = dev_get_by_name(net, IF_NAME);
/* LL_RESERVED_SPACE is defined in include/netdevice.h. */
/*skb_len = LL_RESERVED_SPACE(netdev) + sizeof(struct iphdr)
+ sizeof(struct udphdr) + data_len + expand_len;*/
skb_len = data_len + sizeof(struct iphdr)
+ sizeof(struct udphdr) +LL_RESERVED_SPACE(netdev);
printk("iphdr: %d\n", sizeof(struct iphdr));
printk("udphdr: %d\n", sizeof(struct udphdr));
printk("data_len: %d\n", data_len);
printk("skb_len: %d\n", skb_len);
/* dev_alloc_skb是一个缓冲区分配函数,主要被设备驱动使用.
* 这是一个alloc_skb的包装函数, 它会在请求分配的大小上增加
* 16 Bytes的空间以优化缓冲区的读写效率.*/
skb = dev_alloc_skb(skb_len);
//skb = alloc_skb(skb_len, GFP_ATOMIC);
if (!skb) {
return;
}
/* fill the skb.具体参照struct sk_buff.
* skb_reserve()用来为协议头预留空间.
* PACKET_OTHERHOST: packet type is "to someone else".
* ETH_P_IP: Internet Protocol packet.
* CHECKSUM_NONE表示完全由软件来执行校验和. */
skb_reserve(skb, LL_RESERVED_SPACE(netdev));
skb->dev = netdev;
skb->pkt_type = PACKET_OTHERHOST;
skb->protocol = htons(ETH_P_IP);
skb->ip_summed = CHECKSUM_NONE;
skb->priority = 0;
/* 分配内存给ip头 */
skb_set_network_header(skb, 0);
skb_put(skb, sizeof(struct iphdr));
/* 分配内存给udp头 */
skb_set_transport_header(skb, sizeof(struct iphdr));
skb_put(skb, sizeof(struct udphdr));
/* construct udp header in skb */
udp_header = udp_hdr(skb);
udp_header->source = htons(SPORT);
udp_header->dest = htons(DPORT);
udp_header->check = 0;
/* construct ip header in skb */
ip_header = ip_hdr(skb);
ip_header->version = 4;
ip_header->ihl = sizeof(struct iphdr) >> 2;
ip_header->frag_off = 0;
ip_header->protocol = IPPROTO_UDP;
ip_header->tos = 0;
ip_header->daddr = dip;
ip_header->saddr = sip;
ip_header->ttl = 0x40;
ip_header->tot_len = htons(skb->len);
ip_header->check = 0;
/* caculate checksum */
skb->csum = skb_checksum(skb, ip_header->ihl*4,
skb->len-ip_header->ihl*4, 0);
ip_header->check = ip_fast_csum(ip_header, ip_header->ihl);
udp_header->check
= csum_tcpudp_magic(sip, dip, skb->len-ip_header->ihl*4,
IPPROTO_UDP, skb->csum);
/* insert data in skb */
pdata = skb_put(skb, data_len);
if (pdata) {
memcpy(pdata, buf, data_len);
}
printk("payload:%20s\n", pdata);
/* construct ethernet header in skb */
eth_header = (struct ehthdr *)skb_push(skb, 14);
memcpy(eth_header->h_dest, dst_mac, ETH_ALEN);
memcpy(eth_header->h_source, src_mac, ETH_ALEN);
eth_header->h_proto = htons(ETH_P_IP);
/* send packet */
if (dev_queue_xmit(skb) < 0) {
dev_put(netdev);
kfree_skb(skb);
printk("send packet by skb failed.\n");
return;
}
printk("send packet by skb success.\n");
}
static int __init sendUDP_init(void)
{
/* send_by_sock(); */
send_by_skb();
printk("testmod kernel module load!\n");
return 0;
}
static void __exit sendUDP_exit(void)
{
sock_release(sock);
printk("testmod kernel module removed!\n");
}
module_init(sendUDP_init);
module_exit(sendUDP_exit);
MODULE_LICENSE("GPL");
遇到的问题
写这个程序一定要搞清楚 sk_buff
结构中几个重要的指针 data
、tail
、head
和 end
。还有几个函数(《Understanding Linux Network Internals》第二章这本书讲的很详细):skb_put
、skb_reserver
、skb_set_network_header
、skb_set_transport_header
和 dev_alloc_skb
。
此程序开始遇到的问题是 checksum 部分,一开始忘了写 ip_fast_csum
这一步,导致用 tcpdump 抓包时出现 “Bad checksum 0”。后来加上就好了。
输出
这是在加载模块后,在电脑 C1 里面查看 /var/log/message
得到的结果(注意 “hello world” 的那一行):
Aug 10 20:25:05 sam-desktop kernel: [21389.116002] iphdr: 20
Aug 10 20:25:05 sam-desktop kernel: [21389.116008] udphdr: 8
Aug 10 20:25:05 sam-desktop kernel: [21389.116012] data_len: 16
Aug 10 20:25:05 sam-desktop kernel: [21389.116016] skb_len: 60
Aug 10 20:25:05 sam-desktop kernel: [21389.116022] hello world
Aug 10 20:25:05 sam-desktop kernel: [21389.116043] send packet by skb success.
Aug 10 20:25:05 sam-desktop kernel: [21389.116047] testmod kernel module load!
Aug 10 20:25:06 sam-desktop kernel: [21390.550944] testmod kernel module removed!
接收模块
packetCaptureWithNetfilter.c
/***********************************************************************
* File: packetCaptureWithNetfilter.c
* Abstract Description:
* To catch the packet from the network with netfilter.
*
*------------------------Revision History------------------------
* No. Date Revised By Description
* 1 2011/7/28 Sam +print the payload.(unsuccessful)
* 2 2011/7/30 Sam +get the packet from the specific ip.
* 3 2011/8/10 Sam correct the checksum and
* +print the payload.
***********************************************************************/
#ifndef __KERNEL__
#define __KERNEL__
#endif
#ifndef MODULE
#define MODULE
#endif
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/skbuff.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/netdevice.h>
#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/tcp.h>
#include <net/udp.h>
#include <linux/netfilter_ipv4.h>
static struct nf_hook_ops nfho;
unsigned int hook_func(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
if(skb){
struct sk_buff *sb = skb;
struct tcphdr *tcph = NULL;
struct udphdr *udph = NULL;
struct iphdr *iph = NULL;
u8 *payload; // The pointer for the tcp payload.
char sourceAddr[20];
char myAddr[20];
iph = ip_hdr(sb);
if(iph){
/* NIPQUAD() was defined in the linux/kernel.h.
* Display an IP address in readable format.*/
/* These two sprintf are used to get the packet
* from the specific ip.*/
sprintf(myAddr, "10.14.1.122");
sprintf(sourceAddr, "%u.%u.%u.%u",
NIPQUAD(iph->saddr));
if(!(strcmp(sourceAddr, myAddr))){
printk("IP:[%u.%u.%u.%u]-->[%u.%u.%u.%u];\n",
NIPQUAD(iph->saddr), NIPQUAD(iph->daddr));
printk("IP (version %u, ihl %u, tos 0x%x, ttl %u, id %u, length %u, ",
iph->version, iph->ihl, iph->tos, iph->ttl,
ntohs(iph->id), ntohs(iph->tot_len));
/* 此处读取udp或tcp报头时, 不能用udp_hdr或tcp_hdr.
* 因为对于skbuff结构中的指向各层协议头部的指针, 只
* 有当到达该层时才对他们赋值.而netfilter处于网络层.
* 所以不能直接访问skbuff中的传输层协议头指针,而必
* 须用skb->data+iph->ihl*4来得到指向传输层头部的指
* 针。 */
switch(iph->protocol){
case IPPROTO_UDP:
/*get the udp information*/
udph = (struct udphdr *)(sb->data + iph->ihl*4);
printk("UDP: [%u]-->[%u];\n",
ntohs(udph->source),
ntohs(udph->dest));
payload = udph + ntohs(udph->len);
/* 此处不能用"printk("payload: %20s\n", payload);"
* 否则会出现乱码并且"hello world"打印不出来.
* 不过用下面的方法打印出来的是"hello world"
* 前面有一些乱码. */
char i;
for(i=0;i<20;i++){
printk("%c", payload[i]);
}
break;
case IPPROTO_TCP:
/*get the tcp header*/
tcph = sb->data + iph->ihl*4;
//payload = (char *)((__u32 *)tcph+tcph->doff);
printk("TCP: [%u]-->[%u];\n",
ntohs(tcph->source),
ntohs(tcph->dest));
break;
default:
printk("unkown protocol!\n");
break;
}
}
}
else
{
printk("iph is null\n");
}
}
else
{
printk("skb is null,hooknum:%d\n", hooknum);
}
return NF_ACCEPT;
}
int init_module()
{
nfho.hook = hook_func;
nfho.hooknum = NF_INET_PRE_ROUTING;
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST;
nf_register_hook(&nfho);
printk("init module----------------ok\n");
return 0;
}
void cleanup_module()
{
nf_unregister_hook(&nfho);
printk("exit module----------------ok\n");
}
MODULE_LICENSE("GPL");
遇到的问题
读取 UDP port 时,要用
udph = (struct udphdr *)sb->data + iph->ihl*4
来获取 UDP header,不能用udph = udp_hdr(sb)
。因为对于skbuff
结构中的指向各层协议头部的指针,只有当到达该层时才对他们赋值。而 netfilter 处于网络层,所以不能直接访问skbuff
中的传输层协议头指针,而必须用skb->data+iph->ihl*4
来得到指向传输层头部的指针。下面输出结果中,printk data 时出现乱码。原因是:
ntohs(udph->len)
这一步没有获得 UDP header 的长度(发送端没有给udp_header->len
赋值!)。将payload = (char *)udph + ntohs(udph->len);
改为payload = (char *)udph + (char)sizeof(struct udphdr);
即可。
输出
sam@sam-laptop:~$ cat /var/log/messages | grep 'Aug 10 20:25'
Aug 10 20:24:27 sam-laptop kernel: [16363.962297] init module----------------ok
Aug 10 20:24:45 sam-laptop kernel: [16382.101649] IP:[10.14.1.122]-->[224.0.0.251];
Aug 10 20:24:45 sam-laptop kernel: [16382.101662] IP (version 4, ihl 6, tos 0xc0, ttl 1, id 0, length 32, unkown protocol!
Aug 10 20:24:55 sam-laptop kernel: [16392.189151] device eth0 entered promiscuous mode
Aug 10 20:25:08 sam-laptop kernel: [16404.803001] IP:[10.14.1.122]-->[10.14.1.20];
Aug 10 20:25:08 sam-laptop kernel: [16404.803013] IP (version 4, ihl 5, tos 0x0, ttl 64, id 212, length 28, UDP: [319]-->[319];
Aug 10 20:25:08 sam-laptop kernel: [16404.803025] ????hello world
Aug 10 20:25:13 sam-laptop kernel: [16409.792133] device eth0 left promiscuous mode
Aug 10 20:25:18 sam-laptop kernel: [16414.416250] exit module----------------ok
sam@sam-laptop:~$ sudo tcpdump -w result -v host 10.14.1.122
[sudo] password for sam:
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes
^C1 packets captured
1 packets received by filter
0 packets dropped by kernel
sam@sam-laptop:~$ vim result
??2?^B^@^D^@^@^@^@^@^@^@^@^@`^@^@^@^A^@^@^@¤xBN??^G^@<^@^@^@<^@^@^@ ?0W^Z^X^@àM<8b><×^H^@E^@^@^\^@?^@^@@^QcT
^N^Az
^N^A^T^A?^A?^@^@??hello world^@^@^@^@^@^@^@
Makefile 文件
加上下面的 Makefile,直接 make 一下就可以使用了。
Makefile:
obj-m := sendUDPWithKernelModule.o/packetCaptureWithNetfilter.o
all:
make -C /lib/modules/$(shell uname -r)/build \
M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build \
M=$(PWD) clean
参考文献
- http://www.cnblogs.com/piky/articles/1587767.html
- Hacking the Linux Kernel Network Stack(译本)
- 教你修改以及重构 skb
- Linux内核发送构造数据包的方式
- 《Understanding Linux Network Internals》
- 《Linux Device Driver 3rd》
- 《Professional Linux Kernel Architecture》