]> git.sesse.net Git - nms/blob - mbd/mbd.pl
Put the survey data into SQL.
[nms] / mbd / mbd.pl
1 #! /usr/bin/perl
2 use strict;
3 use warnings;
4 use Socket;
5 use Net::CIDR;
6 use Net::RawIP;
7 use Time::HiRes;
8 require './access_list.pl';
9 require './nets.pl';
10 require './survey.pl';
11 require './mbd.pm';
12 use lib '../include';
13 use nms;
14 use strict;
15 use warnings;
16
17 my ($dbh, $q);
18
19 sub fhbits {
20         my $bits = 0;
21         for my $fh (@_) {
22                 vec($bits, fileno($fh), 1) = 1;
23         }
24         return $bits;
25 }
26
27 # used for rate limiting
28 my %last_sent = ();
29
30 # for own surveying
31 my %active_surveys = ();
32 my %last_survey = ();
33
34 my %cidrcache = ();
35 sub cache_cidrlookup {
36         my ($addr, $net) = @_;
37         my $key = $addr . " " . $net;
38
39         if (!exists($cidrcache{$key})) {
40                 $cidrcache{$key} = Net::CIDR::cidrlookup($addr, $net);
41         }
42         return $cidrcache{$key};
43 }
44
45 my %rangecache = ();
46 sub cache_cidrrange {
47         my ($net) = @_;
48
49         if (!exists($rangecache{$net})) {
50                 ($rangecache{$net}) = Net::CIDR::cidr2range($net);
51         }
52
53         return $rangecache{$net};
54 }
55
56 open LOG, ">>", "mbd.log";
57
58 my @ports = ( mbd::find_all_ports() , $Config::survey_port_low .. $Config::survey_port_high );
59
60 # Open a socket for each port
61 my @socks = ();
62 my $udp = getprotobyname("udp");
63 for my $p (@ports) {
64         my $sock;
65         socket($sock, PF_INET, SOCK_DGRAM, $udp);
66         bind($sock, sockaddr_in($p, INADDR_ANY));
67         push @socks, $sock;
68 }
69
70 my $sendsock = Net::RawIP->new({udp => {}});
71
72 print "Listening on " . scalar @ports . " ports.\n";
73
74 # Main loop
75 while (1) {
76         my $rin = fhbits(@socks);
77         my $rout;
78
79         my $nfound = select($rout=$rin, undef, undef, undef);
80         my $now = [Time::HiRes::gettimeofday];
81
82         # First of all, close any surveys that are due.
83         for my $sport (keys %active_surveys) {
84                 my $age = Time::HiRes::tv_interval($active_surveys{$sport}{start}, $now);
85                 if ($age > $Config::survey_time && $active_surveys{$sport}{active}) {
86                         print "Survey for '" . $Config::access_list[$active_surveys{$sport}{entry}]->{name} . "'/" .
87                                 $active_surveys{$sport}{dport} . ": " . $active_surveys{$sport}{num} . " active servers.\n";
88                         $active_surveys{$sport}{active} = 0;
89         
90                         # (re)connect to the database if needed 
91                         if (!defined($dbh) || !$dbh->ping) {
92                                 $dbh = nms::db_connect();
93                                 $q = $dbh->prepare("INSERT INTO mbd_log (ts,game,port,description,active_servers) VALUES (CURRENT_TIMESTAMP,?,?,?,?)")
94                                         or die "Couldn't prepare query";
95                         }
96                         $q->execute($active_surveys{$sport}{entry}, $active_surveys{$sport}{dport}, $Config::access_list[$active_surveys{$sport}{entry}]->{name}, $active_surveys{$sport}{num});
97                 }
98                 if ($age > $Config::survey_time * 3.0) {
99                         delete $active_surveys{$sport};
100                 }
101         }
102
103         for my $sock (@socks) {
104                 next unless (vec($rout, fileno($sock), 1) == 1);
105
106                 my $data;
107                 my $addr = recv($sock, $data, 8192, 0);   # jumbo broadcast! :-P
108                 my ($sport, $saddr) = sockaddr_in($addr);
109                 my ($dport, $daddr) = sockaddr_in(getsockname($sock));
110                 my $size = length($data);
111         
112                 # Check if this is a survey reply
113                 if ($dport >= $Config::survey_port_low && $dport <= $Config::survey_port_high) {
114                         if (!exists($active_surveys{$dport})) {
115                                 print "WARNING: Unknown survey port $dport, ignoring\n";
116                                 next;
117                         }
118                         if (!$active_surveys{$dport}{active}) {
119                                 # remains
120                                 next;
121                         }
122                         
123                         ++$active_surveys{$dport}{num};
124
125                         next;
126                 }
127                 
128                 # Rate limiting
129                 if (exists($last_sent{$saddr}{$dport})) {
130                         my $elapsed = Time::HiRes::tv_interval($last_sent{$saddr}{$dport}, $now);
131                         if ($elapsed < 1.0) {
132                                 print LOG "$dport $size 2\n";
133                                 print inet_ntoa($saddr), ", $dport, $size bytes => rate-limited ($elapsed secs since last)\n";
134                                 next;
135                         }
136                 }
137                 
138                 # We don't get the packet's destination address, but I guess this should do...
139                 # Check against the ACL.
140                 my $pass = 0;
141                 my $entry = -1;
142                 for my $rule (@Config::access_list) {
143                         ++$entry;
144
145                         next unless (mbd::match_ranges($dport, $rule->{'ports'}));
146                         next unless (mbd::match_ranges($size, $rule->{'sizes'}));
147
148                         if ($rule->{'filter'}) {
149                                 next unless ($rule->{'filter'}($data));
150                         }
151
152                         $pass = 1;
153                         last;
154                 }
155
156                 print LOG "$dport $size $pass\n";
157
158                 if (!$pass) {
159                         print inet_ntoa($saddr), ", $dport, $size bytes => filtered\n";
160                         next;
161                 }
162
163                 $last_sent{$saddr}{$dport} = $now;
164
165                 # The packet is OK! Do we already have a recent enough survey
166                 # for this port, or should we use this packet?
167                 my $survey = 1;
168                 if (exists($last_survey{$entry . "/" . $dport})) {
169                         my $age = Time::HiRes::tv_interval($last_survey{$entry . "/" . $dport}, $now);
170                         if ($age < $Config::survey_freq) {
171                                 $survey = 0;
172                         }
173                 }
174
175                 # New survey; find an unused port
176                 my $survey_sport;
177                 if ($survey) {
178                         for my $port ($Config::survey_port_low..$Config::survey_port_high) {
179                                 if (!exists($active_surveys{$port})) {
180                                         $survey_sport = $port;
181
182                                         $active_surveys{$port} = {
183                                                 start => $now,
184                                                 active => 1,
185                                                 dport => $dport,
186                                                 entry => $entry,
187                                                 num => 0
188                                         };
189                                         $last_survey{$entry . "/" . $dport} = $now;
190
191                                         last;
192                                 }
193                         }
194
195                         if (!defined($survey_sport)) {
196                                 print "WARNING: no free survey source ports, not surveying.\n";
197                                 $survey = 0;
198                         }
199                 }
200
201                 my $num_nets = 0;
202                 for my $net (@Config::networks) {
203                         next if (cache_cidrlookup(inet_ntoa($saddr), $net));
204
205                         my ($range) = cache_cidrrange($net);
206                         $range =~ /-(.*?)$/;
207                         my $broadcast = $1;
208
209                         $sendsock->set({
210                                 ip => {
211                                         saddr => inet_ntoa($saddr),
212                                         daddr => $broadcast
213                                 },
214                                 udp => {
215                                         source => $sport,
216                                         dest => $dport,
217                                         data => $data
218                                 }
219                         });
220                         $sendsock->send;
221
222                         if ($survey) {
223                                 $sendsock->set({
224                                         ip => {
225                                                 saddr => $Config::survey_ip,
226                                                 daddr => $broadcast
227                                         },
228                                         udp => {
229                                                 source => $survey_sport,
230                                                 dest => $dport,
231                                                 data => $data
232                                         }
233                                 });
234                                 $sendsock->send;
235                         }
236
237                         ++$num_nets;
238                 }
239
240                 if ($survey) {
241                         print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks) [+survey from port $survey_sport]\n";
242                 } else {
243                         print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks)\n";
244                 }
245         }
246 }
247