From e80e7f3c2289a93955ffeda6cf1a2e8e47eb351a Mon Sep 17 00:00:00 2001 From: "David A. Madore" Date: Mon, 15 Feb 2010 21:33:17 +0100 Subject: Initial creation of the emergency daemon, and a stupid client. These don't do much for now. --- emergencyd.pl | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100755 emergencyd.pl (limited to 'emergencyd.pl') diff --git a/emergencyd.pl b/emergencyd.pl new file mode 100755 index 0000000..5766484 --- /dev/null +++ b/emergencyd.pl @@ -0,0 +1,188 @@ +#! /usr/local/bin/perl -w + +# The Emergency Daemon - wait for simple emergency commands and execute them + +# David A. Madore - Public Domain + +# *** The emergency daemon protocol *** +# +# Commands are sent through UDP datagrams, normally addressed to port +# 911. Commands consist of three parts separated or terminated by '|' +# or \n (but \r\n is also accepted). First part is the command +# proper. Second is a UTC timestamp in yyyy-mm-ddThh:mm:ssZ format +# (here 'T' and 'Z' are literal 'T' and 'Z'). Third is the +# HMAC-SHA256, in hexadecimal, of command|timestamp (separated by '|', +# even if they were separated differently in the input, and not +# terminated by anything), with a HMAC key shared by client and +# server. +# +# The server response consists of one or more lines terminated by \n. +# If the command is PING, the server responds PONG. If the command is +# DATE, the server responds DATE followed by the current time and the +# timestamp of the last authenticated command. Apart from PING and +# DATE, all other commands are authenticated: if the HMAC does not +# match what it should be, the server responds !MAC. If the timestamp +# does not match the current date +/- 30 seconds, the server responds +# !DAT; it also does so if the timestamp is not (strictly) greater +# than the timestamp of the last request. +# +# The NOOP command does nothing (but must still be authenticated): the +# server responds NOOP. The DPID command returns the daemon's PID: +# the server responds DPID and then the PID itself (on a separate +# line). The DIE! command causes the daemon to respond BYE! and then +# quit. The RKEY command causes the daemon to reread its key file: +# the server responds either DONE or !ERR in case of error, in which +# case the next line in the response is a human-readable error line. +# The SYRQ command is followed by whitespace and then by data to be +# written to /proc/sysrq-trigger: the server responds either DONE or +# !ERR in case of error, in which case the next line in the response +# is a human-readable error line. + +# *** The emergency daemon itself *** +# +# The daemon understands three options: +# +# -p indicates which port it should bind to. The daemon +# binds to the IPv6 unspecified address with the IPV6_V6ONLY option +# set to 0, thus listening on both IPv6 and IPv4 families. +# +# -k specifies the keyfile to use. This file contains one +# or more keys, one per line, which will all be equally valid when +# computing the MAC. +# +# -f requests that the daemon ignore HUP and INT signals, and fork +# once it has successfully set up its listening socket: the father +# then prints its child's PID and exits successfully. + +use strict; +use warnings; +use Digest::SHA qw(hmac_sha256_hex); +use Socket; +use Socket6; +use POSIX (); +use Getopt::Std; + +use constant { + DEFAULT_PORT => 911 +}; + +my %opts; + +getopts("k:p:f", \%opts); + +my @authorized_keys; + +my $keyfilename = $opts{k}; +die "No key file specified (use -k option)" unless defined($keyfilename); +sub readkeys { + open my $keyfile, "<", $keyfilename + or die "Cannot open key file $opts{k}: $!"; + @authorized_keys = (); + while (<$keyfile>) { + chomp; + push @authorized_keys, $_; + } + close $keyfile; +} +readkeys; + +my $proto = getprotobyname("udp") or die "Can't resolve udp protocol: $!"; +my $port; +if ( defined($opts{p}) ) { + $port = $opts{p}; + $port =~ /^(\d+)$/ or die "Invalid port number (-p option) $port"; +} else { + $port = DEFAULT_PORT; +} + +my $socket; +socket $socket, PF_INET6, SOCK_DGRAM, $proto or die "Can't create socket: $!"; +if ( defined(IPV6_V6ONLY) ) { + setsockopt $socket, IPPROTO_IPV6, IPV6_V6ONLY, 0 or die "Can't set IPV6_V6ONLY option to 0: $!"; +} +bind $socket, sockaddr_in6($port, in6addr_any) or die "Can't bind socket: $!"; + +if ( $opts{f} ) { + $SIG{HUP} = "IGNORE"; + $SIG{INT} = "IGNORE"; + my $childpid = fork; + die "Can't fork: $!" unless defined($childpid); + if ( $childpid ) { + print "$childpid\n"; + exit 0; + } +} + +sub curtime { + my $fiddle = shift; $fiddle = 0 unless defined($fiddle); + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time+$fiddle); + return sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ",$year+1900,$mon+1,$mday,$hour,$min,$sec); +} + +my $mintime = "0"; + +PACKET: +while (1) { + my $buf; + my $sender = recv($socket, $buf, 16384, 0); + my @lines = split /\015*\012|\|/s, $buf; + my $command = $lines[0]; $command = "" unless defined($command); + my $timestamp = $lines[1]; $timestamp = "" unless defined($timestamp); + my $maccheck = $lines[2]; $maccheck = "" unless defined($maccheck); + next PACKET if $command eq ""; + if ( $command eq "PING" ) { + send $socket, "PONG\n", 0, $sender; + } elsif ( $command eq "DATE" ) { + send $socket, ("DATE\n".curtime."\n".$mintime."\n"), 0, $sender; + } else { + my $validate = "$command|$timestamp"; + my $macchecked = 0; + foreach my $key ( @authorized_keys ) { + if ( $maccheck eq hmac_sha256_hex($validate, $key) ) { + $macchecked = 1; + } + } + unless ( $macchecked ) { + send $socket, "!MAC\n", 0, $sender; + next PACKET; + } + my $datechecked = ($timestamp ge curtime(-30)) + && ($timestamp le curtime(30)) + && ($timestamp gt $mintime); + unless ( $datechecked ) { + send $socket, "!DAT\n", 0, $sender; + next PACKET; + } + $mintime = $timestamp; + if ( $command eq "NOOP" ) { + send $socket, "NOOP\n", 0, $sender; + } elsif ( $command eq "DPID" ) { + my $pid = POSIX::getpid; + send $socket, "DPID\n$pid\n", 0, $sender; + } elsif ( $command eq "DIE!" ) { + send $socket, "BYE!\n", 0, $sender; + exit 0; + } elsif ( $command eq "RKEY" ) { + my $resp = "DONE\n"; + eval { readkeys }; + if ( $@ ) { + $resp = "!ERR\n$@"; + } + send $socket, $resp, 0, $sender; + } elsif ( $command =~ /^SYRQ\s+(.*)$/ ) { + my $s = $1; + my $resp = "DONE\n"; + eval { + open my $sysrq_trigger, ">", "/proc/sysrq-trigger" or die "Couldn't open /proc/sysrq-trigger for writing: $!"; + print $sysrq_trigger $s or die "Couldn't write to /proc/sysrq-trigger: $!"; + close $sysrq_trigger; + }; + if ( $@ ) { + $resp = "!ERR\n$@"; + } + send $socket, $resp, 0, $sender; + } else { + send $socket, "!UNK\n", 0, $sender; + } + } +} -- cgit v1.2.3