diff options
Diffstat (limited to 'mbd')
-rw-r--r-- | mbd/access_list.pl | 294 | ||||
-rw-r--r-- | mbd/derpspan.c | 48 | ||||
-rwxr-xr-x | mbd/generate-helper-list.pl | 15 | ||||
-rw-r--r-- | mbd/make-mbd-nets.pl | 26 | ||||
-rw-r--r-- | mbd/mbd-unicast-segfaulting.pl | 273 | ||||
-rw-r--r-- | mbd/mbd-unicast.pl | 254 | ||||
-rw-r--r-- | mbd/mbd.pl | 254 | ||||
-rw-r--r-- | mbd/mbd.pm | 50 | ||||
-rw-r--r-- | mbd/nets.pl | 171 | ||||
-rw-r--r-- | mbd/packetpusher.c | 127 | ||||
-rw-r--r-- | mbd/survey.pl | 10 |
11 files changed, 1522 insertions, 0 deletions
diff --git a/mbd/access_list.pl b/mbd/access_list.pl new file mode 100644 index 0000000..89a8182 --- /dev/null +++ b/mbd/access_list.pl @@ -0,0 +1,294 @@ + +package Config; + +sub game_id { + my ($data, $offset) = @_; + my $id = ((ord(substr($data, $offset, 1)) << 8) | ord(substr($data, $offset + 1, 1))); + return $id; +} + +our @access_list = ( + # half-life - untested (packet dump only) + { + name => 'Half-Life', + ports => [ 27015 ], + sizes => [ 16 ] + }, + + # cs 1.6 - verified + # (funker muligens for _alle_ source-spill inkl. hl2/cs:s) + { + name => 'CS:Source', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 25 ], + filter => sub { return (game_id(shift, 4) == 0x4325); } + }, + { + name => 'Left 4 Dead', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 25 ], + filter => sub { return (game_id(shift, 4) == 0x43f3); } + }, + { + name => 'CS 1.6', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 25 ], + filter => sub { return (game_id(shift, 4) == 0x5453); } + }, + { + name => 'Unknown Source-based game (ID 0x4326)', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 25 ], + filter => sub { return (game_id(shift, 4) == 0x4326); } + }, + { + name => 'Other Source game (unknown game ID)', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 25 ], + }, + { + name => 'Other Source game (unknown game ID, odd length 33)', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 33 ], + }, + { + name => 'Other Source game (unknown game ID, odd length 58)', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 58 ], + }, + { + name => 'Other Source game (unknown game ID, odd length 15)', + ports => [ "26900..26903", "27015..27017" ], + sizes => [ 15 ], + }, + + # doom 3 - verified + { + name => 'Doom 3', + ports => [ "27666" ], + sizes => [ 14 ] + }, + + # quake 1 - verified + { + name => 'Quake 1', + ports => [ 26000 ], + sizes => [ 12 ] + }, + + # q3a - tested with demo only + # rtcw: enemy territory - untested (packet dump only) + { + name => 'Quake 3 Arena, RTCW: ET', +# ports => [ "27960..27969" ], + ports => [ "27960..27961" ], + sizes => [ 15 ] + }, + + # bf2 - tested with demo only + # bf2142 reportedly uses same engine + { + name => 'BF2/BF2142', + ports => [ "29900" ], + sizes => [ 8 ] + }, + + # bf1942 - unverified (packet dump only) + { + name => 'BF1942', + ports => [ "22000..22010" ], + sizes => [ 8 ] + }, + + # quake 4 - tested with demo only, MUST select "internet" + { + name => 'Quake 4', + ports => [ 27950, 28004 ], + sizes => [ 14 ] + }, + + # quake 2 - untested (packet dump only) + { + name => 'Quake 2', + ports => [ 27910 ], + sizes => [ 11 ] + }, + + # warcraft 3 - untested (packet dump only) + { + name => 'Warcraft 3: Reign of Chaos (1.00)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 0; } + }, + { + name => 'Warcraft 3: Reign of Chaos (1.07)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 7; } + }, + { + name => 'Warcraft 3: Reign of Chaos (1.20)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 20; } + }, + { + name => 'Warcraft 3: Reign of Chaos (1.22)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 22; } + }, + { + name => 'Warcraft 3: Reign of Chaos (1.23)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 23; } + }, + { + name => 'Warcraft 3: Reign of Chaos (other patch level)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352; } + }, + { + name => 'Warcraft 3: The Frozen Throne (1.17)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 17; } + }, + { + name => 'Warcraft 3: The Frozen Throne (1.18)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 18; } + }, + { + name => 'Warcraft 3: The Frozen Throne (1.20)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 20; } + }, + { + name => 'Warcraft 3: The Frozen Throne (1.21)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 21; } + }, + { + name => 'Warcraft 3: The Frozen Throne (1.22)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 22; } + }, + { + name => 'Warcraft 3: The Frozen Throne (1.23)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 23; } + }, + { + name => 'Warcraft 3: The Frozen Throne (1.26)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 26; } + }, + { + name => 'Warcraft 3: The Frozen Throne (other patch level)', + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058; } + }, + { + name => 'Warcraft 3 (unknown version)', +# ports => [ "6112..6119" ], + ports => [ 6112 ], + sizes => [ 16 ], + filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) != 0x5058 && game_id($data, 4) != 0x3352; } + }, + { + name => 'Warcraft 3 (unknown version, odd length)', + ports => [ 6112 ], + sizes => [ 19 ], + }, + + # ut2003/ut2004 - untested (packet dump only) + { + name => 'UT2003/UT2004', + ports => [ 10777 ], + sizes => [ 5 ] + }, + + # soldat - untested (packet dump only) + { + name => 'Soldat', + ports => [ 23073 ], + sizes => [ 8 ] + }, + + # starcraft - untested (packet dump only) + { + name => 'Starcraft', + ports => [ 6111, 6112 ], + sizes => [ 8 ], + filter => sub { return (game_id(shift, 0) == 0x08ef); } + }, + { + name => 'Starcraft: Brood War', + ports => [ 6111, 6112 ], + sizes => [ 8 ], + filter => sub { return (game_id(shift, 0) == 0xf733); } + }, + { + name => 'Starcraft (unknown game ID)', + ports => [ 6111, 6112 ], + sizes => [ 8 ], + filter => sub { my $id = game_id(shift, 0); return ($id != 0x08ef && $id != 0xf733); } + }, + + # trackmania nations - untested (packet dump only) + { + name => 'Trackmania Nations', + ports => [ "2350" ], + sizes => [ 42, 30 ] + }, + + # company of heroes - untested (packet dump only) + { + name => 'Company of Heroes', + ports => [ 9100 ], + sizes => [ 39 ] + }, + + # command & conquer 3 - untested (packet dump only, reported to have some kind + # of chat functionality) +# { +# name => 'Command & Conquer 3', +# ports => [ "8086..8093" ], +# sizes => [ 476 ], +# filter => sub { return 0; } +# }, + + # openttd + { + name => 'OpenTTD', + ports => [ 3979 ], + sizes => [ 3 ] + }, + + # CoD4 + { + name => 'Call of Duty 4', + ports => [ 28960 ], + sizes => [ 15 ], + }, + + # Far Cry 2 + { + name => 'Far Cry 2', + ports => [ 9004 ], + sizes => [ 114, 118, 122, 126 ], + }, + + # unreal tournament, port 9777? +) diff --git a/mbd/derpspan.c b/mbd/derpspan.c new file mode 100644 index 0000000..b9fb362 --- /dev/null +++ b/mbd/derpspan.c @@ -0,0 +1,48 @@ +// gcc -O2 -o derspan derspan.c -lpcap -std=gnu99 -Wall + +#include <pcap.h> +#include <stdlib.h> +#include <netinet/ip.h> +#include <stdint.h> +#include <stdio.h> + +int rawsock; + +void my_callback(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes) +{ + int len = h->caplen; + if (len < 40) { + //printf("skipped short packet\n"); + return; + } + if (bytes[14] != 0x88 || bytes[15] != 0xbe) { + //printf("skipped non-ethernet packet\n"); + return; + } + if (bytes[36] != 0x08 || bytes[37] != 0x00) { + //printf("skipped non-IPv4 packet\n"); + return; + } + + struct sockaddr_in self; + self.sin_family = AF_INET; + self.sin_addr.s_addr = htonl(0x7f000001); // localhost + self.sin_port = htons(1337); + + sendto(rawsock, bytes + 38, len - 38, 0, (struct sockaddr *)&self, sizeof(self)); +} + +int main(int argc, char **argv) +{ + rawsock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); + if (rawsock == -1) { + perror("socket"); + exit(0); + } + + pcap_t *pcap = pcap_open_live(argv[1], 1500, 1, 1000, NULL); + pcap_activate(pcap); + pcap_loop(pcap, -1, my_callback, NULL); + return 0; +} + diff --git a/mbd/generate-helper-list.pl b/mbd/generate-helper-list.pl new file mode 100755 index 0000000..fd89475 --- /dev/null +++ b/mbd/generate-helper-list.pl @@ -0,0 +1,15 @@ +#! /usr/bin/perl +use strict; +use warnings; +require './access_list.pl'; +require './nets.pl'; +require './mbd.pm'; + +my @ports = mbd::find_all_ports(); + +print "no ip forward-protocol udp 137\n"; +print "no ip forward-protocol udp 138\n"; + +for my $port (@ports) { + print "ip forward-protocol udp $port\n"; +} diff --git a/mbd/make-mbd-nets.pl b/mbd/make-mbd-nets.pl new file mode 100644 index 0000000..6af76f0 --- /dev/null +++ b/mbd/make-mbd-nets.pl @@ -0,0 +1,26 @@ +#!/usr/bin/perl -I /root/tgmanage/ +use strict; +use warnings; + +unless (@ARGV > 0) { + print "No arguments. Need netlist.txt.\n"; + exit 1; +} + +my $n = open(NETLIST, "$ARGV[0]") or die ("Cannot open netlist.txt"); + +print "# Autogenerated. Do not touch!\n"; +print "package Config;\n"; +print "our \@networks = (\n"; + +while(<NETLIST>) { + next if /^(.*#|\s+$)/; # skip if comment, or blank line + + chomp; + my ($network, $prefix, $switchname, undef) = split; + + print "\t\"$network/$prefix\",\n"; +} + +print ");\n"; +print "1;\n"; diff --git a/mbd/mbd-unicast-segfaulting.pl b/mbd/mbd-unicast-segfaulting.pl new file mode 100644 index 0000000..c167511 --- /dev/null +++ b/mbd/mbd-unicast-segfaulting.pl @@ -0,0 +1,273 @@ +#! /usr/bin/perl +use strict; +use warnings; +use Socket; +use Net::CIDR; +use Net::RawIP; +use Time::HiRes; +require './access_list.pl'; +require './nets.pl'; +require './survey.pl'; +require './mbd.pm'; +use lib '../include'; +use nms; +use strict; +use warnings; +use threads; + +# Mark packets with DSCP CS7 +my $tos = 56; + +my ($dbh, $q); + +sub fhbits { + my $bits = 0; + for my $fh (@_) { + vec($bits, fileno($fh), 1) = 1; + } + return $bits; +} + +# used for rate limiting +my %last_sent = (); + +# for own surveying +my %active_surveys = (); +my %last_survey = (); + +my %cidrcache = (); +sub cache_cidrlookup { + my ($addr, $net) = @_; + my $key = $addr . " " . $net; + + if (!exists($cidrcache{$key})) { + $cidrcache{$key} = Net::CIDR::cidrlookup($addr, $net); + } + return $cidrcache{$key}; +} + +my %rangecache = (); +sub cache_cidrrange { + my ($net) = @_; + + if (!exists($rangecache{$net})) { + my ($range) = Net::CIDR::cidr2range($net); + $range =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.(\d+)\.(\d+)/ or die "Did not understand range: $range"; + my @range = (); + for my $l (($4+1)..($8-1)) { + push @range, "$1.$2.$3.$l"; + } + ($rangecache{$net}) = \@range; + } + + return @{$rangecache{$net}}; +} + +open LOG, ">>", "mbd.log"; + +my @ports = ( mbd::find_all_ports() , $Config::survey_port_low .. $Config::survey_port_high ); + +# Open a socket for each port +my @socks = (); +my $udp = getprotobyname("udp"); +for my $p (@ports) { + my $sock; + socket($sock, PF_INET, SOCK_DGRAM, $udp); + bind($sock, sockaddr_in($p, INADDR_ANY)); + push @socks, $sock; +} + +my $sendsock = Net::RawIP->new({udp => {}}); + +print "Listening on " . scalar @ports . " ports.\n"; + +# Main loop +while (1) { + my $rin = fhbits(@socks); + my $rout; + + my $nfound = select($rout=$rin, undef, undef, undef); + my $now = [Time::HiRes::gettimeofday]; + + # First of all, close any surveys that are due. + for my $sport (keys %active_surveys) { + my $age = Time::HiRes::tv_interval($active_surveys{$sport}{start}, $now); + if ($age > $Config::survey_time && $active_surveys{$sport}{active}) { + my $hexdump = join(' ', map { sprintf "0x%02x", ord($_) } (split //, $active_surveys{$sport}{data})); + print "Survey ($hexdump) for '" . $Config::access_list[$active_surveys{$sport}{entry}]->{name} . "'/" . + $active_surveys{$sport}{dport} . ": " . $active_surveys{$sport}{num} . " active servers.\n"; + $active_surveys{$sport}{active} = 0; + + # (re)connect to the database if needed + if (!defined($dbh) || !$dbh->ping) { + $dbh = nms::db_connect(); + $q = $dbh->prepare("INSERT INTO mbd_log (ts,game,port,description,active_servers) VALUES (CURRENT_TIMESTAMP,?,?,?,?)") + or die "Couldn't prepare query"; + } + $q->execute($active_surveys{$sport}{entry}, $active_surveys{$sport}{dport}, $Config::access_list[$active_surveys{$sport}{entry}]->{name}, $active_surveys{$sport}{num}); + } + if ($age > $Config::survey_time * 3.0) { + delete $active_surveys{$sport}; + } + } + + for my $sock (@socks) { + next unless (vec($rout, fileno($sock), 1) == 1); + + my $data; + my $addr = recv($sock, $data, 8192, 0); # jumbo broadcast! :-P + my ($sport, $saddr) = sockaddr_in($addr); + my ($dport, $daddr) = sockaddr_in(getsockname($sock)); + my $size = length($data); + + # Check if this is a survey reply + if ($dport >= $Config::survey_port_low && $dport <= $Config::survey_port_high) { + if (!exists($active_surveys{$dport})) { + print "WARNING: Unknown survey port $dport, ignoring\n"; + next; + } + if (!$active_surveys{$dport}{active}) { + # remains + next; + } + + ++$active_surveys{$dport}{num}; + + next; + } + + # Rate limiting + if (exists($last_sent{$saddr}{$dport})) { + my $elapsed = Time::HiRes::tv_interval($last_sent{$saddr}{$dport}, $now); + if ($elapsed < 1.0) { + print LOG "$dport $size 2\n"; + print inet_ntoa($saddr), ", $dport, $size bytes => rate-limited ($elapsed secs since last)\n"; + next; + } + } + + # We don't get the packet's destination address, but I guess this should do... + # Check against the ACL. + my $pass = 0; + my $entry = -1; + for my $rule (@Config::access_list) { + ++$entry; + + next unless (mbd::match_ranges($dport, $rule->{'ports'})); + next unless (mbd::match_ranges($size, $rule->{'sizes'})); + + if ($rule->{'filter'}) { + next unless ($rule->{'filter'}($data)); + } + + $pass = 1; + last; + } + + print LOG "$dport $size $pass\n"; + + if (!$pass) { + print inet_ntoa($saddr), ", $dport, $size bytes => filtered\n"; + next; + } + + $last_sent{$saddr}{$dport} = $now; + + # The packet is OK! Do we already have a recent enough survey + # for this port, or should we use this packet? + my $survey = 1; + if (exists($last_survey{$entry . "/" . $dport})) { + my $age = Time::HiRes::tv_interval($last_survey{$entry . "/" . $dport}, $now); + if ($age < $Config::survey_freq) { + $survey = 0; + } + } + + # New survey; find an unused port + my $survey_sport; + if ($survey) { + for my $port ($Config::survey_port_low..$Config::survey_port_high) { + if (!exists($active_surveys{$port})) { + $survey_sport = $port; + + $active_surveys{$port} = { + start => $now, + active => 1, + dport => $dport, + entry => $entry, + num => 0, + data => $data, + }; + $last_survey{$entry . "/" . $dport} = $now; + + last; + } + } + + if (!defined($survey_sport)) { + print "WARNING: no free survey source ports, not surveying.\n"; + $survey = 0; + } + } + + # precache + for my $net (@Config::networks) { + cache_cidrrange($net); + cache_cidrlookup(inet_ntoa($saddr), $net); + } + + threads->create(sub { + my $sendsock = Net::RawIP->new({udp => {}}); + my ($survey_sport, $dport, $data) = @_; + + my $num_nets = 0; + for my $net (@Config::networks) { + my @daddrs = cache_cidrrange($net); + + if ($survey) { + for my $daddr (@daddrs) { + $sendsock->set({ + ip => { + saddr => $Config::survey_ip, + daddr => $daddr, + tos => $tos + }, + udp => { + source => $survey_sport, + dest => $dport, + data => $data + } + }); + $sendsock->send; + } + } + + next if (cache_cidrlookup(inet_ntoa($saddr), $net)); + + for my $daddr (@daddrs) { + $sendsock->set({ + ip => { + saddr => inet_ntoa($saddr), + daddr => $daddr, + tos => $tos + }, + udp => { + source => $sport, + dest => $dport, + data => $data + } + }); + $sendsock->send; + } + + ++$num_nets; + } + if ($survey) { + print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks) [+survey from port $survey_sport]\n"; + } else { + print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks)\n"; + } + }, $survey_sport, $dport, $data)->detach(); + } +} + diff --git a/mbd/mbd-unicast.pl b/mbd/mbd-unicast.pl new file mode 100644 index 0000000..6e63dee --- /dev/null +++ b/mbd/mbd-unicast.pl @@ -0,0 +1,254 @@ +#! /usr/bin/perl +use strict; +use warnings; +use Socket; +use Net::CIDR; +use Net::RawIP; +use Time::HiRes; +require './access_list.pl'; +require './nets.pl'; +require './survey.pl'; +require './mbd.pm'; +use lib '../include'; +use nms; +use strict; +use warnings; +use threads; + +# Mark packets with DSCP CS7 +my $tos = 56; + +my ($dbh, $q); + +sub fhbits { + my $bits = 0; + for my $fh (@_) { + vec($bits, fileno($fh), 1) = 1; + } + return $bits; +} + +# used for rate limiting +my %last_sent = (); + +# for own surveying +my %active_surveys = (); +my %last_survey = (); + +my %cidrcache = (); +sub cache_cidrlookup { + my ($addr, $net) = @_; + my $key = $addr . " " . $net; + + if (!exists($cidrcache{$key})) { + $cidrcache{$key} = Net::CIDR::cidrlookup($addr, $net); + } + return $cidrcache{$key}; +} + +my %rangecache = (); +sub cache_cidrrange { + my ($net) = @_; + + if (!exists($rangecache{$net})) { + my ($range) = Net::CIDR::cidr2range($net); + $range =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.(\d+)\.(\d+)/ or die "Did not understand range: $range"; + my @range = (); + for my $l (($4+1)..($8-1)) { + push @range, "$1.$2.$3.$l"; + } + ($rangecache{$net}) = \@range; + } + + return @{$rangecache{$net}}; +} + +open LOG, ">>", "mbd.log"; + +my @ports = ( mbd::find_all_ports() , $Config::survey_port_low .. $Config::survey_port_high ); + +# Open a socket for each port +my @socks = (); +my $udp = getprotobyname("udp"); +for my $p (@ports) { + my $sock; + socket($sock, PF_INET, SOCK_DGRAM, $udp); + bind($sock, sockaddr_in($p, INADDR_ANY)); + push @socks, $sock; +} + +my $sendsock = Net::RawIP->new({udp => {}}); + +print "Listening on " . scalar @ports . " ports.\n"; + +open PKTS, "| ./packetpusher" + or die "./packetpusher: $!"; + +# Main loop +while (1) { + my $rin = fhbits(@socks); + my $rout; + + my $nfound = select($rout=$rin, undef, undef, undef); + my $now = [Time::HiRes::gettimeofday]; + + # First of all, close any surveys that are due. + for my $sport (keys %active_surveys) { + my $age = Time::HiRes::tv_interval($active_surveys{$sport}{start}, $now); + if ($age > $Config::survey_time && $active_surveys{$sport}{active}) { + my $hexdump = join(' ', map { sprintf "0x%02x", ord($_) } (split //, $active_surveys{$sport}{data})); + print "Survey ($hexdump) for '" . $Config::access_list[$active_surveys{$sport}{entry}]->{name} . "'/" . + $active_surveys{$sport}{dport} . ": " . $active_surveys{$sport}{num} . " active servers.\n"; + $active_surveys{$sport}{active} = 0; + + # (re)connect to the database if needed + if (!defined($dbh) || !$dbh->ping) { + $dbh = nms::db_connect(); + $q = $dbh->prepare("INSERT INTO mbd_log (ts,game,port,description,active_servers) VALUES (CURRENT_TIMESTAMP,?,?,?,?)") + or die "Couldn't prepare query"; + } + $q->execute($active_surveys{$sport}{entry}, $active_surveys{$sport}{dport}, $Config::access_list[$active_surveys{$sport}{entry}]->{name}, $active_surveys{$sport}{num}); + } + if ($age > $Config::survey_time * 3.0) { + delete $active_surveys{$sport}; + } + } + + for my $sock (@socks) { + next unless (vec($rout, fileno($sock), 1) == 1); + + my $data; + my $addr = recv($sock, $data, 8192, 0); # jumbo broadcast! :-P + my ($sport, $saddr) = sockaddr_in($addr); + my ($dport, $daddr) = sockaddr_in(getsockname($sock)); + my $size = length($data); + + # Check if this is a survey reply + if ($dport >= $Config::survey_port_low && $dport <= $Config::survey_port_high) { + if (!exists($active_surveys{$dport})) { + print "WARNING: Unknown survey port $dport, ignoring\n"; + next; + } + if (!$active_surveys{$dport}{active}) { + # remains + next; + } + + ++$active_surveys{$dport}{num}; + + next; + } + + # Rate limiting + if (exists($last_sent{$saddr}{$dport})) { + my $elapsed = Time::HiRes::tv_interval($last_sent{$saddr}{$dport}, $now); + if ($elapsed < 1.0) { + print LOG "$dport $size 2\n"; + print inet_ntoa($saddr), ", $dport, $size bytes => rate-limited ($elapsed secs since last)\n"; + next; + } + } + + # We don't get the packet's destination address, but I guess this should do... + # Check against the ACL. + my $pass = 0; + my $entry = -1; + for my $rule (@Config::access_list) { + ++$entry; + + next unless (mbd::match_ranges($dport, $rule->{'ports'})); + next unless (mbd::match_ranges($size, $rule->{'sizes'})); + + if ($rule->{'filter'}) { + next unless ($rule->{'filter'}($data)); + } + + $pass = 1; + last; + } + + print LOG "$dport $size $pass\n"; + + if (!$pass) { + print inet_ntoa($saddr), ", $dport, $size bytes => filtered\n"; + next; + } + + $last_sent{$saddr}{$dport} = $now; + + # The packet is OK! Do we already have a recent enough survey + # for this port, or should we use this packet? + my $survey = 1; + if (exists($last_survey{$entry . "/" . $dport})) { + my $age = Time::HiRes::tv_interval($last_survey{$entry . "/" . $dport}, $now); + if ($age < $Config::survey_freq) { + $survey = 0; + } + } + + # New survey; find an unused port + my $survey_sport; + if ($survey) { + for my $port ($Config::survey_port_low..$Config::survey_port_high) { + if (!exists($active_surveys{$port})) { + $survey_sport = $port; + + $active_surveys{$port} = { + start => $now, + active => 1, + dport => $dport, + entry => $entry, + num => 0, + data => $data, + }; + $last_survey{$entry . "/" . $dport} = $now; + + last; + } + } + + if (!defined($survey_sport)) { + print "WARNING: no free survey source ports, not surveying.\n"; + $survey = 0; + } + } + + # precache + for my $net (@Config::networks) { + cache_cidrrange($net); + cache_cidrlookup(inet_ntoa($saddr), $net); + } + + my @packets = (); + + my $num_nets = 0; + for my $net (@Config::networks) { + my @daddrs = cache_cidrrange($net); + + if ($survey) { + for my $daddr (@daddrs) { + push @packets, [ $Config::survey_ip, $survey_sport, $daddr, $dport ]; + } + } + + next if (cache_cidrlookup(inet_ntoa($saddr), $net)); + + for my $daddr (@daddrs) { + push @packets, [ inet_ntoa($saddr), $sport, $daddr, $dport ]; + } + + ++$num_nets; + } + if ($survey) { + print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks) [+survey from port $survey_sport]\n"; + } else { + print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks)\n"; + } + + printf PKTS "%d %s\n", scalar @packets, unpack('h*', $data); + for my $pkt (@packets) { + printf PKTS "%s %s %s %s\n", $pkt->[0], $pkt->[1], $pkt->[2], $pkt->[3]; + } + } +} + diff --git a/mbd/mbd.pl b/mbd/mbd.pl new file mode 100644 index 0000000..065e76c --- /dev/null +++ b/mbd/mbd.pl @@ -0,0 +1,254 @@ +#! /usr/bin/perl +use strict; +use warnings; +use Socket; +use Net::CIDR; +use Net::RawIP; +use Time::HiRes; +require './access_list.pl'; +require './nets.pl'; +require './survey.pl'; +require './mbd.pm'; +use lib '../include'; +use nms; +use strict; +use warnings; + +# Mark packets with DSCP CS7 +my $tos = 56; + +my ($dbh, $q); + +sub fhbits { + my $bits = 0; + for my $fh (@_) { + vec($bits, fileno($fh), 1) = 1; + } + return $bits; +} + +# used for rate limiting +my %last_sent = (); + +# for own surveying +my %active_surveys = (); +my %last_survey = (); + +my %cidrcache = (); +sub cache_cidrlookup { + my ($addr, $net) = @_; + my $key = $addr . " " . $net; + + if (!exists($cidrcache{$key})) { + $cidrcache{$key} = Net::CIDR::cidrlookup($addr, $net); + } + return $cidrcache{$key}; +} + +my %rangecache = (); +sub cache_cidrrange { + my ($net) = @_; + + if (!exists($rangecache{$net})) { + ($rangecache{$net}) = Net::CIDR::cidr2range($net); + } + + return $rangecache{$net}; +} + +open LOG, ">>", "mbd.log"; + +my @ports = ( mbd::find_all_ports() , $Config::survey_port_low .. $Config::survey_port_high ); + +# Open a socket for each port +my @socks = (); +my $udp = getprotobyname("udp"); +for my $p (@ports) { + my $sock; + socket($sock, PF_INET, SOCK_DGRAM, $udp); + bind($sock, sockaddr_in($p, INADDR_ANY)); + push @socks, $sock; +} + +my $sendsock = Net::RawIP->new({udp => {}}); + +print "Listening on " . scalar @ports . " ports.\n"; + +# Main loop +while (1) { + my $rin = fhbits(@socks); + my $rout; + + my $nfound = select($rout=$rin, undef, undef, undef); + my $now = [Time::HiRes::gettimeofday]; + + # First of all, close any surveys that are due. + for my $sport (keys %active_surveys) { + my $age = Time::HiRes::tv_interval($active_surveys{$sport}{start}, $now); + if ($age > $Config::survey_time && $active_surveys{$sport}{active}) { + my $hexdump = join(' ', map { sprintf "0x%02x", ord($_) } (split //, $active_surveys{$sport}{data})); + print "Survey ($hexdump) for '" . $Config::access_list[$active_surveys{$sport}{entry}]->{name} . "'/" . + $active_surveys{$sport}{dport} . ": " . $active_surveys{$sport}{num} . " active servers.\n"; + $active_surveys{$sport}{active} = 0; + + # (re)connect to the database if needed + if (!defined($dbh) || !$dbh->ping) { + $dbh = nms::db_connect(); + $q = $dbh->prepare("INSERT INTO mbd_log (ts,game,port,description,active_servers) VALUES (CURRENT_TIMESTAMP,?,?,?,?)") + or die "Couldn't prepare query"; + } + $q->execute($active_surveys{$sport}{entry}, $active_surveys{$sport}{dport}, $Config::access_list[$active_surveys{$sport}{entry}]->{name}, $active_surveys{$sport}{num}); + } + if ($age > $Config::survey_time * 3.0) { + delete $active_surveys{$sport}; + } + } + + for my $sock (@socks) { + next unless (vec($rout, fileno($sock), 1) == 1); + + my $data; + my $addr = recv($sock, $data, 8192, 0); # jumbo broadcast! :-P + my ($sport, $saddr) = sockaddr_in($addr); + my ($dport, $daddr) = sockaddr_in(getsockname($sock)); + my $size = length($data); + + # Check if this is a survey reply + if ($dport >= $Config::survey_port_low && $dport <= $Config::survey_port_high) { + if (!exists($active_surveys{$dport})) { + print "WARNING: Unknown survey port $dport, ignoring\n"; + next; + } + if (!$active_surveys{$dport}{active}) { + # remains + next; + } + + ++$active_surveys{$dport}{num}; + + next; + } + + # Rate limiting + if (exists($last_sent{$saddr}{$dport})) { + my $elapsed = Time::HiRes::tv_interval($last_sent{$saddr}{$dport}, $now); + if ($elapsed < 1.0) { + print LOG "$dport $size 2\n"; + print inet_ntoa($saddr), ", $dport, $size bytes => rate-limited ($elapsed secs since last)\n"; + next; + } + } + + # We don't get the packet's destination address, but I guess this should do... + # Check against the ACL. + my $pass = 0; + my $entry = -1; + for my $rule (@Config::access_list) { + ++$entry; + + next unless (mbd::match_ranges($dport, $rule->{'ports'})); + next unless (mbd::match_ranges($size, $rule->{'sizes'})); + + if ($rule->{'filter'}) { + next unless ($rule->{'filter'}($data)); + } + + $pass = 1; + last; + } + + print LOG "$dport $size $pass\n"; + + if (!$pass) { + print inet_ntoa($saddr), ", $dport, $size bytes => filtered\n"; + next; + } + + $last_sent{$saddr}{$dport} = $now; + + # The packet is OK! Do we already have a recent enough survey + # for this port, or should we use this packet? + my $survey = 1; + if (exists($last_survey{$entry . "/" . $dport})) { + my $age = Time::HiRes::tv_interval($last_survey{$entry . "/" . $dport}, $now); + if ($age < $Config::survey_freq) { + $survey = 0; + } + } + + # New survey; find an unused port + my $survey_sport; + if ($survey) { + for my $port ($Config::survey_port_low..$Config::survey_port_high) { + if (!exists($active_surveys{$port})) { + $survey_sport = $port; + + $active_surveys{$port} = { + start => $now, + active => 1, + dport => $dport, + entry => $entry, + num => 0, + data => $data, + }; + $last_survey{$entry . "/" . $dport} = $now; + + last; + } + } + + if (!defined($survey_sport)) { + print "WARNING: no free survey source ports, not surveying.\n"; + $survey = 0; + } + } + + my $num_nets = 0; + for my $net (@Config::networks) { + my ($range) = cache_cidrrange($net); + $range =~ /-(.*?)$/; + my $broadcast = $1; + + if ($survey) { + $sendsock->set({ + ip => { + saddr => $Config::survey_ip, + daddr => $broadcast, + tos => $tos + }, + udp => { + source => $survey_sport, + dest => $dport, + data => $data + } + }); + $sendsock->send; + } + + next if (cache_cidrlookup(inet_ntoa($saddr), $net)); + + $sendsock->set({ + ip => { + saddr => inet_ntoa($saddr), + daddr => $broadcast, + tos => $tos + }, + udp => { + source => $sport, + dest => $dport, + data => $data + } + }); + $sendsock->send; + + ++$num_nets; + } + + if ($survey) { + print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks) [+survey from port $survey_sport]\n"; + } else { + print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks)\n"; + } + } +} + diff --git a/mbd/mbd.pm b/mbd/mbd.pm new file mode 100644 index 0000000..b844e5b --- /dev/null +++ b/mbd/mbd.pm @@ -0,0 +1,50 @@ +#! /usr/bin/perl +use strict; +use warnings; +use Socket; +use Net::CIDR; +use Net::RawIP; +require './access_list.pl'; +require './nets.pl'; + +package mbd; + +sub expand_range { + my $range = shift; + + if ($range =~ /^(\d+)\.\.(\d+)$/) { + return $1..$2; + } else { + return $range; + } +} + +sub match_ranges { + my ($elem, $ranges) = @_; + + for my $range (@$ranges) { + if ($range =~ /^(\d+)\.\.(\d+)$/) { + return 1 if ($elem >= $1 && $elem <= $2); + } else { + return 1 if ($elem == $range); + } + } + + return 0; +} + +sub find_all_ports { + # Find what ports we need to listen on + my %port_hash = (); + for my $e (@Config::access_list) { + for my $r (@{$e->{'ports'}}) { + for my $p (expand_range($r)) { + $port_hash{$p} = 1; + } + } + } + my @ports = sort { $a <=> $b } keys %port_hash; + return @ports; +} + +1; diff --git a/mbd/nets.pl b/mbd/nets.pl new file mode 100644 index 0000000..3298657 --- /dev/null +++ b/mbd/nets.pl @@ -0,0 +1,171 @@ +# Autogenerated. Do not touch! +package Config; +our @networks = ( + "176.110.98.0/24", + "176.110.99.0/24", + "176.110.100.0/24", + "176.110.102.0/24", + "176.110.103.0/24", + "176.110.106.0/24", + "176.110.107.0/24", + "176.110.108.0/22", + "176.110.112.0/24", + "176.110.113.0/24", + "176.110.114.0/24", + "176.110.115.0/24", + "176.110.116.0/24", + "176.110.117.0/24", + "176.110.118.0/24", + "176.110.119.0/24", + "176.110.120.0/24", + "176.110.121.0/24", + "176.110.122.0/24", + "176.110.123.0/24", + "176.110.124.0/24", + "176.110.126.0/24", + "176.110.2.0/25", + "176.110.2.128/25", + "176.110.4.0/25", + "176.110.4.128/25", + "176.110.6.0/25", + "176.110.6.128/25", + "176.110.8.0/25", + "176.110.8.128/25", + "176.110.10.0/25", + "176.110.10.128/25", + "176.110.11.0/25", + "176.110.11.128/25", + "176.110.12.0/25", + "176.110.12.128/25", + "176.110.13.0/25", + "176.110.13.128/25", + "176.110.14.0/25", + "176.110.14.128/25", + "176.110.15.0/25", + "176.110.15.128/25", + "176.110.16.0/25", + "176.110.16.128/25", + "176.110.17.0/25", + "176.110.17.128/25", + "176.110.18.0/25", + "176.110.18.128/25", + "176.110.19.0/25", + "176.110.19.128/25", + "176.110.20.0/25", + "176.110.20.128/25", + "176.110.21.0/25", + "176.110.21.128/25", + "176.110.22.0/25", + "176.110.22.128/25", + "176.110.23.0/25", + "176.110.23.128/25", + "176.110.24.0/25", + "176.110.24.128/25", + "176.110.25.0/25", + "176.110.25.128/25", + "176.110.26.0/25", + "176.110.26.128/25", + "176.110.27.0/25", + "176.110.27.128/25", + "176.110.29.0/25", + "176.110.29.128/25", + "176.110.31.0/25", + "176.110.31.128/25", + "176.110.33.0/25", + "176.110.33.128/25", + "176.110.35.0/25", + "176.110.35.128/25", + "176.110.37.0/25", + "176.110.37.128/25", + "176.110.39.0/25", + "176.110.39.128/25", + "176.110.41.0/25", + "176.110.41.128/25", + "176.110.43.0/25", + "176.110.43.128/25", + "176.110.44.0/25", + "176.110.44.128/25", + "176.110.45.0/25", + "176.110.45.128/25", + "176.110.46.0/25", + "176.110.46.128/25", + "176.110.47.0/25", + "176.110.47.128/25", + "176.110.48.0/25", + "176.110.48.128/25", + "176.110.49.0/25", + "176.110.49.128/25", + "176.110.50.0/25", + "176.110.50.128/25", + "176.110.51.0/25", + "176.110.51.128/25", + "176.110.52.0/25", + "176.110.52.128/25", + "176.110.53.0/25", + "176.110.53.128/25", + "176.110.54.0/25", + "176.110.54.128/25", + "176.110.55.0/25", + "176.110.55.128/25", + "176.110.56.0/25", + "176.110.56.128/25", + "176.110.57.0/25", + "176.110.57.128/25", + "176.110.58.0/25", + "176.110.58.128/25", + "176.110.59.0/25", + "176.110.59.128/25", + "176.110.60.0/25", + "176.110.60.128/25", + "176.110.61.0/25", + "176.110.61.128/25", + "176.110.62.0/25", + "176.110.62.128/25", + "176.110.63.0/25", + "176.110.63.128/25", + "176.110.64.0/25", + "176.110.64.128/25", + "176.110.65.0/25", + "176.110.65.128/25", + "176.110.66.0/25", + "176.110.66.128/25", + "176.110.67.0/25", + "176.110.67.128/25", + "176.110.68.0/25", + "176.110.68.128/25", + "176.110.69.0/25", + "176.110.69.128/25", + "176.110.70.0/25", + "176.110.70.128/25", + "176.110.71.0/25", + "176.110.71.128/25", + "176.110.72.0/25", + "176.110.72.128/25", + "176.110.73.0/25", + "176.110.73.128/25", + "176.110.74.0/25", + "176.110.74.128/25", + "176.110.75.0/25", + "176.110.75.128/25", + "176.110.76.0/25", + "176.110.76.128/25", + "176.110.77.0/25", + "176.110.77.128/25", + "176.110.79.0/25", + "176.110.79.128/25", + "176.110.81.0/25", + "176.110.81.128/25", + "176.110.83.0/25", + "176.110.83.128/25", + "176.110.84.0/25", + "176.110.84.128/25", + "176.110.85.0/25", + "176.110.85.128/25", + "176.110.86.0/25", + "176.110.86.128/25", + "176.110.87.0/25", + "176.110.87.128/25", + "176.110.88.0/25", + "176.110.88.128/25", +); +1; diff --git a/mbd/packetpusher.c b/mbd/packetpusher.c new file mode 100644 index 0000000..c21a084 --- /dev/null +++ b/mbd/packetpusher.c @@ -0,0 +1,127 @@ +#include <stdio.h> +#include <sys/socket.h> +#include <stdlib.h> +#include <string.h> +#include <netinet/in.h> +#include <netinet/ip.h> +#include <netinet/udp.h> +#include <stdint.h> +#include <arpa/inet.h> + +char encoded_pkt[4096], workspace[4096]; +unsigned char pkt[2048]; + +typedef uint32_t u_int32_t; + +u_int32_t checksum(unsigned char *buf, unsigned nbytes, u_int32_t sum) +{ + int i; + /* Checksum all the pairs of bytes first... */ + for (i = 0; i < (nbytes & ~1U); i += 2) { + sum += (u_int16_t)ntohs(*((u_int16_t *)(buf + i))); + if (sum > 0xFFFF) + sum -= 0xFFFF; + } + + /* + * If there's a single byte left over, checksum it, too. + * Network byte order is big-endian, so the remaining byte is + * the high byte. + */ + + if (i < nbytes) { + sum += buf[i] << 8; + if (sum > 0xFFFF) + sum -= 0xFFFF; + } + + return (sum); +} + +u_int32_t wrapsum(u_int32_t sum) +{ + sum = ~sum & 0xFFFF; + return (htons(sum)); +} + +int main(int argc, char **argv) +{ + int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); + if (sock == -1) { + perror("socket"); + exit(1); + } + + for ( ;; ) { + int num_pkts; + int ret = scanf("%d %s", &num_pkts, encoded_pkt); + if (ret != 2) { + fprintf(stderr, "error parsing! ret=%d\n", ret); + exit(1); + } + if ((strlen(encoded_pkt) % 2) != 0) { + fprintf(stderr, "hex packet has odd length\n"); + exit(1); + } + + // de-hex packet + for (int i = 0; i < strlen(encoded_pkt); i += 2) { + char c[3]; + c[0] = encoded_pkt[i]; + c[1] = encoded_pkt[i + 1]; + c[2] = 0; + int h = strtol(c, NULL, 16); + pkt[i / 2] = h; + } + + int datalen = strlen(encoded_pkt) / 2; + + for (int i = 0; i < num_pkts; ++i) { + char from_ip[256], to_ip[256]; + int sport, dport; + if (scanf("%s %d %s %d", from_ip, &sport, to_ip, &dport) != 4) { + fprintf(stderr, "error parsing packet %d!\n", i); + exit(1); + } + + // IP header + struct iphdr *ip = (struct iphdr *)workspace; + ip->version = 4; + ip->ihl = 5; + ip->tos = 0; + ip->tot_len = htons(datalen + sizeof(struct iphdr) + sizeof(struct udphdr)); + ip->id = 0; + ip->frag_off = 0; + ip->ttl = 64; + ip->protocol = 17; // UDP + ip->saddr = inet_addr(from_ip); + ip->daddr = inet_addr(to_ip); + ip->check = 0; + ip->check = wrapsum(checksum((unsigned char *)ip, sizeof(*ip), 0)); + + // UDP header + struct udphdr *udp = (struct udphdr *)(workspace + sizeof(struct iphdr)); + udp->source = htons(sport); + udp->dest = htons(dport); + udp->len = htons(datalen + sizeof(struct udphdr)); + udp->check = 0; + + int sum = checksum((unsigned char *)&ip->saddr, 2 * sizeof(ip->saddr), IPPROTO_UDP + ntohs(udp->len)); + sum = checksum((unsigned char *)pkt, datalen, sum); + sum = checksum((unsigned char *)udp, sizeof(*udp), sum); + udp->check = wrapsum(sum); + + // Data + memcpy(workspace + sizeof(struct iphdr) + sizeof(struct udphdr), + pkt, datalen); + + // Send out the packet physically + struct sockaddr_in to; + to.sin_family = AF_INET; + to.sin_addr.s_addr = inet_addr(to_ip); + to.sin_port = htons(dport); + + sendto(sock, workspace, ntohs(ip->tot_len), 0, (struct sockaddr *)&to, sizeof(to)); + } + } +} diff --git a/mbd/survey.pl b/mbd/survey.pl new file mode 100644 index 0000000..be33038 --- /dev/null +++ b/mbd/survey.pl @@ -0,0 +1,10 @@ +package Config; + +our $survey_ip = "176.110.125.15"; +our $survey_port_low = 60100; +our $survey_port_high = 60200; +our $survey_freq = 60.0; +our $survey_time = 10.0; + +1; + |