clinuxnetwork-programminglinux-kernelnetfilter

Netfilter hook stateful connection packet filtering


I am writing a Netfilter hook and want to do a stateful analysis of incoming TCP packets, whether they belong to an existing connection or a new connection is starting. This is my first try at writing code using Netfilter and after reading https://people.netfilter.org/pablo/docs/login.pdf I understand I need to check if a packet is categorized as a NEW or ESTABLISHED state. But I cannot find any documentation of how to write code for this.

static unsigned int hfunc(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) {
    struct iphdr *iph;
    struct udphdr *udph;
    if (!skb)
        return NF_ACCEPT;

    iph = ip_hdr(skb);
    if (iph->protocol == IPPROTO_TCP) {
        /*
        if packet SYN flag is enabled and state==NEW:
            return NF_ACCEPT
        else if SYN flag is disabled and state==NEW:
            return NF_DROP
        */      
    }
    return NF_ACCEPT
}

static int __init my_net_module_init(void) {
    printk(KERN_INFO "Initializing my netfilter module\n");

    // Allocating memory for hook structure.
    my_nf_hook = (struct nf_hook_ops*) kzalloc(sizeof(struct nf_hook_ops), GFP_KERNEL);

    // Constructing the structure
    my_nf_hook->hook    = (nf_hookfn*)hfunc;        /* hook function */
    my_nf_hook->hooknum     = NF_INET_PRE_ROUTING;      /* received packets */
    my_nf_hook->pf  = PF_INET;                      /* IPv4 */
    my_nf_hook->priority    = NF_IP_PRI_FIRST;          /* max hook priority */

    nf_register_net_hook(&init_net, my_nf_hook);
    return 0;
}

static void __exit my_net_module_exit(void) {

    nf_unregister_net_hook(&init_net, my_nf_hook);
    kfree(my_nf_hook);
    printk(KERN_INFO "Exiting my netfilter module\n");
}

module_init(my_net_module_init);
module_exit(my_net_module_exit);

Edit: Added code snippet for registering hook in pre-routing.


Solution

  • Seems that in your hook you want to make a decision on packet based on conntrack(CT) info about the connection state - to block (drop) all the TCP packets which are in the middle of connection, i.e. packets both without SYN flag and without connection entry in CT.

    So if you want to reap the benefits of CT, you have to let him work a bit.
    Now your hook is in NF_INET_PRE_ROUTING with NF_IP_PRI_FIRST priority. Just look at the picture of Linux kernel packet flow. If we talk about pre-routing chain CT-handling is somewhere after RAW table (i.e. with a lower priority).
    The list of priorities you can see here:

    enum nf_ip_hook_priorities {
        NF_IP_PRI_FIRST = INT_MIN,
        NF_IP_PRI_CONNTRACK_DEFRAG = -400,
        NF_IP_PRI_RAW = -300,
        NF_IP_PRI_SELINUX_FIRST = -225,
        NF_IP_PRI_CONNTRACK = -200,
        NF_IP_PRI_MANGLE = -150,
        NF_IP_PRI_NAT_DST = -100,
        NF_IP_PRI_FILTER = 0,
        NF_IP_PRI_SECURITY = 50,
        NF_IP_PRI_NAT_SRC = 100,
        NF_IP_PRI_SELINUX_LAST = 225,
        NF_IP_PRI_CONNTRACK_HELPER = 300,
        NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
        NF_IP_PRI_LAST = INT_MAX,
    };
    

    Thus to stick in after CT (after nf_conntrack_in()) you must register your hook with priority lower than NF_IP_PRI_CONNTRACK (i.e. with greater number, e.g. -50).
    So you do:

    static struct nf_hook_ops hooks[] __read_mostly = {
         {    
              .hook = hfunc,
              .pf = PF_INET,
              .hooknum = NF_INET_PRE_ROUTING,
              .priority = NF_IP_PRI_CONNTRACK + 150
         },
         // ...
    };
    // ...
    int ret;
    ret = nf_register_hooks(hooks, ARRAY_SIZE(hooks));
    if (ret < 0)
        // error
    

    Then you should access the CT info from within your hook:

    static unsigned int hfunc(void *priv, struct sk_buff *skb,
                              const struct nf_hook_state *state) {
        struct iphdr *iph;
    
        iph = ip_hdr(skb);
        if (iph->protocol == IPPROTO_TCP) {
            struct nf_conn *ct;
            enum ip_conntrack_info ctinfo;
            struct tcphdr *tcph;
    
            ct = nf_ct_get(skb, &ctinfo);
            if (!ct)
                return NF_ACCEPT;
    
            tcph = tcp_hdr(skb)
            if (tcph->syn) { // && !tcph->ack ???
                if (ctinfo == IP_CT_NEW)
                    return NF_ACCEPT;
            } else {
                if (ctinfo == IP_CT_NEW)
                    return NF_DROP;
            }      
        }
        return NF_ACCEPT
    }
    

    Also remember that CT must be involved in your Linux kernel network processing. There should be CT modules inserted into kernel and an appropriate iptables rule added.