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