#!/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;
use Carp qw/confess cluck/;

BEGIN
{
    $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
}
use lib "$::XCATROOT/lib/perl";
use strict;
use warnings "all";
use Time::HiRes qw/time/;
use xCAT::SvrUtils;

use IO::Socket::INET qw/!AF_INET6 !PF_INET6/;
my $initialtimeout = 0.5;
use constant STATE_OPENSESSION    => 1;
use constant STATE_EXPECTINGRAKP2 => 2;
use constant STATE_EXPECTINGRAKP4 => 3;
use constant STATE_ESTABLISHED    => 4;
use constant STATE_FAILED         => 5;

#my $ipmidbg;
#open($ipmidbg,">","/tmp/ipmidbg");
#sub dprint {
#	return;
#	my $self = shift;
#	foreach (@_) {
#		foreach (split /\n/,$_) {
#			print $ipmidbg $self->{bmc}.": ".$_."\n";
#		}
#	}
#}

my $doipv6 = eval {
    require Socket6;
    require IO::Socket::INET6;
    IO::Socket::INET6->import();
    1;
};
use IO::Select;

#use Data::Dumper;
use Digest::MD5 qw/md5/;
my $pendingpackets = 0;
my %tabooseq; #TODO: this is a global which means one taboo in the whole set causes unrelated session objects to consider it taboo unnecessarily
my $maxpending;    #determined dynamically based on rcvbuf detection

my $ipmi2support;
if (-f "/etc/debian_release" or -f "/etc/debian_version") {
    $ipmi2support = eval {
        require Digest::SHA;
        Digest::SHA->import(qw/sha1/);
        require Digest::HMAC_SHA1;
        Digest::HMAC_SHA1->import(qw/hmac_sha1/);
        1;
    };
}
else {
    $ipmi2support = eval {
        require Digest::SHA1;
        Digest::SHA1->import(qw/sha1/);
        require Digest::HMAC_SHA1;
        Digest::HMAC_SHA1->import(qw/hmac_sha1/);
        1;
    };
}
use Crypt::Rijndael;
use Crypt::CBC;

sub hexdump {
    foreach (@_) {
        printf "%02X ", $_;
    }
    print "\n";
}

my %command_info = (
    6 => {
        56 => "Get Channel Authentication Capabilities",
        59 => "Set session privilege level",
        60 => "Close Session",
        58 => "activate session",
        57 => "Get Session Challenge",
        1 => "Get Device ID",
        2 => "Cold Reset",
        55 => "Get System GUID",
        64  => "Set Channel Access",
        76 => "Set User Payload Access",
        10 => "Get Command Support",
    },	
    4 => {
        18 => "Set PEF Configuration Parameters",
        19 => "Get PEF Configuration Parameters",
        45 => "Get Sensor Reading",
    },
    0 => {
        8 => "Set System Boot Options",
        9 => "Get System Boot Options",
        4 => "Chassis Identify(beacon)",
        2 => "Chassis Control",
        1 => "Get Chassis Status",
    },
    12 => {
        1 => "Set LAN Configuration Parameters",
        2 => "Get LAN Configuration Parameters",
        33 => "Set SOL Configuration Parameters",
        34 => "Get SOL Configuration Parameters",
    },
    10 => {
        35 => "Get SDR",
        34 => "Reserve SDR Repository",
        71 => "Clear SEL",
        67 => "Get SEL Entry",
        66 => "Reserve SEL",
        72 => "Get SEL Time",
        64 => "Get SEL Info",
        17 => "Read FRU Data",
        16 => "Get FRU Inventory Area Info",
        32 => "Get SDR Repository Info",
    },
);

my %netfn_types = (
    0 => "Chassis",
    2 => "Bridge",
    4 => "Sensor/Event",
    6 => "App",
    8 => "Firmware",
    10 => "Storage",
    12 => "Transport",
    # do we need to define anymore then these?
);

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 %rmcp_codes = (    #human friendly translations of rmcp+ code numbers
    1 => "Insufficient resources to create new session (wait for existing sessions to timeout)",
    2 => "Invalid Session ID",      #this shouldn't occur...
    3 => "Invalid payload type",    #shouldn't occur..
    4 => "Invalid authentication algorithm", #if this happens, we need to enhance our mechanism for detecting supported auth algorithms
    5 => "Invalid integrity algorithm",          #same as above
    6 => "No matching authentication payload",
    7 => "No matching integrity payload",
    8 => "Inactive Session ID", #this suggests the session was timed out while trying to negotiate, shouldn't happen
    9    => "Invalid role",
    0xa  => "Unauthorised role or privilege level requested",
    0xb  => "Insufficient resources to create a session at the requested role",
    0xc  => "Invalid username length",
    0xd  => "Unauthorized name",
    0xe  => "Unauthorized GUID",
    0xf  => "Invalid integrity check value",
    0x10 => "Invalid confidentiality algorithm",
    0x11 => "No cipher suite match with proposed security algorithms",
    0x12 => "Illegal or unrecognized parameter", #have never observed this, would most likely mean a bug in xCAT or IPMI device
);
my $socket;    #global socket for all sessions to share.  Fun fun
my $select = IO::Select->new();

