PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/PowerDNS/Control/Server.pm

https://github.com/augieschwer/PowerDNS-Control-Server
Perl | 746 lines | 559 code | 170 blank | 17 comment | 46 complexity | 4d1a9e48a46f363556fd8d35d0842e86 MD5 | raw file
  1. # $Id: Server.pm 4430 2012-01-14 00:27:53Z augie $
  2. # Provides an interface to create a server to control both the
  3. # PowerDNS Authoritative and Recursive servers.
  4. package PowerDNS::Control::Server;
  5. use warnings;
  6. use strict;
  7. use IO::Socket;
  8. use POSIX;
  9. use Unix::Syslog qw(:subs :macros);
  10. use Carp;
  11. use English;
  12. use Unix::PID;
  13. use Net::CIDR;
  14. use File::Temp qw/ :mktemp /;
  15. =head1 NAME
  16. PowerDNS::Control::Server - Provides an interface to control the PowerDNS daemon.
  17. =head1 VERSION
  18. Version 0.03
  19. =cut
  20. our $VERSION = '0.03';
  21. =head1 SYNOPSIS
  22. use PowerDNS::Control::Server;
  23. # Setting parameters and their default values.
  24. my $params = { port => 988,
  25. listen_address => '0.0.0.0',
  26. allowed_methods => ['auth_retrieve' , 'rec_wipe_cache'],
  27. debug => 0,
  28. syslog_ident => 'pdns-control-server',
  29. syslog_option => LOG_PID | LOG_PERROR,
  30. syslog_facility => LOG_LOCAL3,
  31. syslog_priority => LOG_INFO,
  32. pid_file => '/var/run/pdns-control-server.pid',
  33. auth_cred => 'pa55word',
  34. allowed_ips => ['127.0.0.1/23' , '192.168.0.1/32'],
  35. socket_path => '/var/run/',
  36. };
  37. my $pdns = PowerDNS::Control::Server->new($params);
  38. =head1 DESCRIPTION
  39. PowerDNS::Control::Server provides a way to create a server to control
  40. both the PowerDNS Authoritative and Recursive servers.
  41. PowerDNS::Control::Server was written in tandem with PowerDNS::Control::Client,
  42. but there is no reason why you could not write your own client.
  43. The protocol PowerDNS::Control::Server implements is very simple and is based
  44. off of SMTP; after successful connection the client can expect a banner, then
  45. the client can execute commands agains the server; the server returns "+OK" if
  46. all is well and "-ERR <error_message>" if there was a problem. A sample session
  47. showing the protocol in use is below:
  48. [augie@augnix Control]$ telnet localhost 10988
  49. Trying 127.0.0.1...
  50. Connected to augnix.noc.sonic.net (127.0.0.1).
  51. Escape character is '^]'.
  52. +OK Welcome 127.0.0.1
  53. auth_retrieve schwer.us
  54. +OK
  55. quit
  56. +OK Bye
  57. The commands executed are based on the pdns_control and rec_control programs
  58. on the server. Documentation for these programs can be found at:
  59. http://docs.powerdns.com/
  60. Note: All the commands may not be supported in this module, but the list of
  61. supported commands is listed in the Methods section below. Methods that begin
  62. with 'auth' control the Authoritative PowerDNS Server and methods that begin
  63. with 'rec' control the Recursive PowerDNS Server.
  64. =head1 METHODS
  65. =head2 new(\%params)
  66. my $params = { port => 988,
  67. listen_address => '0.0.0.0',
  68. allowed_methods => ['auth_retrieve' , 'rec_wipe_cache'],
  69. debug => 0,
  70. syslog_ident => 'pdns-control-server',
  71. syslog_option => LOG_PID | LOG_PERROR,
  72. syslog_facility => LOG_LOCAL3,
  73. syslog_priority => LOG_INFO,
  74. pid_file => '/var/run/pdns-control-server.pid',
  75. auth_cred => 'pa55word',
  76. allowed_ips => ['127.0.0.1/23' , '192.168.0.1/32'],
  77. socket_path => '/var/run/',
  78. };
  79. my $pdns = PowerDNS::Control::Server->new($params);
  80. Creates a PowerDNS::Control::Server object.
  81. =over 4
  82. =item port
  83. Port to listen on. Default is 988.
  84. =item listen_address
  85. Address to listen on. Default is 0.0.0.0 .
  86. =item allowed_methods
  87. List of methods the server is allowed to run; if not specified, then none of the
  88. control methods are allowed.
  89. =item debug
  90. Set to 1 to keep the server in the foreground for debugging. The default is 0.
  91. =item syslog_ident
  92. Use to set the Unix::Syslog::openlog($ident) variable. The default is 'pdns-control-server'.
  93. =item syslog_option
  94. Use to set the Unix::Syslog::openlog($option) variable. The default is LOG_PID | LOG_PERROR
  95. =item syslog_facility
  96. Use to set the Unix::Syslog::openlog($facility) variable. The default is LOG_LOCAL3
  97. =item syslog_priority
  98. Use to set the Unix::Syslog::syslog($priority) variable. The default is LOG_INFO
  99. =item pid_file
  100. Where to store the PID file; default is '/var/run/pdns-control-server.pid'.
  101. =item auth_cred
  102. Set if you want the server to require password authentication.
  103. If set, then the client should expect to see
  104. "+OK ready for authentication"
  105. to which it should reply
  106. "AUTH pa55word"
  107. Valid authentication will move the server into the main request loop;
  108. invalid authentication will disconnect the client.
  109. =item allowed_ips
  110. Set if you want the server to only accept connections from the IPs in this list.
  111. The list elements are IPs in CIDR notation, this means if you want to specify a single
  112. IP, then you must give it a '/32' this is an unfortunate bug in Net::CIDR .
  113. =item socket_path
  114. The path where the PowerDNS recursor and authoritative server control sockets are located.
  115. The default is '/var/run/'; this is also where temporary sockets will be placed for
  116. communicating with the PowerDNS control sockets, so make sure it is accessible by this
  117. program for reading and writing.
  118. =item rec_control_socket
  119. If the recursor's control socket is located someplace other then in socket_path, then
  120. you can set that location here.
  121. =item pdns_control_socket
  122. If the authoritative server's control socket is located someplace other then in socket_path, then
  123. you can set that location here.
  124. =back
  125. =cut
  126. sub new
  127. {
  128. my $class = shift;
  129. my $params= shift;
  130. my $self = {};
  131. $SIG{CHLD} = 'IGNORE'; # auto. reap zombies.
  132. $OUTPUT_AUTOFLUSH = 1;
  133. bless $self , ref $class || $class;
  134. $self->{'port'} = defined $params->{'port'} ? $params->{'port'} : 988;
  135. $self->{'listen_address'} = defined $params->{'listen_address'} ? $params->{'listen_address'} : '0.0.0.0';
  136. $self->{'pdns_control'} = defined $params->{'pdns_control'} ? $params->{'pdns_control'} : '/usr/bin/pdns_control';
  137. $self->{'rec_control'} = defined $params->{'rec_control'} ? $params->{'rec_control'} : '/usr/bin/rec_control';
  138. $self->{'debug'} = defined $params->{'debug'} ? $params->{'debug'} : 0;
  139. $self->{'syslog_ident'} = defined $params->{'syslog_ident'} ? $params->{'syslog_ident'} : 'pdns-control-server';
  140. $self->{'syslog_option'} = defined $params->{'syslog_option'} ? $params->{'syslog_option'} : LOG_PID | LOG_PERROR;
  141. $self->{'syslog_facility'} = defined $params->{'syslog_facility'} ? $params->{'syslog_facility'} : LOG_LOCAL3;
  142. $self->{'syslog_priority'} = defined $params->{'syslog_priority'} ? $params->{'syslog_priority'} : LOG_INFO;
  143. $self->{'pid_file'} = defined $params->{'pid_file'} ? $params->{'pid_file'} : '/var/run/pdns-control-server.pid';
  144. $self->{'auth_cred'} = defined $params->{'auth_cred'} ? $params->{'auth_cred'} : '';
  145. $self->{'allowed_ips'} = defined $params->{'allowed_ips'} ? $params->{'allowed_ips'} : undef;
  146. $self->{'socket_path'} = defined $params->{'socket_path'} ? $params->{'socket_path'} : '/var/run/';
  147. $self->{'pdns_control_socket'} = defined $params->{'pdns_control_socket'} ? $params->{'pdns_control_socket'} : $self->{'socket_path'} . '/pdns.controlsocket';
  148. $self->{'rec_control_socket'} = defined $params->{'rec_control_socket'} ? $params->{'rec_control_socket'} : $self->{'socket_path'} . '/pdns_recursor.controlsocket';
  149. $self->{'pid'} = Unix::PID->new();
  150. $self->{'sock'} = new IO::Socket::INET (
  151. LocalAddr => $self->{'listen_address'},
  152. LocalPort => $self->{'port'},
  153. Proto => 'tcp',
  154. Reuse => 1,
  155. Listen => 20 ) or croak "Could not open socket : $!\n";
  156. # populate the allowed_methods list.
  157. # the default is to not allow any methods.
  158. if ( defined $params->{'allowed_methods'} )
  159. {
  160. for my $method ( @{$params->{'allowed_methods'}} )
  161. {
  162. $self->{'allowed_methods'}->{$method} = 1;
  163. }
  164. }
  165. return $self;
  166. }
  167. =head2 control_socket_comm($message , $socket)
  168. Internal method.
  169. Deal with the communication to and from the PowerDNS rec|auth. server.
  170. Expects a message to send and a control socket to send to.
  171. Returns the message received.
  172. =cut
  173. sub control_socket_comm
  174. {
  175. my $self = shift;
  176. my $msg = shift;
  177. my $c_socket = shift;
  178. my $timeout = 10;
  179. my $sock_type = '';
  180. # rec_control uses a DGRAM socket and pdns_control uses a STREAM socket.
  181. if ( $c_socket eq $self->{'rec_control_socket'} )
  182. { $sock_type = SOCK_DGRAM; }
  183. else
  184. { $sock_type = SOCK_STREAM; }
  185. my $t_socket = $self->{'socket_path'} . '/asockXXXXXX';
  186. my $sock_fh;
  187. eval
  188. { ($sock_fh , $t_socket) = mkstemp($t_socket); };
  189. # if the eval above failed.
  190. if ( $@ )
  191. {
  192. carp "Could not create temporary socket $t_socket : $!";
  193. return "Could not create temporary socket $t_socket : $!";
  194. }
  195. local $SIG{INT} = $SIG{TERM} = sub { unlink($t_socket); croak "Caught SIG_INT or SIG_TERM." };
  196. socket($sock_fh , PF_UNIX , $sock_type , 0);
  197. unlink $t_socket;
  198. if ( ! bind($sock_fh , sockaddr_un($t_socket)) )
  199. {
  200. unlink($t_socket);
  201. carp "Cannont bind to temp. socket $t_socket : $!";
  202. return "Cannont bind to temp. socket $t_socket : $!";
  203. }
  204. chmod(0666 , $t_socket);
  205. if ( ! connect($sock_fh , sockaddr_un($c_socket)) )
  206. {
  207. unlink($t_socket);
  208. carp "Cannot connect to control socket $c_socket : $!";
  209. return "Cannot connect to control socket $c_socket : $!";
  210. }
  211. send($sock_fh , "$msg\n" , 0);
  212. $msg = '';
  213. eval
  214. {
  215. local $SIG{ALRM} = sub { $msg = 'Timeout waiting to receive from server.'; carp 'Timeout waiting to receive from server.' };
  216. alarm($timeout);
  217. recv($sock_fh , $msg , 16384 , 0);
  218. alarm(0);
  219. };
  220. if ( $@ ) # if the eval above failed.
  221. {
  222. $msg = "Could not get response from server: $@";
  223. carp "Could not get response from server: $@";
  224. }
  225. chomp $msg;
  226. close($sock_fh);
  227. unlink($t_socket);
  228. return $msg;
  229. }
  230. =head2 auth_retrieve($domain)
  231. Expects a scalar domain name to be retrieved.
  232. Calls pdns_control retrieve domain .
  233. Returns "+OK" if successful or "-ERR error message" otherwise.
  234. =cut
  235. sub auth_retrieve($)
  236. {
  237. my $self = shift;
  238. my $domain = shift;
  239. my $msg = $self->control_socket_comm("retrieve $domain" , $self->{'pdns_control_socket'});
  240. if ( $msg =~ /^Added/ )
  241. {
  242. $self->logmsg('+OK');
  243. return "+OK\n";
  244. }
  245. else
  246. {
  247. $self->logmsg("Error: $msg");
  248. return "-ERR $msg\n";
  249. }
  250. }
  251. =head2 auth_wipe_cache($domain)
  252. Expects a scalar domain name to be wiped out of cache.
  253. Calls pdns_control purge domain$ .
  254. Returns "+OK" if successful or "-ERR error message" otherwise.
  255. =cut
  256. sub auth_wipe_cache($)
  257. {
  258. my $self = shift;
  259. my $domain = shift;
  260. my $msg = $self->control_socket_comm("purge $domain\$" , $self->{'pdns_control_socket'});
  261. if ( $msg =~ /^\d+/ )
  262. {
  263. $self->logmsg('+OK');
  264. return "+OK\n";
  265. }
  266. else
  267. {
  268. $self->logmsg("Error: $msg");
  269. return "-ERR $msg\n";
  270. }
  271. }
  272. =head2 rec_wipe_cache($domain)
  273. Expects a scalar domain name to be wiped out of cache.
  274. Calls rec_control wipe-cache domain .
  275. Returns "+OK" if successful or "-ERR error message" otherwise.
  276. =cut
  277. sub rec_wipe_cache($)
  278. {
  279. my $self = shift;
  280. my $domain = shift;
  281. my $msg = $self->control_socket_comm("wipe-cache $domain" , $self->{'rec_control_socket'});
  282. if ( $msg =~ /^wiped/ )
  283. {
  284. $self->logmsg('+OK');
  285. return "+OK\n";
  286. }
  287. else
  288. {
  289. $self->logmsg("Error: $msg");
  290. return "-ERR $msg\n";
  291. }
  292. }
  293. =head2 rec_ping
  294. Does not expect anything.
  295. Calls rec_control ping.
  296. Returns "+OK" if the recursor is running and "-ERR error message" otherwise.
  297. =cut
  298. sub rec_ping
  299. {
  300. my $self = shift;
  301. my $msg = $self->control_socket_comm('ping' , $self->{'rec_control_socket'});
  302. if ( $msg =~ /^pong/ )
  303. {
  304. $self->logmsg("+OK");
  305. return "+OK\n";
  306. }
  307. else
  308. {
  309. $self->logmsg("Error: $msg");
  310. return "-ERR $msg\n";
  311. }
  312. }
  313. =head2 auth_ping
  314. Does not expect anything.
  315. Calls pdns_control ping.
  316. Returns "+OK" if the auth. server is running and "-ERR error message" otherwise.
  317. =cut
  318. sub auth_ping
  319. {
  320. my $self = shift;
  321. my $msg = $self->control_socket_comm('ping' , $self->{'pdns_control_socket'});
  322. if ( $msg eq 'PONG' )
  323. {
  324. $self->logmsg("+OK");
  325. return "+OK\n";
  326. }
  327. else
  328. {
  329. $self->logmsg("Error: $msg");
  330. return "-ERR $msg\n";
  331. }
  332. }
  333. =head2 start
  334. Does not expect anything.
  335. Forks the server to the background unless "debug" was set.
  336. =cut
  337. sub start
  338. {
  339. my $self = shift;
  340. my ($conn , $peer , $pid , $command , $action , $arg1 , $arg2);
  341. &daemonize unless $self->{'debug'};
  342. # Note the PID so we can kill it later and check if another server is already running.
  343. $self->{'pid'}->pid_file_no_unlink($self->{'pid_file'}) or croak "The server is already running: $!";
  344. $self->logmsg("Server startup complete, accepting connections on port $self->{'port'}");
  345. while ( $conn = $self->{'sock'}->accept() )
  346. {
  347. $peer = $conn->peerhost();
  348. $self->logmsg("Incoming connection from $peer");
  349. # Check to see if we should validate the client IP against our
  350. # allowed_ips list.
  351. if ( defined $self->{'allowed_ips'} )
  352. {
  353. if ( ! Net::CIDR::cidrlookup( $peer , @{$self->{'allowed_ips'}} ) )
  354. {
  355. $self->logmsg("Unauthorized connection from $peer");
  356. $conn->shutdown(2);
  357. next;
  358. }
  359. }
  360. # Parent goes back up to wait for new connections.
  361. # Child continues on; handling this session.
  362. $pid = fork(); next if $pid;
  363. # Check if we should ask for auth. cred.
  364. if ( $self->{'auth_cred'} )
  365. {
  366. print $conn "+OK ready for authentication\n";
  367. my $auth = <$conn>;
  368. $auth =~ s/[\r]//g;
  369. chomp($auth);
  370. if ( $auth ne "AUTH $self->{'auth_cred'}" )
  371. {
  372. $self->logmsg("Invalid authentication from " . $conn->peerhost);
  373. print $conn "-ERR invalid authentication\n";
  374. $conn->shutdown(2);
  375. exit;
  376. }
  377. else
  378. {
  379. $self->logmsg("Auth succesful from " . $conn->peerhost);
  380. print $conn "+OK Auth sucessful\n";
  381. }
  382. }
  383. else
  384. {
  385. print $conn "+OK Welcome $peer\n";
  386. }
  387. # Main request loop; try to fulfill requests until the client is done.
  388. while(1)
  389. {
  390. $command = <$conn>;
  391. $command =~ s/[\r\n]//g;
  392. chomp($command);
  393. ($action,$arg1,$arg2) = split(/ /,$command);
  394. my $method_is_allowed = $self->method_is_allowed($action);
  395. if (!$method_is_allowed && ($action ne 'quit'))
  396. {
  397. $self->logmsg("Recieved method ($action) that was not allowed\n");
  398. print $conn "-ERR method not allowed\n";
  399. }
  400. elsif ($action eq 'auth_retrieve')
  401. {
  402. unless ($arg1)
  403. {
  404. $self->logmsg("Recieved improper command syntax :: '$command'\n");
  405. print $conn "-ERR invalid command syntax\n";
  406. next;
  407. }
  408. my $result = $self->auth_retrieve($arg1);
  409. print $conn $result;
  410. }
  411. elsif ($action eq 'auth_wipe_cache')
  412. {
  413. unless ($arg1)
  414. {
  415. $self->logmsg("Recieved improper command syntax :: '$command'\n");
  416. print $conn "-ERR invalid command syntax\n";
  417. next;
  418. }
  419. my $result = $self->auth_wipe_cache($arg1);
  420. print $conn $result;
  421. }
  422. elsif ($action eq 'rec_wipe_cache')
  423. {
  424. unless ($arg1)
  425. {
  426. $self->logmsg("Recieved improper command syntax :: '$command'\n");
  427. print $conn "-ERR invalid command syntax\n";
  428. next;
  429. }
  430. my $result = $self->rec_wipe_cache($arg1);
  431. print $conn $result;
  432. }
  433. elsif ($action eq 'rec_ping')
  434. {
  435. my $result = $self->rec_ping;
  436. print $conn $result;
  437. }
  438. elsif ($action eq 'auth_ping')
  439. {
  440. my $result = $self->auth_ping;
  441. print $conn $result;
  442. }
  443. elsif ($action eq 'quit')
  444. {
  445. $self->logmsg("Shutting down.");
  446. print $conn "+OK Bye\n";
  447. $conn->shutdown(2);
  448. exit;
  449. }
  450. else
  451. {
  452. print $conn "-ERR '$action' unknown command.\n";
  453. $self->logmsg("'$action' unknown command.");
  454. }
  455. }
  456. }
  457. }
  458. =head2 stop
  459. Does not expect anything.
  460. Kills the running server.
  461. =cut
  462. sub stop
  463. {
  464. my $self = shift;
  465. $self->logmsg("Stopping parent server.");
  466. my $ret = $self->{'pid'}->kill_pid_file($self->{'pid_file'});
  467. # Check the return value for errors.
  468. if ( $ret == 0)
  469. {
  470. $self->logmsg("PID file ($self->{'pid_file'}) exists but could not be opened : $!");
  471. croak "PID file ($self->{'pid_file'}) exists but could not be opened : $!";
  472. }
  473. elsif ( ! defined $ret )
  474. {
  475. $self->logmsg("Server could not be killed from PID file ($self->{'pid_file'}) : $!");
  476. croak "Server could not be killed from PID file ($self->{'pid_file'}) : $!";
  477. }
  478. elsif ( $ret == -1 )
  479. {
  480. $self->logmsg("Could not clean up PID file ($self->{'pid_file'}) after successful termination of server : $!");
  481. carp "Could not clean up PID file ($self->{'pid_file'}) after successful termination of server : $!";
  482. }
  483. else
  484. {
  485. $self->logmsg("Abnormal termination: $!");
  486. croak "Abnormal termination: $!";
  487. }
  488. exit;
  489. }
  490. =head2 daemonize
  491. Internal method.
  492. Close all file handles and fork to the background.
  493. =cut
  494. sub daemonize
  495. {
  496. # Redirect STDIN, STDOUT and STDERR.
  497. open STDIN , '/dev/null' or croak "Could not read /dev/null : $!";
  498. open STDOUT, '>/dev/null' or croak "Could not write to /dev/null : $!";
  499. my $pid = fork;
  500. croak "fork: $!" unless defined ($pid);
  501. if ($pid != 0) { exit; }
  502. open STDERR , '>&STDOUT' or croak "Could not dup STDOUT : $!";
  503. }
  504. =head2 logmsg($message)
  505. Internal method.
  506. Logs to syslog if debug is not turned on.
  507. If debug is on, then log to STDOUT.
  508. =cut
  509. sub logmsg
  510. {
  511. my $self = shift;
  512. my $msg = shift;
  513. carp "logmsg: $msg\n" if $self->{'debug'};
  514. openlog($self->{'syslog_ident'} , $self->{'syslog_option'} , $self->{'syslog_facility'});
  515. eval { syslog($self->{'syslog_priority'} , '%s', $msg); };
  516. closelog;
  517. if ( $EVAL_ERROR )
  518. { carp "syslog() failed ($msg) :: $@\n"; }
  519. }
  520. =head2 method_is_allowed($method)
  521. Internal method.
  522. Verify that the method is 'allowed'; i.e. that it is in the
  523. allowed_methods list.
  524. =cut
  525. sub method_is_allowed
  526. {
  527. my $self = shift;
  528. my $method = shift;
  529. return defined $self->{'allowed_methods'}->{$method};
  530. }
  531. =head1 AUTHOR
  532. Augie Schwer, C<< <augie at cpan.org> >>
  533. http://www.schwer.us
  534. =head1 BUGS
  535. Please report any bugs or feature requests to
  536. C<bug-powerdns-control-server at rt.cpan.org>, or through the web interface at
  537. L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=PowerDNS-Control-Server>.
  538. I will be notified, and then you'll automatically be notified of progress on
  539. your bug as I make changes.
  540. =head1 SUPPORT
  541. You can find documentation for this module with the perldoc command.
  542. perldoc PowerDNS::Control::Server
  543. You can also look for information at:
  544. =over 4
  545. =item * AnnoCPAN: Annotated CPAN documentation
  546. L<http://annocpan.org/dist/PowerDNS-Control-Server>
  547. =item * CPAN Ratings
  548. L<http://cpanratings.perl.org/d/PowerDNS-Control-Server>
  549. =item * RT: CPAN's request tracker
  550. L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=PowerDNS-Control-Server>
  551. =item * Search CPAN
  552. L<http://search.cpan.org/dist/PowerDNS-Control-Server>
  553. =back
  554. =head1 ACKNOWLEDGEMENTS
  555. I would like to thank Sonic.net for allowing me to release this to the public.
  556. =head1 COPYRIGHT & LICENSE
  557. Copyright 2007 Augie Schwer, all rights reserved.
  558. This program is free software; you can redistribute it and/or modify it
  559. under the same terms as Perl itself.
  560. =head1 VERSION
  561. 0.03
  562. $Id: Server.pm 4430 2012-01-14 00:27:53Z augie $
  563. =cut
  564. 1; # End of PowerDNS::Control::Server