| /* SIP extension for IP connection tracking. |
| * |
| * (C) 2005 by Christian Hentschel <chentschel@arnet.com.ar> |
| * 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 <linux/module.h> |
| #include <linux/ctype.h> |
| #include <linux/skbuff.h> |
| #include <linux/inet.h> |
| #include <linux/in.h> |
| #include <linux/udp.h> |
| #include <linux/netfilter.h> |
| |
| #include <net/netfilter/nf_conntrack.h> |
| #include <net/netfilter/nf_conntrack_expect.h> |
| #include <net/netfilter/nf_conntrack_helper.h> |
| #include <linux/netfilter/nf_conntrack_sip.h> |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Christian Hentschel <chentschel@arnet.com.ar>"); |
| 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, |
| enum ip_conntrack_info ctinfo, |
| struct nf_conn *ct, |
| const char **dptr) __read_mostly; |
| EXPORT_SYMBOL_GPL(nf_nat_sip_hook); |
| |
| unsigned int (*nf_nat_sdp_hook)(struct sk_buff *skb, |
| enum ip_conntrack_info ctinfo, |
| struct nf_conntrack_expect *exp, |
| const char *dptr) __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_REG_REQ_URI] = { /* SIP REGISTER request URI */ |
| .lname = "sip:", |
| .lnlen = sizeof("sip:") - 1, |
| .ln_str = ":", |
| .ln_strlen = sizeof(":") - 1, |
| .match_len = epaddr_len, |
| }, |
| [POS_REQ_URI] = { /* SIP request URI */ |
| .lname = "sip:", |
| .lnlen = sizeof("sip:") - 1, |
| .ln_str = "@", |
| .ln_strlen = sizeof("@") - 1, |
| .match_len = epaddr_len, |
| }, |
| [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 |
| }, |
| [POS_MEDIA] = { /* SDP media info */ |
| .case_sensitive = 1, |
| .lname = "\nm=", |
| .lnlen = sizeof("\nm=") - 1, |
| .sname = "\rm=", |
| .snlen = sizeof("\rm=") - 1, |
| .ln_str = "audio ", |
| .ln_strlen = sizeof("audio ") - 1, |
| .match_len = digits_len |
| }, |
| [POS_OWNER_IP4] = { /* SDP owner address*/ |
| .case_sensitive = 1, |
| .lname = "\no=", |
| .lnlen = sizeof("\no=") - 1, |
| .sname = "\ro=", |
| .snlen = sizeof("\ro=") - 1, |
| .ln_str = "IN IP4 ", |
| .ln_strlen = sizeof("IN IP4 ") - 1, |
| .match_len = epaddr_len |
| }, |
| [POS_CONNECTION_IP4] = {/* SDP connection info */ |
| .case_sensitive = 1, |
| .lname = "\nc=", |
| .lnlen = sizeof("\nc=") - 1, |
| .sname = "\rc=", |
| .snlen = sizeof("\rc=") - 1, |
| .ln_str = "IN IP4 ", |
| .ln_strlen = sizeof("IN IP4 ") - 1, |
| .match_len = epaddr_len |
| }, |
| [POS_OWNER_IP6] = { /* SDP owner address*/ |
| .case_sensitive = 1, |
| .lname = "\no=", |
| .lnlen = sizeof("\no=") - 1, |
| .sname = "\ro=", |
| .snlen = sizeof("\ro=") - 1, |
| .ln_str = "IN IP6 ", |
| .ln_strlen = sizeof("IN IP6 ") - 1, |
| .match_len = epaddr_len |
| }, |
| [POS_CONNECTION_IP6] = {/* SDP connection info */ |
| .case_sensitive = 1, |
| .lname = "\nc=", |
| .lnlen = sizeof("\nc=") - 1, |
| .sname = "\rc=", |
| .snlen = sizeof("\rc=") - 1, |
| .ln_str = "IN IP6 ", |
| .ln_strlen = sizeof("IN IP6 ") - 1, |
| .match_len = epaddr_len |
| }, |
| [POS_SDP_HEADER] = { /* SDP version header */ |
| .case_sensitive = 1, |
| .lname = "\nv=", |
| .lnlen = sizeof("\nv=") - 1, |
| .sname = "\rv=", |
| .snlen = sizeof("\rv=") - 1, |
| .ln_str = "=", |
| .ln_strlen = sizeof("=") - 1, |
| .match_len = 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 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); |
| } |
| |
| /* 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); |
| |
| static int set_expected_rtp(struct sk_buff *skb, |
| struct nf_conn *ct, |
| enum ip_conntrack_info ctinfo, |
| union nf_inet_addr *addr, |
| __be16 port, |
| const char *dptr) |
| { |
| struct nf_conntrack_expect *exp; |
| 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, ctinfo, exp, dptr); |
| 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 sip_header_pos pos; |
| 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, ctinfo, ct, &dptr)) { |
| ret = NF_DROP; |
| goto out; |
| } |
| } |
| |
| datalen = skb->len - dataoff; |
| if (datalen < sizeof("SIP/2.0 200") - 1) |
| goto out; |
| |
| /* RTP info only in some SDP pkts */ |
| if (memcmp(dptr, "INVITE", sizeof("INVITE") - 1) != 0 && |
| memcmp(dptr, "UPDATE", sizeof("UPDATE") - 1) != 0 && |
| memcmp(dptr, "SIP/2.0 180", sizeof("SIP/2.0 180") - 1) != 0 && |
| memcmp(dptr, "SIP/2.0 183", sizeof("SIP/2.0 183") - 1) != 0 && |
| memcmp(dptr, "SIP/2.0 200", sizeof("SIP/2.0 200") - 1) != 0) { |
| goto out; |
| } |
| /* Get address and port from SDP packet. */ |
| pos = family == AF_INET ? POS_CONNECTION_IP4 : POS_CONNECTION_IP6; |
| if (ct_sip_get_info(ct, dptr, datalen, &matchoff, &matchlen, pos) > 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_info(ct, dptr, datalen, &matchoff, &matchlen, |
| POS_MEDIA) > 0) { |
| |
| port = simple_strtoul(dptr + matchoff, NULL, 10); |
| if (port < 1024) { |
| ret = NF_DROP; |
| goto out; |
| } |
| ret = set_expected_rtp(skb, ct, ctinfo, &addr, |
| htons(port), dptr); |
| } |
| } |
| 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); |