/* SIP extension for IP connection tracking. * * (C) 2005 by Christian Hentschel * based on RR's ip_conntrack_ftp.c and other modules. * * 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 #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Christian Hentschel "); MODULE_DESCRIPTION("SIP connection tracking helper"); MODULE_ALIAS("ip_conntrack_sip"); #define MAX_PORTS 8 static unsigned short ports[MAX_PORTS]; static unsigned int ports_c; module_param_array(ports, ushort, &ports_c, 0400); MODULE_PARM_DESC(ports, "port numbers of SIP servers"); static unsigned int sip_timeout __read_mostly = SIP_TIMEOUT; module_param(sip_timeout, uint, 0600); MODULE_PARM_DESC(sip_timeout, "timeout for the master SIP session"); unsigned int (*nf_nat_sip_hook)(struct sk_buff *skb, const char **dptr, unsigned int *datalen) __read_mostly; EXPORT_SYMBOL_GPL(nf_nat_sip_hook); unsigned int (*nf_nat_sdp_hook)(struct sk_buff *skb, const char **dptr, unsigned int *datalen, struct nf_conntrack_expect *exp) __read_mostly; EXPORT_SYMBOL_GPL(nf_nat_sdp_hook); static int digits_len(const struct nf_conn *, const char *, const char *, int *); static int epaddr_len(const struct nf_conn *, const char *, const char *, int *); static int skp_digits_len(const struct nf_conn *, const char *, const char *, int *); static int skp_epaddr_len(const struct nf_conn *, const char *, const char *, int *); struct sip_header_nfo { const char *lname; const char *sname; const char *ln_str; size_t lnlen; size_t snlen; size_t ln_strlen; int case_sensitive; int (*match_len)(const struct nf_conn *, const char *, const char *, int *); }; static const struct sip_header_nfo ct_sip_hdrs[] = { [POS_FROM] = { /* SIP From header */ .lname = "From:", .lnlen = sizeof("From:") - 1, .sname = "\r\nf:", .snlen = sizeof("\r\nf:") - 1, .ln_str = "sip:", .ln_strlen = sizeof("sip:") - 1, .match_len = skp_epaddr_len, }, [POS_TO] = { /* SIP To header */ .lname = "To:", .lnlen = sizeof("To:") - 1, .sname = "\r\nt:", .snlen = sizeof("\r\nt:") - 1, .ln_str = "sip:", .ln_strlen = sizeof("sip:") - 1, .match_len = skp_epaddr_len }, [POS_VIA] = { /* SIP Via header */ .lname = "Via:", .lnlen = sizeof("Via:") - 1, .sname = "\r\nv:", .snlen = sizeof("\r\nv:") - 1, /* rfc3261 "\r\n" */ .ln_str = "UDP ", .ln_strlen = sizeof("UDP ") - 1, .match_len = epaddr_len, }, [POS_CONTACT] = { /* SIP Contact header */ .lname = "Contact:", .lnlen = sizeof("Contact:") - 1, .sname = "\r\nm:", .snlen = sizeof("\r\nm:") - 1, .ln_str = "sip:", .ln_strlen = sizeof("sip:") - 1, .match_len = skp_epaddr_len }, [POS_CONTENT] = { /* SIP Content length header */ .lname = "Content-Length:", .lnlen = sizeof("Content-Length:") - 1, .sname = "\r\nl:", .snlen = sizeof("\r\nl:") - 1, .ln_str = ":", .ln_strlen = sizeof(":") - 1, .match_len = skp_digits_len }, }; /* get line length until first CR or LF seen. */ int ct_sip_lnlen(const char *line, const char *limit) { const char *k = line; while ((line < limit) && (*line == '\r' || *line == '\n')) line++; while (line < limit) { if (*line == '\r' || *line == '\n') break; line++; } return line - k; } EXPORT_SYMBOL_GPL(ct_sip_lnlen); /* Linear string search, case sensitive. */ const char *ct_sip_search(const char *needle, const char *haystack, size_t needle_len, size_t haystack_len, int case_sensitive) { const char *limit = haystack + (haystack_len - needle_len); while (haystack < limit) { if (case_sensitive) { if (strncmp(haystack, needle, needle_len) == 0) return haystack; } else { if (strnicmp(haystack, needle, needle_len) == 0) return haystack; } haystack++; } return NULL; } EXPORT_SYMBOL_GPL(ct_sip_search); static int string_len(const struct nf_conn *ct, const char *dptr, const char *limit, int *shift) { int len = 0; while (dptr < limit && isalpha(*dptr)) { dptr++; len++; } return len; } static int digits_len(const struct nf_conn *ct, const char *dptr, const char *limit, int *shift) { int len = 0; while (dptr < limit && isdigit(*dptr)) { dptr++; len++; } return len; } /* get digits length, skipping blank spaces. */ static int skp_digits_len(const struct nf_conn *ct, const char *dptr, const char *limit, int *shift) { for (; dptr < limit && *dptr == ' '; dptr++) (*shift)++; return digits_len(ct, dptr, limit, shift); } static int parse_addr(const struct nf_conn *ct, const char *cp, const char **endp, union nf_inet_addr *addr, const char *limit) { const char *end; int family = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.l3num; int ret = 0; switch (family) { case AF_INET: ret = in4_pton(cp, limit - cp, (u8 *)&addr->ip, -1, &end); break; case AF_INET6: ret = in6_pton(cp, limit - cp, (u8 *)&addr->ip6, -1, &end); break; default: BUG(); } if (ret == 0 || end == cp) return 0; if (endp) *endp = end; return 1; } /* skip ip address. returns its length. */ static int epaddr_len(const struct nf_conn *ct, const char *dptr, const char *limit, int *shift) { union nf_inet_addr addr; const char *aux = dptr; if (!parse_addr(ct, dptr, &dptr, &addr, limit)) { pr_debug("ip: %s parse failed.!\n", dptr); return 0; } /* Port number */ if (*dptr == ':') { dptr++; dptr += digits_len(ct, dptr, limit, shift); } return dptr - aux; } /* get address length, skiping user info. */ static int skp_epaddr_len(const struct nf_conn *ct, const char *dptr, const char *limit, int *shift) { const char *start = dptr; int s = *shift; /* Search for @, but stop at the end of the line. * We are inside a sip: URI, so we don't need to worry about * continuation lines. */ while (dptr < limit && *dptr != '@' && *dptr != '\r' && *dptr != '\n') { (*shift)++; dptr++; } if (dptr < limit && *dptr == '@') { dptr++; (*shift)++; } else { dptr = start; *shift = s; } return epaddr_len(ct, dptr, limit, shift); } /* Parse a SIP request line of the form: * * Request-Line = Method SP Request-URI SP SIP-Version CRLF * * and return the offset and length of the address contained in the Request-URI. */ int ct_sip_parse_request(const struct nf_conn *ct, const char *dptr, unsigned int datalen, unsigned int *matchoff, unsigned int *matchlen) { const char *start = dptr, *limit = dptr + datalen; unsigned int mlen; int shift = 0; /* Skip method and following whitespace */ mlen = string_len(ct, dptr, limit, NULL); if (!mlen) return 0; dptr += mlen; if (++dptr >= limit) return 0; /* Find SIP URI */ limit -= strlen("sip:"); for (; dptr < limit; dptr++) { if (*dptr == '\r' || *dptr == '\n') return -1; if (strnicmp(dptr, "sip:", strlen("sip:")) == 0) break; } *matchlen = skp_epaddr_len(ct, dptr, limit, &shift); if (!*matchlen) return 0; *matchoff = dptr - start + shift; return 1; } EXPORT_SYMBOL_GPL(ct_sip_parse_request); /* Returns 0 if not found, -1 error parsing. */ int ct_sip_get_info(const struct nf_conn *ct, const char *dptr, size_t dlen, unsigned int *matchoff, unsigned int *matchlen, enum sip_header_pos pos) { const struct sip_header_nfo *hnfo = &ct_sip_hdrs[pos]; const char *limit, *aux, *k = dptr; int shift = 0; limit = dptr + (dlen - hnfo->lnlen); while (dptr < limit) { if ((strncmp(dptr, hnfo->lname, hnfo->lnlen) != 0) && (hnfo->sname == NULL || strncmp(dptr, hnfo->sname, hnfo->snlen) != 0)) { dptr++; continue; } aux = ct_sip_search(hnfo->ln_str, dptr, hnfo->ln_strlen, ct_sip_lnlen(dptr, limit), hnfo->case_sensitive); if (!aux) { pr_debug("'%s' not found in '%s'.\n", hnfo->ln_str, hnfo->lname); return -1; } aux += hnfo->ln_strlen; *matchlen = hnfo->match_len(ct, aux, limit, &shift); if (!*matchlen) return -1; *matchoff = (aux - k) + shift; pr_debug("%s match succeeded! - len: %u\n", hnfo->lname, *matchlen); return 1; } pr_debug("%s header not found.\n", hnfo->lname); return 0; } EXPORT_SYMBOL_GPL(ct_sip_get_info); /* SDP header parsing: a SDP session description contains an ordered set of * headers, starting with a section containing general session parameters, * optionally followed by multiple media descriptions. * * SDP headers always start at the beginning of a line. According to RFC 2327: * "The sequence CRLF (0x0d0a) is used to end a record, although parsers should * be tolerant and also accept records terminated with a single newline * character". We handle both cases. */ static const struct sip_header ct_sdp_hdrs[] = { [SDP_HDR_VERSION] = SDP_HDR("v=", NULL, digits_len), [SDP_HDR_OWNER_IP4] = SDP_HDR("o=", "IN IP4 ", epaddr_len), [SDP_HDR_CONNECTION_IP4] = SDP_HDR("c=", "IN IP4 ", epaddr_len), [SDP_HDR_OWNER_IP6] = SDP_HDR("o=", "IN IP6 ", epaddr_len), [SDP_HDR_CONNECTION_IP6] = SDP_HDR("c=", "IN IP6 ", epaddr_len), [SDP_HDR_MEDIA] = SDP_HDR("m=", "audio ", digits_len), }; /* Linear string search within SDP header values */ static const char *ct_sdp_header_search(const char *dptr, const char *limit, const char *needle, unsigned int len) { for (limit -= len; dptr < limit; dptr++) { if (*dptr == '\r' || *dptr == '\n') break; if (strncmp(dptr, needle, len) == 0) return dptr; } return NULL; } /* Locate a SDP header (optionally a substring within the header value), * optionally stopping at the first occurence of the term header, parse * it and return the offset and length of the data we're interested in. */ int ct_sip_get_sdp_header(const struct nf_conn *ct, const char *dptr, unsigned int dataoff, unsigned int datalen, enum sdp_header_types type, enum sdp_header_types term, unsigned int *matchoff, unsigned int *matchlen) { const struct sip_header *hdr = &ct_sdp_hdrs[type]; const struct sip_header *thdr = &ct_sdp_hdrs[term]; const char *start = dptr, *limit = dptr + datalen; int shift = 0; for (dptr += dataoff; dptr < limit; dptr++) { /* Find beginning of line */ if (*dptr != '\r' && *dptr != '\n') continue; if (++dptr >= limit) break; if (*(dptr - 1) == '\r' && *dptr == '\n') { if (++dptr >= limit) break; } if (term != SDP_HDR_UNSPEC && limit - dptr >= thdr->len && strnicmp(dptr, thdr->name, thdr->len) == 0) break; else if (limit - dptr >= hdr->len && strnicmp(dptr, hdr->name, hdr->len) == 0) dptr += hdr->len; else continue; *matchoff = dptr - start; if (hdr->search) { dptr = ct_sdp_header_search(dptr, limit, hdr->search, hdr->slen); if (!dptr) return -1; dptr += hdr->slen; } *matchlen = hdr->match_len(ct, dptr, limit, &shift); if (!*matchlen) return -1; *matchoff = dptr - start + shift; return 1; } return 0; } EXPORT_SYMBOL_GPL(ct_sip_get_sdp_header); static int set_expected_rtp(struct sk_buff *skb, const char **dptr, unsigned int *datalen, union nf_inet_addr *addr, __be16 port) { struct nf_conntrack_expect *exp; enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo); int family = ct->tuplehash[!dir].tuple.src.l3num; int ret; typeof(nf_nat_sdp_hook) nf_nat_sdp; exp = nf_ct_expect_alloc(ct); if (exp == NULL) return NF_DROP; nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, family, &ct->tuplehash[!dir].tuple.src.u3, addr, IPPROTO_UDP, NULL, &port); nf_nat_sdp = rcu_dereference(nf_nat_sdp_hook); if (nf_nat_sdp && ct->status & IPS_NAT_MASK) ret = nf_nat_sdp(skb, dptr, datalen, exp); else { if (nf_ct_expect_related(exp) != 0) ret = NF_DROP; else ret = NF_ACCEPT; } nf_ct_expect_put(exp); return ret; } static int sip_help(struct sk_buff *skb, unsigned int protoff, struct nf_conn *ct, enum ip_conntrack_info ctinfo) { int family = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.l3num; union nf_inet_addr addr; unsigned int dataoff, datalen; const char *dptr; int ret = NF_ACCEPT; unsigned int matchoff, matchlen; u_int16_t port; enum sdp_header_types type; typeof(nf_nat_sip_hook) nf_nat_sip; /* No Data ? */ dataoff = protoff + sizeof(struct udphdr); if (dataoff >= skb->len) return NF_ACCEPT; nf_ct_refresh(ct, skb, sip_timeout * HZ); if (!skb_is_nonlinear(skb)) dptr = skb->data + dataoff; else { pr_debug("Copy of skbuff not supported yet.\n"); goto out; } nf_nat_sip = rcu_dereference(nf_nat_sip_hook); if (nf_nat_sip && ct->status & IPS_NAT_MASK) { if (!nf_nat_sip(skb, &dptr, &datalen)) { ret = NF_DROP; goto out; } } datalen = skb->len - dataoff; if (datalen < strlen("SIP/2.0 200")) goto out; /* RTP info only in some SDP pkts */ if (strnicmp(dptr, "INVITE", strlen("INVITE")) != 0 && strnicmp(dptr, "UPDATE", strlen("UPDATE")) != 0 && strnicmp(dptr, "SIP/2.0 180", strlen("SIP/2.0 180")) != 0 && strnicmp(dptr, "SIP/2.0 183", strlen("SIP/2.0 183")) != 0 && strnicmp(dptr, "SIP/2.0 200", strlen("SIP/2.0 200")) != 0) { goto out; } /* Get address and port from SDP packet. */ type = family == AF_INET ? SDP_HDR_CONNECTION_IP4 : SDP_HDR_CONNECTION_IP6; if (ct_sip_get_sdp_header(ct, dptr, 0, datalen, type, SDP_HDR_UNSPEC, &matchoff, &matchlen) > 0) { /* We'll drop only if there are parse problems. */ if (!parse_addr(ct, dptr + matchoff, NULL, &addr, dptr + datalen)) { ret = NF_DROP; goto out; } if (ct_sip_get_sdp_header(ct, dptr, 0, datalen, SDP_HDR_MEDIA, SDP_HDR_UNSPEC, &matchoff, &matchlen) > 0) { port = simple_strtoul(dptr + matchoff, NULL, 10); if (port < 1024) { ret = NF_DROP; goto out; } ret = set_expected_rtp(skb, &dptr, &datalen, &addr, htons(port)); } } out: return ret; } static struct nf_conntrack_helper sip[MAX_PORTS][2] __read_mostly; static char sip_names[MAX_PORTS][2][sizeof("sip-65535")] __read_mostly; static const struct nf_conntrack_expect_policy sip_exp_policy = { .max_expected = 2, .timeout = 3 * 60, }; static void nf_conntrack_sip_fini(void) { int i, j; for (i = 0; i < ports_c; i++) { for (j = 0; j < 2; j++) { if (sip[i][j].me == NULL) continue; nf_conntrack_helper_unregister(&sip[i][j]); } } } static int __init nf_conntrack_sip_init(void) { int i, j, ret; char *tmpname; if (ports_c == 0) ports[ports_c++] = SIP_PORT; for (i = 0; i < ports_c; i++) { memset(&sip[i], 0, sizeof(sip[i])); sip[i][0].tuple.src.l3num = AF_INET; sip[i][1].tuple.src.l3num = AF_INET6; for (j = 0; j < 2; j++) { sip[i][j].tuple.dst.protonum = IPPROTO_UDP; sip[i][j].tuple.src.u.udp.port = htons(ports[i]); sip[i][j].expect_policy = &sip_exp_policy; sip[i][j].me = THIS_MODULE; sip[i][j].help = sip_help; tmpname = &sip_names[i][j][0]; if (ports[i] == SIP_PORT) sprintf(tmpname, "sip"); else sprintf(tmpname, "sip-%u", i); sip[i][j].name = tmpname; pr_debug("port #%u: %u\n", i, ports[i]); ret = nf_conntrack_helper_register(&sip[i][j]); if (ret) { printk("nf_ct_sip: failed to register helper " "for pf: %u port: %u\n", sip[i][j].tuple.src.l3num, ports[i]); nf_conntrack_sip_fini(); return ret; } } } return 0; } module_init(nf_conntrack_sip_init); module_exit(nf_conntrack_sip_fini);