9.6 IPTables: The Frontend Implementation of Rules
In the previous section, we discussed Xtables' extension mechanism and saw how Targets and Matches are registered in the kernel. Think of it as preparing a set of standard molds in a factory.
But molds alone aren't enough. We need a production line to string them together, and someone to decide which mold to use at which stage. That's the role IPTables plays in the Netfilter architecture—it's the user-space interface we're most familiar with, and the direct consumer of the underlying Netfilter hooks.
In this section, we tear down this IPTables "production line" to see how it operates.
Tables and Hooks: Establishing the Mapping
There's no mysterious magic behind IPTables at the kernel level. At its core, it's just another Netfilter client.
The core kernel code resides in net/ipv4/netfilter/ip_tables.c (IPv4) and net/ipv6/netfilter/ip6_tables.c (IPv6). Each so-called "Table"—such as the commonly used filter table or nat table—is represented in the kernel by a xt_table structure.
Registering and unregistering these tables is straightforward:
- IPv4:
ipt_register_table()/ipt_unregister_table() - IPv6:
ip6t_register_table()/ip6t_unregister_table()
These table pointers are ultimately stored in the network namespace object. The netns_ipv4 structure holds pointers like iptable_filter, iptable_mangle, and nat_table; similarly, IPv6's netns_ipv6 hangs the corresponding ip6tables tables.
To see exactly how this works, let's skip the textbook theory and dissect the most commonly used filter table. Suppose we only compile the filter table and enable only the LOG target.
First, let's look at the filter table definition:
#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
(1 << NF_INET_FORWARD) | \
(1 << NF_INET_LOCAL_OUT))
static const struct xt_table packet_filter = {
.name = "filter",
.valid_hooks = FILTER_VALID_HOOKS,
.me = THIS_MODULE,
.af = NFPROTO_IPV4,
.priority = NF_IP_PRI_FILTER,
};
(net/ipv4/netfilter/iptable_filter.c)
Notice anything? The FILTER_VALID_HOOKS macro defines three bits: LOCAL_IN, FORWARD, and LOCAL_OUT. This means the table only takes effect at these three Netfilter hook points.
The initialization process happens in two steps.
Step 1: Set up the hook callbacks.
We call xt_hook_link() to attach iptable_filter_hook to the Netfilter hook points:
static struct nf_hook_ops *filter_ops __read_mostly;
static int __init iptable_filter_init(void)
{
. . .
// 将 packet_filter 表与 iptable_filter_hook 函数关联
filter_ops = xt_hook_link(&packet_filter, iptable_filter_hook);
. . .
}
Step 2: Register the table itself.
We register the table into the network namespace via ipt_register_table(), letting the kernel know it exists:
static int __net_init iptable_filter_net_init(struct net *net)
{
. . .
// 将注册好的表指针存入 net->ipv4 命名空间
net->ipv4.iptable_filter =
ipt_register_table(net, &packet_filter, repl);
. . .
return PTR_RET(net->ipv4.iptable_filter);
}
(net/ipv4/netfilter/iptable_filter.c)
Now the table is in place within the kernel. Next, let's add a rule and see what happens when a packet arrives.
Hands-on Tracing: The Life Journey of a UDP Packet
Suppose we run the following command:
iptables -A INPUT -p udp --dport=5001 -j LOG --log-level 1
This command tells the kernel: log the header information of all UDP packets destined for the local machine (the INPUT chain) with a destination port of 5001, and set the log level to 1.
Note There's a prerequisite here: to use the
LOGtarget, your kernel configuration must haveCONFIG_NETFILTER_XT_TARGET_LOGenabled. Its implementation lives innet/netfilter/xt_LOG.c, and it's a standard iptables target module.
Now, imagine a UDP packet with a destination port of 5001 arriving from the network hardware and making its way up to the network layer (L3).
First Stop: PRE_ROUTING
The first hook point the packet encounters is NF_INET_PRE_ROUTING.
But remember the filter table definition we just looked at—its valid_hooks doesn't include PRE_ROUTING. So the filter table is completely oblivious here.
Nothing happens; the packet passes straight through and enters ip_rcv_finish() for a routing lookup.
Second Stop: Routing Decision
The routing subsystem determines the packet's fate: is it meant for me (local delivery), or should I forward it? Let's look at both scenarios.
Scenario 1: Local Delivery
The routing decision finds that the destination IP belongs to the local machine, so the packet is sent to ip_local_deliver().
int ip_local_deliver(struct sk_buff *skb)
{
. . .
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
See that? NF_INET_LOCAL_IN.
This is one of the hook points registered by the filter table. The NF_HOOK macro triggers iptable_filter_hook() here.
Let's see what this hook function does:
static unsigned int iptable_filter_hook(unsigned int hook, struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
const struct net *net;
. . .
net = dev_net((in != NULL) ? in : out);
. . .
// 关键调用:查表,执行规则
return ipt_do_table(skb, hook, in, out, net->ipv4.iptable_filter);
}
(net/ipv4/netfilter/iptable_filter.c)
It directly calls ipt_do_table(). This function is the execution engine for that "rule-checking table." It iterates through the rules in the table to see if the packet matches.
In our example, the packet is UDP on port 5001—it's a match!
Consequently, ipt_do_table() invokes the LOG target's callback function, ipt_log_packet(). This function prints the packet header information to syslog.
The rule execution finishes and returns NF_ACCEPT. The NF_HOOK macro then calls its okfn, which is ip_local_deliver_finish(). The packet continues its journey up to the transport layer (L4) and gets handed off to the corresponding Socket.
Scenario 2: Forwarding the Packet
If the routing lookup determines the packet isn't for us but needs to be forwarded out another interface, the kernel calls ip_forward().
int ip_forward(struct sk_buff *skb)
{
. . .
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dst.dev,
rt->dst.dev, ip_forward_finish);
. . .
}
NF_INET_FORWARD.
This is also a hook point supported by the filter table. So iptable_filter_hook() gets called again, and ipt_do_table() runs through once more.
But here's a critical pitfall: the rule we added earlier was attached to the INPUT chain.
The INPUT chain only corresponds to the LOCAL_IN hook. When the packet reaches the FORWARD hook, it walks through the FORWARD chain in the filter table, which is empty.
So, even though the filter table is triggered, this UDP packet finds no matching rules in the FORWARD chain and passes straight through (the default policy is typically ACCEPT).
Next, ip_forward_finish() executes, and finally ip_output() is called to prepare for transmission.
Inside ip_output(), there's a POST_ROUTING hook, but the filter table isn't attached there either, so the packet just continues on its way.
Integrating with Conntrack
At this point, you might be wondering: what if I want to filter packets based on connection state? For example, only allowing "established" connections through?
This is exactly where the conntrack mechanism we discussed in the previous section shines.
We could add a rule like this:
iptables -A INPUT -p tcp -m conntrack --ctstate ESTABLISHED -j LOG --log-level 1
Here we use -m conntrack, which is a match extension. When ipt_do_table() reaches this point, it queries conntrack's hash tree (the nf_conn structure we saw in earlier sections).
If this TCP packet belongs to a confirmed connection, the conntrack module returns a successful match, and the LOG target records it.
Returning to our "production line" analogy: Can you see the full picture now?
xt_tableis your workbench, andipt_do_tableis the conveyor belt. Match extensions (like conntrack) act as "quality inspection sensors" on the belt, responsible for checking the packet's attributes; Target extensions (like LOG) act as the "robotic arms" at the end of the belt, responsible for performing the final action on the packet. And stringing it all together is what we saw in this section—Netfilter's Hook mechanism.