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