Skip to main content

9.2 Netfilter Hooks

Let's turn our focus back inside the kernel Network Stack.

In the previous section, we mentioned that the core of Netfilter is a "vast net" spread across the network stack. Now we need to see exactly which key stakes this net is hung on.

When we discussed the send and receive paths of IPv4 and IPv6, we actually passed by these stakes multiple times—we were just moving too fast at the time to stop and take a closer look. Netfilter defines five key hook points in the network stack. Whether for IPv4 or IPv6, the names and meanings of these hook points are unified. They form the universal skeleton for the kernel to intercept and modify traffic.

Below are the exact locations of these five hooks, each corresponding to a specific moment in a packet's life.

Five Checkpoints for Packets

Imagine a packet as a train speeding along a railway. Netfilter's hooks are the five checkpoints on that railway.

NF_INET_PRE_ROUTING: This is the first stop for all inbound trains.

  • Location: Inside the ip_rcv() method for IPv4 and the ipv6_rcv() method for IPv6.
  • Timing: This is the first hook a packet hits after entering the network stack. At this point, the kernel has just received the packet and hasn't even looked up the routing table yet.
  • Significance: Intercepting here means you get the first look at a packet before the kernel decides where to send it. If you want to build a "universal" packet catcher, this is the best spot—because whether the packet is destined for the local machine or being forwarded, it must pass through here first.

NF_INET_LOCAL_IN: This is a stop only for trains destined for the local machine.

  • Location: Inside the ip_local_deliver() method for IPv4 and the ip6_input() method for IPv6.
  • Prerequisite: The packet must have first passed through PRE_ROUTING and a routing subsystem lookup (confirming the destination is local).
  • Significance: Only packets truly intended for local upper-layer applications will reach here. If you only care about "things sent to me from the outside," this is where you should intercept.

NF_INET_FORWARD: This is the dedicated line for transit trains.

  • Location: Inside the ip_forward() method for IPv4 and the ip6_forward() method for IPv6.
  • Prerequisite: It also passed through PRE_ROUTING and a routing lookup, but the result is "this packet needs to be forwarded to someone else."
  • Significance: This is the core path for Linux acting as a router. If you want to control the machine's forwarding behavior (like a firewall rejecting forwards), this is the mandatory passage.

NF_INET_POST_ROUTING: This is the last stop for all outbound trains.

  • Location: Inside the ip_output() method for IPv4 and the ip6_finish_output2() method for IPv6.
  • Traffic sources: The situation here is slightly more complex. Two types of trains merge here:
    1. Forwarded trains: They just passed through the FORWARD checkpoint.
    2. Locally generated trains: They just passed through the LOCAL_OUT checkpoint.
  • Significance: No matter where the packet came from, as long as it's about to leave this machine, the last chance to intercept and modify it is right here. Source Network Address Translation (SNAT) typically happens here.

NF_INET_LOCAL_OUT: This is the starting station for locally generated trains.

  • Location: Inside the __ip_local_out() method for IPv4 and the __ip6_local_out() method for IPv6.
  • Timing: After a locally originating packet from a process goes through a routing lookup to determine which exit to take, but before it actually enters the transmit queue.
  • Significance: This is the first checkpoint for packets sent by local applications. If you want to restrict a local process's outbound access or mark outgoing packets, this is the starting point.

Returning to the "train" analogy: The order of these five checkpoints isn't random; it's strictly dictated by the physical connections of the railway.

  • Locally sent packets only pass through LOCAL_OUT -> POST_ROUTING.
  • Packets destined for the local machine only travel PRE_ROUTING -> LOCAL_IN.
  • Forwarded packets have the longest journey: PRE_ROUTING -> FORWARD -> POST_ROUTING. If you wait at LOCAL_IN for a forwarded packet, you'll wait forever—it's physically disconnected.

How the Kernel "Pulls Over" Traffic at These Points

Conceptual checkpoints alone aren't enough; the kernel needs a mechanism to actually attach code at these points. This mechanism is the NF_HOOK macro we mentioned in earlier chapters.

This macro is hardcoded into the critical paths of the network stack (that is, inside those ip_rcv, ip_forward, and other functions mentioned above). Its definition is located at include/linux/netfilter.h:

static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct sk_buff *))
{
return NF_HOOK_THRESH(pf, hook, skb, in, out, okfn, INT_MIN);
}

