]> git.datanom.net - pve-dhcp-server.git/blame - DHCPServer.pm
Some minor fixes
[pve-dhcp-server.git] / DHCPServer.pm
CommitLineData
48b79db9
MR
1package PVE::DHCPServer;
2
3use strict;
4use warnings;
5use Carp qw(croak);
6use Sys::Hostname;
7use Socket;
8use Socket6;
4a195854 9use NetAddr::IP;
48b79db9
MR
10use IO::Socket::IP;
11use IO::File;
12use IO::Select;
13use Sys::Syslog;
14use Fcntl qw(:DEFAULT :flock SEEK_END);
15use POSIX qw(EINTR setsid strftime);
16use Data::Dumper;
17use Time::Local;
18
4a195854
MR
19use Net::DHCP::Packet;
20use Net::DHCP::Constants;
21
48b79db9 22use constant {
245c6d23
MR
23 DEBUG => 0,
24 INFO => 1,
25 NOTICE => 2,
26 WARNING => 3,
27 ERROR => 4,
28 CRITICAL => 5,
29 ALERT => 6,
30 EMERGENCY => 7,
31 DHCP_OFFER => 100,
32 DHCP_ACK => 101,
48b79db9
MR
33};
34
35use Exporter;
36our @ISA = qw(Exporter);
4a195854
MR
37our @EXPORT = qw(
38 run
39 DEBUG
40 INFO
41 NOTICE
42 WARNING
43 ERROR
44 CRITICAL
45 ALERT
46 EMERGENCY
48b79db9
MR
47);
48
48b79db9
MR
49our $VERSION = '0.01';
50our $NAME = 'PVE::DHCPServer';
4a195854
MR
51our $DEFAULT_LEASE = 7200;
52our $DEFAULT_LEASE_RENEW = 5400;
48b79db9
MR
53my $time_to_die = 0;
54
4a195854
MR
55#########################################################################
56# Private methods
57#########################################################################
48b79db9 58
4a195854
MR
59my $logger = sub {
60 my ($self, $message, $level) = @_;
48b79db9 61
4a195854
MR
62 $level ||= DEBUG;
63 return unless ($level >= $self->{LOG_LEVEL});
48b79db9 64
4a195854
MR
65 $level = "debug" if $level eq DEBUG;
66 $level = "info" if $level eq INFO;
67 $level = "notice" if $level eq NOTICE;
68 $level = "warning" if $level eq WARNING;
69 $level = "err" if $level eq ERROR;
70 $level = "crit" if $level eq CRITICAL;
71 $level = "alert" if $level eq ALERT;
72 $level = "emerg" if $level eq EMERGENCY;
48b79db9 73
4a195854
MR
74 if ($self->{DEBUG}) {
75 print STDOUT strftime "[%d/%b/%Y:%H:%M:%S] ", localtime;
76 print STDOUT "$level: " . ($message ? $message : 'No message') . "\n";
77 } elsif ($self->{log_file} eq 'syslog') {
78 openlog($NAME, 'ndelay,pid', 'user');
79 syslog($level, $message);
80 closelog();
81 } else {
82 my $fh = new IO::File;
83 if (! $fh->open("> $self->{log_file}")) {
84 croak "$self->{log_file}: $!";
48b79db9 85 }
4a195854
MR
86 print $fh strftime "[%d/%b/%Y:%H:%M:%S] ", localtime;
87 print $fh "$level: $message\n";
88 undef $fh;
48b79db9 89 }
4a195854 90};
48b79db9 91
4a195854 92my $run_with_timeout = sub {
48b79db9
MR
93 my ($self, $code, @param) = @_;
94
95 die "got timeout" if $self->{timeout} <= 0;
96
97 my $prev_alarm;
98
99 my $sigcount = 0;
100
101 my $res;
102
103 local $SIG{ALRM} = sub { $sigcount++; }; # catch alarm outside eval
104
105 eval {
106 local $SIG{ALRM} = sub { $sigcount++; die "got timeout"; };
107 local $SIG{PIPE} = sub { $sigcount++; die "broken pipe" };
108 local $SIG{__DIE__}; # see SA bug 4631
109
110 $prev_alarm = alarm($self->{timeout});
111
112 $res = &$code(@param);
113
114 alarm(0); # avoid race conditions
115 };
116
117 my $err = $@;
118
119 alarm($prev_alarm) if defined($prev_alarm);
120
121 die "unknown error" if $sigcount && !$err; # seems to happen sometimes
122
123 die $err if $err;
124
125 return $res;
4a195854 126};
48b79db9 127
4a195854 128my $lock = sub {
cb0a294f 129 my ($self, $file, $shared) = @_;
48b79db9
MR
130
131 my $mode = $shared ? LOCK_SH : LOCK_EX;
132
133 my $lock_func = sub {
134 if ($mode == LOCK_SH) {
cb0a294f
MR
135 $self->{file_handle} = new IO::File ("<$file") ||
136 die "can't open file '$file' for read - $!";
48b79db9 137 } else {
cb0a294f
MR
138 $self->{file_handle} = new IO::File (">$file") ||
139 die "can't open file '$file' for write - $!";
48b79db9 140 }
4a195854 141 $self->$logger("trying to aquire lock on '$file'...");
48b79db9
MR
142 if (!flock ($self->{file_handle}, $mode|LOCK_NB)) {
143 my $success;
144 while(1) {
145 $success = flock($self->{file_handle}, $mode);
146 # try again on EINTR (see bug #273)
147 if ($success || ($! != EINTR)) {
148 last;
149 }
150 }
151 if ($mode == LOCK_SH) {
152 seek($self->{file_handle}, 0, SEEK_END) or $success = 0;
153 }
154 if (!$success) {
4a195854 155 $self->$logger(" failed");
48b79db9
MR
156 die "can't aquire lock - $!";
157 }
158 }
4a195854 159 $self->$logger(" OK");
48b79db9
MR
160 };
161
162 my $res;
163 my $err = undef;
164
165 eval {
4a195854 166 $res = $self->$run_with_timeout($lock_func);
48b79db9
MR
167 };
168 if ($@) {
4a195854 169 $self->$logger("can't lock file '$file' - $@", ERROR);
48b79db9
MR
170 $self->{file_handle} = undef;
171 return undef;
172 }
173
174 return $res;
4a195854 175};
48b79db9 176
4a195854 177my $unlock = sub {
cb0a294f 178 my ($self, $file) = @_;
48b79db9
MR
179
180 return '' unless($self->{file_handle});
181 my $unlock_func = sub {
4a195854 182 $self->$logger("trying to unlock '$file'...");
48b79db9
MR
183 if (!flock($self->{file_handle}, LOCK_UN)) {
184
185 my $success;
186 while(1) {
187 $success = flock($self->{file_handle}, LOCK_UN);
188 # try again on EINTR (see bug #273)
189 if ($success || ($! != EINTR)) {
190 last;
191 }
192 }
193 if (!$success) {
4a195854 194 $self->$logger(" failed");
48b79db9
MR
195 die "can't unlock - $!";
196 }
197 }
4a195854 198 $self->$logger(" OK");
48b79db9
MR
199 };
200
201 my $res;
202 my $err = undef;
203
204 eval {
4a195854 205 $res = $self->$run_with_timeout($unlock_func);
48b79db9
MR
206 };
207 if ($@) {
4a195854 208 $self->$logger("can't lock file '$file' - $@", ERROR);
48b79db9
MR
209 $self->{file_handle} = undef;
210 $res = undef;
211 }
212
213 return $res;
4a195854 214};
48b79db9 215
4a195854 216my $convert_timestamp = sub {
48b79db9
MR
217 my ($self, $timestamp, $strtotime) = @_;
218 my ($res, $mday, $mon, $year, $hour, $min, $sec);
219
4a195854 220 $self->$logger("Timestamp: $timestamp");
48b79db9
MR
221 if ($strtotime) {
222 if ($timestamp !~ /^\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}$/) {
4a195854 223 $self->$logger("$timestamp: (strtotime) Bad format", ERROR);
48b79db9
MR
224 $res = undef;
225 } else {
226 ($year,$mon,$mday,$hour,$min,$sec) = split(/[\s\/:]+/, $timestamp);
227 $res = timelocal($sec,$min,$hour,$mday,$mon-1,$year);
228 }
229 } else{
4a195854 230 $self->$logger($timestamp);
48b79db9 231 if ($timestamp !~ /^\d+$/) {
4a195854 232 $self->$logger("$timestamp: (timetostr) Bad format", ERROR);
48b79db9
MR
233 $res = undef;
234 } else {
235 ($sec,$min,$hour,$mday,$mon,$year) = localtime($timestamp);
4a195854 236 $self->$logger("Timestamp: $sec,$min,$hour,$mday,$mon,$year");
48b79db9 237 $res = sprintf("%d/%02d/%02d %02d:%02d:%02d", ($year+1900),($mon+1),$mday,$hour,$min,$sec);
4a195854 238 $self->$logger("Timestamp: $res");
48b79db9
MR
239 }
240 }
241
242 return $res;
4a195854 243};
48b79db9
MR
244
245
4a195854 246my $add_lease = sub {
48b79db9
MR
247 my ($self, $ip, $lease) = @_;
248 my $ts;
249
250 my $mac = $lease->{'hardware ethernet'};
251 $mac =~ tr/://d;
252 $lease->{'hardware ethernet'} = $mac;
4a195854 253 $ts = $self->$convert_timestamp($lease->{starts}, 1);
48b79db9
MR
254 return unless $ts;
255 $lease->{starts} = $ts;
4a195854 256 $ts = $self->$convert_timestamp($lease->{ends}, 1);
48b79db9
MR
257 return unless $ts;
258 $lease->{ends} = $ts;
259
260 $self->{_leases}->{$ip} = $lease;
4a195854 261 $self->$logger(Dumper($self->{_leases}->{$ip}));
48b79db9 262 $self->{_reverse}->{$mac} = $ip;
4a195854
MR
263 $self->$logger("$mac => $self->{_reverse}->{$mac}");
264};
48b79db9
MR
265
266#lease vvv.xxx.yyy.zzz {
267# starts yyyy/mm/dd hh:mm:ss;
268# ends yyyy/mm/dd hh:mm:ss;
269# binding state active|free;
270# hardware ethernet MAC;
271# client-hostname "name"
272#}
4a195854 273my $read_lease_file = sub {
48b79db9
MR
274 my ($self) = @_;
275 my ($res, $key, $lease);
276 my $error = 0;
277
4a195854
MR
278 # Start with empty leases file?
279 if (! -e $self->{lease_file}) {
280 return 0;
281 }
282
283 $self->$lock($self->{lease_file}, 1);
48b79db9
MR
284 if ($self->{file_handle}) {
285 my $fh = $self->{file_handle};
286 my @lines = <$fh>;
287 foreach (@lines) {
4a195854 288 $self->$logger("Read: $_");
48b79db9 289 if ($_ =~ /^\s*lease\s+([\d\.]+)\s+{\s*/) {
4a195854 290 $self->$add_lease($key, $lease) if $lease;
48b79db9
MR
291 $key = $1;
292 $lease = undef;
293 $error = 0;
4a195854 294 $self->$logger("Key: $key");
48b79db9
MR
295 } else {
296 next if $error;
a23b2728 297 next if ($_ =~ /^\s*}\s*/ || $_ =~ /^\s*$/ || $_ =~ /^\s*#.*/);
48b79db9
MR
298 if ($_ =~ /^\s*(starts|ends|binding state|hardware ethernet|client-hostname)\s+(.+)\s*;/) {
299 $lease->{$1} = $2;
4a195854 300 $self->$logger("Key: $1 Value: $2");
48b79db9
MR
301 } else {
302 $key = 'UNDEF' unless $key;
4a195854 303 $self->$logger("$key: Bad format", ERROR);
48b79db9
MR
304 $key = undef;
305 $lease = undef;
306 $error = 1;
307 }
308 }
309 }
310 if ($lease && !$error) {
4a195854
MR
311 $self->$logger("Key: $key");
312 $self->$add_lease($key, $lease);
48b79db9 313 }
4a195854
MR
314 $self->$logger("Leases data structure: \n" . Dumper($self->{_leases}));
315 $self->$unlock($self->{lease_file});
48b79db9
MR
316 $res = 1;
317 } else {
245c6d23 318 $self->$logger("Could not read leases file", INFO);
48b79db9
MR
319 $res = 0;
320 }
321
322 return $res;
4a195854 323};
48b79db9 324
4a195854 325my $write_lease_file = sub {
48b79db9
MR
326 my ($self) = @_;
327 my $res;
328
4a195854 329 $res = $self->$lock($self->{lease_file}, 0);
48b79db9 330 if ($self->{file_handle}) {
4a195854
MR
331 if ($self->{_leases}) {
332 my $fh = $self->{file_handle};
245c6d23
MR
333 my $leases = $self->{_leases};
334 while ((my $lease, my $elems) = each (%$leases)) {
4a195854
MR
335 $self->$logger("Writing: $lease");
336 print $fh "lease $lease {\n";
337 while ((my $key, my $val) = each %$elems) {
338 if ($key =~ /^(starts|ends)$/) {
339 $val = $self->$convert_timestamp($val, 0);
340 }
341 $self->$logger("Writing: $key $val");
342 print $fh "\t$key $val;\n";
48b79db9 343 }
4a195854 344 print $fh "}\n";
48b79db9 345 }
48b79db9 346 }
4a195854 347 $self->$unlock($self->{lease_file});
48b79db9
MR
348 $res = 1;
349 } else {
245c6d23 350 $self->$logger("$self->{lease_file}: Could not write leases file", ERROR);
48b79db9
MR
351 $res = 0;
352 }
353
354 return $res;
4a195854 355};
48b79db9 356
cb0a294f
MR
357#subnet 192.168.9.0 netmask 255.255.255.0 {
358# range 192.168.9.2 192.168.9.100;
359# ttl 7200;
360# rttl 3600;
361# router 192.168.9.254;
362# dns-servers 192.168.2.201;
363# ntp-servers 192.168.9.254;
364# broadcast 192.168.9.255;
365# domain-name "foo.bar";
366# {
367# allow 001cc0c33317,001cc0c33318,001cc0c33319,001cc0c33320;
368# static 001cc0c33317 192.168.9.100,001cc0c33318 192.168.9.200;
369# }
370#}
4a195854 371my $read_config = sub {
cb0a294f
MR
372 my ($self) = @_;
373 my ($res, $key, $netmask, $config, $subopt);
374
4a195854 375 $self->$lock($self->{conf_file}, 1);
cb0a294f
MR
376 if ($self->{file_handle}) {
377 my $fh = $self->{file_handle};
378 my @lines = <$fh>;
379 $subopt = 0;
380 foreach (@lines) {
4a195854 381 $self->$logger("Read: $_");
cb0a294f
MR
382 if ($_ =~ /^\s*subnet\s+([\d\.]+)\s+netmask\s+([\d\.]+)\s+{\s*/) {
383 $self->{_config}->{$key} = $config if $config;
384 $key = $1;
385 $config = undef;
386 $config->{netmask} = $2;
4a195854 387 $self->$logger("Key: $key Netmask: $config->{netmask}");
cb0a294f
MR
388 } else {
389 next if (($_ =~ /^\s*}\s*/ && ! $subopt) || $_ =~ /^\s*$/ || $_ =~ /^\s*#.*/);
390 if (! $subopt && $_ =~ /^\s*(range|ttl|rttl|router|dns-servers|ntp-servers|broadcast|domain-name)\s+(.+)\s*;/) {
391 $config->{$1} = $2;
4a195854 392 $self->$logger("Key: $1 Value: $2");
cb0a294f
MR
393 } elsif ($subopt &&$_ =~ /^\s*}\s*/) {
394 $subopt = 0;
395 } elsif ($subopt || $_ =~ /^\s*{\s*/) {
396 if ($subopt) {
397 if ($_ =~ /^\s*(allow|static)\s+(.+)\s*;/) {
2470397f 398 my @vals = split(/\s*,\s*/, $2);
cb0a294f 399 $config->{$1} = [@vals];
4a195854 400 $self->$logger("Key: $1 Value: $2");
cb0a294f
MR
401 } else {
402 $key = 'UNDEF' unless $key;
403 my $err = "$key: 'suboptions' Bad format";
4a195854 404 $self->$logger($err, ERROR);
cb0a294f
MR
405 $key = undef;
406 $config = undef;
407 die $err;
408 }
409 } else {
410 $subopt = 1;
411 }
412 } else {
413 $key = 'UNDEF' unless $key;
414 my $err = "$key: Bad format";
4a195854 415 $self->$logger($err, ERROR);
cb0a294f
MR
416 $key = undef;
417 $config = undef;
418 die $err;
419 }
420 }
421 }
422 if ($config) {
423 $self->{_config}->{$key} = $config;
424 }
4a195854
MR
425 $self->$logger("Config data structure: \n" . Dumper($self->{_config}));
426 $self->$unlock($self->{conf_file});
cb0a294f
MR
427 if (!$self->{_config}) {
428 die "Empty config file";
429 }
430 } else {
431 die "Could not read config file";
432 }
4a195854 433};
cb0a294f 434
4a195854
MR
435#########################################################################
436# Private methods which handle DHCP4 requests
437#########################################################################
48b79db9 438
4a195854 439my $get_mac_ip4 = sub {
245c6d23 440 my ($self, $req) = @_;
4a195854 441 my $mac;
48b79db9 442
245c6d23 443 $mac = $req->chaddr();
4a195854 444 $mac =~ s/0+$//;
48b79db9 445
4a195854
MR
446 return $mac;
447};
448
449my $can_client_use_net_ip4 = sub {
245c6d23
MR
450 my ($self, $req, $network) = @_;
451 my ($found);
4a195854
MR
452
453 # Is client allowed to request IP?
454 $found = 0;
455 if ($self->{_config}->{$network}->{allow}) {
245c6d23
MR
456 $self->$logger("Allow: " . Dumper($self->{_config}->{$network}->{allow}));
457 foreach (@{$self->{_config}->{$network}->{allow}}) {
458 if ($_ eq $self->$get_mac_ip4($req)) {
4a195854
MR
459 $found = 1;
460 last;
461 }
48b79db9 462 }
4a195854
MR
463 } else {
464 $found = 1;
48b79db9 465 }
48b79db9 466
4a195854
MR
467 return $found;
468};
469
4a195854 470my $add_options_ip4 = sub {
245c6d23 471 my ($self, $resp) = @_;
48b79db9
MR
472
473 if ($self->{lease_time}) {
245c6d23 474 $resp->addOptionValue(DHO_DHCP_LEASE_TIME, $self->{lease_time});
4a195854
MR
475 }
476 if ($self->{lease_time_renew}) {
245c6d23 477 $resp->addOptionValue(DHO_DHCP_RENEWAL_TIME, $self->{lease_time_renew});
48b79db9
MR
478 }
479 if ($self->{subnet_mask}) {
245c6d23 480 $resp->addOptionValue(DHO_SUBNET_MASK, $self->{subnet_mask});
48b79db9
MR
481 }
482 if ($self->{routers}) {
245c6d23 483 $resp->addOptionValue(DHO_ROUTERS, $self->{routers});
48b79db9
MR
484 }
485 if ($self->{broadcast_addr}) {
245c6d23 486 $resp->addOptionValue(DHO_BROADCAST_ADDRESS, $self->{broadcast_addr});
48b79db9
MR
487 }
488 if ($self->{domain_name}) {
245c6d23 489 $resp->addOptionValue(DHO_DOMAIN_NAME, $self->{domain_name});
48b79db9
MR
490 }
491 if ($self->{ntp_servers}) {
245c6d23 492 $resp->addOptionValue(DHO_NTP_SERVERS, $self->{ntp_servers});
48b79db9
MR
493 }
494 if ($self->{dns_servers}) {
245c6d23 495 $resp->addOptionValue(DHO_DOMAIN_NAME_SERVERS, $self->{dns_servers});
48b79db9 496 }
4a195854 497};
48b79db9 498
245c6d23
MR
499my $send_nak = sub {
500 my ($self, $req, $message) = @_;
501
502 $message = 'Bad request' unless $message;
503 my $peeraddr = ($req->ciaddr() && $req->ciaddr() ne inet_ntoa(INADDR_ANY)) ?
504 $req->ciaddr() : inet_ntoa(INADDR_BROADCAST);
505
506 my $sock = IO::Socket::IP->new(
507 Broadcast => 1,
508 PeerPort => 68,
509 PeerAddr => $peeraddr,
510 Proto => 'udp'
511 ) || do {
512 my $err = $@;
513 $self->$logger("[discover_ip4] Socket creation error: $err", ERROR);
514 die "[discover_ip4] Socket creation error: $err\n";
515 };
516
517 my $resp = new Net::DHCP::Packet(
518 Comment => $req->comment(),
519 Op => BOOTREPLY(),
520 Hops => $req->hops(),
521 Xid => $req->xid(),
522 Flags => $req->flags(),
523 Ciaddr => $req->ciaddr(),
524 Yiaddr => "0.0.0.0",
525 Siaddr => $req->siaddr(),
526 Giaddr => $req->giaddr(),
527 Chaddr => $req->chaddr(),
528 DHO_DHCP_MESSAGE_TYPE() => DHCPNAK(),
529 DHO_DHCP_MESSAGE(), $message,
530 );
531
532 $self->$logger("Sending NAK to " . $sock->peerhost . ':' . $sock->peerport .
533 "\nReason: $message", INFO);
534 $self->$logger($resp->toString());
535
536 my $xid = $req->xid() ? $req->xid() : 'Missing';
537 $self->$logger("Sending OFFER tr=$xid", INFO);
538
539 $sock->send($resp->serialize()) || die "Error sending OFFER: $!\n";
540 $sock->close;
541};
542
543my $send_accept = sub {
544 my ($self, $req, $calc_ip, $reply) = @_;
545 my $msg;
546
547 my $peeraddr = ($req->ciaddr() && $req->ciaddr() ne inet_ntoa(INADDR_ANY)) ?
548 $req->ciaddr() : inet_ntoa(INADDR_BROADCAST);
549
550 if ($reply == DHCP_OFFER) {
551 $reply = DHCPOFFER();
552 $msg = 'DHCP_OFFER';
553 } elsif ($reply == DHCP_ACK) {
554 $reply = DHCPACK();
555 $msg = 'DHCP_ACK';
4a195854 556 } else {
245c6d23
MR
557 my $err = "$reply: Unknown reply";
558 $self->$logger($err, ERROR);
559 die $err;
4a195854
MR
560 }
561
245c6d23
MR
562 my $sock = IO::Socket::IP->new(
563 Broadcast => 1,
564 PeerPort => 68,
565 PeerAddr => $peeraddr,
566 Proto => 'udp'
567 ) || do {
568 my $err = $@;
569 $self->$logger("[discover_ip4] Socket creation error: $err", ERROR);
570 die "[discover_ip4] Socket creation error: $err\n";
571 };
4a195854 572
245c6d23
MR
573 my $resp = new Net::DHCP::Packet(
574 Comment => $req->comment(),
575 Op => BOOTREPLY(),
576 Hops => $req->hops(),
577 Xid => $req->xid(),
578 Flags => $req->flags(),
579 Ciaddr => $req->ciaddr(),
580 Yiaddr => $calc_ip,
581 Siaddr => $req->siaddr(),
582 Giaddr => $req->giaddr(),
583 Chaddr => $req->chaddr(),
584 DHO_DHCP_MESSAGE_TYPE() => $reply,
585 DHO_DHCP_SERVER_IDENTIFIER() => $sock->sockhost
586 );
587 $self->$add_options_ip4($resp);
588 my $xid = $req->xid();
589 $self->{_transaction}->{$xid}->{me} = $sock->sockhost;
590
591 $self->$logger("Sending $msg to " . $sock->peerhost . ':' . $sock->peerport, INFO);
592 $self->$logger($resp->toString());
593
594 $self->$logger("Sending OFFER tr=".$req->xid(), INFO);
595
596 $sock->send($resp->serialize()) || die "Error sending $msg: $!\n";
597 $sock->close;
4a195854
MR
598};
599
245c6d23
MR
600my $update_transaction = sub {
601 my ($self, $req, $tx) = @_;
602 my ($res, $xid, $offer);
4a195854 603
245c6d23
MR
604 $xid = $req->xid();
605 return -1 unless $xid;
4a195854 606
245c6d23
MR
607 if ($tx) {
608 $self->{_transaction}->{$xid} = $tx;
609 $res = 0;
610 } else {
611 if ($self->{_transaction}->{$xid} && $self->{_transaction}->{$xid}->{me}) {
612 my $me = $req->getOptionValue(DHO_DHCP_SERVER_IDENTIFIER());
613 $me = $req->ciaddr() unless $me;
614 $offer = $self->{_transaction}->{$xid}->{offer_ip};
615 if ($me) {
616 if ($me ne $self->{_transaction}->{$xid}->{me}) {
617 # Another DHCP server is chosen by client
618 $self->$logger("$me: Offer '".($offer? $offer : 'None')."' refused by client xid=$xid", INFO);
619 delete($self->{_transaction}->{$xid});
620 delete($self->{_leases}->{$offer}) if $offer;
621 $self->$write_lease_file();
622 $res = 1;
4a195854 623 } else {
245c6d23
MR
624 $self->$logger("Offer '$offer' accepted by client xid=$xid", INFO);
625 $res = 0;
4a195854 626 }
245c6d23
MR
627 } else {
628 # Caught request for other DHCP server
4a195854 629 }
245c6d23
MR
630 } else {
631 if ($self->{_transaction}->{$xid}) {
632 $offer = $self->{_transaction}->{$xid}->{offer_ip};
633 $self->$logger("Offer '$offer' wait approval from client xid=$xid", INFO);
634 $res = 0;
4a195854
MR
635 }
636 }
245c6d23 637 }
4a195854 638
245c6d23
MR
639 return $res;
640};
4a195854 641
245c6d23
MR
642my $create_new_lease_ip4 = sub {
643 my ($self, $req, $network) = @_;
644 my $lease;
645
646 $lease->{'hardware ethernet'} = $self->$get_mac_ip4($req);
647 my $client = $req->getOptionValue(DHO_HOST_NAME());
648 $lease->{'client-hostname'} = $client ? $client : $self->$get_mac_ip4($req);
649 $lease->{'binding state'} = 'active';
650 my $start = time;
651 my $end = $start + $self->{_config}->{$network}->{ttl};
652 $lease->{starts} = $self->$convert_timestamp($start, 0);
653 $lease->{ends} = $self->$convert_timestamp($end, 0);
654
655 return $lease;
656};
657
658my $add_lease_ip4 = sub {
659 my ($self, $req, $network, $ip) = @_;
660
661 my $lease = $self->$create_new_lease_ip4($req, $network);
662 $self->$add_lease($ip, $lease);
663 $self->{lease_time} = $DEFAULT_LEASE;
664 if ($self->{_config}->{$network}->{ttl}) {
665 $self->{lease_time} = $self->{_config}->{$network}->{ttl};
666 }
667 $self->{lease_time_renew} = $DEFAULT_LEASE_RENEW;
668 if ($self->{_config}->{$network}->{rttl}) {
669 $self->{lease_time_renew} = $self->{_config}->{$network}->{rttl};
4a195854 670 }
245c6d23
MR
671 if ($self->{_config}->{$network}->{netmask}) {
672 $self->{subnet_mask} = $self->{_config}->{$network}->{netmask};
673 }
674 if ($self->{_config}->{$network}->{router}) {
675 $self->{routers} = $self->{_config}->{$network}->{router};
676 }
677 if ($self->{_config}->{$network}->{broadcast}) {
678 $self->{broadcast_addr} = $self->{_config}->{$network}->{broadcast};
679 }
680 if ($self->{_config}->{$network}->{'domain-name'}) {
681 $self->{domain_name} = $self->{_config}->{$network}->{'domain-name'};
682 }
683 if ($self->{_config}->{$network}->{'dns-servers'}) {
684 $self->{dns_servers} = $self->{_config}->{$network}->{'dns-servers'};
685 }
686 if ($self->{_config}->{$network}->{'ntp-servers'}) {
687 $self->{ntp_servers} = $self->{_config}->{$network}->{'ntp-servers'};
688 }
689};
4a195854 690
245c6d23
MR
691my $find_ip_ip4 = sub {
692 my ($self, $req, $network, $reqaddr) = @_;
693 my ($start, $end, $ip);
694
695 my @range_str = split(/\s+/, $self->{_config}->{$network}->{range});
696 $self->$logger("Range: " . $range_str[0] . " - " . $range_str[1], INFO);
697 $start = NetAddr::IP->new($range_str[0].'/'.$self->{_config}->{$network}->{netmask});
698 $end = NetAddr::IP->new($range_str[1].'/'.$self->{_config}->{$network}->{netmask});
699 $self->$logger(Dumper($start) . Dumper($end));
700
701 if ($reqaddr) {
702 my $request = NetAddr::IP->new($reqaddr);
703 if ($start->numeric() <= $request->numeric() && $request->numeric() <= $start->numeric()) {
704 my $cip = $request->addr();
705 $self->$logger("[find_ip_ip4] reqaddr: $reqaddr IP: $cip", INFO);
706 if ($self->{_leases}->{$cip}) {
707 my $lease = $self->{_leases}->{$cip};
708 my $mac = $self->$get_mac_ip4($req);
709 if ($lease->{'hardware ethernet'} eq $mac) {
710 $ip = $cip;
711 }
712 } else {
713 $ip = $cip;
4a195854 714 }
245c6d23
MR
715 }
716 } else {
717 my $free = undef;
718 for (; $start <= $end; $start = $start + 1) {
719 my $cip = $start->addr();
720 $self->$logger("[find_ip_ip4] IP: $cip");
721 if ($self->{_leases}->{$cip} && ! $free) {
722 my $lease = $self->{_leases}->{$cip};
723 my $mac = $self->$get_mac_ip4($req);
724 if ($lease->{'hardware ethernet'} eq $mac) {
725 $ip = $cip;
726 } elsif ($lease->{'binding state'} eq 'free') {
727 $free = $cip;
728 }
729 } else {
730 $ip = $cip;
4a195854 731 }
245c6d23
MR
732 last if $ip;
733 }
734 if (! $ip && $free) {
735 $ip = $free;
4a195854
MR
736 }
737 }
738
245c6d23
MR
739 $self->$logger("[find_ip_ip4] IP: " . ($ip ? $ip : 'None'), INFO);
740
4a195854
MR
741 return $ip;
742};
743
245c6d23
MR
744my $calculate_net_ip4 = sub {
745 my ($self, $req, $req_addr) = @_;
746 my ($network, $net, $ip);
747
748 $self->$logger("Req IP: " . ($req_addr ? $req_addr : 'None'), INFO);
749 foreach $net (keys %{$self->{_config}}) {
750 my $opt = $self->{_config}->{$net};
751 $self->$logger("Network: $net/$opt->{netmask}\n" . Dumper($opt), INFO);
752 $network = $net if ($self->$can_client_use_net_ip4($req, $net));
753 if ($network) {
754 if ($req_addr) {
755 $ip = $self->$find_ip_ip4($req, $network, $req_addr);
756 } else {
757 $ip = $self->$find_ip_ip4($req, $network);
4a195854 758 }
245c6d23
MR
759 last if $ip;
760 $network = undef;
4a195854
MR
761 }
762 }
245c6d23
MR
763 $self->$logger("Network: " . ($network ? $network : 'None') . " IP: " . ($ip ? $ip : 'None'), INFO);
764
765 return ($network, $ip);
766};
767
768my $calculate_ip_ip4 = sub {
769 my ($self, $req, $state, $reqaddr) = @_;
770 my ($network, $ip);
771
772 if ($state == DHCP_OFFER) {
773 if ($reqaddr) {
774 ($network, $ip) = $self->$calculate_net_ip4($req, $reqaddr);
4a195854 775 } else {
245c6d23
MR
776 ($network, $ip) = $self->$calculate_net_ip4($req);
777 }
778 } elsif ($state == DHCP_ACK) {
779 # If no $reqaddr then client fail
780 if ($reqaddr) {
781 my $xid = $req->xid();
782 if ($self->{_transaction}->{$xid}) {
783 my $offer = $self->{_transaction}->{$xid}->{offer_ip};
784 if ($offer eq $reqaddr) {
785 $network = $self->{_transaction}->{$xid}->{network};
786 $ip = $self->{_transaction}->{$xid}->{offer_ip}
787 }
788 delete($self->{_transaction}->{$xid});
789 } else {
790 # No prior discovery. We maintain transaction
791 ($network, $ip) = $self->$calculate_net_ip4($req, $reqaddr);
792 }
4a195854
MR
793 }
794 } else {
4a195854
MR
795 }
796
245c6d23 797 return ($network, $ip);
4a195854
MR
798};
799
800my $discover_ip4 = sub {
245c6d23
MR
801 my ($self, $req) = @_;
802 my ($tx, $res, $resp, $network, $calc_ip, $req_addr);
48b79db9 803
245c6d23 804 $self->$logger("Got ip4 discover request: \n" . $req->toString(), INFO);
48b79db9 805
245c6d23
MR
806 $res = $self->$update_transaction($req);
807 if ($res) {
808 my $err = "Missing transaction ID";
809 $self->$send_nak($req, $err);
810 $self->$logger($err, ERROR);
811 die $err;
812 }
48b79db9 813
245c6d23
MR
814 $req_addr = $req->getOptionValue(DHO_DHCP_REQUESTED_ADDRESS());
815 $res = $self->$read_lease_file();
816 $self->$logger("Starting with empty lease file", INFO) unless $res;
48b79db9 817
48b79db9 818
4a195854 819 if ($self->{LOG_LEVEL} <= INFO) {
4a195854 820 if ($req_addr) {
245c6d23 821 $self->$logger("[D] Requested IP: $req_addr", INFO);
4a195854 822 } else {
245c6d23 823 $self->$logger("[D] Requested IP: None", INFO);
4a195854
MR
824 }
825 }
245c6d23
MR
826
827 $tx->{req_ip} = $req_addr ? $req_addr : 'None';
828
829 ($network, $calc_ip) = $self->$calculate_ip_ip4($req, DHCP_OFFER, $req_addr);
830 $tx->{offer_ip} = $calc_ip ? $calc_ip : 'None';
831 $tx->{network} = $network ? $network : 'None';
832
833 $self->$logger("Offer: $tx->{offer_ip}");
834
835 if ($network && $calc_ip) {
836 $self->$logger("Creating lease for $calc_ip", INFO);
837 $res = $self->$update_transaction($req, $tx);
838 if ($res) {
839 my $err = "Could not create transaction";
840 $self->$logger($err, ERROR);
841 $self->$send_nak($req, $err);
842 } else {
843 $self->$add_lease_ip4($req, $network, $calc_ip);
844 $res = $self->$write_lease_file();
845 if (! $res) {
846 my $err = "Could not write lease file. Bailing";
847 $self->$logger($err, ERROR);
848 my $xid = $req->xid();
849 delete($self->{_transaction}->{$xid});
850 $self->$send_nak($req, $err);
851 } else {
852 $self->$send_accept($req, $calc_ip, DHCP_OFFER);
853 }
854 }
48b79db9
MR
855 } else {
856 # bad request, we send a NAK
245c6d23
MR
857 my $err = "$req_addr: Not available";
858 $self->$logger($err, INFO);
859 $self->$send_nak($req, $err);
48b79db9
MR
860 }
861
245c6d23 862 $self->$logger("Transaction:\n".Dumper($self->{_transaction}), INFO);
4a195854 863};
48b79db9 864
4a195854 865my $request_ip4 = sub {
245c6d23
MR
866 my ($self, $req) = @_;
867 my ($calc_ip, $network, $res);
868
869 $self->$logger("Got request\n".$req->toString());
870
871 $res = $self->$update_transaction($req);
872 if ($res) {
873 if ($res < 0) {
874 my $err = "Missing transaction ID";
875 $self->$send_nak($req, $err);
876 $self->$logger($err, ERROR);
877 die $err;
878 } else {
879 return;
880 }
881 }
48b79db9 882
245c6d23
MR
883 my $req_addr = $req->getOptionValue(DHO_DHCP_REQUESTED_ADDRESS());
884 if ($self->{LOG_LEVEL} <= INFO) {
885 if ($req_addr) {
886 $self->$logger("[R] Requested IP: $req_addr", INFO);
887 } else {
888 $self->$logger("[R] Requested IP: None", INFO);
889 }
4a195854
MR
890 }
891
245c6d23
MR
892 ($network, $calc_ip) = $self->$calculate_ip_ip4($req, DHCP_ACK, $req_addr);
893 if ($network && $calc_ip) {
894 $self->$logger("Creating lease for $calc_ip", INFO);
895 $self->$add_lease_ip4($req, $network, $calc_ip);
896 $res = $self->$write_lease_file();
897 if (! $res) {
898 my $err = "Could not write lease file. Bailing";
899 $self->$logger($err, ERROR);
900 $self->$send_nak($req, $err);
901 } else {
902 $self->$send_accept($req, $calc_ip, DHCP_ACK);
903 }
48b79db9
MR
904 } else {
905 # bad request, we send a NAK
245c6d23 906 $self->$send_nak($req);
48b79db9
MR
907 }
908
245c6d23
MR
909 # This transaction is finished with either NAK or ACK
910 my $xid = $req->xid();
911 delete($self->{_transaction}->{$xid});
48b79db9 912
245c6d23 913 $self->$logger("Transaction:\n".Dumper($self->{_transaction}), INFO);
4a195854 914};
48b79db9 915
4a195854 916my $release_ip4 = sub {
21630ca3 917 my ($self, $req) = @_;
4a195854 918 my ($ip, $mac);
48b79db9 919
21630ca3
MR
920 $self->$logger($req->toString());
921 $ip = $req->ciaddr();
922 $mac = $self->$get_mac_ip4($req);
4a195854 923 $self->$logger("Release request for IP: $ip MAC: $mac", INFO);
48b79db9 924
4a195854
MR
925 if ($self->{_leases}->{$ip}) {
926 my $lease = $self->{_leases}->{$ip};
927 if ($lease->{'hardware ethernet'} eq $mac) {
21630ca3 928 $self->$logger("Set binding state free IP: $ip MAC: $mac", INFO);
4a195854 929 $lease->{'binding state'} = 'free';
21630ca3 930 $self->$write_lease_file();
4a195854 931 }
4a195854 932 }
245c6d23 933 $self->$logger("Transaction:\n".Dumper($self->{_transaction}), INFO);
4a195854
MR
934};
935
936#########################################################################
937# Private methods which handle DHCP6 requests
938#########################################################################
939
940my $excuse_me_ip6 = sub {
245c6d23 941 my ($self, $addr, $req) = @_;
48b79db9 942
245c6d23 943 $self->$logger("IPv6 request from [$addr]: $req", INFO);
21630ca3 944 my $sock = IO::Socket::IP->new(
48b79db9
MR
945 Domain => PF_INET6,
946 V6Only => 1,
947 Broadcast => 1,
948 PeerPort => 546,
949 PeerAddr => $addr,
950 Proto => 'udp',
951 ) || do {
952 my $err = $@;
4a195854 953 $self->$logger("[excuse_me_ip6] Socket creation error: $err", ERROR);
48b79db9
MR
954 die "[excuse_me_ip6] Socket creation error: $err\n";
955 };
4a195854 956 $self->$logger("$addr: Not implemented here", INFO);
21630ca3
MR
957 $sock->send("Not implemented here") || die "Error sending excuse: $!\n";
958 $sock->close;
4a195854
MR
959};
960
961#########################################################################
962# Public methods
963#########################################################################
964
965# generic signal handler to cause daemon to stop
966sub signal_handler {
967 $time_to_die = 1;
968}
969$SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signal_handler;
970
971# ignore any PIPE signal: standard behaviour is to quit process
972$SIG{PIPE} = 'IGNORE';
973
974sub new {
975 my ($class, %self) = @_;
976
977 # OOP stuff
978 $class = ref($class) || $class;
979 my $self = \%self;
980 bless $self, $class;
981
982 # private
983 $self->{_sock_in_ip4} = undef;
4a195854 984 $self->{_sock_in_ip6} = undef;
4a195854
MR
985 $self->{_leases} = undef;
986 $self->{_reverse} = undef;
987 $self->{_config} = undef;
4a195854 988 $self->{_dhpcp_ip4} = undef;
245c6d23 989 $self->{_transaction} = ();
4a195854
MR
990
991 # public
992 $self->{log_file} ||= 'syslog';
993 $self->{lease_time} ||= $DEFAULT_LEASE;
994 $self->{lease_time_renew} ||= $DEFAULT_LEASE_RENEW;
995 $self->{subnet_mask} ||= undef;
996 $self->{routers} ||= undef;
997 $self->{broadcast_addr} ||= undef;
998 $self->{domain_name} ||= undef;
999 $self->{dns_servers} ||= undef;
1000 $self->{ntp_servers} ||= undef;
1001 $self->{LOG_LEVEL} = ERROR unless defined $self->{LOG_LEVEL};
1002 $self->{NODAEMON} ||= 0;
1003 $self->{DEBUG} ||= 0;
1004 $self->{timeout} ||= 10;
1005 $self->{lease_file} ||= '/tmp/dhcpd.leases';
1006 $self->{conf_file} ||= '/tmp/dhcpd.cfg';
1007
1008 return $self;
1009}
1010
1011sub run {
1012 my ($self) = @_;
1013 my ($sel, @ready, $socket, $res);
1014
1015 eval {
1016 $self->$read_config();
1017 };
1018 if ($@) {
1019 my $err = $@;
1020 $self->$logger($err, ERROR);
1021 die $err;
1022 }
1023 $self->$logger("Starting dhcpd", INFO);
1024 if ($self->{NODAEMON} < 1) {
1025 $self->$logger("Entering Daemon mode");
1026 chdir '/' or die "Can't chdir to /: $!";
1027 umask 0;
1028
1029 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
1030 open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
1031 open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!";
1032
1033 my $pid = fork;
1034 exit if $pid;
1035 do {
1036 my $err = $!;
1037 $self->$logger("Couldn't fork: $err", ERROR);
1038 die "Couldn't fork: $err";
1039 } unless defined($pid);
1040
1041 POSIX::setsid() || do {
1042 my $err = $!;
1043 $self->$logger("Can't start a new session: $err", ERROR);
1044 die "Can't start a new session: $err";
1045 };
1046 $self->$logger("Now in Daemon mode", INFO);
1047 }
1048
1049 $res = $self->$read_lease_file();
1050 $self->$logger("Starting with empty leases file '$self->{lease_file}'", INFO)
1051 if (! $res || ! $self->{_leases});
1052
1053 $self->$logger("Initialization complete", INFO);
1054
1055 # open listening socket
1056 $self->{_sock_in_ip4} = IO::Socket::IP->new(
1057 Domain => PF_INET,
1058 LocalPort => 67,
1059 LocalAddr => inet_ntoa(INADDR_ANY),
1060 Proto => 'udp'
1061 ) || do {
1062 my $err = $@;
1063 $self->$logger("IP4 Socket creation error: $err", ERROR);
1064 die "IP4 Socket creation error: $err\n";
1065 };
1066 $self->{_sock_in_ip6} = IO::Socket::IP->new(
1067 Domain => PF_INET6,
1068 V6Only => 1,
1069 LocalPort => 547,
1070 LocalAddr => '::',
1071 Proto => 'udp'
1072 ) || do {
1073 my $err = $@;
1074 $self->$logger("IP6 Socket creation error: $err", ERROR);
1075 die "IP6 Socket creation error: $err\n";
1076 };
1077
1078 $sel = IO::Select->new($self->{_sock_in_ip4});
1079 $sel->add($self->{_sock_in_ip6});
1080
1081 until ($time_to_die) {
1082 my $buf = undef;
1083 my $fromaddr;
245c6d23 1084 my $req;
4a195854
MR
1085
1086 eval { # catch fatal errors
1087 while (@ready = $sel->can_read) {
1088 $self->$logger("Waiting for incoming packet", INFO);
1089 foreach $socket (@ready) {
1090 if ($socket == $self->{_sock_in_ip4}) {
1091 # receive ipv4 packet
21630ca3 1092 $fromaddr = $socket->recv($buf, 4096)
4a195854
MR
1093 || $self->$logger("recv: $!", ERROR);
1094 next if ($!); # continue loop if an error occured
245c6d23
MR
1095
1096 $req = new Net::DHCP::Packet($buf);
4a195854
MR
1097
1098 {
1099 use bytes;
245c6d23
MR
1100 my $xid = $req->xid();
1101 $xid = $xid ? $xid : 'None';
4a195854
MR
1102 my ($port,$addr) = unpack_sockaddr_in($fromaddr);
1103 my $ipaddr = inet_ntoa($addr);
245c6d23 1104 $self->$logger("Got a packet tr=$xid src=$ipaddr:$port length=".length($buf), INFO);
4a195854
MR
1105 }
1106
245c6d23 1107 my $messagetype = $req->getOptionValue(DHO_DHCP_MESSAGE_TYPE());
4a195854
MR
1108
1109 if ($messagetype eq DHCPDISCOVER()) {
245c6d23 1110 $self->$discover_ip4($req);
4a195854 1111 } elsif ($messagetype eq DHCPREQUEST()) {
245c6d23 1112 $self->$request_ip4($req);
4a195854
MR
1113 } elsif ($messagetype eq DHCPINFORM()) {
1114 $self->$logger("Not implemented: DHCPINFORM", WARNING);
1115 } elsif ($messagetype eq DHCPRELEASE()) {
245c6d23 1116 $self->$release_ip4($req);
4a195854 1117 } else {
245c6d23 1118 $self->$logger("$messagetype: Packet dropped since unknown message type", WARNING);
4a195854
MR
1119 # bad messagetype, we drop it
1120 }
1121 } else {
1122 # Receive ipv6 packet
21630ca3 1123 my $myaddr = $socket->sockhost;
4a195854 1124
21630ca3 1125 $fromaddr = $socket->recv($buf, 4096)
4a195854
MR
1126 || $self->$logger("recv: $!", ERROR);
1127 next if ($!); # continue loop if an error occured
4a195854
MR
1128 $self->$logger("recv: $buf", INFO);
1129 {
1130 use bytes;
1131 my ($port,$addr) = unpack_sockaddr_in6($fromaddr);
1132 my $ipaddr = inet_ntop(AF_INET6, $addr);
1133 $self->$logger("Got a packet tr=$self->{_transaction_ip6} src=$ipaddr:$port length=".length($buf), INFO);
1134 }
1135 $self->$excuse_me_ip6($myaddr, $buf);
1136 }
1137 }
1138 }
1139 }; # end of 'eval' blocks
1140 if ($@) {
1141 $self->$logger("Caught error in main loop: $@", ERROR);
1142 }
1143 }
1144 $self->{_sock_in_ip4}->close;
1145 $self->{_sock_in_ip6}->close;
1146 $self->$logger("Exiting dhcpd", INFO);
48b79db9
MR
1147}
1148
11491;
This page took 0.2134 seconds and 5 git commands to generate.