#!/usr/bin/perl use strict; use warnings; use Socket; use Socket6; use Digest::SHA qw(hmac_sha1); use Digest::CRC qw(crc32); my ($controlling, $port, $username, $pwd, @addresses) = @ARGV; my %attrs = ( 6 => \&attr_un, 8 => \&attr_mi, 0x8028 => \&attr_fp, 0x25 => \&attr_use, 0x8029 => \&attr_controlled, 0x802a => \&attr_controlling, ); my (@sockets, $packet, $tract, $code); for my $addr (@addresses) { my ($fam, $pka, $sin, $meth) = addrparse($addr, $port); socket(my $fd, $fam, SOCK_DGRAM, 0) or die $!; bind($fd, $sin) or die $!; push(@sockets, {fd => $fd, fam => $fam, addr => $pka, sin => $sin, xormethod => $meth}); } while (1) { my $rin = ''; for my $s (@sockets) { vec($rin, fileno($$s{fd}), 1) = 1; } select($rin, undef, undef, 1); for my $s (@sockets) { vec($rin, fileno($$s{fd}), 1) or next; my $src = recv($$s{fd}, my $buf, 200, 0) or die $!; print("\npacket from " . addrdeparse($$s{fam}, $src) . " on ". addrdeparse($$s{fam}, $$s{sin}) . "\n"); my ($cmd, $len, $cookie, $attrs); ($cmd, $len, $cookie, $tract, $attrs) = unpack('nnN a12 a*', $buf); if ($cookie != 0x2112A442) { if ($buf =~ /^[\x14-\x3f]/s) { print("DTLS\n"); } else { print("not stun: " . unpack("H*", $buf)."\n"); } next; } if ($cmd != 1) { print("not stun request\n") next; } if (length($attrs) != $len) { print("length mismatch\n") next; } my ($list, $hash) = unpack_attrs($attrs); if ($$list[$#$list]{name} ne 'fingerprint') { print("last attr not fingerprint\n"); next; } if ($$list[$#$list-1]{name} ne 'message-integrity') { print("last but one attr not MI\n") next; } unless ($$hash{username}) { print("no username\n") next; } $$hash{controlling} and print("is controlling\n"); $$hash{controlled} and print("is controlled\n"); $$hash{'use-candidate'} and print("nominated\n"); print("local username is $$hash{username}{split}[0], remote is $$hash{username}{split}[1]\n"); if ($$hash{controlling} && $controlling || $$hash{controlled} && !$controlling) { print("role conflict, replying with 487"); $code = 0x0111; # binding error $packet = attr(0x9, pack('CCCC a16', 0, 0, 4, 87, 'Role conflict')); $packet .= integrity(); $packet .= fingerprint(); } else { $code = 0x101; # binding success my $xorattr = $$s{xormethod}($src); $packet = attr(0x20, $xorattr); $packet .= integrity(); $packet .= fingerprint(); } $packet = header() . $packet; print("sending reply\n"); send($$s{fd}, $packet, 0, $src); } } exit; sub xor4 { my ($src) = @_; my @a = unpack_sockaddr_in($src); return pack('nna4', 1, $a[0] ^ 0x2112, $a[1] ^ "\x21\x12\xa4\x42"); } sub xor6 { my ($src) = @_; my @a = unpack_sockaddr_in6($src); return pack('nna16', 2, $a[0] ^ 0x2112, $a[1] ^ ("\x21\x12\xa4\x42" . $tract)); } sub addrparse { my ($addr, $port) = @_; my $r = inet_pton(AF_INET, $addr); $r and return (AF_INET, $r, pack_sockaddr_in($port, $r), \&xor4); $r = inet_pton(AF_INET6, $addr); $r and return (AF_INET6, $r, pack_sockaddr_in6($port, $r), \&xor6); die; } sub addrdeparse { my ($fam, $sin) = @_; if ($fam == AF_INET) { my @up = unpack_sockaddr_in($sin); return inet_ntop(AF_INET, $up[1]) . ":$up[0]"; } if ($fam == AF_INET6) { my @up = unpack_sockaddr_in6($sin); return '['.inet_ntop(AF_INET6, $up[1]) . "]:$up[0]"; } die; } sub attr_un { my ($cont) = @_; return {name => 'username', split => [$cont =~ /(.*):(.*)/]}; } sub attr_mi { return {name => 'message-integrity'}; } sub attr_fp { my ($cont) = @_; return {name => 'fingerprint', value => unpack('N', $cont)}; } sub attr_use { return {name => 'use-candidate'}; } sub attr_controlling { my ($cont) = @_; return {name => 'controlling', tiebreaker => unpack('Q', $cont)}; } sub attr_controlled { my ($cont) = @_; return {name => 'controlled', tiebreaker => unpack('Q', $cont)}; } sub unpack_attrs { my ($s) = @_; my (@out, %out); while ($s ne '') { my ($type, $len, $cont); ($type, $len, $s) = unpack('nn a*', $s); my $pad = 0; while ((($len + $pad) % 4) != 0) { $pad++; } ($cont, $pad, $s) = unpack("a$len a$pad a*", $s); my $ins = {type => $type, len => $len, content => $cont, raw => pack('nna*a*', $type, $len, $cont, $pad)}; push(@out, $ins); $out{$type} = $ins; my $pars = $attrs{$type}; $pars or next; my $ai = $pars->($cont); %$ins = (%$ins, %$ai); $out{$$ins{name}} = $ins; } return (\@out, \%out); } sub attr { my ($type, $data) = @_; my $len = length($data); while ((length($data) % 4) != 0) { $data .= "\0"; } return pack('nn a*', $type, $len, $data); } sub header { my ($add_length) = @_; $add_length ||= 0; return pack('nnN a12', $code, length($packet) + $add_length, 0x2112A442, $tract); } sub integrity { my $h = header(24); my $hmac = hmac_sha1($h.$packet, $pwd); return attr(8, $hmac); } sub fingerprint { my $h = header(8); my $crc = crc32($h.$packet); return attr(0x8028, pack('N', ($crc ^ 0x5354554e))); }