/* FTP extension for IP connection tracking. */ /* (C) 1999-2001 Paul `Rusty' Russell * (C) 2002-2004 Netfilter Core Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. */ #include #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Rusty Russell "); MODULE_DESCRIPTION("ftp connection tracking helper"); /* This is slow, but it's simple. --RR */ static char *ftp_buffer; static DEFINE_SPINLOCK(ip_ftp_lock); #define MAX_PORTS 8 static unsigned short ports[MAX_PORTS]; static int ports_c; module_param_array(ports, ushort, &ports_c, 0400); static int loose; module_param(loose, bool, 0600); unsigned int (*ip_nat_ftp_hook)(struct sk_buff **pskb, enum ip_conntrack_info ctinfo, enum ip_ct_ftp_type type, unsigned int matchoff, unsigned int matchlen, struct ip_conntrack_expect *exp, u32 *seq); EXPORT_SYMBOL_GPL(ip_nat_ftp_hook); #if 0 #define DEBUGP printk #else #define DEBUGP(format, args...) #endif static int try_rfc959(const char *, size_t, u_int32_t [], char); static int try_eprt(const char *, size_t, u_int32_t [], char); static int try_epsv_response(const char *, size_t, u_int32_t [], char); static const struct ftp_search { const char *pattern; size_t plen; char skip; char term; enum ip_ct_ftp_type ftptype; int (*getnum)(const char *, size_t, u_int32_t[], char); } search[IP_CT_DIR_MAX][2] = { [IP_CT_DIR_ORIGINAL] = { { .pattern = "PORT", .plen = sizeof("PORT") - 1, .skip = ' ', .term = '\r', .ftptype = IP_CT_FTP_PORT, .getnum = try_rfc959, }, { .pattern = "EPRT", .plen = sizeof("EPRT") - 1, .skip = ' ', .term = '\r', .ftptype = IP_CT_FTP_EPRT, .getnum = try_eprt, }, }, [IP_CT_DIR_REPLY] = { { .pattern = "227 ", .plen = sizeof("227 ") - 1, .skip = '(', .term = ')', .ftptype = IP_CT_FTP_PASV, .getnum = try_rfc959, }, { .pattern = "229 ", .plen = sizeof("229 ") - 1, .skip = '(', .term = ')', .ftptype = IP_CT_FTP_EPSV, .getnum = try_epsv_response, }, }, }; static int try_number(const char *data, size_t dlen, u_int32_t array[], int array_size, char sep, char term) { u_int32_t i, len; memset(array, 0, sizeof(array[0])*array_size); /* Keep data pointing at next char. */ for (i = 0, len = 0; len < dlen && i < array_size; len++, data++) { if (*data >= '0' && *data <= '9') { array[i] = array[i]*10 + *data - '0'; } else if (*data == sep) i++; else { /* Unexpected character; true if it's the terminator and we're finished. */ if (*data == term && i == array_size - 1) return len; DEBUGP("Char %u (got %u nums) `%u' unexpected\n", len, i, *data); return 0; } } DEBUGP("Failed to fill %u numbers separated by %c\n", array_size, sep); return 0; } /* Returns 0, or length of numbers: 192,168,1,1,5,6 */ static int try_rfc959(const char *data, size_t dlen, u_int32_t array[6], char term) { return try_number(data, dlen, array, 6, ',', term); } /* Grab port: number up to delimiter */ static int get_port(const char *data, int start, size_t dlen, char delim, u_int32_t array[2]) { u_int16_t port = 0; int i; for (i = start; i < dlen; i++) { /* Finished? */ if (data[i] == delim) { if (port == 0) break; array[0] = port >> 8; array[1] = port; return i + 1; } else if (data[i] >= '0' && data[i] <= '9') port = port*10 + data[i] - '0'; else /* Some other crap */ break; } return 0; } /* Returns 0, or length of numbers: |1|132.235.1.2|6275| */ static int try_eprt(const char *data, size_t dlen, u_int32_t array[6], char term) { char delim; int length; /* First character is delimiter, then "1" for IPv4, then delimiter again. */ if (dlen <= 3) return 0; delim = data[0]; if (isdigit(delim) || delim < 33 || delim > 126 || data[1] != '1' || data[2] != delim) return 0; DEBUGP("EPRT: Got |1|!\n"); /* Now we have IP address. */ length = try_number(data + 3, dlen - 3, array, 4, '.', delim); if (length == 0) return 0; DEBUGP("EPRT: Got IP address!\n"); /* Start offset includes initial "|1|", and trailing delimiter */ return get_port(data, 3 + length + 1, dlen, delim, array+4); } /* Returns 0, or length of numbers: |||6446| */ static int try_epsv_response(const char *data, size_t dlen, u_int32_t array[6], char term) { char delim; /* Three delimiters. */ if (dlen <= 3) return 0; delim = data[0]; if (isdigit(delim) || delim < 33 || delim > 126 || data[1] != delim || data[2] != delim) return 0; return get_port(data, 3, dlen, delim, array+4); } /* Return 1 for match, 0 for accept, -1 for partial. */ static int 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)) { size_t i; DEBUGP("find_pattern `%s': dlen = %u\n", pattern, dlen); if (dlen == 0) return 0; if (dlen <= plen) { /* Short packet: try for partial? */ if (strnicmp(data, pattern, dlen) == 0) return -1; else return 0; } if (strnicmp(data, pattern, plen) != 0) { #if 0 size_t i; DEBUGP("ftp: string mismatch\n"); for (i = 0; i < plen; i++) { DEBUGP("ftp:char %u `%c'(%u) vs `%c'(%u)\n", i, data[i], data[i], pattern[i], pattern[i]); } #endif return 0; } DEBUGP("Pattern matches!\n"); /* Now we've found the constant string, try to skip to the 'skip' character */ for (i = plen; data[i] != skip; i++) if (i == dlen - 1) return -1; /* Skip over the last character */ i++; DEBUGP("Skipped up to `%c'!\n", skip); *numoff = i; *numlen = getnum(data + i, dlen - i, array, term); if (!*numlen) return -1; DEBUGP("Match succeeded!\n"); return 1; } /* Look up to see if we're just after a \n. */ static int find_nl_seq(u32 seq, const struct ip_ct_ftp_master *info, int dir) { unsigned int i; for (i = 0; i < info->seq_aft_nl_num[dir]; i++) if (info->seq_aft_nl[dir][i] == seq) return 1; return 0; } /* We don't update if it's older than what we have. */ static void update_nl_seq(u32 nl_seq, struct ip_ct_ftp_master *info, int dir, struct sk_buff *skb) { unsigned int i, oldest = NUM_SEQ_TO_REMEMBER; /* Look for oldest: if we find exact match, we're done. */ for (i = 0; i < info->seq_aft_nl_num[dir]; i++) { if (info->seq_aft_nl[dir][i] == nl_seq) return; if (oldest == info->seq_aft_nl_num[dir] || before(info->seq_aft_nl[dir][i], oldest)) oldest = i; } if (info->seq_aft_nl_num[dir] < NUM_SEQ_TO_REMEMBER) { info->seq_aft_nl[dir][info->seq_aft_nl_num[dir]++] = nl_seq; ip_conntrack_event_cache(IPCT_HELPINFO_VOLATILE, skb); } else if (oldest != NUM_SEQ_TO_REMEMBER) { info->seq_aft_nl[dir][oldest] = nl_seq; ip_conntrack_event_cache(IPCT_HELPINFO_VOLATILE, skb); } } static int help(struct sk_buff **pskb, struct ip_conntrack *ct, enum ip_conntrack_info ctinfo) { unsigned int dataoff, datalen; struct tcphdr _tcph, *th; char *fb_ptr; int ret; u32 seq, array[6] = { 0 }; int dir = CTINFO2DIR(ctinfo); unsigned int matchlen, matchoff; struct ip_ct_ftp_master *ct_ftp_info = &ct->help.ct_ftp_info; struct ip_conntrack_expect *exp; unsigned int i; int found = 0, ends_in_nl; /* Until there's been traffic both ways, don't look in packets. */ if (ctinfo != IP_CT_ESTABLISHED && ctinfo != IP_CT_ESTABLISHED+IP_CT_IS_REPLY) { DEBUGP("ftp: Conntrackinfo = %u\n", ctinfo); return NF_ACCEPT; } th = skb_header_pointer(*pskb, (*pskb)->nh.iph->ihl*4, sizeof(_tcph), &_tcph); if (th == NULL) return NF_ACCEPT; dataoff = (*pskb)->nh.iph->ihl*4 + th->doff*4; /* No data? */ if (dataoff >= (*pskb)->len) { DEBUGP("ftp: pskblen = %u\n", (*pskb)->len); return NF_ACCEPT; } datalen = (*pskb)->len - dataoff; spin_lock_bh(&ip_ftp_lock); fb_ptr = skb_header_pointer(*pskb, dataoff, (*pskb)->len - dataoff, ftp_buffer); BUG_ON(fb_ptr == NULL); ends_in_nl = (fb_ptr[datalen - 1] == '\n'); seq = ntohl(th->seq) + datalen; /* Look up to see if we're just after a \n. */ if (!find_nl_seq(ntohl(th->seq), ct_ftp_info, dir)) { /* Now if this ends in \n, update ftp info. */ DEBUGP("ip_conntrack_ftp_help: wrong seq pos %s(%u) or %s(%u)\n", ct_ftp_info->seq_aft_nl[0][dir] old_seq_aft_nl_set ? "":"(UNSET) ", old_seq_aft_nl); ret = NF_ACCEPT; goto out_update_nl; } /* Initialize IP array to expected address (it's not mentioned in EPSV responses) */ array[0] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 24) & 0xFF; array[1] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 16) & 0xFF; array[2] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 8) & 0xFF; array[3] = ntohl(ct->tuplehash[dir].tuple.src.ip) & 0xFF; for (i = 0; i < ARRAY_SIZE(search[dir]); i++) { found = find_pattern(fb_ptr, (*pskb)->len - dataoff, search[dir][i].pattern, search[dir][i].plen, search[dir][i].skip, search[dir][i].term, &matchoff, &matchlen, array, search[dir][i].getnum); if (found) break; } if (found == -1) { /* We don't usually drop packets. After all, this is connection tracking, not packet filtering. However, it is necessary for accurate tracking in this case. */ if (net_ratelimit()) printk("conntrack_ftp: partial %s %u+%u\n", search[dir][i].pattern, ntohl(th->seq), datalen); ret = NF_DROP; goto out; } else if (found == 0) { /* No match */ ret = NF_ACCEPT; goto out_update_nl; } DEBUGP("conntrack_ftp: match `%s' (%u bytes at %u)\n", fb_ptr + matchoff, matchlen, ntohl(th->seq) + matchoff); /* Allocate expectation which will be inserted */ exp = ip_conntrack_expect_alloc(ct); if (exp == NULL) { ret = NF_DROP; goto out; } /* We refer to the reverse direction ("!dir") tuples here, * because we're expecting something in the other direction. * Doesn't matter unless NAT is happening. */ exp->tuple.dst.ip = ct->tuplehash[!dir].tuple.dst.ip; if (htonl((array[0] << 24) | (array[1] << 16) | (array[2] << 8) | array[3]) != ct->tuplehash[dir].tuple.src.ip) { /* Enrico Scholz's passive FTP to partially RNAT'd ftp server: it really wants us to connect to a different IP address. Simply don't record it for NAT. */ DEBUGP("conntrack_ftp: NOT RECORDING: %u,%u,%u,%u != %u.%u.%u.%u\n", array[0], array[1], array[2], array[3], NIPQUAD(ct->tuplehash[dir].tuple.src.ip)); /* Thanks to Cristiano Lincoln Mattos for reporting this potential problem (DMZ machines opening holes to internal networks, or the packet filter itself). */ if (!loose) { ret = NF_ACCEPT; goto out_put_expect; } exp->tuple.dst.ip = htonl((array[0] << 24) | (array[1] << 16) | (array[2] << 8) | array[3]); } exp->tuple.src.ip = ct->tuplehash[!dir].tuple.src.ip; exp->tuple.dst.u.tcp.port = htons(array[4] << 8 | array[5]); exp->tuple.src.u.tcp.port = 0; /* Don't care. */ exp->tuple.dst.protonum = IPPROTO_TCP; exp->mask = ((struct ip_conntrack_tuple) { { htonl(0xFFFFFFFF), { 0 } }, { htonl(0xFFFFFFFF), { .tcp = { htons(0xFFFF) } }, 0xFF }}); exp->expectfn = NULL; exp->flags = 0; /* Now, NAT might want to mangle the packet, and register the * (possibly changed) expectation itself. */ if (ip_nat_ftp_hook) ret = ip_nat_ftp_hook(pskb, ctinfo, search[dir][i].ftptype, matchoff, matchlen, exp, &seq); else { /* Can't expect this? Best to drop packet now. */ if (ip_conntrack_expect_related(exp) != 0) ret = NF_DROP; else ret = NF_ACCEPT; } out_put_expect: ip_conntrack_expect_put(exp); out_update_nl: /* Now if this ends in \n, update ftp info. Seq may have been * adjusted by NAT code. */ if (ends_in_nl) update_nl_seq(seq, ct_ftp_info,dir, *pskb); out: spin_unlock_bh(&ip_ftp_lock); return ret; } static struct ip_conntrack_helper ftp[MAX_PORTS]; static char ftp_names[MAX_PORTS][sizeof("ftp-65535")]; /* Not __exit: called from init() */ static void ip_conntrack_ftp_fini(void) { int i; for (i = 0; i < ports_c; i++) { DEBUGP("ip_ct_ftp: unregistering helper for port %d\n", ports[i]); ip_conntrack_helper_unregister(&ftp[i]); } kfree(ftp_buffer); } static int __init ip_conntrack_ftp_init(void) { int i, ret; char *tmpname; ftp_buffer = kmalloc(65536, GFP_KERNEL); if (!ftp_buffer) return -ENOMEM; if (ports_c == 0) ports[ports_c++] = FTP_PORT; for (i = 0; i < ports_c; i++) { ftp[i].tuple.src.u.tcp.port = htons(ports[i]); ftp[i].tuple.dst.protonum = IPPROTO_TCP; ftp[i].mask.src.u.tcp.port = htons(0xFFFF); ftp[i].mask.dst.protonum = 0xFF; ftp[i].max_expected = 1; ftp[i].timeout = 5 * 60; /* 5 minutes */ ftp[i].me = THIS_MODULE; ftp[i].help = help; tmpname = &ftp_names[i][0]; if (ports[i] == FTP_PORT) sprintf(tmpname, "ftp"); else sprintf(tmpname, "ftp-%d", ports[i]); ftp[i].name = tmpname; DEBUGP("ip_ct_ftp: registering helper for port %d\n", ports[i]); ret = ip_conntrack_helper_register(&ftp[i]); if (ret) { ip_conntrack_ftp_fini(); return ret; } } return 0; } module_init(ip_conntrack_ftp_init); module_exit(ip_conntrack_ftp_fini);