1/* FTP extension for IP connection tracking. */
2#include <linux/config.h>
3#include <linux/module.h>
4#include <linux/netfilter.h>
5#include <linux/ip.h>
6#include <linux/ctype.h>
7#include <net/checksum.h>
8#include <net/tcp.h>
9
10#include <linux/netfilter_ipv4/lockhelp.h>
11#include <linux/netfilter_ipv4/ip_conntrack_helper.h>
12#include <linux/netfilter_ipv4/ip_conntrack_ftp.h>
13
14DECLARE_LOCK(ip_ftp_lock);
15struct module *ip_conntrack_ftp = THIS_MODULE;
16
17#define MAX_PORTS 8
18static int ports[MAX_PORTS];
19static int ports_c = 0;
20#ifdef MODULE_PARM
21MODULE_PARM(ports, "1-" __MODULE_STRING(MAX_PORTS) "i");
22#endif
23
24static int loose = 0;
25MODULE_PARM(loose, "i");
26
27#define DEBUGP(format, args...)
28
29static int try_rfc959(const char *, size_t, u_int32_t [], char);
30static int try_eprt(const char *, size_t, u_int32_t [], char);
31static int try_espv_response(const char *, size_t, u_int32_t [], char);
32
33static struct ftp_search {
34	enum ip_conntrack_dir dir;
35	const char *pattern;
36	size_t plen;
37	char skip;
38	char term;
39	enum ip_ct_ftp_type ftptype;
40	int (*getnum)(const char *, size_t, u_int32_t[], char);
41} search[] = {
42	{
43		IP_CT_DIR_ORIGINAL,
44		"PORT",	sizeof("PORT") - 1, ' ', '\r',
45		IP_CT_FTP_PORT,
46		try_rfc959,
47	},
48	{
49		IP_CT_DIR_REPLY,
50		"227 ",	sizeof("227 ") - 1, '(', ')',
51		IP_CT_FTP_PASV,
52		try_rfc959,
53	},
54	{
55		IP_CT_DIR_ORIGINAL,
56		"EPRT", sizeof("EPRT") - 1, ' ', '\r',
57		IP_CT_FTP_EPRT,
58		try_eprt,
59	},
60	{
61		IP_CT_DIR_REPLY,
62		"229 ", sizeof("229 ") - 1, '(', ')',
63		IP_CT_FTP_EPSV,
64		try_espv_response,
65	},
66};
67
68static int try_number(const char *data, size_t dlen, u_int32_t array[],
69		      int array_size, char sep, char term)
70{
71	u_int32_t i, len;
72
73	memset(array, 0, sizeof(array[0])*array_size);
74
75	/* Keep data pointing at next char. */
76	for (i = 0, len = 0; len < dlen && i < array_size; len++, data++) {
77		if (*data >= '0' && *data <= '9') {
78			array[i] = array[i]*10 + *data - '0';
79		}
80		else if (*data == sep)
81			i++;
82		else {
83			/* Unexpected character; true if it's the
84			   terminator and we're finished. */
85			if (*data == term && i == array_size - 1)
86				return len;
87
88			DEBUGP("Char %u (got %u nums) `%u' unexpected\n",
89			       len, i, *data);
90			return 0;
91		}
92	}
93	DEBUGP("Failed to fill %u numbers separated by %c\n", array_size, sep);
94
95	return 0;
96}
97
98/* Returns 0, or length of numbers: 192,168,1,1,5,6 */
99static int try_rfc959(const char *data, size_t dlen, u_int32_t array[6],
100		       char term)
101{
102	return try_number(data, dlen, array, 6, ',', term);
103}
104
105/* Grab port: number up to delimiter */
106static int get_port(const char *data, int start, size_t dlen, char delim,
107		    u_int32_t array[2])
108{
109	u_int16_t port = 0;
110	int i;
111
112	for (i = start; i < dlen; i++) {
113		/* Finished? */
114		if (data[i] == delim) {
115			if (port == 0)
116				break;
117			array[0] = port >> 8;
118			array[1] = port;
119			return i + 1;
120		}
121		else if (data[i] >= '0' && data[i] <= '9')
122			port = port*10 + data[i] - '0';
123		else /* Some other crap */
124			break;
125	}
126	return 0;
127}
128
129/* Returns 0, or length of numbers: |1|132.235.1.2|6275| */
130static int try_eprt(const char *data, size_t dlen, u_int32_t array[6],
131		    char term)
132{
133	char delim;
134	int length;
135
136	/* First character is delimiter, then "1" for IPv4, then
137           delimiter again. */
138	if (dlen <= 3) return 0;
139	delim = data[0];
140	if (isdigit(delim) || delim < 33 || delim > 126
141	    || data[1] != '1' || data[2] != delim)
142		return 0;
143
144	DEBUGP("EPRT: Got |1|!\n");
145	/* Now we have IP address. */
146	length = try_number(data + 3, dlen - 3, array, 4, '.', delim);
147	if (length == 0)
148		return 0;
149
150	DEBUGP("EPRT: Got IP address!\n");
151	/* Start offset includes initial "|1|", and trailing delimiter */
152	return get_port(data, 3 + length + 1, dlen, delim, array+4);
153}
154
155/* Returns 0, or length of numbers: |||6446| */
156static int try_espv_response(const char *data, size_t dlen, u_int32_t array[6],
157			     char term)
158{
159	char delim;
160
161	/* Three delimiters. */
162	if (dlen <= 3) return 0;
163	delim = data[0];
164	if (isdigit(delim) || delim < 33 || delim > 126
165	    || data[1] != delim || data[2] != delim)
166		return 0;
167
168	return get_port(data, 3, dlen, delim, array+4);
169}
170
171/* Return 1 for match, 0 for accept, -1 for partial. */
172static int find_pattern(const char *data, size_t dlen,
173			const char *pattern, size_t plen,
174			char skip, char term,
175			unsigned int *numoff,
176			unsigned int *numlen,
177			u_int32_t array[6],
178			int (*getnum)(const char *, size_t, u_int32_t[], char))
179{
180	size_t i;
181
182	DEBUGP("find_pattern `%s': dlen = %u\n", pattern, dlen);
183	if (dlen == 0)
184		return 0;
185
186	if (dlen <= plen) {
187		/* Short packet: try for partial? */
188		if (strnicmp(data, pattern, dlen) == 0)
189			return -1;
190		else return 0;
191	}
192
193	if (strnicmp(data, pattern, plen) != 0) {
194		return 0;
195	}
196
197	DEBUGP("Pattern matches!\n");
198	/* Now we've found the constant string, try to skip
199	   to the 'skip' character */
200	for (i = plen; data[i] != skip; i++)
201		if (i == dlen - 1) return -1;
202
203	/* Skip over the last character */
204	i++;
205
206	DEBUGP("Skipped up to `%c'!\n", skip);
207
208	*numoff = i;
209	*numlen = getnum(data + i, dlen - i, array, term);
210	if (!*numlen)
211		return -1;
212
213	DEBUGP("Match succeeded!\n");
214	return 1;
215}
216
217static int help(const struct iphdr *iph, size_t len,
218		struct ip_conntrack *ct,
219		enum ip_conntrack_info ctinfo)
220{
221	/* tcplen not negative guaranteed by ip_conntrack_tcp.c */
222	struct tcphdr *tcph = (void *)iph + iph->ihl * 4;
223	const char *data = (const char *)tcph + tcph->doff * 4;
224	unsigned int tcplen = len - iph->ihl * 4;
225	unsigned int datalen = tcplen - tcph->doff * 4;
226	u_int32_t old_seq_aft_nl;
227	int old_seq_aft_nl_set;
228	u_int32_t array[6] = { 0 };
229	int dir = CTINFO2DIR(ctinfo);
230	unsigned int matchlen, matchoff;
231	struct ip_ct_ftp_master *ct_ftp_info = &ct->help.ct_ftp_info;
232	struct ip_conntrack_expect expect, *exp = &expect;
233	struct ip_ct_ftp_expect *exp_ftp_info = &exp->help.exp_ftp_info;
234
235	unsigned int i;
236	int found = 0;
237
238	/* Until there's been traffic both ways, don't look in packets. */
239	if (ctinfo != IP_CT_ESTABLISHED
240	    && ctinfo != IP_CT_ESTABLISHED+IP_CT_IS_REPLY) {
241		DEBUGP("ftp: Conntrackinfo = %u\n", ctinfo);
242		return NF_ACCEPT;
243	}
244
245	/* Not whole TCP header? */
246	if (tcplen < sizeof(struct tcphdr) || tcplen < tcph->doff*4) {
247		DEBUGP("ftp: tcplen = %u\n", (unsigned)tcplen);
248		return NF_ACCEPT;
249	}
250
251	/* Checksum invalid?  Ignore. */
252	if (tcp_v4_check(tcph, tcplen, iph->saddr, iph->daddr,
253			 csum_partial((char *)tcph, tcplen, 0))) {
254		DEBUGP("ftp_help: bad csum: %p %u %u.%u.%u.%u %u.%u.%u.%u\n",
255		       tcph, tcplen, NIPQUAD(iph->saddr),
256		       NIPQUAD(iph->daddr));
257		return NF_ACCEPT;
258	}
259
260	LOCK_BH(&ip_ftp_lock);
261	old_seq_aft_nl_set = ct_ftp_info->seq_aft_nl_set[dir];
262	old_seq_aft_nl = ct_ftp_info->seq_aft_nl[dir];
263
264	DEBUGP("conntrack_ftp: datalen %u\n", datalen);
265	if ((datalen > 0) && (data[datalen-1] == '\n')) {
266		DEBUGP("conntrack_ftp: datalen %u ends in \\n\n", datalen);
267		if (!old_seq_aft_nl_set
268		    || after(ntohl(tcph->seq) + datalen, old_seq_aft_nl)) {
269			DEBUGP("conntrack_ftp: updating nl to %u\n",
270			       ntohl(tcph->seq) + datalen);
271			ct_ftp_info->seq_aft_nl[dir] =
272						ntohl(tcph->seq) + datalen;
273			ct_ftp_info->seq_aft_nl_set[dir] = 1;
274		}
275	}
276	UNLOCK_BH(&ip_ftp_lock);
277
278	if(!old_seq_aft_nl_set ||
279			(ntohl(tcph->seq) != old_seq_aft_nl)) {
280		DEBUGP("ip_conntrack_ftp_help: wrong seq pos %s(%u)\n",
281		       old_seq_aft_nl_set ? "":"(UNSET) ", old_seq_aft_nl);
282		return NF_ACCEPT;
283	}
284
285	/* Initialize IP array to expected address (it's not mentioned
286           in EPSV responses) */
287	array[0] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 24) & 0xFF;
288	array[1] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 16) & 0xFF;
289	array[2] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 8) & 0xFF;
290	array[3] = ntohl(ct->tuplehash[dir].tuple.src.ip) & 0xFF;
291
292	for (i = 0; i < sizeof(search) / sizeof(search[0]); i++) {
293		if (search[i].dir != dir) continue;
294
295		found = find_pattern(data, datalen,
296				     search[i].pattern,
297				     search[i].plen,
298				     search[i].skip,
299				     search[i].term,
300				     &matchoff, &matchlen,
301				     array,
302				     search[i].getnum);
303		if (found) break;
304	}
305	if (found == -1) {
306		/* We don't usually drop packets.  After all, this is
307		   connection tracking, not packet filtering.
308		   However, it is neccessary for accurate tracking in
309		   this case. */
310		if (net_ratelimit())
311			printk("conntrack_ftp: partial %s %u+%u\n",
312			       search[i].pattern,
313			       ntohl(tcph->seq), datalen);
314		return NF_DROP;
315	} else if (found == 0) /* No match */
316		return NF_ACCEPT;
317
318	DEBUGP("conntrack_ftp: match `%.*s' (%u bytes at %u)\n",
319	       (int)matchlen, data + matchoff,
320	       matchlen, ntohl(tcph->seq) + matchoff);
321
322	memset(&expect, 0, sizeof(expect));
323
324	/* Update the ftp info */
325	LOCK_BH(&ip_ftp_lock);
326	if (htonl((array[0] << 24) | (array[1] << 16) | (array[2] << 8) | array[3])
327	    == ct->tuplehash[dir].tuple.src.ip) {
328		exp->seq = ntohl(tcph->seq) + matchoff;
329		exp_ftp_info->len = matchlen;
330		exp_ftp_info->ftptype = search[i].ftptype;
331		exp_ftp_info->port = array[4] << 8 | array[5];
332	} else {
333		/* Enrico Scholz's passive FTP to partially RNAT'd ftp
334		   server: it really wants us to connect to a
335		   different IP address.  Simply don't record it for
336		   NAT. */
337		DEBUGP("conntrack_ftp: NOT RECORDING: %u,%u,%u,%u != %u.%u.%u.%u\n",
338		       array[0], array[1], array[2], array[3],
339		       NIPQUAD(ct->tuplehash[dir].tuple.src.ip));
340
341		/* Thanks to Cristiano Lincoln Mattos
342		   <lincoln@cesar.org.br> for reporting this potential
343		   problem (DMZ machines opening holes to internal
344		   networks, or the packet filter itself). */
345		if (!loose) goto out;
346	}
347
348	exp->tuple = ((struct ip_conntrack_tuple)
349		{ { ct->tuplehash[!dir].tuple.src.ip,
350		    { 0 } },
351		  { htonl((array[0] << 24) | (array[1] << 16)
352			  | (array[2] << 8) | array[3]),
353		    { htons(array[4] << 8 | array[5]) },
354		    IPPROTO_TCP }});
355	exp->mask = ((struct ip_conntrack_tuple)
356		{ { 0xFFFFFFFF, { 0 } },
357		  { 0xFFFFFFFF, { 0xFFFF }, 0xFFFF }});
358
359	exp->expectfn = NULL;
360
361	/* Ignore failure; should only happen with NAT */
362	ip_conntrack_expect_related(ct, &expect);
363 out:
364	UNLOCK_BH(&ip_ftp_lock);
365
366	return NF_ACCEPT;
367}
368
369static struct ip_conntrack_helper ftp[MAX_PORTS];
370static char ftp_names[MAX_PORTS][10];
371
372/* Not __exit: called from init() */
373static void fini(void)
374{
375	int i;
376	for (i = 0; i < ports_c; i++) {
377		DEBUGP("ip_ct_ftp: unregistering helper for port %d\n",
378				ports[i]);
379		ip_conntrack_helper_unregister(&ftp[i]);
380	}
381}
382
383static int __init init(void)
384{
385	int i, ret;
386	char *tmpname;
387
388	if (ports[0] == 0)
389		ports[0] = FTP_PORT;
390
391	for (i = 0; (i < MAX_PORTS) && ports[i]; i++) {
392		memset(&ftp[i], 0, sizeof(struct ip_conntrack_helper));
393		ftp[i].tuple.src.u.tcp.port = htons(ports[i]);
394		ftp[i].tuple.dst.protonum = IPPROTO_TCP;
395		ftp[i].mask.src.u.tcp.port = 0xFFFF;
396		ftp[i].mask.dst.protonum = 0xFFFF;
397		ftp[i].max_expected = 1;
398		ftp[i].timeout = 0;
399		ftp[i].flags = IP_CT_HELPER_F_REUSE_EXPECT;
400		ftp[i].me = ip_conntrack_ftp;
401		ftp[i].help = help;
402
403		tmpname = &ftp_names[i][0];
404		if (ports[i] == FTP_PORT)
405			sprintf(tmpname, "ftp");
406		else
407			sprintf(tmpname, "ftp-%d", ports[i]);
408		ftp[i].name = tmpname;
409
410		DEBUGP("ip_ct_ftp: registering helper for port %d\n",
411				ports[i]);
412		ret = ip_conntrack_helper_register(&ftp[i]);
413
414		if (ret) {
415			fini();
416			return ret;
417		}
418		ports_c++;
419	}
420	return 0;
421}
422
423#ifdef CONFIG_IP_NF_NAT_NEEDED
424EXPORT_SYMBOL(ip_ftp_lock);
425#endif
426
427MODULE_LICENSE("GPL");
428module_init(init);
429module_exit(fini);
430