9.5 Connection Tracking Helpers and Expected Connections
So far, the connection tracking we've discussed has been "single-threaded" — one connection comes in, one record goes out. In reality, though, network protocols are often much more complex than this.
You've likely encountered protocols that aren't just "a single connection." They separate control and data flows — first greeting each other on a "control channel," and then saying: "Hey, I'm about to send data on another port, get ready to receive it."
FTP and SIP (a VoIP protocol) are classic examples.
This poses a challenge for Netfilter. The kernel isn't an application; it doesn't understand the FTP protocol. It only sees two isolated packets: one chatting on port 21, and another suddenly barging in from some random high port. Without extra context, the kernel can only assume these two packets are completely unrelated, and that late-arriving data packet will likely be ruthlessly DROPped by firewall rules.
To solve this problem, Netfilter introduced a mechanism: Connection Tracking Helpers.
Think of a Helper as a "knowledgeable translator" — it embeds itself in the kernel and specifically monitors the control channels of these complex protocols (like the PORT command in FTP). When it spots the "I'm opening a data connection" signal, it makes a note in the kernel's ledger in advance: "If a connection comes in on a specific port shortly, it was introduced by our old friend just now, don't block it."
This mechanism is called Expectations.
Establishing the Relationship
When a connection is expected to come in as a "child connection" of another, we establish a parent-child relationship between the two connections. This is incredibly useful when writing firewall rules.
Instead of writing individual rules for every random data port, we only need a single generic rule: "Accept any packet related to an existing connection."
iptables -A INPUT -m conntrack --ctstate RELATED -j ACCEPT
This rule means: as long as your state is RELATED (meaning you are an expected child of an existing connection), let it through.
⚠️ Warning: Don't narrow down the relationship too much The
RELATEDstate isn't only triggered by Expectations created by Helpers. For example, when an ICMP error packet (like "Fragmentation needed") comes back, Netfilter also tries to find the corresponding connection in the original packet header embedded within the ICMP packet. If found, this ICMP packet is also marked asRELATED. This is an automatic correlation at the network stack level, requiring no intervention from a Helper.
Helper Implementation and Registration
At the code level, these "translators" (Helpers) are represented by the nf_conntrack_helper structure (defined in include/net/netfilter/nf_conntrack_helper.h).
Registration and unregistration are straightforward, handled by nf_conntrack_helper_register() and nf_conntrack_helper_unregister() respectively.
Take FTP as an example. When we load the nf_conntrack_ftp module, its initialization function nf_conntrack_ftp_init() (located in net/netfilter/nf_conntrack_ftp.c) registers the FTP Helper into the kernel. All Helpers are stored in a hash table nf_ct_helper_hash for efficient lookup.
Where the Real "Monitoring" Happens
Registration is just the first step. When does a Helper actually start working?
This brings us back to our familiar Hook points. The ipv4_helper() callback function is mounted at two key locations:
NF_INET_POST_ROUTING(packet is about to go out,ip_output)NF_INET_LOCAL_IN(packet is destined for the local machine,ip_local_deliver)
This means that when a packet (whether it's being forwarded out or delivered locally) reaches these nodes, ipv4_helper() is triggered. It iterates through the hash table we just mentioned to see if any registered Helper is interested in this connection.
If it's FTP traffic, the help() method gets called. This is where the actual work happens.
Diving into the FTP Helper: Pattern Matching
The FTP Helper's core task is tedious but critical: finding specific "patterns" within the data stream.
It searches for specific command strings in the FTP control channel's payload. It cares most about the PORT command — which the client uses to tell the server which port it opened to receive data.
Let's look at how the source code (net/netfilter/nf_conntrack_ftp.c) finds it:
static int help(struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo)
{
struct nf_conntrack_expect *exp;
// ...
for (i = 0; i < ARRAY_SIZE(search[dir]); i++) {
found = find_pattern(fb_ptr, datalen,
search[dir][i].pattern,
search[dir][i].plen,
search[dir][i].skip,
search[dir][i].term,
&matchoff, &matchlen,
&cmd,
search[dir][i].getnum);
if (found) break;
}
Here we have a loop that scans the entire FTP payload, attempting to match predefined patterns (like the ASCII codes for the PORT command).
⚠️ Warning: Dropping Packets in Special Cases Under normal circumstances, the connection tracking layer should not drop packets — it's only responsible for tracking; filtering is the firewall layer's job. But this is an exception. If an anomaly occurs during pattern matching (for example,
find_patternreturns-1, meaning only a partial match was found and the packet might be truncated), the code will directly:if (found == -1) {/* ... */ret = NF_DROP;goto out;}This prevents erroneous tracking information from polluting the state table. Rather than recording bad data, it's better to outright drop the problematic packet.
If the pattern is successfully found (like a complete PORT command), the found variable is set, and we extract the IP and port information from it.
The next step is to actually create the Expectation.
pr_debug("conntrack_ftp: match `%.*s' (%u bytes at %u)\n",
matchlen, fb_ptr + matchoff,
matchlen, ntohl(th->seq) + matchoff);
exp = nf_ct_expect_alloc(ct);
// ...
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, cmd.l3num,
&ct->tuplehash[!dir].tuple.src.u3, daddr,
IPPROTO_TCP, NULL, &cmd.u.tcp.port);
// ...
}
A few things happen here:
- A
nf_conntrack_expectobject is allocated. - Details are filled in using
nf_ct_expect_init: protocol type (TCP), expected source/destination addresses, and most importantly — the port we just parsed.
This record is inserted into the system's Expectation table. The kernel now knows: "If a TCP connection to this port comes in shortly, associate it with the current FTP control connection."
The Child Connection "Finding Its Roots"
When that expected data connection is actually initiated, what happens?
This brings us back to the init_conntrack() function we mentioned earlier. When a new connection tracking entry is created, the kernel checks the Expectation table:
static struct nf_conntrack_tuple_hash *
init_conntrack(struct net *net, struct nf_conn *tmpl,
const struct nf_conntrack_tuple *tuple,
// ... 参数省略 ...)
{
struct nf_conn *ct;
// ...
struct nf_conntrack_expect *exp;
// ...
exp = nf_ct_find_expectation(net, zone, tuple);
if (exp) {
pr_debug("conntrack: expectation arrives ct=%p exp=%p\n",
ct, exp);
/* Welcome, Mr. Bond. We've been expecting you... */
__set_bit(IPS_EXPECTED_BIT, &ct->status);
ct->master = exp->master;
if (exp->helper) {
help = nf_ct_helper_ext_add(ct, exp->helper,
GFP_ATOMIC);
if (help)
rcu_assign_pointer(help->helper, exp->helper);
}
// ...
There are two key operations here, marking the child connection's "finding of its roots":
__set_bit(IPS_EXPECTED_BIT, ...): Tags this new connection, telling the world "I didn't just appear out of nowhere; I was EXPECTED." This is exactly whyiptables -m conntrack --ctstate RELATEDcan match it.ct->master = exp->master: Points this child connection'smasterpointer to its parent connection. This is extremely useful for policy deployment — find the parent, and you can find all the children.
Non-Standard Ports: How to Teach a Helper the Way
By default, the FTP Helper listens on port 21. This is hardcoded in its definition (include/linux/netfilter/nf_conntrack_ftp.h's FTP_PORT).
But real-world networks are rarely standard. What if your FTP server runs on port 2121?
There are two ways to teach this Helper the way.
Method 1: Load-Time Parameters
The simplest approach is to tell it directly when loading the module:
modprobe nf_conntrack_ftp ports=2121
Or if there are multiple non-standard ports:
modprobe nf_conntrack_ftp ports=2022,2023,2024
This binds the Helper to these new ports.
Method 2: Using the CT Target
If you don't know the port at module load time, or want to bind dynamically, you can use iptables' CT target.
This target was introduced in kernel 2.6.34 (located in net/netfilter/xt_CT.c). You can write a rule to specifically catch traffic on certain ports, then tell the kernel: "This traffic might not look like standard FTP, but please handle it with the FTP Helper":
iptables -A PREROUTING -t raw -p tcp --dport 8888 -j CT --helper ftp
Here we operate in the raw table's PREROUTING chain, ensuring the Helper is bound at the earliest stage of connection tracking establishment.
A Closer Look: How Are Targets and Matches Hooked into the Kernel? You might wonder how a target like
CT, or a match likeconntrack, is recognized by the kernel?They are actually Xtables extensions.
- Target extensions (like
CT,ACCEPT,DROP) are represented by thext_targetstructure. They are registered into the kernel viaxt_register_target()(single) orxt_register_targets()(batch).- Match extensions (like
state,conntrack,length) are represented by thext_matchstructure, registered viaxt_register_match()orxt_register_matches().When iptables rules are parsed, the kernel relies on these registered structures to know "which function to call when encountering
-j CT," or how to check a packet's length when encountering-m length.
So far, we've broken down the connection tracking initialization process and how it handles complex protocols like FTP. Connection tracking is the cornerstone of Netfilter, but it operates behind the scenes. In the next chapter, we'll step into the spotlight and talk about the partner you're probably most familiar with — iptables — and see how these underlying mechanisms are exposed through rules.