These few short lines of code are actually the bridge connecting the Network Stack and the Netfilter subsystem. Let's look at its parameters, each carrying critical information:

  • pf: This asks "which protocol family?" It's usually NFPROTO_IPV4 or NFPROTO_IPV6. Netfilter needs to know this because while the processing logic for different protocols shares commonalities, the details differ.
  • hook: This is one of the five points we discussed above (such as NF_INET_PRE_ROUTING). It tells the kernel "which checkpoint to hang the code on right now."
  • skb: Needless to say, this is the "suspect" being escorted under guard (the packet).
  • in: The inbound network device. If the packet came from the outside, this is the net_device structure corresponding to that network interface.
  • out: The outbound network device. Note a common pitfall here: At certain stages (like PRE_ROUTING), the kernel hasn't made a routing decision yet and has no idea which interface the packet will leave from, so this parameter will be NULL. Never try to dereference it at this point, or you'll trigger a direct panic.
  • okfn: This is a "if all goes well, where to next" callback function pointer. If all hooks tell this packet "let it pass (NF_ACCEPT)," the kernel will call this function, allowing the packet to continue its original journey through the network stack.

The "Verdict Power" of Hook Functions

When your hook function is called, you hold the packet's fate in your hands. You must return a "verdict value" telling the kernel what to do next. These return values are defined in include/uapi/linux/netfilter.h:

  • NF_DROP (0): Death sentence. Drop the packet immediately without leaving a trace (aside from your own logs, of course). The sender won't receive any ICMP error; it's as if the packet fell into a black hole.
  • NF_ACCEPT (1): Let it pass. This packet is fine, or I'm done processing it, so let it continue on its original path.
  • NF_STOLEN (2): Hijack. This is an interesting verdict. It tells the kernel "forget about this packet, I've stolen it." Subsequent network stack code will never see this packet. This means your module is now fully responsible for handling this packet—either you send it out yourself, or you free it. If you steal a packet but don't free it, you'll cause a memory leak.
  • NF_QUEUE (3): Hand off to a third party. Stuff the packet into a queue and pass it to a userspace process for handling. This is the implementation foundation of the nfqueue mechanism, allowing userspace programs (like libnetfilter_queue) to decide the packet's fate.
  • NF_REPEAT (4): Try again. Ask the kernel to call the current hook function again. This is a rare but powerful option, typically used in complex scenarios that require processing the same packet multiple times.

Registration: How to Hang Your Hook

Knowing the checkpoint locations and how to issue a verdict, all that's left is how to dispatch your "police" to that post.

To register a hook callback, you need to prepare a nf_hook_ops structure (or an array of them). This structure is your "work order." Its definition is in include/linux/netfilter.h:

struct nf_hook_ops {
struct list_head list;

/* 用户填充以下字段 */
nf_hookfn *hook; /* 钩子函数指针 */
struct module *owner; /* 拥有此模块的指针(通常是 THIS_MODULE) */
u_int8_t pf; /* 协议族 */
unsigned int hooknum; /* 钩子编号 */
/* 钩子按优先级升序调用 */
int priority; /* 优先级 */
};

There are a few fields here worth discussing in detail, as they directly determine whether your code will work correctly:

  • hook: This is a pointer to the actual hook function you wrote. Its prototype must strictly follow the nf_hookfn definition:

    unsigned int nf_hookfn(unsigned int hooknum,
    struct sk_buff *skb,
    const struct net_device *in,
    const struct net_device *out,
    int (*okfn)(struct sk_buff *));

    Note that even though you might only care about one hook point when registering, the hooknum parameter is still passed into the callback. This allows you to handle multiple hook points with a single function (though usually, for the sake of clarity, we write them separately).

  • pf and hooknum: These two tell the kernel "where to hang the function above." For example, if you want to monitor IPv4 inbound packets, pf would be NFPROTO_IPV4, and hooknum would be NF_INET_PRE_ROUTING.

  • priority: This is a very critical mechanism.

    Returning to the "train checkpoint" analogy: multiple departments' police officers might be on duty at a single checkpoint simultaneously. Some check for drugs, some for customs, some for contraband. Who goes first? This is what priority determines.

    A smaller numerical value means a higher priority, and it gets called sooner. The kernel provides a set of standard priority constants (such as NF_IP_PRI_FIRST, NF_IP_PRI_CONNTRACK, NF_IP_PRI_NAT_SRC, etc.), defined in include/uapi/linux/netfilter_ipv4.h.

    ⚠️ Be extremely careful not to slip up on the priority. If your firewall rule's priority is higher than connection tracking, you might see connection tracking completely fail—because the packet gets dropped or modified before it's ever recorded.

Once the structure is ready, we can register it with the system. There are two APIs we can use:

  • nf_register_hook(struct nf_hook_ops *reg): Register a single hook. Suitable for simple modules.
  • nf_register_hooks(struct nf_hook_ops *reg, unsigned int n): Register an array of hooks. The second parameter is the array length. When you need to attach functions across multiple protocol families or multiple hook points, this function saves you the trouble of multiple loop calls and guarantees atomicity—either all succeed, or all fail.

In the next two sections, we'll see two actual registration examples. And in the diagram of the following section, we'll intuitively see how priority determines their execution order when multiple hooks are hung on the same checkpoint.

Now, let's write the code and truly insert our logic into the network data flow.