精华内容
下载资源
问答
  • linux-3.10.36版本的内核相比linux-2.6.36版本中netlink的代码有所变化,以前的代码已经不能...netlink是linux内核的一套基于socket的通信机制,那么,只需要知道怎么创建套接字,发送数据接收数据就行了。

    转自 http://blog.csdn.net/u012819339/article/details/51334600


    linux-3.10.36版本的内核相比linux-2.6.36版本中netlink的代码有所变化,以前的代码已经不能成功编译了。

    netlink是linux内核的一套基于socket的通信机制,那么,只需要知道怎么创建套接字,发送数据,接收数据就行了。

    内核层:

    初始化操作:

    新的netlink_kernel_create()函数只有3个参数了。

    /******************************
    net:    linux网络命名空间结构体指针
    uint:   netlink协议类型
    cfg:    netlink内核配置参数结构体指针
    
    返回: sock结构指针
    ******************************/
    
    static inline struct sock *
    netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第一个参数:传入一个网络命名空间结构的地址,这块我不怎么清楚,只知道linux的网络命名空间的出现是为了让 用户创建的进程能够与系统分离得更加彻底,从而不需要使用更多的底层虚拟化技术。未深究,调用该函数时就传入&init_net吧,它是在net_namespace.c文件中定义的一个结构体。

    第二个参数:是代表netlink协议类型,内核目前定义了如下一些类型:

    #define NETLINK_ROUTE       0   /* Routing/device hook              */
    #define NETLINK_UNUSED      1   /* Unused number                */
    #define NETLINK_USERSOCK    2   /* Reserved for user mode socket protocols  */
    #define NETLINK_FIREWALL    3   /* Unused number, formerly ip_queue     */
    #define NETLINK_SOCK_DIAG   4   /* socket monitoring                */
    #define NETLINK_NFLOG       5   /* netfilter/iptables ULOG */
    #define NETLINK_XFRM        6   /* ipsec */
    #define NETLINK_SELINUX     7   /* SELinux event notifications */
    #define NETLINK_ISCSI       8   /* Open-iSCSI */
    #define NETLINK_AUDIT       9   /* auditing */
    #define NETLINK_FIB_LOOKUP  10  
    #define NETLINK_CONNECTOR   11
    #define NETLINK_NETFILTER   12  /* netfilter subsystem */
    #define NETLINK_IP6_FW      13
    #define NETLINK_DNRTMSG     14  /* DECnet routing messages */
    #define NETLINK_KOBJECT_UEVENT  15  /* Kernel messages to userspace */
    #define NETLINK_GENERIC     16
    /* leave room for NETLINK_DM (DM Events) */
    #define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */
    #define NETLINK_ECRYPTFS    19
    #define NETLINK_RDMA        20
    #define NETLINK_CRYPTO      21  /* Crypto layer */
    
    #define NETLINK_INET_DIAG   NETLINK_SOCK_DIAG
    
    #define MAX_LINKS 32
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    最多可以定义32中类型,若 uint > MAX_LINKS 则该函数返回NULL,源代码片段如下

    __netlink_kernel_create(struct net *net, int unit, struct module *module,
                struct netlink_kernel_cfg *cfg)
    {
        struct socket *sock;
        struct sock *sk;
        struct netlink_sock *nlk;
        struct listeners *listeners = NULL;
        struct mutex *cb_mutex = cfg ? cfg->cb_mutex : NULL;
        unsigned int groups;
    
        BUG_ON(!nl_table);
    
        if (unit < 0 || unit >= MAX_LINKS)
            return NULL;
        ...
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    第三个参数:cfg存放的是netlink内核配置参数,配置参数中的input成员用于处理接收到的消息。该函数的参数变短了,实际是因为放到该结构体中来了

    struct netlink_kernel_cfg {
        unsigned int    groups;// netlink组,应该是多播时使用,单播的情况就是0了
        unsigned int    flags;
        void        (*input)(struct sk_buff *skb); //回调函数,当收到消息时,内核会调用该函数指针指向的函数进行处理
        struct mutex    *cb_mutex;
        void        (*bind)(int group);
    };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    发送

    单播netlink_unicast() 和 多播netlink_broadcast()

    /******************************
    ssk:    sock结构体指针
    skb:    skb存放消息,它的data字段指向要发送的 netlink消息结构,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块
    portid: 端口id
    nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利 用时睡眠
    返回: 发送数据的长度
    ******************************/
    int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    /******************************
    ssk:    sock结构体指针
    skb:    skb存放消息,它的data字段指向要发送的 netlink消息结构,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块
    portid: 端口id
    group:  netlink组
    allocation: 内核内存分配类型,一般地为GFP_ATOMIC或 GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文
    返回: 发送数据的长度
    ******************************/
    int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
                     __u32 group, gfp_t allocation);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接收

    在调用netlink_kernel_create()函数是已经向cfg结构体中指定了回调函数,这个函数就是接收函数了。内核模块收到消息后会自动调用该函数。

    用户层

    使用标准的socket API即可( socket(), bind(), sendmsg(), recvmsg() 和 close())

    消息结构

    如图: 
    这里写图片描述

    源代码

    示例代码由用户层向内核发送一个字符串,内核打印接收到的消息并且将字符串发送给用户层。

    内核层:

    /*******************************
    file:           k_netlink.c
    description:    netlink demo
    kernel:         linux-3.10.36
    author:         arvik
    email:          1216601195@qq.com
    blog:           http://blog.csdn.net/u012819339
    *******************************/
    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/types.h>
    #include <linux/sched.h>
    #include <net/sock.h>
    #include <linux/netlink.h>
    
    #define NETLINK_USER  22
    #define USER_MSG    (NETLINK_USER + 1)
    #define USER_PORT   50
    
    MODULE_LICENSE("GPL");  
    MODULE_AUTHOR("arvik");  
    MODULE_DESCRIPTION("netlink_demo");
    
    static struct sock *netlinkfd = NULL;
    
    int send_msg(int8_t *pbuf, uint16_t len)
    {
        struct sk_buff *nl_skb;
        struct nlmsghdr *nlh;
    
        int ret;
    
        nl_skb = nlmsg_new(len, GFP_ATOMIC);
        if(!nl_skb)
        {
            printk("netlink_alloc_skb error\n");
            return -1;
        }
    
        nlh = nlmsg_put(nl_skb, 0, 0, USER_MSG, len, 0);
        if(nlh == NULL)
        {
            printk("nlmsg_put() error\n");
            nlmsg_free(nl_skb);
            return -1;
        }
        memcpy(nlmsg_data(nlh), pbuf, len);
    
        ret = netlink_unicast(netlinkfd, nl_skb, USER_PORT, MSG_DONTWAIT);
    
        return ret;
    }
    
    
    static void recv_cb(struct sk_buff *skb)
    {
        struct nlmsghdr *nlh = NULL;
        void *data = NULL;
    
        printk("skb->len:%u\n", skb->len);
        if(skb->len >= nlmsg_total_size(0))
        {
            nlh = nlmsg_hdr(skb);
            data = NLMSG_DATA(nlh);
            if(data)
            {
                printk("kernel receive data: %s\n", (int8_t *)data);
                send_msg(data, nlmsg_len(nlh));
            }
        }
    } 
    
    
    struct netlink_kernel_cfg cfg = 
    {
        .input = recv_cb,
    };
    
    static int __init test_netlink_init(void)
    {
        printk("init netlink_demo!\n");
    
    
        netlinkfd = netlink_kernel_create(&init_net, USER_MSG, &cfg);
        if(!netlinkfd)
        {
            printk(KERN_ERR "can not create a netlink socket!\n");
            return -1;
        }
    
        printk("netlink demo init ok!\n");
        return 0;
    }
    
    static void __exit test_netlink_exit(void)
    {
        sock_release(netlinkfd->sk_socket);
        printk(KERN_DEBUG "netlink exit\n!");
    }
    
    module_init(test_netlink_init);
    module_exit(test_netlink_exit);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    用户层

    /*******************************
    file:           u_netlink.c
    description:    netlink demo
    author:         arvik
    email:          1216601195@qq.com
    blog:           http://blog.csdn.net/u012819339
    *******************************/
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <string.h>
    #include <linux/netlink.h>
    #include <stdint.h>
    #include <unistd.h>
    #include <errno.h>
    
    #define NETLINK_USER 22
    #define USER_MSG    (NETLINK_USER + 1)
    #define MSG_LEN 100
    
    #define MAX_PLOAD 100
    
    struct _my_msg
    {
        struct nlmsghdr hdr;
        int8_t  data[MSG_LEN];
    };
    
    
    
    int main(int argc, char **argv)
    {
        char *data = "hello kernel";
        struct sockaddr_nl  local, dest_addr;
    
        int skfd;
        struct nlmsghdr *nlh = NULL;
        struct _my_msg info;
        int ret;
    
        skfd = socket(AF_NETLINK, SOCK_RAW, USER_MSG);
        if(skfd == -1)
        {
            printf("create socket error...%s\n", strerror(errno));
            return -1;
        }
    
        memset(&local, 0, sizeof(local));
        local.nl_family = AF_NETLINK;
        local.nl_pid = 50; 
        local.nl_groups = 0;
        if(bind(skfd, (struct sockaddr *)&local, sizeof(local)) != 0)
        {
            printf("bind() error\n");
            close(skfd);
            return -1;
        }
    
        memset(&dest_addr, 0, sizeof(dest_addr));
        dest_addr.nl_family = AF_NETLINK;
        dest_addr.nl_pid = 0; // to kernel
        dest_addr.nl_groups = 0;
    
        nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
        memset(nlh, 0, sizeof(struct nlmsghdr));
        nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
        nlh->nlmsg_flags = 0;
        nlh->nlmsg_type = 0;
        nlh->nlmsg_seq = 0;
        nlh->nlmsg_pid = local.nl_pid; //self port
    
        memcpy(NLMSG_DATA(nlh), data, strlen(data));
        ret = sendto(skfd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr_nl));
    
        if(!ret)
        {
            perror("sendto error1\n");
            close(skfd);
            exit(-1);
        }
        printf("wait kernel msg!\n");
        memset(&info, 0, sizeof(info));
        ret = recvfrom(skfd, &info, sizeof(struct _my_msg), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        if(!ret)
        {
            perror("recv form kernel error\n");
            close(skfd);
            exit(-1);
        }
    
        printf("msg receive from kernel:%s\n", info.data);
        close(skfd);
    
        free((void *)nlh);
        return 0;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    说明,一定要先加载内核模块,要不然用户层创建套接字会失败!(因为内核模块会注册netlink所使用的协议类型,本例子中协议类型为USER_MSG,即23)


    展开全文
  • 分类: linux 网络编程2012...linux内核struct数据结构sockethook测试 作者:Godbach Blog:http://Godbach.cublog.cn  本文的大纲如下: 一、基础知识 1. Netfilter 2. Netlink机制 二、IP Queue编程接口

    分类: linux 网络编程 1999人阅读 评论(0) 收藏 举报
    作者:Godbach

    Blog:http://Godbach.cublog.cn

          本文的大纲如下:

    一、基础知识

    1. Netfilter

    2. Netlink机制

    二、IP Queue编程接口

    三、一个实现接收内核态发送的IP Queue数据包的用户态例程

    1. libipq.h

    2. libipq.c

    3. ipq_user.c

    四、应用程序的测试

    1. 测试环境的建立

    2. 程序的测试

     

    一、基础知识

    基础知识部分的很多部分内容都重点参考或者直接引用了《如何用IP Queue机制编写用户态防火墙》,原文的链接为:http://www.syue.com/Firewall/HTML/3357.html。在此,向该文的作者表示感谢。

     

    1. Netfilter

    Linux内核在Netfilter(下文简称NF)框架的基础上提供了IP Queue机制,使得基于用户态(User Mode)的防火墙开发成为可能。

     

    内核中NF对网络报文的处理这里不做详细描述。假设读者已经熟悉NF的工作原理和工作流程。但这里还是要简单介绍一下NF中各个钩子(hook)函数对数据包处理的返回值,即该函数告诉内核对该数据包的处理意见。所有的返回值如下:

     

    NF_DROP: 丢弃该报文,释放所有与该报文相关的资源;

    NF_ACCEPT: 接受该报文,并继续处理;

    NF_STOLEN: 该报文已经被HOOK函数接管,协议栈无须继续处理;

    NF_QUEUE: 将该报文传递到用户态去做进一步的处理;

    NF_REPEAT: 再次调用本HOOK函数。

     

    当HOOK处理函数返回值为NF_QUEUE时,内核协议栈将通过IP Queue机制把当前报文传递到用户态,由用户态的应用程序进行处理。这样,只要能够在相应的HOOK点上返回NF_QUEUE值,就可以将符合要求的报文传送到用户态去做进一步对报文行处理。随后,用户态程序会将处理后的报文以及对报文的处理意见(ACCEPT,DROP等)传递给内核协议栈。内核协议栈就会按照用户态对报文的处理意见将报文做接受、丢弃等处理。整个处理的过程就相当于一个用户态的防火墙,所有数据包的实质性处理都放在用户态进行。这样,即使是不具有深入内核知识的开发人员,也可以开发出适应一定应用场合的用户态防火墙。

     

    2. Netlink机制

          前面讲到,所谓IP Queue机制,只是当NF上Hook函数对数据包处理的返回值为NF_QUEUE时,协议栈会将数据包交给内核中的ip_queue模块。而ip_queue又是怎么将数据包传递给用户态的呢?这里就涉及到在内核开发中常见的问题,如何将内核态的数据传递到用户态,实现内核空间和用户空间的通信。具体实现的方法有多种。本人的博客中也总结了若干种,并配有测试的例程:http://blog.chinaunix.net/u/33048/article.html.对于IP Queue,则是使用Netlink机制实现内核态和用户态的交互。

    NetLink是Linux系统特有的、基于socket编程接口的通信机制。它是一个面向数据报文的服务,并提供NETLINK_ROUTE(更新和修改路由操作)、NETLINK_FIREWALL (接受和发送IPv4协议NF传输的包,基于内核的ip_queue模块),NETLINK_ARPD(用户态ARP表操作)等多种通信协议。在创建基于IP Queue的NetLink Socket时,将采用如下系统调用:

    fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
      这里,PF_NETLINK指明要创建NetLink Socket;SOCK_RAW指明采用原始套接字,也可以采用SOCK_DGRAM,因为NetLink机制的实现并不区分SOCK_RAW和SOCK_DGRAM;参数NETLINK_FIREWALL则指明通信协议采用IP Queue。

    既然IP Queue是基于NetLink的,其消息格式自然也遵从NetLink的规范。NetLink消息由两部分组成:消息头(struct nlmsghdr)和数据负载(data payload)。

    消息头的定义如下(include/linux/netlink.h):

    1. struct nlmsghdr  
    2. {  
    3. __u32 nlmsg_len; /*消息长度*/  
    4. __u16 nlmsg_type;/*消息类型*/  
    5. __u16 nlmsg_flags;/*额外的标志*/  
    6. __u32 nlmsg_seq; /*序列号*/  
    7. __u32 nlmsg_pid; /*进程号*/  
    8. };   
    9. struct nlmsghdr  
    10. {  
    11. __u32 nlmsg_len; /*消息长度*/  
    12. __u16 nlmsg_type;/*消息类型*/  
    13. __u16 nlmsg_flags;/*额外的标志*/  
    14. __u32 nlmsg_seq; /*序列号*/  
    15. __u32 nlmsg_pid; /*进程号*/  
    16. };   

    所有的IP Queue消息都将包含一个struct nlmsghdr消息头,具体的IP Queue消息则包含在NetLink消息的数据负载中。有关NetLink消息格式的详情可以参见手册页Netlink(7)。

    二、IP Queue编程接口

    使用IP Queue机制的程序必须包含如下的头文件:

    #include<linux/netfilter_ipv4/ip_queue.h>

    在这个头文件中定义了所有IP Queue消息的格式。以下谈到关于IP Queue的若干个数据结构以及宏定义都包含在该文头件。

    IP Queue消息可以分为两大类:由内核协议栈发给用户态进程的IP Queue消息和由用户态进程发给内核的IP Queue消息。

    由内核协议栈发给用户态进程的IP Queue消息(nlmsghdr.nlmsg_type =

    IPQM_PACKET),其数据结构为ipq_packet_msg_t,定义如下:

    1. /* Messages sent from kernel */  
    2. typedef struct ipq_packet_msg {  
    3.       unsigned long packet_id;    /* 报文的ID号 */  
    4.       unsigned long mark;          /* NF标记值 */  
    5.       long timestamp_sec;          /*报文到达时间(秒)  */  
    6.       long timestamp_usec;        /* 报文到达时间(毫秒) */  
    7.       unsigned int hook;       /* 报文所处的NF hook点 */  
    8.       char indev_name[IFNAMSIZ];    /* 流入网口名称 */  
    9.       char outdev_name[IFNAMSIZ]; /* 流出网口名称 */  
    10.       unsigned short hw_protocol;    /*硬件协议(网络顺序)*/  
    11.       unsigned short hw_type;         /* 硬件类型 */  
    12.       unsigned char hw_addrlen; /*硬件地址长度*/  
    13.       unsigned char hw_addr[8]; /* 硬件地址 */  
    14.       size_t data_len;           /* 报文数据的长度 */  
    15.       unsigned char payload[0];  /* 报文本身的数据,可选 */  
    16. } ipq_packet_msg_t;  

    这个数据结构也被称为“报文的元数据”。个人理解,所谓“报文的元数据”应该是关于报文的摘要信息,而不包括报文数据的本身。从上面的这个结构体中也可以看出来。内核除可以单独向用户进程传递“报文的元数据”以外,也可以同时传递报文本身。此时,报文本身的数据将存储在ipq_packet_msg_t数据成员payload开始的地方。

    至于内核在什么情况下向用户传递报文的元数据,什么情况下向用户传递报文的元数据加报文本身的数据,那就要看用户所请求的模式了。下面将讲述用户态发到内核态消息的格式,也正好解答了我们这里提出的问题。

    用户态发到内核态的消息,其数据结构如下所示:

    1. typedef struct ipq_peer_msg {  
    2.       union {  
    3.            ipq_verdict_msg_t verdict;  
    4.            ipq_mode_msg_t mode;  
    5.       } msg;  
    6. } ipq_peer_msg_t;  

    通过该数据结构可知,这类消息又分为“模式设置消息(nlmsghdr.nlmsg_type = IPQM_MODE)”和“断言消息(nlmsghdr.nlmsg_type = IPQM_VERDICT)”两个子类。

    “模式设置消息”的数据结构定义如下:

    1. typedef struct ipq_mode_msg {  
    2.  unsigned char value;/* 请求的模式 */  
    3.  size_t range;/* 请求拷贝的报文长度*/  
    4. } ipq_mode_msg_t;  

    这里,请求模式value的值可以是IPQ_COPY_NONE、IPQ_COPY_META和IPQ_COPY_PACKET,具体解释如下:

    (1)请求模式value为IPQ_COPY_NONE时,报文将被丢弃;

    (2)请求模式value为IPQ_COPY_META时,内核将在其后的报文传递中只传递“报文的元数据”。

    以上两种情形传递range的值将被内核忽略。内核中会将该值置为0。

    (3)请求模式value为IPQ_COPY_PACKET时,内核将同时传递“报文的元数据”和报文本身,报文本身的传递长度由ipq_mode_msg_t的另一个数据成员range指定。 range的最大值不能超过IP报文的最大长度,也就是0xFFFF。否则,会被自动置为0xFFFF。如果请求的长度大于报文自身的长度,将会按照报文自身长度进行传递。

     

    另一子类即“断言消息”,其数据类型定义如下:

    1.   typedef struct ipq_verdict_msg {  
    2. unsigned int value;  
    3. unsigned long id;  
    4. size_t data_len;  
    5. unsigned char payload[0];  
    6. } ipq_verdict_msg_t;  
    7.    

    其中,value是用户态程序回传给内核的对当前报文的处理意见,可以是NF_ACCEPT或NF_DROP等值。id则是用以区分报文的标识号,即内核传来的ipq_packet_msg_t结构中的packet_id。当用户态程序修改了当前报文以后,需要将报文重新传递回内核,此时,新的报文内容必须存储在payload的开始处,并由data_len指明新报文的长度。

     

    从上述内容可以看出,在整个IP Queue的报文传递过程中,用户态程序和内核协议栈之间的互动顺序是:

    (1)用户态程序利用“模式设置消息”告诉内核协议栈所请求的报文传递模式;

    (2)根据这个模式,内核组织好等待传递的消息,通过NetLink Socket发给用户态程序;

    (3)用户态程序对接收到的数据包进行处理,得出该报文的处理意见(可能同时修改当前报文),并回传给内核。

     

    三、一个实现接收内核态发送的IP Queue数据包的用户态例程

          由于IP Queue是使用Netlink机制进行内核态和用户态通信的。因此,用户态要接收内核态发送的IP Queue数据包,就需要设计相应的Netlink程序,也就是设计相应的基于Netlink的socket程序即可。这里,我不会详细介绍如何使用Netlink机制实现用户态和内核态进行通信。我假设阅读本文的朋友,已经熟悉了Netlink的使用。如果对Netlink的使用还不是很熟悉,那么可以参考独孤九贱大侠的文章——《Linux 用户态与内核态的交互——netlink 篇》,其链接为:

    http://linux.chinaunix.net/bbs/thread-822500-1-1.html.

    这篇文章提供了一个使用netlink的完整的例程,包括内核态和用户态。讲的非常清楚,我看完这篇文章,又跑了一下上面提供的例程,基本上熟悉了Netlink的使用方法。

    当然,如果读者不想花时间再去了解netlink的话,也可以通过这篇文章熟悉Netlink的使用。因为我这里提供的是完整的用户态例程,我会将源码完全提供出来,对于急于通过执行程序观察结果来学习Netlink和IP Queue的朋友,也可以通过随后提供的方法编译并执行程序。

    以下讲述用户态例程接收IP Queue数据包的程序设计。

    其实,由于Netlink程序也是使用socket的方式进行通信。那么接收IP Queue报文的方式应该遵循socket的标准流程,具体流程如下:

    (1)调用socket()创建一个地址类型为PF_NETLINK(AF_NETLINK)的套接字。该套接字使用SOCK_RAW方式传输数据,协议类型为NETLINK_FIREWALL,即使用IP Queue;

    (2)调用bind()将本地地址(Netlink通信双方使用该协议特有的地址格式,见下面struct sockaddr_nl)绑定到已建立的套接字上;

    1. struct sockaddr_nl {  
    2.     sa_family_t     nl_family;  /* AF_NETLINK */  
    3.     unsigned short  nl_pad;     /* Zero. */  
    4.     pid_t           nl_pid;     /* Process ID. */  
    5.     __u32           nl_groups;  /* Multicast groups mask. */  
    6. };  

    (3)调用sendto()发送相关的配置信息,告诉内核应用程序准备接受的是数据包的元数据,还是同时包括数据包本身;

    (4)调用recvfrom()接受内核态发送来的IP Queue报文;

    (5)调用close()关闭套接字,结束通信。

    看了以上流程,我相信很多熟悉socket编程的朋友已经可以写出接收IP Queue报文的用户态程序了。

    本文中的示例代码的实现整体也是依照上面的步骤。但在细节的实现上,参考了iptables源码给给出的libipq库的实现代码。libipq库是iptables中封装的实现用户态接收和发送IP Queue报文操作的,也就相当于对上面总结的IP Queue报文接受流程进行封装。整个libipq库分别由libipq.c和libipq.h两个源文件。我这里将两个源文件移植(基于iptables-1.3.5版本)到示例代码中并裁剪,并编写了测试程序ip_user.c。因此,整个实现代码包含三个源文件:ip_user.c、libipq.c和libipq.h。

    以下将对三个源文件进行分析。

    1. libipq.h

          该头文件定义了一个关键的数据结构,并提供了所有进行Netlink通信的API.

          数据结构的定义如下:

    1. struct ipq_handle  
    2. {  
    3.       int fd;  
    4.       struct sockaddr_nl local;  
    5.       struct sockaddr_nl peer;  
    6. };  

    其中,fd是socket通信的描述符,local和peer分别是Netlink通信双方的地址。   

    除了定义数据结构,剩下的主要就是提供给用户调用的API,函数列表如下:

    1. struct ipq_handle *ipq_create_handle(u_int32_t flags, u_int32_t protocol);  
    2.    
    3. int ipq_destroy_handle(struct ipq_handle *h);  
    4.    
    5. ssize_t ipq_read(const struct ipq_handle *h, unsigned char *buf, size_t len);  
    6.    
    7. int ipq_set_mode(const struct ipq_handle *h, u_int8_t mode, size_t len);  
    8.    
    9. ipq_packet_msg_t *ipq_get_packet(const unsigned char *buf);  
    10.    
    11. int ipq_message_type(const unsigned char *buf);  
    12.    
    13. int ipq_get_msgerr(const unsigned char *buf);  
    14.    
    15. int ipq_set_verdict(const struct ipq_handle *h,  
    16.                     ipq_id_t id,  
    17.                     unsigned int verdict,  
    18.                     size_t data_len,  
    19.                     unsigned char *buf);  
    20.    
    21. int ipq_ctl(const struct ipq_handle *h, int request, ...);  
    22.    
    23. char *ipq_errstr(void);  
    24. void ipq_perror(const char *s);  

    我将在下面libipq.c的讲解中对若干我们将要用到的一些函数进行分析。

    2. libipq.c

    该源文件实现了libipq.h中定义的所有函数,并定义了一些出错信息。

    (1)ipq_create_handle()函数申请了一个struct ipq_handle *h结构体,用来存储随后创建的IPv4 socket通信的fd,以及通信双方的地址。本函数完成了通信双方地址的初始化,并将本地地址绑定到已生成的fd上。

    ipq_create_handle()函数的源码如下 

    1. struct ipq_handle *ipq_create_handle()  
    2. {  
    3.       int status;  
    4.       struct ipq_handle *h;  
    5.    
    6.       h = (struct ipq_handle *)malloc(sizeof(struct ipq_handle));  
    7.       if (h == NULL) {  
    8.            ipq_errno = IPQ_ERR_HANDLE;  
    9.            return NULL;  
    10.       }  
    11.        
    12.       memset(h, 0, sizeof(struct ipq_handle));  
    13.     if (protocol == PF_INET)  
    14.           h->fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);  
    15.     else {  
    16.            ipq_errno = IPQ_ERR_PROTOCOL;  
    17.            free(h);  
    18.            return NULL;  
    19.     }  
    20.          
    21.       if (h->fd == -1) {  
    22.            ipq_errno = IPQ_ERR_SOCKET;  
    23.            close(h->fd);  
    24.            free(h);  
    25.            return NULL;  
    26.       }  
    27.       memset(&h->local, 0, sizeof(struct sockaddr_nl));  
    28.       h->local.nl_family = AF_NETLINK;  
    29.       /*传递本地的pid*/  
    30.       h->local.nl_pid = getpid();  
    31.       h->local.nl_groups = 0;  
    32.       status = bind(h->fd, (struct sockaddr *)&h->local, sizeof(h->local));  
    33.       if (status == -1) {  
    34.            ipq_errno = IPQ_ERR_BIND;  
    35.            close(h->fd);  
    36.            free(h);  
    37.            return NULL;  
    38.       }  
    39.       memset(&h->peer, 0, sizeof(struct sockaddr_nl));  
    40.       h->peer.nl_family = AF_NETLINK;  
    41.       /*代表通信的另一方为内核*/  
    42.       h->peer.nl_pid = 0;  
    43.       h->peer.nl_groups = 0;  
    44.       return h;  
    45. }  
    46.    
    47. ipq_destroy_handle()函数关闭由ipq_create_handle()建立起来的fd,并释放申请的内存。源码如下:  
    48. int ipq_destroy_handle(struct ipq_handle *h)  
    49. {  
    50.       if (h) {  
    51.            close(h->fd);  
    52.            free(h);  
    53.       }  
    54.       return 0;  
    55. }  

    (2)向内核发送模式请求的函数

    1. int ipq_set_mode(const struct ipq_handle *h,  
    2.                  u_int8_t mode, size_t range)  
    3. {  
    4.       /*构造一个向内核发送报文的结构体*/  
    5.       struct {  
    6.            struct nlmsghdr nlh;  
    7.            ipq_peer_msg_t pm;  
    8.       } req;  
    9.    
    10.       memset(&req, 0, sizeof(req));  
    11.       req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req));  
    12.       req.nlh.nlmsg_flags = NLM_F_REQUEST;  
    13.       req.nlh.nlmsg_type = IPQM_MODE;  
    14.       req.nlh.nlmsg_pid = h->local.nl_pid;  
    15.       /*告诉协议栈所请求的报文传递模式*/  
    16.       req.pm.msg.mode.value = mode;  
    17.       /*请求内核返回报文的长度*/  
    18.       req.pm.msg.mode.range = range;  
    19.       return ipq_netlink_sendto(h, (void *)&req, req.nlh.nlmsg_len);  
    20. }  

    在构造完向内核发送的结构体req并设置相关内容之后,调用ipq_netlink_sendto函数发送用户态的请求数据,该函数代码如下:

    1. static ssize_t ipq_netlink_sendto(const struct ipq_handle *h,  
    2.                                   const void *msg, size_t len)  
    3. {  
    4.       int status = sendto(h->fd, msg, len, 0,  
    5.                           (struct sockaddr *)&h->peer, sizeof(h->peer));  
    6.       if (status < 0)  
    7.            ipq_errno = IPQ_ERR_SEND;  
    8.       return status;  
    9. }   

    ipq_netlink_sendto函数直接调用了sendto系统调用发送用户态的数据,返回的是发送出去的数据长度。当sendto调用失败时,对全局变量ipq_errno 赋值IPQ_ERR_SEND。这样方便以后用专门返回出错信息的函数引用。

    (3)用户态发送了请求数据包之后,就处于等待接收内核返回数据包的状态。一旦内核NF得到包处理函数返回NF_QUEUE时,该包就会被ip_queue模块发送到用户态。用户态接收IP Queue数据包的函数为:

    ssize_t ipq_read(const struct ipq_handle *h, unsigned char *buf, size_t len)

    该函数的代码如下。其中buf存储来自内核态的数据包,len为buf的长度。

    ssize_t ipq_read(const struct ipq_handle *h,

                     unsigned char *buf, size_t len)

    {

          return ipq_netlink_recvfrom(h, buf, len);

    }

    该函数直接调用ipq_netlink_recvfrom()函数,其源码为:

    static ssize_t ipq_netlink_recvfrom(const struct ipq_handle *h,

                                        unsigned char *buf, size_t len)

    {

          unsigned int addrlen;

          int status;

          struct nlmsghdr *nlh;

          /*buf长度的校验,不能小于Netlink Message的头部长度*/

          if (len < sizeof(struct nlmsgerr)) {

               ipq_errno = IPQ_ERR_RECVBUF;

               return -1;

          }

          addrlen = sizeof(h->peer);

     

          status = recvfrom(h->fd, buf, len, 0,

                                (struct sockaddr *)&h->peer, &addrlen);

          if (status < 0) {

               ipq_errno = IPQ_ERR_RECV;

               return status;

          }

          /*判断接收到的发送方的地址长度是否正确*/

          if (addrlen != sizeof(h->peer)) {

               ipq_errno = IPQ_ERR_RECV;

               return -1;

          }

          /*内核态向用户态发送数据报文时,其pid=0*/

          if (h->peer.nl_pid != 0) {

               ipq_errno = IPQ_ERR_RECV;

               return -1;

          }

          if (status == 0) {

               ipq_errno = IPQ_ERR_NLEOF;

               return -1;

          }

          nlh = (struct nlmsghdr *)buf;

          /*判断是否发生数据报文被截断的情况*/

          if (nlh->nlmsg_flags & MSG_TRUNC || nlh->nlmsg_len > status) {

               ipq_errno = IPQ_ERR_RTRUNC;

               return -1;

          }

          return status;

    }

    该函数返回读取到报文的实际长度。

    至此,我们已经可以通过上面几个函数实现从内核态接收到既定模式的IP Queue报文。

     

    (4)输出出错信息

    char *ipq_errstr(void)

    {

          return ipq_strerror(ipq_errno);

    }

    static char *ipq_strerror(int errcode)

    {

          if (errcode < 0 || errcode > IPQ_MAXERR)

               errcode = IPQ_ERR_IMPL;

          return ipq_errmap[errcode].message;

    }

    根据函数执行过程中记录的出错信息,打印对相关出错的具体提示。

     

    3. ipq_user.c

    这个函数就是具体的测试函数。功能比较简单,通过调用libipq.c中提供的API实现获取IP Queue数据包的简单信息,包括数据包在本地机上的入口以及报文的长度等,

    整个源码如下:

    1. /* 
    2.  * ipq_usr.c 
    3.  * 
    4.  * Testing program for receiving IP Queue packets from kernel 2.6.18.3 
    5.  * 
    6.  * Dec 1, 2008 
    7.  * Godbach created. 
    8.  * 
    9.  * This program is free software; you can redistribute it and/or modify 
    10.  * it under the terms of the GNU General Public License as published by 
    11.  * the Free Software Foundation; either version 2 of the License, or 
    12.  * (at your option) any later version. 
    13.  * 
    14.  * This program is distributed in the hope that it will be useful, 
    15.  * but WITHOUT ANY WARRANTY; without even the implied warranty of 
    16.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    17.  * GNU General Public License for more details. 
    18.  * 
    19.  */  
    20. #include <stdio.h>  
    21. #include <stdlib.h>  
    22. #include <signal.h>  
    23. #include "libipq.h"  
    24.    
    25. struct ipq_handle *h = NULL;  
    26.    
    27. static void sig_int(int signo)  
    28. {  
    29.       ipq_destroy_handle(h);  
    30.       printf("Exit: %s\n", ipq_errstr());  
    31.       exit(0);  
    32. }  
    33.    
    34. int main(void)  
    35. {  
    36.       unsigned char buf[1024];  
    37.       /* creat handle*/  
    38.       h = ipq_create_handle(0, PF_INET);  
    39.       if(h == NULL){  
    40.            printf("%s\n", ipq_errstr());  
    41.            return 0;  
    42.       }  
    43.       printf("ipq_creat_handle success!\n");  
    44.       /*set mode*/  
    45.       unsigned char mode = IPQ_COPY_PACKET;  
    46.       int range = sizeof(buf);  
    47.       int ret = ipq_set_mode(h, mode, range);  
    48.       printf("ipq_set_mode: send bytes =%d, range=%d\n", ret, range);  
    49.        
    50.       /*register signal handler*/  
    51.       signal(SIGINT, sig_int);  
    52.    
    53.       /*read packet from kernel*/  
    54.       int status;  
    55.       struct nlmsghdr *nlh;  
    56.       ipq_packet_msg_t *ipq_packet;  
    57.        
    58.       while(1){  
    59.            status = ipq_read(h, buf, sizeof(buf));  
    60.            if(status > sizeof(struct nlmsghdr))  
    61.            {  
    62.                  nlh = (struct nlmsghdr *)buf;  
    63.                  ipq_packet = ipq_get_packet(buf);  
    64.                  printf("recv bytes =%d, nlmsg_len=%d, indev=%s, datalen=%d, packet_id=%x\n", status, nlh->nlmsg_len,  
    65.                             ipq_packet->indev_name,  ipq_packet->data_len, ipq_packet->packet_id);  
    66.            }  
    67.       }  
    68.       return 0;  
    69. }  

    四、应用程序的测试

    1. 测试环境的建立

    (1)内核态:要求已编译的内核支持Netlink机制, 并进入内核源码目录net/ipv4/netfilter下,检查是否生成ip_queue.ko。如果有相应的文件,则确保该模块是否加载,没有加载的话,modprobe ip_queue进行加载

    (2)用户态:要求有已经安装iptables,并加上一条如下规则:

    iptables -I INPUT -p icmp -j QUEUE

    这里我们在INPUT链上开始处添加了一条对所有ICMP报文进行IP Queue的规则。通过添加不同的iptables规则,可以对不同的报文进行IP Queue。

     

    如果系统上没有安装iptables的话,那么可以用一个简单的内核模块来实现其功能。即在NF对应的Hook点上注册一个钩子函数,对于某种类型的数据包直接return NF_QUEUE即可。附件的源码中我提供了一个模块程序,在NF的PRE_ROUTING出注册了一个对所有ICMP报文return NF_QUEUE的模块。没有iptables的朋友可以使用这个小模块程序替代。

     

    注意:我这里所使用的内核为2.6.18.3。其他内核版本没有进行测试。不过,个人觉得如果用户态的应用程序应该在2.6上没有问题,只是提供的内核模块程序不能保证。

    2. 程序的测试

    搭建好上面提示的环境之后,可以对应用程序的源码进行编译:

    gcc libipq.c ipq_user.c -o ipq_user

    执行ipq_user:

    [root@localhost ipq_user]# ./ipq_user

    ipq_creat_handle success!

    ipq_set_mode: send bytes =44, range=1024

    随后,程序处于等待接受内核数据包的状态。我们从另外一台主机发送ping包到本地主机,然后看到终端的输出为:

    [root@localhost ipq_user]# ./ipq_user

    ipq_creat_handle success!

    ipq_set_mode: send bytes =44, range=1024

    recv bytes =148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=c2f9b500

    recv bytes =148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=cb0c8c00

    recv bytes =148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=c72aa920

    recv bytes =148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=c2f9b3c0

    Exit: No error

    从以上信息中可以看出:

    (1)用户态发送的模式设置信息的包长度为44 bytes;

    (2)接收到内核态发送的包长度为148bytes, 这和从收到的IP Queue包中保存的长度nlmsg_len一致。
    (3)ping包进入本地主机的eth0网口,报文的长度为60 bytes。这个实际的ping包的长度一致。

     

    好了,到现在为止,我们已经成功的通过程序接收到内核态发送的IP Queue数据包。我将在下一篇文章中讲解用户态对接收到报文的简单处理以及发送给内核的整个过程。

    文中源码请到作者blog下载。http://blog.chinaunix.net/uid-10167808-id-25951.html

    展开全文
  • • 在 Linux 2.4 版以后版本的内核中,几乎全部的...netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数
  • 现在的情况是,我建立这个Netfilter的机器,只用接收流量用作监控,不用对流量包做后续处理。...而且还要考虑到内核态和用户态的并行处理:内核接收数据包、传输给用户态用户态接受数据包,进行处理。
  • 本文分析用户态接收到IP Queue的数据包后,根据数据包的相关信息决定数据包的下一步处理,并将处理后的数据包和处理的结果传递到内核态。文中如有任何疏漏和差错,欢迎各位朋友指正。    本文欢迎自由转载,...

    本文分析用户态接收到IP Queue的数据包后,根据数据包的相关信息决定数据包的下一步处理,并将处理后的数据包和处理的结果传递到内核态。文中如有任何疏漏和差错,欢迎各位朋友指正。

     

          本文欢迎自由转载,但请标明出处,并保证本文的完整性。

          作者:Godbach

          日期:2009/02/19

         

    一、处理IP Queue数据报文的编程接口

    本文中涉及到的Netlink和IP Queue的基础知识、以及如何接收内核的数据包已经在Linux内核IP Queue机制的分析的第一篇文章《Linux内核IP Queue机制的分析(一)­——用户态接收数据包》(以下简称文一)中进行了讲解。这里不再赘述基础知识。

     

    随后讲到的测试程序实际上也是在原先的源码基础上进行修改而成的。唯一和文一中不同的地方就在于,我们会根据获取到的数据报文的内容,进行一些判断或修改等处理,然后将处理后的数据报文以及对报文的处理意见一并传回内核。

    因此,对于IP Queue报文的实现以及接口,都可以参考文一。这里将会对处理数据报文的接口做一个简单的介绍。

    文一中提到IP Queue机制中,用户态发到内核态的消息,其数据结构如下所示:

    typedef struct ipq_peer_msg {

          union {

               ipq_verdict_msg_t verdict;

               ipq_mode_msg_t mode;

          } msg;

    } ipq_peer_msg_t;

     

    可见用户态发到内核的消息有两类。一类是告诉“模式消息”,告诉内核我用户态需要得到数据包的哪些消息(只需要数据报文的概要信息或者是数据报文的内容也要),另外一类就是“断言消息”,即用户态对数据报文处理之后,发送给内核的消息。该消息的内容包含了上文提到的对数据报文的处理意见和报文

     

    “断言消息”的数据结构定义如下:

    另一子类即“断言消息”,其数据类型定义如下:

    [cpp] view plain copy
    1.   typedef struct ipq_verdict_msg {  
    2. unsigned int value;  
    3. unsigned long id;  
    4. size_t data_len;  
    5. unsigned char payload[0];  
    6. } ipq_verdict_msg_t;  

    其中,value是用户态程序回传给内核的对当前报文的处理意见,可以是NF_ACCEPT或NF_DROP等值。id则是用以区分报文的标识号,即内核传来的ipq_packet_msg_t结构中的packet_id。当用户态程序修改了当前报文以后,需要将报文重新传递回内核,此时,新的报文内容必须存储在payload的开始处,并由data_len指明新报文的长度。

    可见,ipq_verdict_msg_t包含了对当前报文的处理意见value和处理后的报文内容payload。

    另外一个socket通信中的数据结构struct msghdr,最终用户态处理完数据报文之后,就要通过调用sendmsg将该结构体传给内核:     

    [cpp] view plain copy
    1. struct msghdr {  
    2.             void         *msg_name;       /* optional address */  
    3.             socklen_t     msg_namelen;    /* size of address */  
    4.             struct iovec *msg_iov;        /* scatter/gather array */  
    5.             size_t        msg_iovlen;     /* # elements in msg_iov */  
    6.             void         *msg_control;    /* ancillary data, see below */  
    7.             socklen_t     msg_controllen; /* ancillary data buffer len */  
    8.             int           msg_flags;      /* flags on received message */  
    9.         };  

      这个结构体的内容可以分为四组。

        第一组是msg_name和msg_namelen,记录这个消息的名字,其实就是数据包的目的地址。通常,如果是在不同主机之间进行的通信,则msg_name是指向一个结构体struct sockaddr的指针。长度为16。但是我们这里用于本地主机上用户态和内核态的通信,msg_name实际指向的是struct sockaddr_nl结构体。

        第二组是msg_iov和msg_iovlen,记录这个消息的内容。msg_iov是一个指向结构体struct iovec的指针,实际上,确切地说,应该是一个结构体strcut iovec的数组。下面是该结构体的定义:

        struct iovec{

            void __user     *iov_base;

            __kernel_size_t iov_len;

        };

        iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度(即有多少个buff)。在我们的实例中,传送的IP Queue信息由两个部分组成:netlink的消息头struct nlmsghdr,IP Queue的消息ipq_peer_msg_t。 而IP Queue的消息主要是指断言消息和处理过的数据报文的内容。因此,我们的实例中使用了三个struct iovec分别存储nlmsghdr,ipq_peer_msg_t 和 数据报文的内容。

        第三组是msg_control和msg_controllen,它们可被用于发送任何的控制信息,在我们的例子中,没有控制信息要发送。暂时略过。

    第四组是msg_flags。其值即为传入的参数flags。raw协议不支持MSG_OOB标志,即带外数据。

    更详细的关于struct msghdr介绍请参考man手册 man 2 recv。同时,本文对该结构体相关成员的解释参考了如下连接上的内容,这里对原文的作者表示感谢:

    http://lameck.blog.163.com/blog/static/388113742008825104426803/

     

    二、一个实现处理并回传数据报文的用户态例程

    1. 用户态例程的功能及设计流程

          本为例程实现的功能是对用户态接收到的ICMP Echo Request 报文的内容(主要就是ICMP报文数据部分的长度)进行判断。如果是Windows系统发送的Requset,则告诉返回给内核NF_ACCEPT,接受该报文;否则,如果是其他系统发送的Request,则告诉返回给内核NF_DROP,丢弃该报文。

     

          同样,这里先总结一下用户态处理并回传数据报文的流程。实际上整个报文的接收部分和文一的流程是一样的,只是增加了对数据报文的处理和回传:

    (1)调用socket()创建一个地址类型为PF_NETLINK(AF_NETLINK)的套接字。该套接字使用SOCK_RAW方式传输数据,协议类型为NETLINK_FIREWALL,即使用IP Queue;

    (2)调用bind()将本地地址(Netlink通信双方使用该协议特有的地址格式,struct sockaddr_nl)绑定到已建立的套接字上;

    (3)调用sendto()发送相关的配置信息,告诉内核应用程序准备接受的是数据包的元数据,还是同时包括数据包本身;

    (4)调用recvfrom()接受内核态发送来的IP Queue报文;

    (5)获取数据报文的内容,根据内容做出相关处理,并确定报文的处理意见;

    (6)构造struct msghdr结构体(见上文对该结构体的分析),并调用sendmsg函数,将数据报文和处理意见发送到内核;

    (7)调用close()关闭套接字,结束通信。

     

          以上的流程中,第(1)~(4)和(7)和文一中的例程是一致的,仅有(5)和(6)是本文实例程序新增加的。这两个步骤的工作就是读取数据报文的内容,进行判断,然后通过调用ipq_set_verdict()函数对用户处理后的数据报文和处理意见封装成断言消息,并传回内核态。

     

    2. 相关函数的调用

    ipq_set_verdict()函数时将用户态的断言小心传递给内核的,该函数也是libipq.c中提供的,其原型和如下:

    [cpp] view plain copy
    1. int ipq_set_verdict(const struct ipq_handle *h,  
    2.                     ipq_id_t id,  
    3.                     unsigned int verdict,  
    4.                     size_t data_len,  
    5.                     unsigned char *buf)  
    6.    

    @: h是通过调用ipq_create_handle()返回的struct ipq_handle类型的结构体,用来存储IPv4 socket通信的fd,以及通信双方的地址;

    @:id即ipq_verdict_msg_t结构体中的id,上文已做解释;

    @:verdict,对报文的处理意见,NF_ACCEPT或NF_DROP等。

    @: data_len,传给内核的数据报文的长度;

    @: buf,传给内核的数据报文。

    具体的实现代码如下:

    [cpp] view plain copy
    1. {  
    2.       unsigned char nvecs;  
    3.       size_t tlen;  
    4.       struct nlmsghdr nlh;  
    5.       ipq_peer_msg_t pm;  
    6.       struct iovec iov[3];  
    7.       struct msghdr msg;  
    8.    
    9.       memset(&nlh, 0, sizeof(nlh));  
    10.       /*构造netlink message的头部*/  
    11.       nlh.nlmsg_flags = NLM_F_REQUEST;  
    12.       nlh.nlmsg_type = IPQM_VERDICT;  
    13.       nlh.nlmsg_pid = h->local.nl_pid;  
    14.       memset(&pm, 0, sizeof(pm));  
    15.       /*构造IP Queue断言消息的头部*/  
    16.       pm.msg.verdict.value = verdict;  
    17.       pm.msg.verdict.id = id;  
    18.       pm.msg.verdict.data_len = data_len;  
    19.       /*将netlink message的头部,IP Queue断言消息的头部以及数据报文的内核保存在struct iovec []中*/  
    20.       iov[0].iov_base = &nlh;  
    21.       iov[0].iov_len = sizeof(nlh);  
    22.       iov[1].iov_base = ±  
    23.       iov[1].iov_len = sizeof(pm);  
    24.       tlen = sizeof(nlh) + sizeof(pm);  
    25.       nvecs = 2;  
    26.     /*根据IP Queue断言消息头部中data_len,将处理后的数据报文的内容添加在断言消息头部的后面*/  
    27.       if (data_len && buf) {  
    28.            iov[2].iov_base = buf;  
    29.            iov[2].iov_len = data_len;  
    30.            tlen += data_len;  
    31.            nvecs++;  
    32.       }  
    33.       /*根据传入的参数以及上面已经构造好的struct iovec[],构造struct msghdr结构体 */  
    34.       msg.msg_name = (void *)&h->peer;  
    35.       msg.msg_namelen = sizeof(h->peer);  
    36.       msg.msg_iov = iov;  
    37.       msg.msg_iovlen = nvecs;  
    38.       msg.msg_control = NULL;  
    39.       msg.msg_controllen = 0;  
    40.       msg.msg_flags = 0;  
    41.       nlh.nlmsg_len = tlen;  
    42.       return ipq_netlink_sendmsg(h, &msg, 0);  
    43. }  


    ipq_set_verdict()函数最后调用了ipq_netlink_sendmsg()函数将封装好的struct msghdr结构体发送到内核。而ipq_netlink_sendmsg()函数其实只是对sendmsg系统调用的简单封装。该函数的实现仍然在libipq.c中,具体代码如下:

    [cpp] view plain copy
    1. static ssize_t ipq_netlink_sendmsg(const struct ipq_handle *h,  
    2.                                    const struct msghdr *msg,  
    3.                                    unsigned int flags)  
    4. {  
    5.       int status = sendmsg(h->fd, msg, flags);  
    6.       if (status < 0)  
    7.            ipq_errno = IPQ_ERR_SEND;  
    8.       return status;  
    9. }  
    10.    

    3. 用户态的例程——ipq_user_rw.c

    [cpp] view plain copy
    1. /* 
    2.  * ipq_usr_rw.c 
    3.  * 
    4.  * Testing program for processing IP Queue packets from kernel 2.6.18.3 
    5.  * 
    6.  * Dec 1, 2008  Godbach created. 
    7.  * Dec 10, 2008 Godbach modified. 
    8.  * 
    9.  * This program is free software; you can redistribute it and/or modify 
    10.  * it under the terms of the GNU General Public License as published by 
    11.  * the Free Software Foundation; either version 2 of the License, or 
    12.  * (at your option) any later version. 
    13.  * 
    14.  * This program is distributed in the hope that it will be useful, 
    15.  * but WITHOUT ANY WARRANTY; without even the implied warranty of 
    16.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    17.  * GNU General Public License for more details. 
    18.  * 
    19.  */  
    20. #include <stdio.h>  
    21. #include <stdlib.h>  
    22. #include <string.h>  
    23. #include <signal.h>  
    24. #include <netinet/ip_icmp.h>  
    25. #include <netinet/ip.h>  
    26. #include <arpa/inet.h>  
    27. #include "libipq.h"  
    28.    
    29. #define ETH_HDRLEN 14  
    30.    
    31. typedef struct{  
    32.       struct iphdr iph;  
    33.       struct icmphdr icmph;  
    34. } ip_icmp_packet_s;  
    35.    
    36. struct ipq_handle *h = NULL;  
    37.    
    38. static void sig_int(int signo)  
    39. {  
    40.       ipq_destroy_handle(h);  
    41.       printf("Exit: %s\n", ipq_errstr());  
    42.       exit(0);  
    43. }  
    44.    
    45. int main(void)  
    46. {  
    47.       unsigned char buf[1024];  
    48.       /* creat handle*/  
    49.       h = ipq_create_handle(0, PF_INET);  
    50.       if(h == NULL){  
    51.            printf("%s\n", ipq_errstr());  
    52.            return 0;  
    53.       }  
    54.       printf("ipq_creat_handle success!\n");  
    55.       /*set mode*/  
    56.       unsigned char mode = IPQ_COPY_PACKET;  
    57.       int range = sizeof(buf);  
    58.       int ret = ipq_set_mode(h, mode, range);  
    59.       printf("ipq_set_mode: send bytes =%d, range=%d\n", ret, range);  
    60.        
    61.       /*register signal handler*/  
    62.       signal(SIGINT, sig_int);  
    63.    
    64.       /*read packet from kernel*/  
    65.       int status;  
    66.       struct nlmsghdr *nlh;  
    67.       ipq_packet_msg_t *ipq_packet;  
    68.        
    69.       while(1){  
    70.            status = ipq_read(h, buf, sizeof(buf));  
    71.            if(status > sizeof(struct nlmsghdr))  
    72.            {  
    73.                  nlh = (struct nlmsghdr *)buf;  
    74.                  ipq_packet = ipq_get_packet(buf);  
    75.                  printf("recv bytes =%d, nlmsg_len=%d, indev=%s, datalen=%d, packet_id=%x\n", status, nlh->nlmsg_len,  
    76.                             ipq_packet->indev_name,  ipq_packet->data_len, ipq_packet->packet_id);  
    77.                  unsigned char payload[128];  
    78.                  memset(payload, 0x00, sizeof(payload));  
    79.                  memcpy(payload + ETH_HDRLEN, ipq_packet->payload, ipq_packet->data_len);  
    80.                  /*display packet data in hex including 14 bytes of ETH hdr(set by 0x00)*/  
    81.                  int i;  
    82.                  for(i = 0; i < ipq_packet->data_len + ETH_HDRLEN; i++){  
    83.                       if(i%16 == 0)  
    84.                             printf("00%.2x:  ", i);  
    85.                       printf("%.2x ", payload[i]);  
    86.                       if(i % 16 == 15)  
    87.                             printf("\n");  
    88.                  }  
    89.                  printf("\n");  
    90.                  /*convert data to struct pattern of icmp packet */  
    91.                  ip_icmp_packet_s *icmp_pkt = (ip_icmp_packet_s*)(payload + ETH_HDRLEN);  
    92.                  unsigned short ip_totlen = ntohs(icmp_pkt->iph.tot_len);  
    93.                  unsigned short iph_len = icmp_pkt->iph.ihl*4;  
    94. /*get icmp data len*/  
    95.                  unsigned short icmp_datalen = ip_totlen - iph_len - sizeof(struct icmphdr);  
    96.                  /*ICMP echo request from WINDOWS OS*/  
    97.                  if(icmp_datalen == 32)  
    98.                  {  
    99.                       ret = ipq_set_verdict(h, ipq_packet->packet_id, NF_ACCEPT,ipq_packet->data_len,payload + ETH_HDRLEN);  
    100.                       printf("Ping request from Win. Accepted!\n");  
    101.                  }     
    102.                  /*ICMP echo request from Linux OS*/  
    103.                  else  
    104.                  {  
    105.                       ret = ipq_set_verdict(h, ipq_packet->packet_id, NF_DROP,ipq_packet->data_len,payload + ETH_HDRLEN);  
    106.                       printf("Ping request from Linux.  Dropped!\n");  
    107.                  }  
    108.            }  
    109.       }  
    110.       return 0;  
    111. }  


    整个例程相对比较简单,也只是在文一的基础上添加了一部分代码。 但是,有两点需要解释一下:

    (1)该例程按照16进制的方式显式了数据报文的内容,每行16 bytes(模仿了Ethreal/Wireshark的数据报文的显式方式)。由于这里获取的报文内容是从IP层开始的,因此MAC头部的14个字节置为0x00.

    (2)通常Window系统下的Ping的 Echo Request报文数据长度为32bytes,Linux下为56 bytes。这里仅作为测试,就认为32 bytes的来自Windows,否则来自Linux。 该程序并不保证在任何与该判断有冲突的情形下正常工作。

    三、例程的测试

    1. 测试环境的建立

          本文中的测试环境和文一的完全一样。内核态要保证已经正确加载ip_queue.ko模块,用户态添加一条规则:

    iptables -I INPUT -p icmp -j QUEUE

          以确保所有进入本地的ICMP报文都进入到ip_queue模块处理。

     

    2. 例程的测试

    搭建好上面提示的环境之后,可以对应用程序的源码进行编译:

    gcc libipq.c ipq_user_rw.c -o ipq_user_rw

    执行ipq_user_rw:

    [root@localhost ipq_user]# ./ipq_user_rw

    ipq_creat_handle success!

    ipq_set_mode: send bytes =44, range=1024

    随后,程序处于等待接受内核数据包的状态。我们分别从Windows系统和Linux系统发送ping包到本地主机,然后看到终端的输出如下:

    recv bytes =148, nlmsg_len=148, indev=eth1, datalen=60, packet_id=d657c4c0

    0000:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 45 00

    0010:  00 3c 42 a6 00 00 80 01 ae 89 0a 01 1a e5 0a 01

    0020:  1a ab 08 00 29 5c 02 00 22 00 61 62 63 64 65 66

    0030:  67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76

    0040:  77 61 62 63 64 65 66 67 68 69

    Ping request from Win. Accepted!

    recv bytes =172, nlmsg_len=172, indev=eth1, datalen=84, packet_id=d6369860

    0000:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 45 00

    0010:  00 54 00 00 40 00 40 01 f1 32 0a 01 1a ca 0a 01

    0020:  1a ab 08 00 9e 36 8f 4e 00 04 ae 0c a2 49 83 1d

    0030:  0c 00 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15

    0040:  16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25

    0050:  26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35

    0060:  36 37

    Ping request from Linux.  Dropped!

     

    以上输出中可以分别看到Windows和Linux发送的Ping包的报文内容。同时,可以分别在发送ping包的Windows和Linux下观察是否有echo reply包,或者进行抓包,看能否收到echo reply包。很显然,Windows下有echo reply包,而Linux下没有。

     

    四、总结

          本文的例程演示了用户态如何接受内核态的IP Queue报文,如何处理报文并回传到内核。由此看出,IP Queue机制的一个应用场合,就是需要根据报文的数据部分,以决定是否修改该数据报文的内容,以及告诉内核接受或丢弃该报文的情形。

    也许读者会有疑问:对于这种情形,为什么不在内核态直接进行处理呢?可以从两个方面进行解释:其一、从内核的设计上来看,内核统一对所有报文的处理主要集中在网络层,内核基本上也只关心报文的IP头部,并根据该报文采用什么样的协议再交付下一层处理。经过传输层的传输,报文就到了应用层了,应用层来处理报文的应用数据也更符合逻辑;其二、相对于内核态,用户态有更丰富和更高效的工具来对报文的数据部分进行处理。

    此外,用户态对数据报文处理的时候要注意两个方面:

    (1)网络字节序和主机字节序之间的相互转换问题。从内核得到的数据报文是网络字节,本地处理的时候要考虑到转换的问题。本地转换之后,构成新的报文时,还要考虑主机字节序到网络字节序的转换;

    (2)本文的例程只是简单的根据报文的内容进行了判断,并未修改报文的内容。实际应用中,如果一旦修改了报文的内容,就考虑对应调整各个部分的校验和问题。读者要熟悉校验和的计算方法,以及各个层计算校验和时包含了数据报文的哪些部分。

    展开全文
  • 当应用程序调用send()等一系列系统调用向UDP套接字写数据时,最终都会调用到传输层的udp_sendmsg(),这篇笔记重点分析下UDP协议是如何将用户态数据封装成skb递交给IP层的。 要点说明 在分析代码之前,有必要对一些...

     

    目录

    1 UDP数据包发送概述

    1.1 MSG_MORE标记

    1.2 socket操作

    2 UDP数据报发送 udp_sendmsg()

    2.1 发送数据报给IP层(填充udp层信息) udp_push_pending_frames()

    2.2 清除发送队列 udp_flush_pending_frames()

    3 小结


    1 UDP数据包发送概述

    当应用程序调用send()等一系列系统调用向UDP套接字写数据时,最终都会调用到传输层的udp_sendmsg(),这篇笔记重点分析下 UDP 协议是如何将用户态数据封装成 skb 递交给IP层的。在分析代码之前,有必要对一些 UDP 的写操作过程中的一些关键点进行说明,否则会看的晕头转向。

    1.1 MSG_MORE标记

    UDP数据报不像TCP,它是有边界的,即发送端的一个UDP数据报会完整的被接收端以一个UDP数据报的方式接收。

    然而,并非一次写操作对应一个UDP数据报,应用程序可以通过MSG_MORE标记或者UDP_CORK选项将多次写操作的数据合并成一个UDP数据报发送。具体操作流程如下:

    1. 在调用sendmsg()时,flag参数中设置MSG_MORE标记,表示还有更多数据要发送。应用期望内核收到设置了该标记的数据时先不要将本次递交的数据发送给IP层,而是将其缓存,并且将后面连续的设定了该标记的数据合并成同一个UDP报文(一个IP报文,但是可能是多个IP片段)。直到没有设定该标记的发送时,将数据报发送给IP层;
    2. 类似的,在使能和关闭UDP_CORK选项期间发送的所有数据也要组合成一个UDP报文发送给IP。

    注意:应用程序在使用这种方式的时候必须要注意多次组合的数据最好不要超过MTU,否则IP层就不得不将这些要组合的数据分成多个IP数据包发送出去,这样会造成性能的下降。

    1.2 socket操作

    用户态调用 socket() 创建 UDP 套接字后,有两种方式可以发送数据:

    1. 直接调用sendto()或者sendmsg(),在这些函数的参数中指定目的地址;
    2. 先调用connect()将UDP套接字和一个目的地址绑定,这时除了上面这两个接口以外,还可以调用write()、send()等没有目的地址参数的接口发送数据。当然,UDP是一个无连接的协议,这里的connect()仅仅是本机内部行为,不会有任何数据报发送出去的。

    2 UDP数据报发送 udp_sendmsg()

    udp_sendmsg 核心流程如下:

    1. 确定路由,如果该sock没有调用connect进行连接,每次调用都需要重复查询路由,已连接直接获取路由即可
    2. 通过 ip_append_data() 将待发送数据组织成 skb 排到发送队列中;
    3. 如果非 pending 状态,通过 udp_push_pending_frames() 直接将发送队列中的所有数据发送给IP层;
    @iocb: 为异步IO预留扩展,暂不关注
    @sk:传输控制块
    @msg:包含了用户空间要发送的数据
    @len:要发送的数据长度
    int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len)
    {
    	struct inet_sock *inet = inet_sk(sk);
    	struct udp_sock *up = udp_sk(sk);
    	// 本次发送要发送的数据长度,包括UDP首部
    	int ulen = len;
    	// UDP的一些控制信息,如IP选项,UDP将这些信息传递给ip_append_data()
    	struct ipcm_cookie ipc;
    	struct rtable *rt = NULL;
    	int free = 0;
    	int connected = 0;
    	__be32 daddr, faddr, saddr;
    	__be16 dport;
    	u8  tos;
    	// UDPlite协议暂不关注,下面只看UDP协议的流程处理
    	int err, is_udplite = IS_UDPLITE(sk);
    	// corkreq表示本次发送是否要按照上面MSG_MORE部分介绍的方式仅仅组织报文,而不发送;
    	// up->corkflag标识了是否设置了UDP_CORK选项
    	int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
    	int (*getfrag)(void *, char *, int, int, int, struct sk_buff *);
    
    	// UDP首部的长度字段只有16bit,所以一个UDP数据报的数据部分长度不能超过65535字节
    	if (len > 0xFFFF)
    		return -EMSGSIZE;
    
    	/*
    	 *	Check the flags.
    	 */
    	// UDP不支持带外数据,所以不能设置MSG_OOB
    	if (msg->msg_flags & MSG_OOB)	/* Mirror BSD error message compatibility */
    		return -EOPNOTSUPP;
    
    	ipc.opt = NULL;
    	// pending标记和前面说的MSG_MORE标记有关。当设置MSG_MORE标记的数据到达时,UDP会将待
    	// 发送的数据暂存到发送队列中,这些数据就处于pending状态,等标记取消时,会将数据发送
    	// 给IP,然后清空发送队列,这时退出pending状态。
    	if (up->pending) {
    		/*
    		 * There are pending frames.
    		 * The socket lock must be held while it's corked.
    		 */
    		lock_sock(sk)
    		 // 再判断一次是因为了lock_sock()可能会导致进程休眠。内核中有许多地方使用这样的方式编程。
    		 // 因为大部分情况下pending标记是没有的,这样的话就不会进入到这里,这种编程方式就可以省掉
    		 // 一个lock_sock(比较复杂、耗时)调用,仅当设置了pending后,才加锁并再检查一次,这样就
    		 // 能在大部分情况下不用锁,这种方法是内核中常用的提升效率的编程技巧之一。
    		if (likely(up->pending)) {
    			// pengding的值只能是0或者AF_INET
    			if (unlikely(up->pending != AF_INET)) {
    				release_sock(sk);
    				return -EINVAL;
    			}
    			// 因为已经有挂起的数据,所以可以不用再次进行地址、路由的选择,直接跳转到do_append_data
    			// 处追加数据即可。因为如果有pending标记,这些工作在处理第一次发送操作时已经完成了
    			goto do_append_data;
    		}
    		release_sock(sk);
    	}
    	// 能到这里,无论是否是MSG_MORE方式的发送,一定是一个UDP报文的第一次发送,所以发送的长度加上UDP首部8个字节
    	ulen += sizeof(struct udphdr);
    
    	// 下面这段逻辑是确定目的端IP地址和端口号
    
    	if (msg->msg_name) {
    		// msg_name不为空,表示用户空间程序本次调用指定了目的地址信息,这种
    		// 情况下校验指定的地址参数并设置地址族、目的地址和目的端口
    		
    		// 目的地址必须是IPv4地址格式:struct sockaddr_in
    		struct sockaddr_in * usin = (struct sockaddr_in*)msg->msg_name;
    		if (msg->msg_namelen < sizeof(*usin))
    			return -EINVAL;
    		// 地址族必须是AF_INET或者AF_UNSPEC
    		if (usin->sin_family != AF_INET) {
    			if (usin->sin_family != AF_UNSPEC)
    				return -EAFNOSUPPORT;
    		}
    		// 目的IP和目的端口
    		daddr = usin->sin_addr.s_addr;
    		dport = usin->sin_port;
    		// 目的端口不能为0
    		if (dport == 0)
    			return -EINVAL;
    	} else {
    		// 用户空间程序本次调用没有指定目的地址,那么需要检查之前是否已经connect()成功,
    		// 否则就是一次失败的调用,因为内核不知道应该将数据发送给谁
    		
    		// 如果之前connect()成功,那么UDP传输控制块的状态一定是TCP_ESTABLISHED
    		if (sk->sk_state != TCP_ESTABLISHED)
    			return -EDESTADDRREQ;
    		// 之前connect()成功过程中会将目的端地址信息保存在inet_sock结构中
    		daddr = inet->daddr;
    		dport = inet->dport;
    		/* Open fast path for connected socket. Route will not be used, if at least one option is set. */
    		// 已连接标记置1
    		connected = 1;
    	}
    
    	ipc.addr = inet->saddr;
    	ipc.oif = sk->sk_bound_dev_if;
    	// 如果发送数据时指定了控制信息(sendmsg()系统调用),用的极少,暂不关注
    	if (msg->msg_controllen) {
    		err = ip_cmsg_send(msg, &ipc);
    		if (err)
    			return err;
    		if (ipc.opt)
    			free = 1;
    		connected = 0;
    	}
    	if (!ipc.opt)
    		ipc.opt = inet->opt;
    
    	saddr = ipc.addr;
    	ipc.addr = faddr = daddr;
    	// 源路由选项相关处理,先忽略
    	if (ipc.opt && ipc.opt->srr) {
    		if (!daddr)
    			return -EINVAL;
    		faddr = ipc.opt->faddr;
    		connected = 0;
    	}
    	// tos默认来自inet->tos,后者又可以通过socket选项进行设置
    	tos = RT_TOS(inet->tos);
    	// 如果设置了SOCK_LOCALROUTE或者发送时设置了MSG_DONTROUTE标记,再或者IP选项中存在严格源站选路
    	// 选项,则说明目的地址或下一跳必然位于本地子网中。此时需要设置tos中的RTO_ONLINK标记,表示
    	// 后续查找路由时与目的地直连。
    	if (sock_flag(sk, SOCK_LOCALROUTE) || (msg->msg_flags & MSG_DONTROUTE) || (ipc.opt && ipc.opt->is_strictroute)) {
    		tos |= RTO_ONLINK;
    		connected = 0;
    	}
    
    	// 多播地址处理,先忽略
    	if (ipv4_is_multicast(daddr)) {
    		if (!ipc.oif)
    			ipc.oif = inet->mc_index;
    		if (!saddr)
    			saddr = inet->mc_addr;
    		connected = 0;
    	}
    
    	// 对于已经连接的情况,之前一定已经查询过路由了,这里需要检查该路由是否依然有效
    	if (connected)
    		rt = (struct rtable*)sk_dst_check(sk, 0);
    	if (rt == NULL) {
    		// 没有路由信息,这里路由表查询条件有:输出设备接口、源和目的IP、TOS、源和目的端口
    		struct flowi fl = {
            	.oif = ipc.oif,
    			.nl_u = {
                	.ip4_u = {
                    	.daddr = faddr,
    					.saddr = saddr,
    					.tos = tos
                    }
                },
    			.proto = sk->sk_protocol,
    			.uli_u = {
                	.ports = {
                    	.sport = inet->sport,
    					.dport = dport
                    }
               }
           };
    		security_sk_classify_flow(sk, &fl);
    		// 调用发送报文的路由表查询接口进行路由查询
    		err = ip_route_output_flow(&init_net, &rt, &fl, sk, 1);
    		// 查询失败、发送失败
    		if (err) {
    			if (err == -ENETUNREACH)
    				IP_INC_STATS_BH(IPSTATS_MIB_OUTNOROUTES);
    			goto out;
    		}
    		err = -EACCES;
    		// 路由结果为广播但是该socket不允许广播,发送失败
    		if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST))
    			goto out;
    		// 如果是已连接套接字,那么将路由信息设置到套接字,下次检查即可,不用重复查询,见上面,
    		// 这里可以解释为何一个未连接的udp套接字,可以每次通过指定不同的目的地址进行报文发送,
    		// 因为未连接情况下,每次都会重新查路由,但是不会将路由查询结果设置到TCB中
    		if (connected)
    			sk_dst_set(sk, dst_clone(&rt->u.dst));
    	}
    
    	// MSG_CONFIRM表示该报文要求接收端的数据链路层进行确认,用的很少,忽略
    	if (msg->msg_flags&MSG_CONFIRM)
    		goto do_confirm;
    back_from_confirm:
    	// 最后确定要使用的源和目的
    	saddr = rt->rt_src;
    	if (!ipc.addr)
    		daddr = ipc.addr = rt->rt_dst;
    
    	lock_sock(sk);
    	// 这种情况不应该出现
    	if (unlikely(up->pending)) {
    		/* The socket is already corked while preparing it. */
    		/* ... which is an evident application bug. --ANK */
    		release_sock(sk);
    
    		LIMIT_NETDEBUG(KERN_DEBUG "udp cork app bug 2\n");
    		err = -EINVAL;
    		goto out;
    	}
    	/*
    	 *	Now cork the socket to pend data.
    	 */
    	// 将一些重要信息暂存到inet->cork中,以备可能存在的后续发送过程使用
    	inet->cork.fl.fl4_dst = daddr;
    	inet->cork.fl.fl_ip_dport = dport;
    	inet->cork.fl.fl4_src = saddr;
    	inet->cork.fl.fl_ip_sport = inet->sport;
    	// 下面就要将待发送数据放入发送队列了,先设置pending标记
    	up->pending = AF_INET;
    
    do_append_data:
    	// up->len变量记录了当前该传输控制块上已经pending的字节数,这里将ulen累加到该变量上
    	up->len += ulen;
    	// 根据是否为UDPlite选用不同的拷贝函数,这两个协议公用一套函数,但是因为校验和计算方法
    	// 有差别,而且可能需要在拷贝过程中顺便计算校验和(这样可以避免再次遍历数据),所以这里需要区分
    	getfrag  =  is_udplite ?  udplite_getfrag : ip_generic_getfrag;
    	// ip_append_data()很重要,而且足够复杂,它属于IP层提供给上层协议使用的一个发送接口,目前
    	// 主要由UDP和raw套接字使用,该函数后面会单独分析,这里只需要知道如下几点:
    	// 1. 该函数将要发送的数据按照MTU大小分割成若干个方便IP处理的片段,每个片段一个skb;并且这些
    	//    skb会放入到套接字的发送缓冲区中;
    	// 2. 该函数只是组织数据包,并不执行发送动作,如果需要发送,需要由调用者主动调用ip_push_frames()
    	// 3. 该函数处理成功返回0,失败返回错误码
    	err = ip_append_data(sk, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, rt,
    			corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
    	// 数据包处理失败,将所有数据包清空,见下文
    	if (err)
    		udp_flush_pending_frames(sk);
    	// 数据包处理没有问题,并且没有启用MSG_MORE特性,那么直接将发送队列中的数据发送给IP。
    	// udp_push_pending_frames()会将up->pending标记清空,这点非常重要;
    	// 对于大多数应用都是走了该分支,即一次写操作对应一个UDP数据包,从发送行为上讲,这种UDP
    	// 套接字相当于没有发送缓冲区
    	else if (!corkreq)
    		err = udp_push_pending_frames(sk);
    	// 这种情况不大可能发生,除非应用程序指定要发送的数据长度为0
    	else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
    		up->pending = 0;
    	release_sock(sk);
    
    out:
    	// 释放对路由缓存的引用
    	ip_rt_put(rt);
    	if (free)
    		kfree(ipc.opt);
    	// 处理过程没有错误,返回已发送的字节数
    	if (!err)
    		return len;
    	/*
    	 * ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space.  Reporting
    	 * ENOBUFS might not be good (it's not tunable per se), but otherwise
    	 * we don't have a good statistic (IpOutDiscards but it can be too many
    	 * things).  We could add another new stat but at least for now that
    	 * seems like overkill.
    	 */
    	if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
    		UDP_INC_STATS_USER(UDP_MIB_SNDBUFERRORS, is_udplite);
    	}
    	return err;
    
    do_confirm:
    	// 确认处理,用的很少,忽略
    	dst_confirm(&rt->u.dst);
    	if (!(msg->msg_flags&MSG_PROBE) || len)
    		goto back_from_confirm;
    	err = 0;
    	goto out;
    }
    

    2.1 发送数据报给IP层(填充udp层信息) udp_push_pending_frames()

    如注释所述,该函数会将当前所有pending的所有skb作为一个UDP数据报发送出去,它实际上是ip_push_pending_frames()的包装,该函数主要干了两件事:

    1. 填充UDP报文首部信息,包括校验和的计算;
    2. 调用ip_push_frames()将所有skb发送给IP层继续处理;
    /*
     * Push out all pending data as one UDP datagram. Socket is locked.
     */
    static int udp_push_pending_frames(struct sock *sk)
    {
    	struct udp_sock  *up = udp_sk(sk);
    	struct inet_sock *inet = inet_sk(sk);
    	struct flowi *fl = &inet->cork.fl;
    	struct sk_buff *skb;
    	struct udphdr *uh;
    	int err = 0;
    	int is_udplite = IS_UDPLITE(sk);
    	__wsum csum = 0;
    
    	/* Grab the skbuff where UDP header space exists. */
    	// 获取发送队列中第一个SKB的指针,注意是获取,并不会将该skb从发送队列上摘除,
    	// 这里获取是为了填充UDP首部,真正的出队列操作由ip_push_pending_frames()执行。
    	// 发送队列中此时可能有多个skb,每个skb携带的数据为一个MTU大小,这是由前面的
    	// ip_append_data()处理好的,目的是方便IP层的后续处理
    	if ((skb = skb_peek(&sk->sk_write_queue)) == NULL)
    		goto out;
    
    	// 组装UDP首部各个字段
    	uh = udp_hdr(skb);
    	uh->source = fl->fl_ip_sport;
    	uh->dest = fl->fl_ip_dport;
    	uh->len = htons(up->len);
    	uh->check = 0;
    
    	// 计算数据报的校验和
    	if (is_udplite)  /*     UDP-Lite      */
    		csum  = udplite_csum_outgoing(sk, skb);
    	else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {   /* UDP csum disabled */
    		skb->ip_summed = CHECKSUM_NONE;
    		goto send;
    	} else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */
    		udp4_hwcsum_outgoing(sk, skb, fl->fl4_src,fl->fl4_dst, up->len);
    		goto send;
    	} else	/*   `normal' UDP    */
    		csum = udp_csum_outgoing(sk, skb);
    
    	// 添加伪首部校验和计算
    	uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst, up->len, sk->sk_protocol, csum);
    	if (uh->check == 0)
    		uh->check = CSUM_MANGLED_0;
    
    send:
    	// 调用ip_push_pending_frames()函数将数据报发送出去。这些数据报虽然可能是由多个片段组成,
    	// 而且每个片段都达到了MTU大小,但是它们共用一个ipid,表明它们属于同一个IP报文,只是分段了而已
    	err = ip_push_pending_frames(sk);
    out:
    	// 无论成功与否,发送队列中不再有数据,所以清空len和pending标记
    	up->len = 0;
    	up->pending = 0;
    	if (!err)
    		UDP_INC_STATS_USER(UDP_MIB_OUTDATAGRAMS, is_udplite);
    	return err;
    }
    

    2.2 清除发送队列 udp_flush_pending_frames()

    如前面udp_sendmsg()函数分析,如果ip_append_data()调用失败,那么会调用该函数将发送队列中的所有skb全部丢弃,并且清除pending标记。

    /*
     * Throw away all pending data and cancel the corking. Socket is locked.
     */
    static void udp_flush_pending_frames(struct sock *sk)
    {
    	struct udp_sock *up = udp_sk(sk);
    
    	if (up->pending) {
    		up->len = 0;
    		up->pending = 0;
    		// 调用IP层的flush接口
    		ip_flush_pending_frames(sk);
    	}
    }
    

    3 小结

    从上面的代码分析过程中可以看出,UDP的发送过程还是相当直接的,它几乎不缓存应用写入的数据,直接将这些数据组装成UDP数据报,然后丢给IP处理。

    展开全文
  • 网卡在接受数据包时会产生中断,即当 有一个以太网帧到来时,网卡向内核产生一次中断...当CPU 空闲的时候,软中断被调用,操作系统从sk_buffer 队列中取出数据包,将它传送至用户态缓冲区,提供给应用程 序使用。 ...
  • Linux的系统运行分为用户态内核态,内核态控制着系统资源。通过定时器进行调度,把cpu分配给用户进程使用;通过中断来响应外设请求;并有一定的框架来管理内存、文件、网络等。 系统调用流程如下: 1.2 ...
  • PPPOE(Point.to.Point Protoeol over Ethernet)是将PPP协议封装在以太网帧上进行传输,它的通信过程分为探测... 传统的PPPoE先会用Raw socket读取数据,然后采用用户态程序对其封包解包,然后再发送给内核。但是
  • Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统...
  • Linux strace命令

    2014-01-02 15:22:15
    Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统...
  • Linux之strace命令

    千次阅读 2013-06-12 21:19:57
    Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用...
  • strace是个功能强大的Linux调试分析诊断工具,可用于跟踪程序执行时进程系统调用(system call)...当进程需要访问硬件设备(如读取磁盘文件或接收网络数据等)时,必须由用户态模式切换至内核态模式,通过系统调用访问...
  • Linux 世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。strace 可以跟踪到一个进程产生的系统...
  • linux strace

    2016-12-06 18:03:00
    Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用...
  • Linux_strace

    2019-05-15 14:30:04
    strace是个功能强大的Linux调试分析诊断工具,可用于跟踪程序执行时进程系统调用...当进程需要访问硬件设备(如读取磁盘文件或接收网络数据等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。s...
  • Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统...
  • Linux 面试常考命令

    2020-12-18 20:29:00
    参考文章: ... 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strac...
  • Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统...

空空如也

空空如也

1 2 3 4 5 ... 15
收藏数 284
精华内容 113
关键字:

linux内核接收用户态数据

linux 订阅