our %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 = @_;
    $self->init();
    unless ($ipmi2support) {
        $self->{ipmi15only} = 1;
    }
    $self->{privlevel} = 4;
    unless ($args{'bmc'} and defined $args{'userid'} and defined $args{'password'}) {
        $self->{error} = "bmc, userid, and password must be specified";
        return $self;
    }
    foreach (keys %args) {    #store all passed parameters
        $self->{$_} = $args{$_};
    }
    unless ($args{'node'}) {    #default to port 623 unless specified
        $self->{'node'} = $args{'bmc'};
    }
    unless ($args{'port'}) {    #default to port 623 unless specified
        $self->{'port'} = 623;
    }
    unless ($socket) {
        if ($doipv6) {
            $socket = IO::Socket::INET6->new(Proto => 'udp');
        } else {
            $socket = IO::Socket::INET->new(Proto => 'udp');
        }
        if (-r "/proc/sys/net/core/rmem_max") { # we can detect the maximum allowed socket, read it.
            my $sysctl;
            open($sysctl, "<", "/proc/sys/net/core/rmem_max");
            my $maxrcvbuf = <$sysctl>;
            my $rcvbuf    = $socket->sockopt(SO_RCVBUF);
            if ($maxrcvbuf > $rcvbuf) {
                $socket->sockopt(SO_RCVBUF, $maxrcvbuf / 2);
            }
            $maxpending = $maxrcvbuf / 1500; #probably could have maxpending go higher, but just go with typical MTU as a guess
        } else { #We do not have a way to determine how high we could set RCVBUF, so read the current value and run with it
            my $rcvbuf = $socket->sockopt(SO_RCVBUF);
            $maxpending = $rcvbuf / 1500; #probably could have maxpending go higher, but just go with typical MTU as a guess
        }
        $select->add($socket);
    }
    my $bmc_n;
    my ($family, $socktype, $protocol, $saddr, $name, $ip, $service);
    if ($doipv6) {
        ($family, $socktype, $protocol, $saddr, $name) = Socket6::getaddrinfo($self->{bmc}, 623, AF_UNSPEC, SOCK_DGRAM, 0);
        ($ip, $service) = Socket6::getnameinfo($saddr, Socket6::NI_NUMERICHOST());
    }
    unless ($saddr or $bmc_n = inet_aton($self->{bmc})) {
        $self->{error} = "Could not resolve " . $self->{bmc} . " to an address";
        return $self;
    }

    if ($ip and $ip =~ /::ffff:\d+\.\d+\.+\d+\.\d+/) {
        $ip =~ s/::ffff://;
    } elsif (not $ip and $bmc_n) {
        $ip = inet_ntoa($bmc_n);
    }
    $bmc_handlers{$ip} = $self;
    if ($saddr) {
        $self->{peeraddr} = $saddr;
    } else {
        $self->{peeraddr} = sockaddr_in($self->{port}, $bmc_n);
    }
    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->{logontries}   = 5;
    $self->get_channel_auth_cap();
}

sub logout {
    my $self = shift;
    my %args = @_;
    $self->{onlogout}      = $args{callback};
    $self->{onlogout_args} = $args{callback_args};
    unless ($self->{logged}) {
        if ($self->{onlogout}) { $self->{onlogout}->("SUCCESS", $self->{onlogout_args}); }
        return;
    }
    $self->{noretry} = 1;
    $self->subcmd(netfn => 0x6, command => 0x3c, data => $self->{sessionid}, callback => \&logged_out, callback_args => $self);
    $self->{logged} = 0; #seeing as how we are going to take it on faith the logout happened, do the callback now
    if ($self->{onlogout}) {
        $self->{onlogout}->("SUCCESS", $self->{onlogout_args});
    }
}

