blob: 56d260344478779c9a93d717769d9ba16ad43311 [file] [log] [blame]
/*
* RTSP extension for TCP NAT alteration
* (C) 2003 by Tom Marshall <tmarshall at real.com>
*
* 2013-03-04: Il'inykh Sergey <sergeyi at inango-sw.com>. Inango Systems Ltd
* - fixed rtcp nat mapping and other port mapping fixes
* - fixed system hard lock because of bug in the parser
* - codestyle fixes and less significant fixes
*
* based on ip_nat_irc.c
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*
* Module load syntax:
* insmod nf_nat_rtsp.o ports=port1,port2,...port<MAX_PORTS>
* stunaddr=<address>
* destaction=[auto|strip|none]
*
* If no ports are specified, the default will be port 554 only.
*
* stunaddr specifies the address used to detect that a client is using STUN.
* If this address is seen in the destination parameter, it is assumed that
* the client has already punched a UDP hole in the firewall, so we don't
* mangle the client_port. If none is specified, it is autodetected. It
* only needs to be set if you have multiple levels of NAT. It should be
* set to the external address that the STUN clients detect. Note that in
* this case, it will not be possible for clients to use UDP with servers
* between the NATs.
*
* If no destaction is specified, auto is used.
* destaction=auto: strip destination parameter if it is not stunaddr.
* destaction=strip: always strip destination parameter (not recommended).
* destaction=none: do not touch destination parameter (not recommended).
*/
#include <linux/module.h>
#include <linux/version.h>
#include <net/tcp.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
# include <net/netfilter/nf_nat.h>
#else
# include <net/netfilter/nf_nat_rule.h>
#endif
#include <net/netfilter/nf_nat_helper.h>
#include <linux/netfilter/nf_conntrack_rtsp.h>
#include <net/netfilter/nf_conntrack_expect.h>
#include <linux/inet.h>
#include <linux/ctype.h>
#define NF_NEED_STRNCASECMP
#define NF_NEED_STRTOU16
#include <linux/netfilter/netfilter_helpers.h>
#define NF_NEED_MIME_NEXTLINE
#include <linux/netfilter/netfilter_mime.h>
#define MAX_PORTS 8
#define DSTACT_AUTO 0
#define DSTACT_STRIP 1
#define DSTACT_NONE 2
static char* stunaddr = NULL;
static char* destaction = NULL;
static u_int32_t extip = 0;
static int dstact = 0;
static void nf_nat_rtsp_expected(struct nf_conn* ct, struct nf_conntrack_expect *exp);
MODULE_AUTHOR("Tom Marshall <tmarshall at real.com>");
MODULE_DESCRIPTION("RTSP network address translation module");
MODULE_LICENSE("GPL");
module_param(stunaddr, charp, 0644);
MODULE_PARM_DESC(stunaddr, "Address for detecting STUN");
module_param(destaction, charp, 0644);
MODULE_PARM_DESC(destaction, "Action for destination parameter (auto/strip/none)");
#define SKIP_WSPACE(ptr,len,off) while(off < len && isspace(*(ptr+off))) { off++; }
/*** helper functions ***/
static void
get_skb_tcpdata(struct sk_buff* skb, char** pptcpdata, uint* ptcpdatalen)
{
struct iphdr* iph = ip_hdr(skb);
struct tcphdr* tcph = (void *)iph + ip_hdrlen(skb);
*pptcpdata = (char*)tcph + tcph->doff*4;
*ptcpdatalen = ((char*)skb_transport_header(skb) + skb->len) - *pptcpdata;
}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
/* copy of sip_sprintf_addr */
static int rtsp_sprintf_addr(const struct nf_conn *ct, char *buffer,
const union nf_inet_addr *addr, bool delim)
{
if (nf_ct_l3num(ct) == NFPROTO_IPV4) {
return sprintf(buffer, "%pI4", &addr->ip);
} else {
if (delim)
return sprintf(buffer, "[%pI6c]", &addr->ip6);
else
return sprintf(buffer, "%pI6c", &addr->ip6);
}
}
#endif
/*** nat functions ***/
/*
* Mangle the "Transport:" header:
* - Replace all occurences of "client_port=<spec>"
* - Handle destination parameter
*
* In:
* ct, ctinfo = conntrack context
* skb = packet
* tranoff = Transport header offset from TCP data
* tranlen = Transport header length (incl. CRLF)
* rport_lo = replacement low port (host endian)
* rport_hi = replacement high port (host endian)
*
* Returns packet size difference.
*
* Assumes that a complete transport header is present, ending with CR or LF
*/
static int
rtsp_mangle_tran(enum ip_conntrack_info ctinfo,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
unsigned int protoff,
#endif
struct nf_conntrack_expect* rtp_exp,
struct nf_conntrack_expect* rtcp_exp,
struct ip_ct_rtsp_expect* prtspexp,
struct sk_buff* skb, uint tranoff, uint tranlen)
{
char* ptcp;
uint tcplen;
char* ptran;
char rbuf1[16]; /* Replacement buffer (one port) */
uint rbuf1len; /* Replacement len (one port) */
char rbufa[16]; /* Replacement buffer (all ports) */
uint rbufalen; /* Replacement len (all ports) */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
union nf_inet_addr newip;
#else
u_int32_t newip;
#endif
u_int16_t loport, hiport;
uint off = 0;
uint diff; /* Number of bytes we removed */
struct nf_conn *ct = rtp_exp->master;
/* struct nf_conn *ct = nf_ct_get(skb, &ctinfo); */
struct nf_conntrack_tuple *rtp_t;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
char szextaddr[INET6_ADDRSTRLEN];
#else
char szextaddr[INET_ADDRSTRLEN];
#endif
uint extaddrlen;
int is_stun;
get_skb_tcpdata(skb, &ptcp, &tcplen);
ptran = ptcp+tranoff;
if (tranoff+tranlen > tcplen || tcplen-tranoff < tranlen ||
tranlen < 10 || !iseol(ptran[tranlen-1]) ||
nf_strncasecmp(ptran, "Transport:", 10) != 0) {
pr_info("sanity check failed\n");
return 0;
}
off += 10;
SKIP_WSPACE(ptcp+tranoff, tranlen, off);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
newip = ct->tuplehash[IP_CT_DIR_REPLY].tuple.dst.u3;
rtp_t = &rtp_exp->tuple;
rtp_t->dst.u3 = newip;
if (rtcp_exp) {
rtcp_exp->tuple.dst.u3 = newip;
}
extaddrlen = rtsp_sprintf_addr(ct, szextaddr, &newip, true); // FIXME handle extip
pr_debug("stunaddr=%s (auto)\n", szextaddr);
#else
newip = ct->tuplehash[IP_CT_DIR_REPLY].tuple.dst.u3.ip;
rtp_t = &rtp_exp->tuple;
rtp_t->dst.u3.ip = newip;
if (rtcp_exp) {
rtcp_exp->tuple.dst.u3.ip = newip;
}
extaddrlen = extip ? sprintf(szextaddr, "%pI4", &extip)
: sprintf(szextaddr, "%pI4", &newip);
pr_debug("stunaddr=%s (%s)\n", szextaddr, (extip?"forced":"auto"));
#endif
hiport = 0;
rbuf1len = rbufalen = 0;
switch (prtspexp->pbtype) {
case pb_single:
for (loport = prtspexp->loport; loport != 0; loport++) { /* XXX: improper wrap? */
rtp_t->dst.u.udp.port = htons(loport);
if (nf_ct_expect_related(rtp_exp) == 0) {
pr_debug("using port %hu\n", loport);
break;
}
}
if (loport != 0) {
rbuf1len = sprintf(rbuf1, "%hu", loport);
rbufalen = sprintf(rbufa, "%hu", loport);
}
break;
case pb_range:
for (loport = prtspexp->loport; loport != 0; loport += 2) { /* XXX: improper wrap? */
rtp_t->dst.u.udp.port = htons(loport);
if (nf_ct_expect_related(rtp_exp) != 0) {
continue;
}
hiport = loport + 1;
rtcp_exp->tuple.dst.u.udp.port = htons(hiport);
if (nf_ct_expect_related(rtcp_exp) != 0) {
nf_ct_unexpect_related(rtp_exp);
continue;
}
/* FIXME: invalid print in case of ipv6 */
pr_debug("nat expect_related %pI4:%u-%u-%pI4:%u-%u\n",
&rtp_exp->tuple.src.u3.ip,
ntohs(rtp_exp->tuple.src.u.udp.port),
ntohs(rtcp_exp->tuple.src.u.udp.port),
&rtp_exp->tuple.dst.u3.ip,
ntohs(rtp_exp->tuple.dst.u.udp.port),
ntohs(rtcp_exp->tuple.dst.u.udp.port));
break;
}
if (loport != 0) {
rbuf1len = sprintf(rbuf1, "%hu", loport);
rbufalen = sprintf(rbufa, "%hu-%hu", loport, hiport);
}
break;
case pb_discon:
for (loport = prtspexp->loport; loport != 0; loport++) { /* XXX: improper wrap? */
rtp_t->dst.u.udp.port = htons(loport);
if (nf_ct_expect_related(rtp_exp) == 0) {
pr_debug("using port %hu (1 of 2)\n", loport);
break;
}
}
for (hiport = prtspexp->hiport; hiport != 0; hiport++) { /* XXX: improper wrap? */
rtp_t->dst.u.udp.port = htons(hiport);
if (nf_ct_expect_related(rtp_exp) == 0) {
pr_debug("using port %hu (2 of 2)\n", hiport);
break;
}
}
if (loport != 0 && hiport != 0) {
rbuf1len = sprintf(rbuf1, "%hu", loport);
rbufalen = sprintf(rbufa, hiport == loport+1 ?
"%hu-%hu":"%hu/%hu", loport, hiport);
}
break;
}
if (rbuf1len == 0)
return 0; /* cannot get replacement port(s) */
/* Transport: tran;field;field=val,tran;field;field=val,...
`off` is set to the start of Transport value from start of line
*/
while (off < tranlen) {
uint saveoff;
const char* pparamend;
uint nextparamoff;
pparamend = memchr(ptran+off, ',', tranlen-off);
pparamend = (pparamend == NULL) ? ptran+tranlen : pparamend+1;
nextparamoff = pparamend-ptran;
/*
* We pass over each param twice. On the first pass, we look for a
* destination= field. It is handled by the security policy. If it
* is present, allowed, and equal to our external address, we assume
* that STUN is being used and we leave the client_port= field alone.
*/
is_stun = 0;
saveoff = off;
while (off < nextparamoff) {
const char* pfieldend;
uint nextfieldoff;
pfieldend = memchr(ptran+off, ';', nextparamoff-off);
nextfieldoff = (pfieldend == NULL) ? nextparamoff : pfieldend-ptran+1;
if (dstact != DSTACT_NONE && strncmp(ptran+off, "destination=", 12) == 0) {
if (strncmp(ptran+off+12, szextaddr, extaddrlen) == 0)
is_stun = 1;
if (dstact == DSTACT_STRIP || (dstact == DSTACT_AUTO && !is_stun)) {
uint dstoff = (ptran-ptcp)+off;
uint dstlen = nextfieldoff-off;
char* pdstrep = NULL;
uint dstreplen = 0;
diff = dstlen;
if (dstact == DSTACT_AUTO && !is_stun) {
pr_debug("RTSP: replace dst addr\n");
dstoff += 12;
dstlen -= 13;
pdstrep = szextaddr;
dstreplen = extaddrlen;
diff = nextfieldoff-off-13-extaddrlen;
}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
if (!nf_nat_mangle_tcp_packet(skb, ct, ctinfo, protoff,
dstoff, dstlen, pdstrep, dstreplen)) {
#else
if (!nf_nat_mangle_tcp_packet(skb, ct, ctinfo,
dstoff, dstlen, pdstrep, dstreplen)) {
#endif
/* mangle failed, all we can do is bail */
nf_ct_unexpect_related(rtp_exp);
if (rtcp_exp)
nf_ct_unexpect_related(rtcp_exp);
return 0;
}
get_skb_tcpdata(skb, &ptcp, &tcplen);
ptran = ptcp+tranoff;
tranlen -= diff;
nextparamoff -= diff;
nextfieldoff -= diff;
}
}
off = nextfieldoff;
}
if (is_stun)
continue;
off = saveoff;
while (off < nextparamoff) {
const char* pfieldend;
uint nextfieldoff;
pfieldend = memchr(ptran+off, ';', nextparamoff-off);
nextfieldoff = (pfieldend == NULL) ? nextparamoff : pfieldend-ptran+1;
if (strncmp(ptran+off, "client_port=", 12) == 0) {
u_int16_t port;
uint numlen;
uint origoff;
uint origlen;
char* rbuf = rbuf1;
uint rbuflen = rbuf1len;
off += 12;
origoff = (ptran-ptcp)+off;
origlen = 0;
numlen = nf_strtou16(ptran+off, &port);
off += numlen;
origlen += numlen;
if (port != prtspexp->loport) {
pr_debug("multiple ports found, port %hu ignored\n", port);
} else {
if (ptran[off] == '-' || ptran[off] == '/') {
off++;
origlen++;
numlen = nf_strtou16(ptran+off, &port);
off += numlen;
origlen += numlen;
rbuf = rbufa;
rbuflen = rbufalen;
}
/*
* note we cannot just memcpy() if the sizes are the same.
* the mangle function does skb resizing, checks for a
* cloned skb, and updates the checksums.
*
* parameter 4 below is offset from start of tcp data.
*/
diff = origlen-rbuflen;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
if (!nf_nat_mangle_tcp_packet(skb, ct, ctinfo, protoff,
origoff, origlen, rbuf, rbuflen)) {
#else
if (!nf_nat_mangle_tcp_packet(skb, ct, ctinfo,
origoff, origlen, rbuf, rbuflen)) {
#endif
/* mangle failed, all we can do is bail */
nf_ct_unexpect_related(rtp_exp);
if (rtcp_exp)
nf_ct_unexpect_related(rtcp_exp);
return 0;
}
get_skb_tcpdata(skb, &ptcp, &tcplen);
ptran = ptcp+tranoff;
tranlen -= diff;
nextparamoff -= diff;
nextfieldoff -= diff;
}
}
off = nextfieldoff;
}
off = nextparamoff;
}
return 1;
}
static uint
help_out(struct sk_buff *skb, enum ip_conntrack_info ctinfo,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
unsigned int protoff,
#endif
unsigned int matchoff, unsigned int matchlen,
struct ip_ct_rtsp_expect* prtspexp,
struct nf_conntrack_expect* rtp_exp,
struct nf_conntrack_expect* rtcp_exp)
{
char* ptcp;
uint tcplen;
uint hdrsoff;
uint hdrslen;
uint lineoff;
uint linelen;
uint off;
int dir = CTINFO2DIR(ctinfo);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
union nf_inet_addr saddr = rtp_exp->master->tuplehash[dir].tuple.src.u3;
#else
__be32 saddr = rtp_exp->master->tuplehash[dir].tuple.src.u3.ip;
#endif
//struct iphdr* iph = (struct iphdr*)(*pskb)->nh.iph;
//struct tcphdr* tcph = (struct tcphdr*)((void*)iph + iph->ihl*4);
get_skb_tcpdata(skb, &ptcp, &tcplen);
hdrsoff = matchoff;//exp->seq - ntohl(tcph->seq);
hdrslen = matchlen;
off = hdrsoff;
pr_debug("NAT rtsp help_out\n");
while (nf_mime_nextline(ptcp, hdrsoff+hdrslen, &off, &lineoff, &linelen)) {
if (linelen == 0)
break;
if (off > hdrsoff+hdrslen) {
pr_info("!! overrun !!");
break;
}
pr_debug("hdr: len=%u, %.*s", linelen, (int)linelen, ptcp+lineoff);
if (nf_strncasecmp(ptcp+lineoff, "Transport:", 10) == 0) {
uint oldtcplen = tcplen;
pr_debug("hdr: Transport\n");
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
if (!rtsp_mangle_tran(ctinfo, protoff, rtp_exp, rtcp_exp,
prtspexp, skb, lineoff, linelen)) {
#else
if (!rtsp_mangle_tran(ctinfo, rtp_exp, rtcp_exp, prtspexp,
skb, lineoff, linelen)) {
#endif
pr_debug("hdr: Transport mangle failed");
break;
}
rtp_exp->expectfn = nf_nat_rtsp_expected;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
rtp_exp->saved_addr = saddr;
#else
rtp_exp->saved_ip = saddr;
#endif
rtp_exp->saved_proto.udp.port = htons(prtspexp->loport);
rtp_exp->dir = !dir;
if (rtcp_exp) {
rtcp_exp->expectfn = nf_nat_rtsp_expected;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
rtcp_exp->saved_addr = saddr;
#else
rtcp_exp->saved_ip = saddr;
#endif
rtcp_exp->saved_proto.udp.port = htons(prtspexp->hiport);
rtcp_exp->dir = !dir;
}
get_skb_tcpdata(skb, &ptcp, &tcplen);
hdrslen -= (oldtcplen-tcplen);
off -= (oldtcplen-tcplen);
lineoff -= (oldtcplen-tcplen);
linelen -= (oldtcplen-tcplen);
pr_debug("rep: len=%u, %.*s", linelen, (int)linelen, ptcp+lineoff);
}
}
return NF_ACCEPT;
}
static unsigned int
nf_nat_rtsp(struct sk_buff *skb, enum ip_conntrack_info ctinfo,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
unsigned int protoff,
#endif
unsigned int matchoff, unsigned int matchlen,
struct ip_ct_rtsp_expect* prtspexp,
struct nf_conntrack_expect* rtp_exp,
struct nf_conntrack_expect* rtcp_exp)
{
int dir = CTINFO2DIR(ctinfo);
int rc = NF_ACCEPT;
switch (dir) {
case IP_CT_DIR_ORIGINAL:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
rc = help_out(skb, ctinfo, protoff, matchoff, matchlen, prtspexp,
rtp_exp, rtcp_exp);
#else
rc = help_out(skb, ctinfo, matchoff, matchlen, prtspexp,
rtp_exp, rtcp_exp);
#endif
break;
case IP_CT_DIR_REPLY:
pr_debug("unmangle ! %u\n", ctinfo);
/* XXX: unmangle */
rc = NF_ACCEPT;
break;
}
//UNLOCK_BH(&ip_rtsp_lock);
return rc;
}
static void nf_nat_rtsp_expected(struct nf_conn* ct, struct nf_conntrack_expect *exp)
{
#if LINUX_VERSION_CODE < KERNEL_VERSION(3,3,0) || LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
struct nf_nat_range range;
#else
struct nf_nat_ipv4_range range;
#endif
/* This must be a fresh one. */
BUG_ON(ct->status & IPS_NAT_DONE_MASK);
/* For DST manip, map port here to where it's expected. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
range.min_proto = range.max_proto = exp->saved_proto;
range.min_addr = range.max_addr = exp->saved_addr;
#else
range.min = range.max = exp->saved_proto;
range.min_ip = range.max_ip = exp->saved_ip;
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,3,0)
range.flags = (NF_NAT_RANGE_MAP_IPS | NF_NAT_RANGE_PROTO_SPECIFIED);
nf_nat_setup_info(ct, &range, NF_NAT_MANIP_DST);
#else
range.flags = (IP_NAT_RANGE_MAP_IPS | IP_NAT_RANGE_PROTO_SPECIFIED);
nf_nat_setup_info(ct, &range, IP_NAT_MANIP_DST);
#endif
/* Change src to where master sends to, but only if the connection
* actually came from the same source. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,7,0)
if (nf_inet_addr_cmp(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u3,
&ct->master->tuplehash[exp->dir].tuple.src.u3)) {
range.min_addr = range.max_addr
= ct->master->tuplehash[!exp->dir].tuple.dst.u3;
#else
if (ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u3.ip ==
ct->master->tuplehash[exp->dir].tuple.src.u3.ip) {
range.min_ip = range.max_ip
= ct->master->tuplehash[!exp->dir].tuple.dst.u3.ip;
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,3,0)
range.flags = NF_NAT_RANGE_MAP_IPS;
nf_nat_setup_info(ct, &range, NF_NAT_MANIP_SRC);
#else
range.flags = IP_NAT_RANGE_MAP_IPS;
nf_nat_setup_info(ct, &range, IP_NAT_MANIP_SRC);
#endif
}
}
static void __exit fini(void)
{
rcu_assign_pointer(nf_nat_rtsp_hook, NULL);
synchronize_net();
}
static int __init init(void)
{
printk("nf_nat_rtsp v" IP_NF_RTSP_VERSION " loading\n");
BUG_ON(nf_nat_rtsp_hook);
rcu_assign_pointer(nf_nat_rtsp_hook, nf_nat_rtsp);
if (stunaddr != NULL)
extip = in_aton(stunaddr);
if (destaction != NULL) {
if (strcmp(destaction, "auto") == 0)
dstact = DSTACT_AUTO;
if (strcmp(destaction, "strip") == 0)
dstact = DSTACT_STRIP;
if (strcmp(destaction, "none") == 0)
dstact = DSTACT_NONE;
}
return 0;
}
module_init(init);
module_exit(fini);