aboutsummaryrefslogtreecommitdiffstats
path: root/mbd
diff options
context:
space:
mode:
Diffstat (limited to 'mbd')
-rw-r--r--mbd/access_list.pl294
-rw-r--r--mbd/derpspan.c48
-rwxr-xr-xmbd/generate-helper-list.pl15
-rw-r--r--mbd/make-mbd-nets.pl26
-rw-r--r--mbd/mbd-unicast-segfaulting.pl273
-rw-r--r--mbd/mbd-unicast.pl254
-rw-r--r--mbd/mbd.pl254
-rw-r--r--mbd/mbd.pm50
-rw-r--r--mbd/nets.pl171
-rw-r--r--mbd/packetpusher.c127
-rw-r--r--mbd/survey.pl10
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;
+