Linnux5.0.0下,基于Netlink
与NetFilter
对本机数据包进行筛选监控
需求:
开发一个Linux lkm
+ app program
,由app program
提供需要监控的源IP
地址,内核模块根据此IP
地址监控本机发送处与该源IP
地址相同的所有的packet
的5元组,源地址、目标地址、原端口、目标端口、协议,并将相关的信息传给应用程序,应用程序将该信息保存在文件中。
程序逻辑:
通信由用户程序发起,用户程序在开始时发送给内核模块一个源IP
地址,之后用户程序将进入监听状态,内核模块将该IP
地址以及用户程序的pid
存下来作为目标IP
地址和目标用户程序。之后用Netfilter
中钩子函数判断每一个从本机发出的数据包中的源IP
是否与目标IP
地址相同,如果相同则钩子程序将数据包中的路由信息保存下来,通过Netlink
发送给用户程序。用户程序接收到路由信息后,存在操作系统文件中。
开发/运行环境:
内核版本:Linux5.0.-37
发行版本:Ubuntu 18.04.1
运行日志
常用命令:
#查看系统日志
cat /var/log/kern.log
#打印系统日志到控制台
tail -f /var/log/kern.log &
#查看内核版本
cat /proc/version
#安装/卸载模块
insmod [mod]
rmmod [mod]
必备知识:
Linux
内核模块编程Netfilter
子系统与hook
函数编程struct sk_buff
,struct iphdr
,struct tcphdr
,struct udphdr
等网络相关结构体使用Netlink
通讯机制
踩坑集锦:
高内核版本Netfilter
hook
函数注册:
在Linux4.13
之前,注册钩子使用的函数为:
nf_register_hook(reg);
高于Linux4.13
版本后,注册钩子使用的函数改变成了:
nf_register_net_hook(&init_net, reg);
若希望兼容Linux4.13
之前和之后的版本,可以这样写:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4,13,0)
nf_register_net_hook(&init_net, reg)
#else
nf_register_hook(reg)
#endif
高内核版本hook函数原型声明:
早期linux
内核中,Netfilter
的hook
函数原型为:
static unsigned int sample( unsigned int hooknum,
struct sk_buff * skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn) (struct sk_buff *));
但在高版本linux
内核(至少4.10以上已改变),Netfilter
的hook
函数原型变成了:
int sample_nf_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state);
高内核版本创建Netlink
处理函数:
在较低版本linux
内核(Linux2.6)中,创建Netlink
处理函数使用:
//假设nl_data_ready为处理函数
nl_sk = netlink_kernel_create(&init_net,
NETLINK_TEST,
1,
nl_data_ready,
NULL,
HIS_MODULE);
高版本linux
内核(至少3.8.13以上已经改变)中,创建netlink
处理函数使用:
struct netlink_kernel_cfg cfg = {
.input = nl_data_ready,//该函数原型可参考内核代码,其他参数默认即可,可参考内核中的调用
};
nl_sk = netlink_kernel_create(&init_net,
NETLINK_TEST,
&cfg);
消息发送后,skb
释放问题:
当执行完netlink_unicast
函数后skb
不需要内核模块去释放,也不能去释放,否则会导致崩溃。因为netlink_unicast
函数的返回不能保证用户层已经接受到消息,如果此时内核模块释放skb
,会导致用户程序接收到一个已经释放掉的消息,当内核尝试处理此消息时会导致崩溃。内核会处理skb
的释放,所以不会出现内存泄露问题, 这里给出了详细解释。
消息封装:
在封装发送到kernel
的消息时,我们需要依次对struct nlmsghdr
,struct iovec
,struct msghdr
进行封装。
内核模块和用户程序之间通讯与正常的使用socket
类似,还需要封装源地址和目的地址,但需要注意此处的地址本质上是进程pid
,而不是IP
地址。
程序代码:
getRoutingInfo.c:
//内核编程需要的头文件
#include <linux/module.h>
#include <linux/kernel.h>
//Netfilter需要的头文件
#include <linux/net.h>
#include <linux/time.h>
#include <linux/init.h>
#include <linux/skbuff.h>
#include <linux/if_vlan.h>
#include <linux/if_ether.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <net/ip.h>
#include <net/tcp.h>
#include <net/icmp.h>
#include <net/protocol.h>
//netlink需要的头文件
#include <net/sock.h>
#include <net/net_namespace.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/netlink.h>
//NIPQUAD宏便于把数字IP地址输出
#define NIPQUAD(addr) \
((unsigned char *)&addr)[0], \
((unsigned char *)&addr)[1], \
((unsigned char *)&addr)[2], \
((unsigned char *)&addr)[3]
#define NETLINK_TEST 17 //用于自定义协议
#define MAX_PAYLOAD 1024 //最大载荷容量
#define ROUTING_INFO_LEN 100 //单个路由信息的容量
//函数声明
unsigned int kern_inet_addr(char *ip_str);
void kern_inet_ntoa(char *ip_str , unsigned int ip_num);
unsigned int getRoutingInfo(void *priv, struct sk_buff *skb, const struct nf_hook_state *state);
static void nl_data_ready(struct sk_buff *skb);
int netlink_to_user(char *msg, int len);
//用于描述钩子函数信息
static struct nf_hook_ops nfho = {
.hook = getRoutingInfo,
.pf = PF_INET,
.hooknum =NF_INET_LOCAL_OUT ,
.priority = NF_IP_PRI_FIRST,
};
//用于描述Netlink处理函数信息
struct netlink_kernel_cfg cfg = {
.input = nl_data_ready,
};
static struct sock *nl_sk = NULL; //用于标记netlink
static int userpid = -1; //用于存储用户程序的pid
static unsigned int filterip = 0; //用于存储需要过滤的源IP,小端格式
unsigned int getRoutingInfo(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state){
struct iphdr *iph=ip_hdr(skb); //指向struct iphdr结构体
struct tcphdr *tcph; //指向struct tcphdr结构体
struct udphdr *udph; //指向struct udphdr结构体
int header=0;
char routingInfo[ROUTING_INFO_LEN] = {0};//用于存储路由信息
if(ntohl(iph->saddr) == filterip){
printk("=======equal========");
printk("srcIP: %u.%u.%u.%u\n", NIPQUAD(iph->saddr));
printk("dstIP: %u.%u.%u.%u\n", NIPQUAD(iph->daddr));
if(likely(iph->protocol==IPPROTO_TCP)){
tcph=tcp_hdr(skb);
if(skb->len-header>0){
printk("srcPORT:%d\n", ntohs(tcph->source));
printk("dstPORT:%d\n", ntohs(tcph->dest));
printk("PROTOCOL:TCP");
sprintf(routingInfo,
"srcIP:%u.%u.%u.%u dstIP:%u.%u.%u.%u srcPORT:%d dstPORT:%d PROTOCOL:%s",
NIPQUAD(iph->saddr),
NIPQUAD(iph->daddr),
ntohs(tcph->source),
ntohs(tcph->dest),
"TCP");
netlink_to_user(routingInfo, ROUTING_INFO_LEN);
}//判断skb是否有数据 结束
}else if(likely(iph->protocol==IPPROTO_UDP)){
udph=udp_hdr(skb);
if(skb->len-header>0){
printk("srcPORT:%d\n", ntohs(udph->source));
printk("dstPORT:%d\n", ntohs(udph->dest));
printk("PROTOCOL:UDP");
sprintf(routingInfo,
"srcIP:%u.%u.%u.%u dstIP:%u.%u.%u.%u srcPORT:%d dstPORT:%d PROTOCOL:%s",
NIPQUAD(iph->saddr),
NIPQUAD(iph->daddr),
ntohs(udph->source),
ntohs(udph->dest),
"UDP");
netlink_to_user(routingInfo, ROUTING_INFO_LEN);
}//判断skb是否有数据 结束
}//判断传输层协议分支 结束
printk("=====equalEnd=======");
}//判断数据包源IP是否等于过滤IP 结束
return NF_ACCEPT;
}
//用于给用户程序发送信息
int netlink_to_user(char *msg, int len){
struct sk_buff *skb;
struct nlmsghdr *nlh;
skb = nlmsg_new(MAX_PAYLOAD, GFP_ATOMIC);
if(!skb){
printk(KERN_ERR"Failed to alloc skb\n");
return 0;
}
nlh = nlmsg_put(skb, 0, 0, 0, MAX_PAYLOAD, 0);
printk("sk is kernel %s\n", ((int *)(nl_sk+1))[3] & 0x1 ? "TRUE" : "FALSE");
printk("Kernel sending routing infomation to client %d.\n", userpid);
//发送信息
memcpy(NLMSG_DATA(nlh), msg, len);
if(netlink_unicast(nl_sk, skb, userpid, 1) < 0){ //此处设置为非阻塞,防止缓冲区已满导致内核停止工作
printk(KERN_ERR"Failed to unicast skb\n");
userpid = -1;
filterip = 0;
return 0;
}
return 1;
}
//当有netlink接收到信息时,此函数将进行处理
static void nl_data_ready(struct sk_buff *skb){
struct nlmsghdr *nlh = NULL;
if(skb == NULL){
printk("skb is NULL\n");
return;
}
nlh = (struct nlmsghdr *)skb->data;
printk("kernel received message from %d: %s\n", nlh->nlmsg_pid, (char *)NLMSG_DATA(nlh));
filterip=kern_inet_addr((char *)NLMSG_DATA(nlh));
userpid=nlh->nlmsg_pid;
}
//用于将字符串IP地址转化为小端格式的数字IP地址
unsigned int kern_inet_addr(char *ip_str){
unsigned int val = 0, part = 0;
int i = 0;
char c;
for(i=0; i<4; ++i){
part = 0;
while ((c=*ip_str++)!='\0' && c != '.'){
if(c < '0' || c > '9') return -1;//字符串存在非数字
part = part*10 + (c-'0');
}
if(part>255) return -1;//单部分超过255
val = ((val << 8) | part);//以小端格式存储数字IP地址
if(i==3){
if(c!='\0') // 结尾存在额外字符
return -1;
}else{
if(c=='\0') // 字符串过早结束
return -1;
}//结束非法字符串判断
}//结束for循环
return val;
}
//用于将数字IP地址转化为字符串IP地址
void kern_inet_ntoa(char *ip_str , unsigned int ip_num){
unsigned char *p = (unsigned char*)(&ip_num);
sprintf(ip_str, "%u.%u.%u.%u", p[0],p[1],p[2],p[3]);
}
static int __init getRoutingInfo_init(void) {
nf_register_net_hook(&init_net, &nfho); //注册钩子函数
nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, &cfg); //注册Netlink处理函数
if(!nl_sk){
printk(KERN_ERR"Failed to create nerlink socket\n");
}
printk("register getRoutingInfo mod\n");
printk("Start...\n");
return 0;
}
static void __exit getRoutingInfo_exit(void){
nf_unregister_net_hook(&init_net, &nfho); //取消注册钩子函数
netlink_kernel_release(nl_sk); //取消注册Netlink处理函数
printk("unregister getRoutingInfo mod\n");
printk("Exit...\n");
}
module_init(getRoutingInfo_init);
module_exit(getRoutingInfo_exit);
MODULE_AUTHOR("zsw");
MODULE_LICENSE("GPL");
Makefile
obj-m += netfilter.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
user.c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define NETLINK_TEST 17 //用于自定义协议
#define MAX_PAYLOAD 1024 //最大载荷容量
#define RECEIVE_CNT 10 //接受路由信息的数量
int n = RECEIVE_CNT; //接受路由信息的数量
int sock_fd, store_fd; //套接字描述符, 文件描述符
struct iovec iov; //
struct msghdr msg; //存储发送的信息
struct nlmsghdr *nlh = NULL; //用于封装信息的头部
struct sockaddr_nl src_addr, dest_addr; //源地址,目的地址(此处地址实际上就是pid)
int main(int argc, char *argv[])
{
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK; //协议族
src_addr.nl_pid = getpid(); //本进程pid
src_addr.nl_groups = 0; //多播组,0表示不加入多播组
bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK; //协议族
dest_addr.nl_pid = 0; //0表示kernel的pid
dest_addr.nl_groups = 0; //多播组,0表示不加入多播组
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); //设置缓存空间
nlh->nlmsg_pid = getpid(); //本进程pdi
nlh->nlmsg_flags = 0; //额外说明信息
if(argc != 2){
printf("Usage : %s <ip>\n", argv[0]);
exit(1);
}
strcpy(NLMSG_DATA(nlh), argv[1]);//将需要捞取的路由信息源地址
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
sendmsg(sock_fd, &msg, 0); // 发送信息到kernel
// 从kernel接受信息
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
store_fd = open("./RoutingInfomation", O_CREAT|O_WRONLY, 0666);
while(n--){
int msgLen = recvmsg(sock_fd, &msg, 0);
printf("Received mesage payload: %d|%s\n", msgLen, (char *)NLMSG_DATA(nlh));
int ret = write(store_fd, (char *)NLMSG_DATA(nlh), strlen((char *)NLMSG_DATA(nlh)));
if(ret <= 0){
printf("write error.");
return -1;
}
ret = write(store_fd, "\n", 1);
if(ret <= 0){
printf("write error.");
return -1;
}
}
close(store_fd);
close(sock_fd);
return 0;
}
参考资料:
解决Linux4.13以上找不到nf_register_hook()函数的问题
Linux2.6下基于Netlink的用户空间与内核空间通信