sub logged_out {
    return;
    my $rsp  = shift;
    my $self = shift;
    if (defined $rsp->{code} and $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",
    );
    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});
        return;
    }
    if ($rsp->{error}) {
        $self->{onlogon}->($rsp->{error}, $self->{onlogon_args});
        return;
    }
    my @data = @{ $rsp->{data} };
    $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 => [ $self->{privlevel} ], 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 == 0x80 or $code == 0x81) and $self->{privlevel} == 4) {
        $self->{privlevel} = 3;
        $self->subcmd(netfn => 0x6, command => 0x3b, data => [ $self->{privlevel} ], callback => \&admin_level_set, callback_args => $self);
        return;
    }

    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;
    if ($rsp->{error}) {
        $self->{onlogon}->("ERROR: " . $rsp->{error}, $self->{onlogon_args});
        return;
    }
    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;
    unless ($self->{localsid}) { $self->{localsid} = 358098297; } #this is an arbitrary number of no significance
    $self->{localsid} += 1;    #new session ID if we are relogging
    my @sidbytes = unpack("C4", pack("N", $self->{localsid}));
    $self->{sidm} = \@sidbytes;
    unless ($self->{rmcptag}) { $self->{rmcptag} = 1; }
    $self->{rmcptag} += 1;
    my @payload = ($self->{rmcptag},    #message tag,
        0,    #requested privilege role, 0 is highest allowed
        0, 0, #reserved
        @sidbytes,
        0, 0, 0, 8, 1, 0, 0, 0,     #table 13-17, request sha
        1, 0, 0, 8, 1, 0, 0, 0);    #sha integrity
    push @payload, (2, 0, 0, 8, 1, 0, 0, 0);    # aes
    $self->{sessionestablishmentcontext} = STATE_OPENSESSION;
    $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 = @_;
    while ($self->{incommand}) {
        $self->waitforrsp(timeout=>1);
    }
    $self->{incommand} = 1;

    $self->{expectedcmd}   = $args{command};
    $self->{expectednetfn} = $args{netfn} + 1;
    if ($self->{onlogon_args}->{xcatdebugmode}) {
        my $command_string = $command_info{$args{netfn}}->{$args{command}};
        my $data_values = join ", ", @{$args{data}};
        my $msg = sprintf ("[ipmi_debug] $self->{onlogon_args}->{command}:$self->{onlogon_args}->{subcommand}(@{$self->{onlogon_args}->{extraargs}}), raw_cmd: netfn(0x%02x=>%s), cmd(0x%02x=>%s), data=[%s]", $args{netfn}, $netfn_types{$args{netfn}}, $args{command}, $command_string, $data_values);
        xCAT::SvrUtils::sendmsg([0, $msg], $self->{onlogon_args}->{outfunc}, $self->{node});
    }
    my $seqincrement = 7;
    while ($tabooseq{ $self->{expectednetfn} }->{ $self->{expectedcmd} }->{ $self->{seqlun} } and $seqincrement) { #avoid using a seqlun formerly marked 'taboo', but don't advance by more than 7, just in case
        $tabooseq{ $self->{expectednetfn} }->{ $self->{expectedcmd} }->{ $self->{seqlun} }--; #forgive a taboo lun over time...
        $self->{seqlun} += 4;    #increment by 1<<2
        $self->{seqlun} &= 0xff; #make sure we don't get too large a seqlun
        $seqincrement--; #assure seq number doesn't go beyond 7 even if it means going taboo, one enhancement would be to pick the *least* taboo instead of just giving up
    }
    my $rsaddr = 0x20;                        #figrue 13-4, rssa by old code
    my @rnl = ($rsaddr, $args{netfn} << 2);
    my @rest = ($self->{rqaddr}, $self->{seqlun}, $args{command}, @{ $args{data} });
    my @payload = (@rnl, $self->checksum(@rnl), @rest, $self->checksum(@rest));
    $self->{ipmicallback}      = $args{callback};
    $self->{ipmicallback_args} = $args{callback_args};
    my $type = $payload_types{'ipmi'};

    if ($self->{integrityalgo}) {
        $type = $type | 0b01000000;           #add integrity
    }
    if ($self->{confalgo}) {
        $type = $type | 0b10000000;           #add secrecy
    }
    $self->sendpayload(payload => \@payload, type => $type, delayxmit => $args{delayxmit});
}

sub waitforrsp {
    my $self = shift;
    my %args = @_;

    my $data;
    my $peerport;
    my $peerhost;
    my $timeout; #TODO: code to scan pending objects to find soonest retry deadline
    my $curtime = time();
    if (defined $args{timeout}) { $timeout = $args{timeout}; }
    foreach (keys %sessions_waiting) {
        if (defined $timeout and $timeout == 0) { last; } #once we get to zero, then there is no lower and anything else is a waste
        if ($sessions_waiting{$_}->{timeout} <= $curtime) {
            $timeout = 0; #this waitforrsp must go as quickly to retry as possible, but give it a chance this iteration to clear without timedout being called
              #if something defferred entry into waitforrsp so long that there was no chance to check for response, this grants at least one shot at getting data
        }
        if (defined $timeout) {
            if ($timeout < $sessions_waiting{$_}->{timeout} - $curtime) {
                next;
            }
        }
        $timeout = $sessions_waiting{$_}->{timeout} - $curtime;
    }
    unless (defined $timeout) {
        return scalar(keys %sessions_waiting);
    }

    if ($select->can_read($timeout)) {
        while ($select->can_read(0)) {
            my @ipmiq = ();
            while ($select->can_read(0)) {
                $peerport = $socket->recv($data, 1500, 0);
                push @ipmiq, [ $peerport, $data ];
            }
            while (@ipmiq) {
                my $datagram = shift @ipmiq;
                ($peerport, $data) = @$datagram;
                route_ipmiresponse($peerport, unpack("C*", $data));
                while ($select->can_read(0)) {
                    $peerport = $socket->recv($data, 1500, 0);
                    push @ipmiq, [ $peerport, $data ];
                }
            }
        }
    }
    foreach (keys %sessions_waiting) { #now that we have given all incoming packets a chance, if some sessions were past due when we entered
                                       #take timeout response action now
        if ($sessions_waiting{$_}->{timeout} <= $curtime) {
            my $session = $sessions_waiting{$_}->{ipmisession};
            delete $sessions_waiting{$_};
            $pendingpackets -= 1;
            $session->timedout();
            next;
        }
    }
    return scalar(keys %sessions_waiting);
}

