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. --- emergencyc.pl | 96 ++++++++++++++++++++++++++++++ emergencyd.pl | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100755 emergencyc.pl create mode 100755 emergencyd.pl diff --git a/emergencyc.pl b/emergencyc.pl new file mode 100755 index 0000000..a7d55d2 --- /dev/null +++ b/emergencyc.pl @@ -0,0 +1,96 @@ +#! /usr/local/bin/perl -w + +# The Emergency Client - send commands to the emergency daemon + +# David A. Madore - Public Domain + +# This is a stupid client which just sends a command verbatim to the +# emergency daemon. All it takes care of is to put a valid timestamp +# and MAC. + +# Options recognized: +# +# -K specifies the key to use; or -k specifies a +# keyfile (the client will use the first line as key). +# +# -h and -p specifies the host and port to connect +# to. +# +# -t specifies an explicit timestamp to use. +# +# The command follows the options. + +use strict; +use warnings; +use Digest::SHA qw(hmac_sha256_hex); +use Socket; +use Socket6; +use Getopt::Std; + +use constant { + DEFAULT_PORT => 911 +}; + +my %opts; + +getopts("K:k:h:p:t:", \%opts); + +my $key; +if ( defined($opts{K}) ) { + $key = $opts{K}; +} elsif ( defined($opts{k}) ) { + open my $keyfile, "<", $opts{k} or die "Cannot open key file $opts{k}: $!"; + $key = <$keyfile>; + chomp $key; + close $keyfile; +} +die "No key specified (use -K or -k option)" unless defined($key); + +my $host; +if ( defined($opts{h}) ) { + $host = $opts{h}; +} else { + $host = "localhost"; +} +my $port; +if ( defined($opts{p}) ) { + $port = $opts{p}; +} else { + $port = DEFAULT_PORT; +} + +my @res = getaddrinfo($host, $port, AF_UNSPEC, SOCK_DGRAM) + or die "Cannot resolve host $host port $port"; +my ($family, $socktype, $proto, $haddr) = @res; + +my $socket; +socket $socket, $family, $socktype, $proto or die "Can't create socket: $!"; +bind $socket, sockaddr_in6(0, in6addr_any); + +sub curtime { + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time); + return sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ",$year+1900,$mon+1,$mday,$hour,$min,$sec); +} + +my $command = $ARGV[0]; +$command = "PING" unless defined($command); +my $timestamp = $opts{t}; +$timestamp = curtime unless defined($timestamp); +my $validate = "$command|$timestamp"; +my $maccheck = hmac_sha256_hex($validate, $key); +send $socket, "$command|$timestamp|$maccheck", 0, $haddr; + +my $buf; +my $sender; +eval { + local $SIG{ALRM} = sub { die "alarm\n" }; + alarm 5; + do { + $sender = recv($socket, $buf, 16384, 0); + } while ( $sender ne $haddr ); +}; +if ( $@ ) { + printf "timeout\n"; +} else { + printf "%s", $buf; +} 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