xcat-core/xCAT-server/lib/perl/xCAT/IPMI.pm

600 lines
20 KiB
Perl

#!/usr/bin/perl
# IBM(c) 2107 EPL license http://www.eclipse.org/legal/epl-v10.html
#(C)IBM Corp
#modified by jbjohnso@us.ibm.com
#This module abstracts the session management aspects of IPMI
package xCAT::IPMI;
BEGIN
{
$::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
}
use lib "$::XCATROOT/lib/perl";
use strict;
use warnings "all";
use IO::Socket::INET;
use IO::Select;
use Data::Dumper;
use Digest::MD5 qw/md5/;
my $ipmi2support = eval {
require Digest::SHA1;
Digest::SHA1->import(qw/sha1/);
require Digest::HMAC_SHA1;
Digest::HMAC_SHA1->import(qw/hmac_sha1/);
1;
};
my $aessupport;
if ($ipmi2support) {
$aessupport = eval {
require Crypt::Rijndael;
require Crypt::CBC;
1;
};
}
sub hexdump {
foreach (@_) {
printf "%02X ",$_;
}
print "\n";
}
my %payload_types = ( #help readability in certain areas of code by specifying payload by name rather than number
'ipmi' => 0,
'sol' => 1,
'rmcpplusopenreq' => 0x10,
'rmcpplusopenresponse' => 0x11,
'rakp1' => 0x12,
'rakp2' => 0x13,
'rakp3' => 0x14,
'rakp4' => 0x15,
);
my $socket; #global socket for all sessions to share. Fun fun
my $select = IO::Select->new();
my %bmc_handlers; #hash from bmc address to a live session management object.
#only one allowed at a time per bmc
my %sessions_waiting; #track session objects that may want to retry a packet, value is timestamp to 'wake' object for retransmit
sub new {
my $proto = shift;
my $class = ref $proto || $proto;
my $self = {};
bless $self,$class;
my %args = @_;
unless ($ipmi2support) {
$self->{ipmi15only} = 1;
}
unless ($args{'bmc'} and defined $args{'userid'} and defined $args{'password'}) {
return (undef,"bmc, userid, and password must be specified");
}
foreach (keys %args) { #store all passed parameters
$self->{$_} = $args{$_};
}
unless ($args{'port'}) { #default to port 623 unless specified
$self->{'port'} = 623;
}
unless ($socket) {
$socket = IO::Socket::INET->new(Proto => 'udp');
$select->add($socket);
}
$bmc_handlers{inet_ntoa(inet_aton($self->{bmc}))}=$self;
$self->{peeraddr} = sockaddr_in($self->{port},inet_aton($self->{bmc}));
$self->{'sequencenumber'} = 0; #init sequence number
$self->{'sequencenumberbytes'} = [0,0,0,0]; #init sequence number
$self->{'sessionid'} = [0,0,0,0]; # init session id
$self->{'authtype'}=0; # first messages will have auth type of 0
$self->{'ipmiversion'}='1.5'; # send first packet as 1.5
$self->{'timeout'}=1; #start at a quick timeout, increase on retry
$self->{'seqlun'}=0; #the IPMB seqlun combo, increment by 4s
$self->{'logged'}=0;
return $self;
}
sub login {
my $self = shift;
my %args = @_;
if ($self->{logged}) {
$args{callback}->("SUCCESS",$args{callback_args});
return;
}
$self->{onlogon} = $args{callback};
$self->{onlogon_args} = $args{callback_args};
$self->get_channel_auth_cap();
}
sub logout {
my $self = shift;
my %args = @_;
$self->{onlogout} = $args{callback};
$self->{onlogout_args} = $args{callback_args};
$self->subcmd(netfn=>0x6,command=>0x3c,data=>$self->{sessionid},callback=>\&logged_out,callback_args=>$self);
}
sub logged_out {
my $rsp = shift;
my $self = shift;
if ($rsp->{code} == 0) {
$self->{logged}=0;
if ( $self->{onlogout}) {
$self->{onlogout}->("SUCCESS",$self->{onlogout_args});
}
} else {
if ( $self->{onlogout}) {
$self->{onlogout}->("ERROR:",$self->{onlogout_args});
}
}
}
sub get_channel_auth_cap { #implement special case for session management command
my $self = shift;
if (defined $self->{ipmi15only}) {
$self->subcmd(netfn=>0x6,command=>0x38,data=>[0x0e,0x04],callback=>\&got_channel_auth_cap,callback_args=>$self);
} else {
$self->subcmd(netfn=>0x6,command=>0x38,data=>[0x8e,0x04],callback=>\&got_channel_auth_cap,callback_args=>$self);
}
#0x8e, set bit to signify recognition of IPMI 2.0 and request channel 'e', current.
#0x04, request administrator privilege
}
sub get_session_challenge {
my $self = shift;
my @user;
if ($self->{userbytes}) {
@user = @{$self->{userbytes}};
} else {
@user = unpack("C*",$self->{userid});
for (my $i=scalar @user;$i<16;$i++) {
$user[$i]=0;
}
$self->{userbytes} = \@user;
}
$self->subcmd(netfn=>0x6,command=>0x39,data=>[2,@user],callback=>\&got_session_challenge,callback_args=>$self); #we only support MD5, we would have errored out if not supported
}
sub got_session_challenge {
my $rsp = shift;
my $self = shift;
my @data = @{$rsp->{data}};
my %localcodes = ( 0x81 => "Invalid user name", 0x82 => "null user disabled" );
my $code = $rsp->{code}; #just to save me some typing
if ($code) {
my $errtxt = sprintf("ERROR: Get challenge failed with %02xh",$code);
if ($localcodes{$code}) {
$errtxt .= " ($localcodes{$code})";
} #TODO: generic codes
$self->{onlogon}->($errtxt, $self->{onlogon_args});
return;
}
$self->{sessionid} = [splice @data,0,4];
$self->{authtype}=2; #switch to auth mode
$self->activate_session(@data);
}
sub activate_session {
my $self = shift;
my @challenge = @_;
my @data = (2,4,@challenge,1,0,0,0);
$self->subcmd(netfn=>0x6,command=>0x3a,data=>\@data,callback=>\&session_activated,callback_args=>$self);
}
sub session_activated {
my $rsp = shift;
my $self = shift;
my $code = $rsp->{code}; #just to save me some typing
my %localcodes = (
0x81 => "No available login slots",
0x82 => "No available login slots for ".$self->{userid},
0x83 => "No slot available as administrator",
0x84 => "Session sequence number out of range",
0x85 => "Invalid session ID",
0x86 => $self->{userid}. " is not allowed to be Administrator or Administrator not allowed over network",
);
my @data = @{$rsp->{data}};
if ($code) {
my $errtxt = sprintf("ERROR: Unable to log in to BMC due to code %02xh",$code);
if ($localcodes{$code}) {
$errtxt .= " ($localcodes{$code})";
}
$self->{onlogon}->($errtxt, $self->{onlogon_args});
}
$self->{sessionid} = [splice @data,1,4];
$self->{sequencenumber}=$data[1]+($data[2]<<8)+($data[3]<<16)+($data[4]<<24);
$self->{sequencenumberbytes} = [splice @data,1,4];
$self->set_admin_level();
}
sub set_admin_level {
my $self= shift;
$self->subcmd(netfn=>0x6,command=>0x3b,data=>[4],callback=>\&admin_level_set,callback_args=>$self);
}
sub admin_level_set {
my $rsp = shift;
my $self = shift;
my %localcodes = (
0x80 => $self->{userid}." is not allowed administrator access",
0x81 => "This user or channel is not allowed administrator access",
0x82 => "Cannot disable User Level authentication",
);
my $code = $rsp->{code};
if ($code) {
my $errtxt = sprintf("ERROR: Failed requesting administrator privilege %02xh",$code);
if ($localcodes{$code}) {
$errtxt .= " (".$localcodes{$code}.")";
}
$self->{onlogon}->($errtxt,$self->{onlogon_args});
} else {
$self->{logged}=1;
$self->{onlogon}->("SUCCESS",$self->{onlogon_args});
}
}
sub got_channel_auth_cap {
my $rsp = shift;
my $self = shift;
my $code = $rsp->{code}; #just to save me some typing
if ($code == 0xcc and not defined $self->{ipmi15only}) { #ok, most likely a stupid ipmi 1.5 bmc
$self->{ipmi15only}=1;
return $self->get_channel_auth_cap();
}
if ($code != 0) {
$self->{onlogon}->("ERROR: Get channel capabilities failed with $code", $self->{onlogon_args});
return;
}
my @data = @{$rsp->{data}};
$self->{currentchannel} = $data[0];
if (($data[1] & 0b10000000) and ($data[3] & 0b10)) {
$self->{ipmiversion} = '2.0';
}
if ($self->{ipmiversion} eq '1.5') {
unless ($data[1] & 0b100) {
$self->{onlogon}->("ERROR: MD5 is required but not enabeld or available on target BMC",$self->{onlogon_args});
}
$self->get_session_challenge();
} elsif ($self->{ipmiversion} eq '2.0') { #do rmcp+
$self->open_rmcpplus_request();
}
}
sub open_rmcpplus_request {
my $self = shift;
$self->{'authtype'}=6;
$self->{sidm} = [0x15,0x58,0x25,0x7a];
my @payload = (0x1f,#message tag, TODO: could be random
0, #requested privilege role, 0 is highest allowed
0,0, #reserved
0x15,0x58,0x25,0x7a, #we only have to sweat one session, so no need to generate
0,0,0,8,1,0,0,0, #table 13-17, request sha
1,0,0,8,1,0,0,0); #sha integrity
if ($aessupport) {
push @payload,(2,0,0,8,1,0,0,0);
} else {
push @payload,(2,0,0,8,0,0,0,0);
}
$self->sendpayload(payload=>\@payload,type=>$payload_types{'rmcpplusopenreq'});
}
sub checksum {
my $self = shift;
my $sum = 0;
foreach(@_) {
$sum += $_;
}
$sum = ~$sum + 1;
return($sum&0xff);
}
sub subcmd {
my $self = shift;
my %args = @_;
my $rqaddr=0x81; #see section 5.5 of ipmi2 spec, rqsa by old code
my $rsaddr=0x20; #figrue 13-4, rssa by old code
my @rnl = ($rsaddr,$args{netfn}<<2);
my @rest = ($rqaddr,$self->{seqlun},$args{command},@{$args{data}});
my @payload=(@rnl,$self->checksum(@rnl),@rest,$self->checksum(@rest));
$self->{seqlun} += 4; #increment by 1<<2
$self->{seqlun} &= 0xff; #keep it one byte
$self->{ipmicallback} = $args{callback};
$self->{ipmicallback_args} = $args{callback_args};
my $type = $payload_types{'ipmi'};
if ($self->{integrityalgo}) {
$type = $type | 0b01000000; #add integrity
}
$self->sendpayload(payload=>\@payload,type=>$type);
}
sub waitforrsp {
my $self=shift;
my $data;
my $peerport;
my $peerhost;
my $timeout; #TODO: code to scan pending objects to find soonest retry deadline
my $curtime=time();
foreach (values %sessions_waiting) {
if (defined $timeout) {
if ($timeout < $_-$curtime) {
next;
}
}
$timeout = $_-$curtime;
}
if ($select->can_read($timeout)) {
while ($select->can_read(0)) {
$peerport = $socket->recv($data,1500,0);
route_ipmiresponse($peerport,unpack("C*",$data));
}
}
return scalar (keys %sessions_waiting);
}
sub route_ipmiresponse {
my $sockaddr=shift;
my @rsp = @_;
unless (
$rsp[0] == 0x6 and
$rsp[2] == 0xff and
$rsp[3] == 0x07) {
return; #ignore non-ipmi packets
}
my $host;
my $port;
($port,$host) = sockaddr_in($sockaddr);
$host = inet_ntoa($host);
if ($bmc_handlers{$host}) {
delete $sessions_waiting{$bmc_handlers{$host}};
$bmc_handlers{$host}->handle_ipmi_packet(@rsp);
}
}
sub handle_ipmi_packet {
my $self = shift;
my @rsp = @_;
if ($rsp[4] == 0 or $rsp[4] == 2) { #IPMI 1.5 (check 0 assumption...)
my $remsequencenumber=$rsp[5]+$rsp[6]>>8+$rsp[7]>>16+$rsp[8]>>24;
if ($self->{remotesequencenumber} and $remsequencenumber < $self->{remotesequencenumber} ) {
return; #ignore malformed sequence number
}
$self->{remotesequencenumber}=$remsequencenumber;
$self->{remotesequencebytes} = [@rsp[5..8]];
if ($rsp[4] != $self->{authtype}) {
return 2; # not thinking about packets that do not match our preferred auth type
}
unless ($rsp[9] == $self->{sessionid}->[0] and
$rsp[10] == $self->{sessionid}->[1] and
$rsp[11] == $self->{sessionid}->[2] and
$rsp[12] == $self->{sessionid}->[3]) {
return 1; #this response does not match our current session id, ignore it
}
my @authcode=();
if ($rsp[4] == 2) {
@authcode = splice @rsp,13,16;
}
my @payload = splice (@rsp,14,$rsp[13]);
if (@authcode) { #authcode is longer than 0, check it
$self->{checkremotecode}=1;
my @expectedauthcode = $self->ipmi15authcode(@payload);
$self->{checkremotecode}=0;
foreach (0..15) {
if ($expectedauthcode[$_] != $authcode[$_]) {
return 3; #invalid authcode
}
}
}
$self->parse_ipmi_payload(@payload);
} elsif ($rsp[4] == 6) { #IPMI 2.0
if (($rsp[5]& 0b00111111) == 0x11) {
hexdump(@rsp);
$self->got_rmcp_response(splice @rsp,16);
} elsif (($rsp[5]& 0b00111111) == 0x13) {
$self->got_rakp2(splice @rsp,16);
} elsif (($rsp[5]& 0b00111111) == 0x15) {
$self->got_rakp4(splice @rsp,16);
}
}
}
sub got_rmcp_response {
my $self = shift;
my @data = @_;
my $byte = shift @data;
unless ($byte == 0x1f) {
return;
}
$byte = shift @data;
unless ($byte == 0x00) {
$self->{onlogon}->("ERROR: $byte code on opening RMCP+ session",$self->{onlogon_args}); #TODO: errors
return;
}
$byte = shift @data;
unless ($byte >= 4) {
$self->{onlogon}->("ERROR: Cannot acquire sufficient privilege",$self->{onlogon_args});
return;
}
splice @data,0,5;
$self->{pendingsessionid} = [splice @data,0,4];
$self->send_rakp1();
}
sub send_rakp3 {
my $self = shift;
my @payload = (0x1f,0,0,0,@{$self->{pendingsessionid}});
my @user = unpack("C*",$self->{userid});
push @payload,unpack("C*",hmac_sha1(pack("C*",@{$self->{remoterandomnumber}},@{$self->{sidm}},4,scalar @user,@user),$self->{password}));
$self->sendpayload(payload=>\@payload,type=>$payload_types{'rakp3'});
}
sub send_rakp1 {
my $self = shift;
my @payload = (0x1f,0,0,0,@{$self->{pendingsessionid}});
$self->{randomnumber}=[];
foreach (1..16) {
my $randomnumber = int(rand(255));
push @{$self->{randomnumber}},$randomnumber;
}
push @payload, @{$self->{randomnumber}};
push @payload,(4,0,0); # request admin
my @user = unpack("C*",$self->{userid});
push @payload,scalar @user;
push @payload,@user;
$self->sendpayload(payload=>\@payload,type=>$payload_types{'rakp1'});
}
sub got_rakp4 {
my $self = shift;
my @data = @_;
my $byte = shift @data;
unless ($byte == 0x1f) {
return;
}
$byte = shift @data;
unless ($byte == 0x00) {
$self->{onlogon}->("ERROR: $byte code on opening RMCP+ session",$self->{onlogon_args}); #TODO: errors
return;
}
splice @data,0,6; #discard reserved bytes and session id
hexdump(@data);
my @expectauthcode = unpack("C*",hmac_sha1(pack("C*",@{$self->{randomnumber}},@{$self->{pendingsessionid}},@{$self->{remoteguid}}),$self->{sik}));
hexdump(@expectauthcode);
foreach (@expectauthcode[0..11]) {
unless ($_ == (shift @data)) {
$self->{onlogon}->("ERROR: failure in final rakp exchange message",$self->{onlogon_args});
return;
}
}
$self->{sessionid} = $self->{pendingsessionid};
$self->{integrityalgo}='sha1';
$self->set_admin_level();
}
sub got_rakp2 {
my $self=shift;
my @data = @_;
hexdump(@data);
my $byte = shift @data;
unless ($byte == 0x1f) {
return;
}
$byte = shift @data;
unless ($byte == 0x00) {
$self->{onlogon}->("ERROR: $byte code on opening RMCP+ session",$self->{onlogon_args}); #TODO: errors
return;
}
splice @data,0,6; # throw away reserved bytes, and session id, might need to check
$self->{remoterandomnumber} = [];
foreach (1..16) {
push @{$self->{remoterandomnumber}},(shift @data);
}
$self->{remoteguid} = [];
foreach (1..16) {
push @{$self->{remoteguid}},(shift @data);
}
#Data now represents authcode.. sha1 only..
my @user = unpack("C*",$self->{userid});
my $ulength = scalar @user;
my $hmacdata = pack("C*",(0x15,0x58,0x25,0x7a,@{$self->{pendingsessionid}},@{$self->{randomnumber}},@{$self->{remoterandomnumber}},@{$self->{remoteguid}},4,$ulength,@user));
hexdump(0x15,0x58,0x25,0x7a,@{$self->{pendingsessionid}},@{$self->{randomnumber}},@{$self->{remoterandomnumber}},@{$self->{remoteguid}},4,$ulength,@user);
my @expectedhash = (unpack("C*",hmac_sha1($hmacdata,$self->{password})));
foreach (0..(scalar(@expectedhash)-1)) {
if ($expectedhash[$_] != $data[$_]) {
$self->{onlogon}->("ERROR: Incorrect password provided",$self->{onlogon_args});
return;
}
}
$self->{sik} = hmac_sha1(pack("C*",@{$self->{randomnumber}},@{$self->{remoterandomnumber}},4,$ulength,@user),$self->{password});
$self->send_rakp3();
}
sub parse_ipmi_payload {
my $self=shift;
my @payload = @_;
#for now, just trash the headers, this has been validated to death anyway
splice @payload,0,5; #remove rsaddr/netfs/lun/checksum/rq/seq/lun
pop @payload; #remove checksum
my $rsp;
$rsp->{cmd} = shift @payload;
$rsp->{code} = shift @payload;
$rsp->{data} = \@payload;
$self->{ipmicallback}->($rsp,$self->{ipmicallback_args});
}
sub ipmi15authcode {
my $self = shift;
#per table 22-22 'authcode algorithms'
my @data = @_;
my @password;
my @code;
if ($self->{passbytes}) {
@password = @{$self->{passbytes}};
} else {
@password = unpack("C*",$self->{password});
for (my $i=scalar @password;$i<16;$i++) {
$password[$i]=0;
}
$self->{passbytes} = \@password;
}
my @sequencebytes = @{$self->{sequencenumberbytes}};
if ($self->{checkremotecode}) {
@sequencebytes = @{$self->{remotesequencebytes}};
}
if ($self->{authtype} == 0) {
return ();
} elsif ($self->{authtype} == 2) {
return unpack("C*",md5(pack("C*",@password,@{$self->{sessionid}},@data,@sequencebytes,@password))); #ignoring single-session channels
}
#Not supporting plaintext passwords, that would be asinine
}
#this function accepts a generic ipmi command and applies current session data and handles the 1.5<->2.0 differences
sub sendpayload {
#implementation used section 13.6, examle ipmi over lan packet
my $self = shift;
my %args = @_;
my @msg = (0x6,0x0,0xff,0x07); #RMCP header is constant in IPMI
my $type = $args{type} & 0b00111111;
$sessions_waiting{$self}=time()+$self->{timeout};
my @payload = @{$args{payload}};
push @msg,$self->{'authtype'}; # add authtype byte (will support 0 only for session establishment, 2 for ipmi 1.5, 6 for ipmi2
if ($self->{'ipmiversion'} eq '2.0') { #TODO: revisit this to see if assembly makes sense
hexdump(@msg);
push @msg, $args{type};
hexdump(@msg);
if ($type == 2) {
push @msg,@{$self->{'iana'}},0;
push @msg,@{$self->{'oem_payload_id'}};
}
push @msg,@{$self->{sessionid}};
}
push @msg,@{$self->{sequencenumberbytes}};
if ($self->{'ipmiversion'} eq '1.5') { #ipmi 2.0 for some reason swapped session id and seq number location
push @msg,@{$self->{sessionid}};
unless ($self->{authtype} == 0) {
push @msg,$self->ipmi15authcode(@payload);
}
push @msg,scalar(@payload);
push @msg,@payload;
#TODO: sweat a pad or not? spec isn't crystal clear on the 'legacy pad' and it sounds like it is just for some old crappy nics that have no business in a good server
} elsif ($self->{'ipmiversion'} eq '2.0') {
#TODO:
#push conf header
my $size = scalar(@payload);
push @msg,($size&0xff,$size>>8);
push @msg,@payload;
#push conf trailer (or had to do it before...
if ($self->{integrityalgo}) {
#push integrity pad
#push @msg,0x7; #reserved byte in 2.0
#push integrity data
}
}
hexdump(@msg);
print "\n";
$socket->send(pack("C*",@msg),0,$self->{peeraddr});
if ($self->{sequencenumber}) { #if using non-zero, increment, otherwise..
$self->{sequencenumber} += 1;
$self->{sequencenumberbytes} = [$self->{sequencenumber}&0xff,($self->{sequencenumber}>>8)&0xff,($self->{sequencenumber}>>16)&0xff,($self->{sequencenumber}>>24)&0xff];
}
}
1;