sub timedout {
    my $self = shift;
    unless (ref $self->{pendingargs}) {
        return;
    }
    $self->{nowait} = 1;
    $self->{timeout} += 1;    #$self->{timeout}*2;
    if ($self->{noretry}) { return; }
    if ($self->{timeout} > 5) {    #giveup, really
        $self->{timeout} = $initialtimeout + (0.5 * rand());
        my $rsp = {};
        $rsp->{error} = "timeout";
	$self->{incommand} = 0;
        $self->{ipmicallback}->($rsp, $self->{ipmicallback_args});
        $self->{nowait} = 0;
        return;
    } elsif ($self->{sessionestablishmentcontext} == STATE_FAILED) {
        return;
    }
    if ($self->{sessionestablishmentcontext} == STATE_OPENSESSION) { #in this particular case, we want to craft a new rmcp session request with a new client side session id, to aid in distinguishing retry from new
        $self->open_rmcpplus_request();

        #experimintation has showed rakp1 and 3 are best done with a straightforward retry, not something fancy...
        #stale rakp3 in a ipmi2 implementation that can't handle it will be detected through rmcp status code rather than assuming we must start over.
    } elsif ($self->{sessionestablishmentcontext} == STATE_EXPECTINGRAKP2) { #in this particular case, we want to craft a new rmcp session request with a new client side session id, to aid in distinguishing retry from new
        $self->relog();
    } elsif ($self->{sessionestablishmentcontext} == STATE_EXPECTINGRAKP4) { #in this particular case, we want to craft a new rmcp session request with a new client side session id, to aid in distinguishing retry from new
        $self->relog();
    } else {
        $self->{hasretried} = 1;    #remember that we have retried at the moment
        $self->sendpayload(%{ $self->{pendingargs} }, nowait => 1); #do not induce the xmit to wait for packets, just spit it out.  timedout is in a wait-for-packets loop already, so it's fine
    }
    $self->{nowait} = 0;
}

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_in6($sockaddr);
    #$host = inet_ntoa($host);
    if ($doipv6) {
        ($host, $port) = Socket6::getnameinfo($sockaddr, Socket6::NI_NUMERICHOST());
    } else {
        ($port, $host) = sockaddr_in($sockaddr);
        $host = inet_ntoa($host);
    }
    if ($host =~ /::ffff:\d+\.\d+\.+\d+\.\d+/) {
        $host =~ s/::ffff://;
    }
    if ($bmc_handlers{$host}) {
        $pendingpackets -= 1;
        $bmc_handlers{$host}->handle_ipmi_packet(@rsp);
    }
}

sub handle_ipmi_packet {

    #return zero if we like the response
    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 5;                      #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
                }
            }
        }
        return $self->parse_ipmi_payload(@payload);
    } elsif ($rsp[4] == 6) {    #IPMI 2.0
        if (($rsp[5] & 0b00111111) == 0x11) {
            return $self->got_rmcp_response(splice @rsp, 16); #the function always leaves ourselves waiting, no need to deregister
        } elsif (($rsp[5] & 0b00111111) == 0x13) {
            return $self->got_rakp2(splice @rsp, 16);         #same as above
        } elsif (($rsp[5] & 0b00111111) == 0x15) {
            return $self->got_rakp4(splice @rsp, 16);         #same as above
        } elsif (($rsp[5] & 0b00111111) == 0x0) { #ipmi payload, sophisticated logic to follow
            my $encrypted;
            if ($rsp[5] & 0b10000000) {
                $encrypted = 1;
            }

#------------------------modified to support openbmc ipmi command----------------
            unless ($rsp[5] & 0b01000000) {
                if ($self->{max_privilege} != 0) {
                    return 3; #we refuse to examine unauthenticated packets in this context
                }
            }

            splice(@rsp, 0, 4);    #ditch the rmcp header
            my @authcode = splice(@rsp, -12); #strip away authcode and remember it
            my @expectedcode = unpack("C*", hmac_sha1(pack("C*", @rsp), $self->{k1}));
            splice(@expectedcode, 12);
            foreach (@expectedcode) {
                unless ($_ == shift @authcode) {
                    return 3;    #authcode bad, pretend it never existed
                }
            }
            my $thissid = unpack("N", pack("C*", $rsp[2], $rsp[3], $rsp[4], $rsp[5]));
            unless ($thissid == $self->{localsid}) {
                return 1; #this response does not match our current session id, ignore it
            }
            my $remsequencenumber = $rsp[6] + $rsp[7] >> 8 + $rsp[8] >> 16 + $rsp[9] >> 24;
            if ($self->{remotesequencenumber} and $remsequencenumber < $self->{remotesequencenumber}) {
                return 5;    #ignore malformed sequence number
            }
            $self->{remotesequencenumber} = $remsequencenumber;
            my $psize = $rsp[10] + ($rsp[11] << 8);
            my @payload = splice(@rsp, 12, $psize);
            if ($encrypted) {
                my $iv = pack("C*", splice @payload, 0, 16);
                my $cipher = Crypt::CBC->new(-literal_key => 1, -key => $self->{aeskey}, -cipher => "Crypt::Rijndael", -header => "none", -iv => $iv, -keysize => 16, -blocksize => 16, -padding => \&cbc_pad);
                my $crypted = pack("C*", @payload);
                @payload = unpack("C*", $cipher->decrypt($crypted));
            }
            return $self->parse_ipmi_payload(@payload);
        } else {
            return 6;    #unsupported payload
        }
    } else {
        return 7;        #unsupported ASF traffic
    }
}

