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