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