sub cbc_pad {
    my $block = shift;
    my $size  = shift;
    my $mode  = shift;
    if ($mode eq 'e') {
        my $neededpad = $size - length($block) % $size;
        $neededpad -= 1;
        my @pad = unpack("C*", $block);
        foreach (1 .. $neededpad) {
            push @pad, $_;
        }
        push @pad, $neededpad;
        return pack("C*", @pad);
    } elsif ($mode eq 'd') {
        my @block = unpack("C*", $block);
        my $count = pop @block;
        unless ($count) {
            return pack("C*", @block);
        }
        splice @block, 0 - $count;
        return pack("C*", @block);
    }
}

sub got_rmcp_response {
    my $self = shift;
    my @data = @_;
    my $byte = shift @data;
    unless ($self->{sessionestablishmentcontext} and $self->{sessionestablishmentcontext} != STATE_ESTABLISHED) {

        #we would ignore an RMCP+ open session response if we are not in an IPMI2 negotiation, so we have to have *some* state that isn't established for this to be kosher
        return 9;    #now's not the time for this response, ignore it
    }
    unless ($byte == $self->{rmcptag}) { #make sure this rmcp response is specifically the last one we sent.... we don't want to happily proceed with the risk a retry request blew up our temp session id without letting us know
        return 9;
    }
    $byte = shift @data;
    unless ($byte == 0x00) {
        if ($rmcp_codes{$byte}) {
            $self->{onlogon}->("ERROR: " . $rmcp_codes{$byte}, $self->{onlogon_args}); #TODO: errors
        } else {
            $self->{onlogon}->("ERROR: $byte code on opening RMCP+ session", $self->{onlogon_args}); #TODO: errors
        }
        return 9;
    }
    $byte = shift @data;

    # add $byte == 0 to support openbmc ipmi command
    unless ($byte >= 4 or $byte == 0) {
        $self->{onlogon}->("ERROR: Cannot acquire sufficient privilege", $self->{onlogon_args});
        return 9;
    }
    $self->{max_privilege} = $byte if ($byte == 0);
    splice @data, 0, 5;
    $self->{pendingsessionid} = [ splice @data, 0, 4 ];

    #TODO: if we retried, and the first answer comes back but the second answer is dropped, log in will fail as we do not know our correct session id
    #basically, we would have to retry open session requested until RAKP2 *confirmed* good
    $self->send_rakp1();
    return 0;
}

sub send_rakp3 {

    #TODO: this is the point where OPEN RMCP SESSION REQUEST should have retry stopped, not send_rakp1
    my $self = shift;
    $self->{rmcptag} += 1;
    my @payload = ($self->{rmcptag}, 0, 0, 0, @{ $self->{pendingsessionid} });
    my @user = unpack("C*", $self->{userid});
    push @payload, unpack("C*", hmac_sha1(pack("C*", @{ $self->{remoterandomnumber} }, @{ $self->{sidm} }, $self->{privlevel}, scalar @user, @user), $self->{password}));
    $self->sendpayload(payload => \@payload, type => $payload_types{'rakp3'});
}

