#!/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;