Linnux5.0.0下,基於Netlink與NetFilter對本機數據包進行篩選監控

Linnux5.0.0下,基於NetlinkNetFilter對本機數據包進行篩選監控

需求:

開發一個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]

必備知識:

  1. Linux內核模塊編程
  2. Netfilter子系統與hook函數編程
  3. struct sk_buff,struct iphdr,struct tcphdr,struct udphdr等網絡相關結構體使用
  4. 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內核中,Netfilterhook函數原型爲:

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以上已改變),Netfilterhook函數原型變成了:

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 nlmsghdrstruct iovecstruct 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;
}

參考資料:

lkm編程教程

Netfilter hook點與函數解析

Netfilter基礎實例

Linux4.10.0版本下Netfilter實例

解決Linux4.13以上找不到nf_register_hook()函數的問題

struct sk_buff結構體詳解

struct iphdr結構體詳解

struct_tcphdr結構體詳解

struct_udphdr結構體詳解

kernel調試 打印IP地址

Linux2.6下基於Netlink的用戶空間與內核空間通信

Linux3.8.13下基於Netlink的用戶空間與內核空間的通訊實例

大小端問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章