sub send_rakp1 {
    my $self = shift;
    $self->{rmcptag} += 1;
    my @payload = ($self->{rmcptag}, 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, ($self->{privlevel}, 0, 0);    # request priv
    my @user = unpack("C*", $self->{userid});
    push @payload, scalar @user;
    push @payload, @user;
    $self->{sessionestablishmentcontext} = STATE_EXPECTINGRAKP2;
    $self->sendpayload(payload => \@payload, type => $payload_types{'rakp1'});
}

sub init {
    my $self = shift;
    $self->{confalgo}                    = undef;
    $self->{integrityalgo}               = undef;
    $self->{sessionestablishmentcontext} = 0;
    $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'} = $initialtimeout + (0.5 * rand()); #start at a quick timeout, increase on retry
    $self->{'seqlun'}  = 0;                                #the IPMB seqlun combo, increment by 4s
    $self->{rqaddr} = 0x81; #Per table '5-4' system sofware ids in the ipmi spec, we are allowed 0x81-0x8d software ids
      #A problem with ipmi is that chatty commands (rinv) can mistake stale data for new if sequence number overflows
      #for example, if 'get firmware information' command is retried, and happens to have sequence number 4,
      #64 transactions later a reply to the retry comes up, the data is passed into the callback function because of ambiguity introduced by the
      #overflowed sequence number
      #to mitigate this, we will iterate rqaddr every time the seqlun counter overflows
      #of course, this still means that rqaddr will, itself, overflow, but it mitigates things because:
      #448 instead of 64 transactions are now required before ambiguity is possible
      #A stale reply has to come in after the conversation has advanced at least 448 transactions, meaning longer delay on extraneous reply before this is a problem
      #even if a stale reply comes in at *about* the right time, it has to match an exact multiple of 448 instead of 64, which is significantly less likely.
      #I abandoned this strategy, but leaving it documented for posterity.  The issue is that some operations don't like the client changing software id
      #midstream (notably SDR reservations).
      #ipmi spec does contain some guidance in the BT section of the spec 'Using the Seq Field'.  There is guidance elsewhere about sequence numbers higher than 8 beyond current
      #but I'm not sure if thet means 'session sequence number' or 'seq', which are very very different.
      #one tangible recommendation is for the combination of command+seq+netfn to be unique, which can certainly help, so we should track netfn and command to make sure response
      #matches at least the expected command and netfn.
      #additionally, it looks like we could implement a sort of 'taboo' sequence number scheme.  When we ascertain that we are retrying, we flag the current sequence number to be
      #'taboo' in the future.  When the time comes to increment sequence number, we mark the current combination of command+netfn+seq as 'taboo'
      #should circumstances suggest that we are about to transmit a packet with a taboo combination, we bump sequence number until no longer taboo, no more than 8 bumps.
      #if we should incur 7 bumps, clear the taboo list and continue on, hoping for best (pessimistically assuming the spec means seq number or that someone could at least interpret it that way)
      #I'll implement this later...
    $self->{'logged'} = 0;
    $self->{'incommand'} = 0;
}

sub relog {
    my $self = shift;
    $self->init();
    $self->{logontries} -= 1;
    $self->get_channel_auth_cap();
}

sub got_rakp4 {
    my $self = shift;
    my @data = @_;
    my $byte = shift @data;
    unless ($self->{sessionestablishmentcontext} == STATE_EXPECTINGRAKP4) { #ignore rakp4 unless we are explicitly expecting RAKP4
        return 9;    #now's not the time for this response, ignore it
    }
    unless ($byte == $self->{rmcptag}) { #make sure this rmcp response is specifically the last one we sent.... we don't want to happily proceed with the risk a retry request blew up our temp session id without letting us know
        return 9;
    }
    $byte = shift @data;
    unless ($byte == 0x00) {
        if (($byte == 0x02) and $self->{logontries}) {

            #ok, turns out an IPMI2 device may optimistically assume that since it has transmitted RAKP4, it's done with this whole RAKP exchange, thus
            #code 2 can happen....  To workaround this, code 2 is taken as a cue to start over if we haven't got an rakp2 yet
            $self->relog();
        }
        if (($byte == 0x02 or $byte == 15) and $self->{logontries}) { # most likely scenario is that a retry earlier in the process invalided the flow this packet came in on, ignore it and hope the retries all sort out
             #UPDATE: turns out open rmcp session request shenanigans were to blame, rakp2 straight retransmits seems safe
             #the biggest risk: that we did not receive the correct rakp2, so the prudent thing to be doing in this time interval would be retrying RAKP1...
             #ipmi2 session negotiation is a bit weird in how retries can corrupt state and we effectively should be rewinding a bit...
             #TODO: think about retry logic hard to decide how many packets we can retry
             #thought: can we match a failed RAKP2 to the last RAKP1 we transmitted?  If we can, and we see the last RAKP1 was in fact the one this response is for, that
             #would definitely mean we should rewinnd to open session rquest..
             #ditto for rakp4, if we can confirm rakp is for the last transmitted rakp3, then we need to rewind to send_rakp1...
             #$self->relog();
            return;
        }
        if ($rmcp_codes{$byte}) {
            $self->{onlogon}->("ERROR: " . $rmcp_codes{$byte}, $self->{onlogon_args}); #TODO: errors
        } else {
            $self->{onlogon}->("ERROR: $byte code on opening RMCP+ session", $self->{onlogon_args}); #TODO: errors
        }
        return 9;
    }
    splice @data, 0, 6;    #discard reserved bytes and session id
    my @expectauthcode = unpack("C*", hmac_sha1(pack("C*", @{ $self->{randomnumber} }, @{ $self->{pendingsessionid} }, @{ $self->{remoteguid} }), $self->{sik}));
    foreach (@expectauthcode[ 0 .. 11 ]) {
        unless ($_ == (shift @data)) {

            #we'll just ignore this transgression...... *this time*
            #$self->{onlogon}->("ERROR: failure in final rakp exchange message",$self->{onlogon_args});
            return 9;
        }
    }
    $self->{sessionid}           = $self->{pendingsessionid};
    $self->{integrityalgo}       = 'sha1';
    $self->{confalgo}            = 'aes';
    $self->{sequencenumber}      = 1;
    $self->{sequencenumberbytes} = [ 1, 0, 0, 0 ];
    $self->{sessionestablishmentcontext} = STATE_ESTABLISHED; #will move on to relying upon session sequence number
    $self->set_admin_level();
    return 0;
}


sub got_rakp2 {
    my $self = shift;
    my @data = @_;
    my $byte = shift @data;
    unless ($self->{sessionestablishmentcontext} >= STATE_EXPECTINGRAKP2 and $self->{sessionestablishmentcontext} != STATE_ESTABLISHED) {

        #we will bail out unless the state is either EXPECTINGRAKP2 or EXPECTINGRAKP4.
        #the reason being that if an old rakp1 retry actually made it and we were just too aggressive, then a previous rakp2 is invalidated and invalid session id or the integrity check value is bad
        return 9;    #now's not the time for this response, ignore it
    }
    unless ($byte == $self->{rmcptag}) { #make sure this rmcp response is specifically the last one we sent.... we don't want to happily proceed with the risk a retry request blew up our temp session id without letting us know
        return 9;
    }
    $byte = shift @data;
    unless ($byte == 0x00) {
        if (($byte == 0x9 or $byte == 0xd) and $self->{privlevel} == 4) {

            # this is probably an environment that wants to give us only operator
            # try to connect again at 3.
            $self->{privlevel} = 3;
            $self->relog();
            return;
        }
        if ($byte == 0x02) { #invalid session id is almost certainly because a retry on rmcp+ open session response rendered our session id invalid, ignore this in the hope that we'll get an answer for our retry that invalidated us..
                             #$self->relog();
             #TODO: probably should disable RAKP1 retry here...  high likelihood that we'll just spew a bad RAKP1 and Open Session Request retry would be more appropriate to try to discern a valid session id
            return;
        }
        if ($rmcp_codes{$byte}) {
            $self->{sessionestablishmentcontext} = STATE_FAILED;
            $self->{onlogon}->("ERROR: " . $rmcp_codes{$byte}, $self->{onlogon_args}); #TODO: errors
        } else {
            $self->{sessionestablishmentcontext} = STATE_FAILED;
            $self->{onlogon}->("ERROR: $byte code on opening RMCP+ session", $self->{onlogon_args}); #TODO: errors
        }
        return 9;
    }
    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*", (@{ $self->{sidm} }, @{ $self->{pendingsessionid} }, @{ $self->{randomnumber} }, @{ $self->{remoterandomnumber} }, @{ $self->{remoteguid} }, $self->{privlevel}, $ulength, @user));
    my @expectedhash = (unpack("C*", hmac_sha1($hmacdata, $self->{password})));
    foreach (0 .. (scalar(@expectedhash) - 1)) {
        if ($expectedhash[$_] != $data[$_]) {
            $self->{sessionestablishmentcontext} = STATE_FAILED;
            $self->{onlogon}->("ERROR: Incorrect password provided", $self->{onlogon_args});
            return 9;
        }
    }
    $self->{sik} = hmac_sha1(pack("C*", @{ $self->{randomnumber} }, @{ $self->{remoterandomnumber} }, $self->{privlevel}, $ulength, @user), $self->{password});
    $self->{k1} = hmac_sha1(pack("C*", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), $self->{sik});
    $self->{k2} = hmac_sha1(pack("C*", 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2), $self->{sik});
    my @aeskey = unpack("C*", $self->{k2});
    $self->{aeskey} = pack("C*", (splice @aeskey, 0, 16));
    $self->{sessionestablishmentcontext} = STATE_EXPECTINGRAKP4;
    $self->send_rakp3();
    return 0;
}

sub parse_ipmi_payload {
    my $self    = shift;
    my @payload = @_;

    #for now, just trash the headers, this has been validated to death anyway
    #except seqlun, that one is important
    unless ($payload[4] == $self->{seqlun} and $payload[1] >> 2 == $self->{expectednetfn} and $payload[5] == $self->{expectedcmd}) {

        #both sequence number and arqaddr must match, because we are using rqaddr to extend the sequence number
        #print "Successfully didn't get confused by stale response ".$payload[4]." and ".($self->{seqlun}-4)."\n";
        #hexdump(@payload);
        return 1;    #response mismatch
    }
    if ($self->{hasretried}) { #if we sent this out multiple times, mark the sequence as taboo
        $self->{hasretried} = 0;
        $tabooseq{ $self->{expectednetfn} }->{ $self->{expectedcmd} }->{ $self->{seqlun} } = 16; #consider a lun taboo for 16 overflow cycles
    }

    #set to impossible values to reflect the fact we expect *no* command/nnetfn at the moment
    $self->{expectednetfn} = 0x1ff;
    $self->{expectedcmd}   = 0x1ff;
    $self->{seqlun} += 4;    #increment by 1<<2
    if ($self->{seqlun} > 0xff) {    #overflow case
         #Problem with rqaddr iteration strategy to get more sequence numbers, changing swid invalidates reservation ids for some BMCs...
         #   if ($self->{rqaddr} == 0x8d) { #rqaddr itself is forced to overflow
         #	$self->{rqaddr}=0x81;
         #   } else {
         #	$self->{rqaddr}+=2; #table 5-4 demands rqaddr be odd for software ids, so we must increment by 2
         #}
        $self->{seqlun} &= 0xff;    #keep it one byte
    }
    delete $sessions_waiting{$self}; #deregister self as satisfied, callback will reregister if appropriate
    delete $self->{pendingargs};
    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->{timeout} = $initialtimeout + (0.5 * rand());
    $self->{incommand} = 0;
    $self->{ipmicallback}->($rsp, $self->{ipmicallback_args});
    return 0;
}

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;
    my @payload = @{ $args{payload} };
    $self->{pendingargs} = \%args;

    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
        push @msg, $args{type};
        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:
        my $size = scalar(@payload);
        if ($self->{confalgo}) {
            my $pad = ($size + 1) % 16;
            if ($pad) { $pad = 16 - $pad; }
            my $newsize = $size + $pad + 17;
            push @msg, ($newsize & 0xff, $newsize >> 8);
            my @iv;
            foreach (1 .. 16) {    #generate a new iv for outbound packet
                my $num = int(rand(255));
                push @msg, $num;
                push @iv,  $num;
            }
            my $cipher = Crypt::CBC->new(-literal_key => 1, -key => $self->{aeskey}, -cipher => "Rijndael", -header => "none", -iv => pack("C*", @iv), -keysize => 16, -padding => \&cbc_pad);
            push @msg, (unpack("C*", $cipher->encrypt(pack("C*", @payload))));
        } else {
            push @msg, ($size & 0xff, $size >> 8);
            push @msg, @payload;
        }
        if ($self->{integrityalgo}) {
            my @integdata = @msg[ 4 .. (scalar @msg) - 1 ];
            my $neededpad = ((scalar @integdata) + 2) % 4;
            if ($neededpad) { $neededpad = 4 - $neededpad; }
            for (my $i = 0 ; $i < $neededpad ; $i++) {
                push @integdata, 0xff;
                push @msg,       0xff;
            }
            push @msg,       $neededpad;
            push @integdata, $neededpad;
            push @msg,       7;
            push @integdata, 7;
            my $intdata = pack("C*", @integdata);
            my @acode = unpack("C*", hmac_sha1($intdata, $self->{k1}));
            push @msg, splice @acode, 0, 12;

            #push integrity pad
            #push @msg,0x7; #reserved byte in 2.0
            #push integrity data
        }
    }
    unless ($args{nowait} or $self->{nowait}) { #if nowait indicated, the packet will be sent regardless of maxpending
         #primary use case would be retries that should represent no delta to pending sessions in aggregate and therefore couldn't exceed maxpending anywy
         #if we did do this on timedout, waitforrsp may recurse, which is a complicated issue.  Theoretically, if waitforrsp protected itself, it
         #would act the same, but best be explicit about nowait if practice does not match theory
         #another scenario is if we have urgent payload for a BMC (PET acknowledge, negotiating login if temp session id is very short lived
        $self->waitforrsp(timeout => 0); #the intent here is to interrupt outgoing activity to give a chance to respond to incoming data
                                         #until we send, the ball is in our court so things are less time critical
        while ($pendingpackets > $maxpending) { #if we hit our ceiling, wait until a slot frees up, which can't happen until either a packet is received or someone gives up
            $self->waitforrsp();
        }
    }
    $socket->send(pack("C*", @msg), 0, $self->{peeraddr});
    $sessions_waiting{$self} = {};
    $sessions_waiting{$self}->{ipmisession} = $self;
    if ($args{delayxmit}) {
        $sessions_waiting{$self}->{timeout} = time() + $args{delayxmit};
        $self->{timeout} = ($initialtimeout + (0.5 * rand())) / 2; #since we are burning one of the retry attempts, start the backoff algorithm faster to make it come out even
        undef $args{delayxmit};
        return; #don't actually transmit packet, use retry timer to start us off
    } else {
        $sessions_waiting{$self}->{timeout} = time() + $self->{timeout};
    }
    $pendingpackets += 1;
    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;
