# # iscsi pdu module # # limited to LOGIN, TEXT and LOGOUT commands. Authentication is not supported # # 2009, Steffen Plotner # package iscsi; use strict; use POSIX; require Exporter; use common; use config; use Data::Dumper; use IO::Socket; our @ISA = qw(Exporter); our @EXPORT = qw( %iscsi_def iscsi_net_connect iscsi_net_send iscsi_net_recv iscsi_net_disconnect iscsi_pdu_initiator_sn iscsi_pdu_target_sn iscsi_pdu_login iscsi_pdu_login_response iscsi_pdu_text_command iscsi_pdu_text_response iscsi_pdu_logout_request iscsi_pdu_logout_response ); our %iscsi_def = ( 'CMD_SEND' => 'net_send', 'CMD_RECV' => 'net_recv', 'OPCODE_IMMEDIATE_DELIVERY' => 0x40, 'OPCODE_LOGIN' => 0x03, 'OPCODE_LOGIN_RESPONSE' => 0x23, 'OPCODE_TEXT_COMMAND' => 0x04, 'OPCODE_TEXT_RESPONSE' => 0x24, 'OPCODE_LOGOUT_REQUEST' => 0x06, 'OPCODE_LOGOUT_RESPONSE' => 0x26, # # OPCODE flags # 'ISCSI_TRANSIT_ON' => 0x80, # initiator is ready to transit to next stage 'ISCSI_TRANSIT_OFF' => 0x00, 'ISCSI_CONTINUE_ON' => 0x40, # more key=value pairs to come, T must be OFF 'ISCSI_CONTINUE_OFF' => 0x00, # no more key=values pairs, T must be ON # 'ISCSI_CSG_SECURITY_NEGOTIATION' => 0x00, 'ISCSI_CSG_LOGINOPERATIONAL_NEGOTIATION' => 0x04, 'ISCSI_CSG_FULLFEATUREPHASE' => 0x0C, 'ISCSI_CSG_MASK' => 0x0C, # 'ISCSI_NSG_SECURITY_NEGOTIATION' => 0x00, 'ISCSI_NSG_LOGINOPERATIONAL_NEGOTIATION' => 0x01, 'ISCSI_NSG_FULLFEATUREPHASE' => 0x03, 'ISCSI_NSG_MASK' => 0x03, 'ISID_TYPE_IEEE_OUI' => 0x00, 'ISID_TYPE_IANA' => 0x01, 'ISID_TYPE_RANDOM' => 0x02, ); my @template_pdu_login = ( { 'field' => 'opcode', 'packt' => 'C', 'length' => 1 }, { 'field' => 'opcode_flag', 'packt' => 'C', 'length' => 1 }, { 'field' => 'version_max', 'packt' => 'C', 'length' => 1 }, { 'field' => 'version_min', 'packt' => 'C', 'length' => 1 }, { 'field' => 'total_ahs_length', 'packt' => 'C', 'length' => 1 }, { 'field' => 'data_segment_length', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'isid_authority_type', 'packt' => 'C', 'length' => 1 }, { 'field' => 'isid_authority', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'isid_authority_qualifier','packt' => 'n', 'length' => 2 }, { 'field' => 'tsih', 'packt' => 'n', 'length' => 2 }, { 'field' => 'initiator_task_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding0', 'packt' => 'n', 'length' => 2 }, { 'field' => 'cid', 'packt' => 'n', 'length' => 2 }, { 'field' => 'cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'exp_stat_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding3', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding4', 'packt' => 'N', 'length' => 4 }, ); my @template_pdu_login_response = ( { 'field' => 'opcode', 'packt' => 'C', 'length' => 1 }, { 'field' => 'opcode_flag', 'packt' => 'C', 'length' => 1 }, { 'field' => 'version_max', 'packt' => 'C', 'length' => 1 }, { 'field' => 'version_active', 'packt' => 'C', 'length' => 1 }, { 'field' => 'total_ahs_length', 'packt' => 'C', 'length' => 1 }, { 'field' => 'data_segment_length', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'isid_authority_type', 'packt' => 'C', 'length' => 1 }, { 'field' => 'isid_authority', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'isid_authority_qualifier','packt' => 'n', 'length' => 2 }, { 'field' => 'tsih', 'packt' => 'n', 'length' => 2 }, { 'field' => 'initiator_task_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding0', 'packt' => 'N', 'length' => 4 }, { 'field' => 'stat_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'exp_cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'max_cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'status', 'packt' => 'n', 'length' => 2 }, { 'field' => 'padding1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding3', 'packt' => 'n', 'length' => 2 }, ); my @template_pdu_text_command = ( { 'field' => 'opcode', 'packt' => 'C', 'length' => 1 }, { 'field' => 'opcode_flag', 'packt' => 'C', 'length' => 1 }, { 'field' => 'padding0', 'packt' => 'n', 'length' => 2 }, { 'field' => 'total_ahs_length', 'packt' => 'C', 'length' => 1 }, { 'field' => 'data_segment_length', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'lun_part1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'lun_part2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'initiator_task_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'target_transfer_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'exp_stat_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding3', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding4', 'packt' => 'N', 'length' => 4 }, ); my @template_pdu_text_response = ( { 'field' => 'opcode', 'packt' => 'C', 'length' => 1 }, { 'field' => 'opcode_flag', 'packt' => 'C', 'length' => 1 }, { 'field' => 'padding0', 'packt' => 'n', 'length' => 2 }, { 'field' => 'total_ahs_length', 'packt' => 'C', 'length' => 1 }, { 'field' => 'data_segment_length', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'lun_part1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'lun_part2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'initiator_task_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'target_transfer_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'stat_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'exp_cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'max_cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding3', 'packt' => 'N', 'length' => 4 }, ); my @template_pdu_logout_request = ( { 'field' => 'opcode', 'packt' => 'C', 'length' => 1 }, { 'field' => 'opcode_flag', 'packt' => 'C', 'length' => 1 }, { 'field' => 'padding0', 'packt' => 'n', 'length' => 2 }, { 'field' => 'total_ahs_length', 'packt' => 'C', 'length' => 1 }, { 'field' => 'data_segment_length', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'padding1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'initiator_task_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'cid', 'packt' => 'n', 'length' => 2 }, { 'field' => 'padding3', 'packt' => 'n', 'length' => 2 }, { 'field' => 'cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'exp_cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding4', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding5', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding6', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding7', 'packt' => 'N', 'length' => 4 }, ); my @template_pdu_logout_response = ( { 'field' => 'opcode', 'packt' => 'C', 'length' => 1 }, { 'field' => 'opcode_flag', 'packt' => 'C', 'length' => 1 }, { 'field' => 'response', 'packt' => 'C', 'length' => 1 }, { 'field' => 'padding0', 'packt' => 'C', 'length' => 1 }, { 'field' => 'total_ahs_length', 'packt' => 'C', 'length' => 1 }, { 'field' => 'data_segment_length', 'packf' => '3byte', 'length' => 3 }, { 'field' => 'padding1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding2', 'packt' => 'N', 'length' => 4 }, { 'field' => 'initiator_task_tag', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding3', 'packt' => 'N', 'length' => 4 }, { 'field' => 'stat_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'exp_cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'max_cmd_sn', 'packt' => 'N', 'length' => 4 }, { 'field' => 'padding1', 'packt' => 'N', 'length' => 4 }, { 'field' => 'time_2_wait', 'packt' => 'n', 'length' => 2 }, { 'field' => 'time_2_retain', 'packt' => 'n', 'length' => 2 }, { 'field' => 'padding3', 'packt' => 'N', 'length' => 4 }, ); #------------------------------------------------------------------------------ # iscsi net connect #------------------------------------------------------------------------------ sub iscsi_net_connect { my ($target_ip, $target_port) = @_; logger("iscsi> target_ip: $target_ip port: $target_port"); # # connect to target # my $socket = IO::Socket::INET->new( PeerAddr => $target_ip, PeerPort => $target_port, Proto => 'tcp', Type => SOCK_STREAM); return $socket; } #------------------------------------------------------------------------------ # iscsi net disconnect #------------------------------------------------------------------------------ sub iscsi_net_disconnect { my ($socket) = @_; close($socket); } #------------------------------------------------------------------------------ # iscsi net send #------------------------------------------------------------------------------ sub iscsi_net_send { my ($socket, $packet) = @_; my $send = length($$packet); my $sent = $socket->send($$packet); logger("iscsi> net bytes send: $send sent: $sent"); if ( $send != $sent ) { logger_err("iscsi_err> unexpected number of bytes sent: $sent, should match packet: $send"); return undef; } return $sent; } #------------------------------------------------------------------------------ # iscsi net recv (no support for AHS (additional header sections) #------------------------------------------------------------------------------ sub iscsi_net_recv { my ($socket) = @_; # # basic header segment support # my $bhs_size = 48; my $packet_bhs; logger("iscsi> receiving BHS ($bhs_size bytes)..."); $socket->recv($packet_bhs, $bhs_size); my $packet_bhs_size = length($packet_bhs); if ( $packet_bhs_size != $bhs_size ) { logger_err("iscsi_err> expected $bhs_size bytes, got $packet_bhs_size"); return undef; } # # fetch the data segment length part (this is a hack, since we are not # parsing here based on a template) # my $data_segment_length_raw = substr($packet_bhs, 5,3); my $data_segment_length = unpack_3byte($data_segment_length_raw); logger("iscsi> data segment length: $data_segment_length"); my $packet_data; if ( $data_segment_length > 0 ) { # # padding required at 4 byte # my $pad_bytes = 4 - ($data_segment_length % 4); if ( $pad_bytes < 4 ) { $data_segment_length += $pad_bytes; } logger("iscsi> data segment length: $data_segment_length (padded)"); $socket->recv($packet_data, $data_segment_length); my $packet_data_size = length($packet_data); if ( $packet_data_size != $data_segment_length ) { logger_err("iscsi_err> expected $data_segment_length bytes, got $packet_data_size"); return undef; } } my $packet = $packet_bhs.$packet_data; my $received = length($packet); logger("iscsi> net bytes received: $received"); hex_dump($packet) if $cfg{'debug'}; return \$packet; } #------------------------------------------------------------------------------ # iscsi pdu initiator increment cmd sn #------------------------------------------------------------------------------ sub iscsi_pdu_initiator_sn { my ($pdu) = @_; logger("iscsi> sn _T stat_sn: $$pdu{'stat_sn'} exp_cmd_sn: $$pdu{'exp_cmd_sn'} max_cmd_sn: $$pdu{'max_cmd_sn'}"); logger("iscsi> sn opcode immmediate: $$pdu{'opcode_immediate'}"); # # inc on non-immediate opcodes # if ( $$pdu{'opcode_immediate'} == 0 ) { $$pdu{'cmd_sn'}++; $$pdu{'exp_stat_sn'} = $$pdu{'cmd_sn'}+1; } logger("iscsi> sn I_ cmd_sn: $$pdu{'cmd_sn'} exp_stat_sn: $$pdu{'exp_stat_sn'}"); } #------------------------------------------------------------------------------ # iscsi pdu target increment cmd sn #------------------------------------------------------------------------------ sub iscsi_pdu_target_sn { my ($pdu) = @_; logger("iscsi> sn I_ cmd_sn: $$pdu{'cmd_sn'} exp_stat_sn: $$pdu{'exp_stat_sn'}"); logger("iscsi> sn opcode immmediate: $$pdu{'opcode_immediate'}"); # # initialize the variables if they are not set # if ( !defined($$pdu{'stat_sn'}) ) { $$pdu{'stat_sn'} = 0; $$pdu{'exp_cmd_sn'} = $$pdu{'cmd_sn'}; $$pdu{'max_cmd_sn'} = $$pdu{'exp_cmd_sn'} + 1; } # # inc on non-immediate opcodes # $$pdu{'opcode_immediate'} = ($$pdu{'opcode'} & $iscsi_def{'OPCODE_IMMEDIATE_DELIVERY'}); if ( $$pdu{'opcode_immediate'} == 0 ) { $$pdu{'stat_sn'} = $$pdu{'exp_stat_sn'}; $$pdu{'exp_cmd_sn'} = $$pdu{'cmd_sn'} + 1; $$pdu{'max_cmd_sn'} = $$pdu{'exp_cmd_sn'} + 1; } logger("iscsi> sn _T stat_sn: $$pdu{'stat_sn'} exp_cmd_sn: $$pdu{'exp_cmd_sn'} max_cmd_sn: $$pdu{'max_cmd_sn'}"); } #------------------------------------------------------------------------------ # iscsi login (send/receive) #------------------------------------------------------------------------------ sub iscsi_pdu_login { my ($pdu, $packet) = @_; my $displayname = "PDU_LOGIN"; my $is_sending = $$pdu{'cmd'} eq $iscsi_def{'CMD_SEND'}; logger("iscsi> $displayname cmd: $$pdu{'cmd'} is_sending: $is_sending"); if ( $is_sending ) { $$pdu{'opcode'} |= $iscsi_def{'OPCODE_IMMEDIATE_DELIVERY'}; $$pdu{'version_max'} = 0x00 if !defined($$pdu{'version_max'}); $$pdu{'version_min'} = 0x00 if !defined($$pdu{'version_min'}); $$pdu{'total_ahs_length'} = 0x00 if !defined($$pdu{'total_ahs_length'}); $$pdu{'isid_authority_type'} = $iscsi_def{'ISID_TYPE_RANDOM'}; $$pdu{'isid_authority'} = 0xaffffe; $$pdu{'isid_authority_qualifier'} = (int(rand(255)) << 8) | (int(rand(255))); $$pdu{'cmd_sn'} = 0x0; $$pdu{'exp_stat_sn'} = 0x0; } iscsi_pdu_packet($pdu, \@template_pdu_login, $displayname, $packet); } #------------------------------------------------------------------------------ # iscsi login response (send/receive) #------------------------------------------------------------------------------ sub iscsi_pdu_login_response { my ($pdu, $packet) = @_; my $displayname = "PDU_LOGIN_RESPONSE"; my $is_sending = $$pdu{'cmd'} eq $iscsi_def{'CMD_SEND'}; logger("iscsi> pdu $displayname cmd: $$pdu{'cmd'} is_sending: $is_sending"); if ( $is_sending ) { # # target session identifier handle (for target to identify a session # with an initiator). # $$pdu{'tsih'} = 0x0001; } iscsi_pdu_packet($pdu, \@template_pdu_login_response, $displayname, $packet); } #------------------------------------------------------------------------------ # iscsi text command (send/receive) #------------------------------------------------------------------------------ sub iscsi_pdu_text_command { my ($pdu, $packet) = @_; my $displayname = "PDU_TEXT_COMMAND"; my $is_sending = $$pdu{'cmd'} eq $iscsi_def{'CMD_SEND'}; logger("iscsi> pdu $displayname cmd: $$pdu{'cmd'} is_sending: $is_sending"); if ( $is_sending ) { $$pdu{'target_transfer_tag'} = 0xffffffff if !defined($$pdu{'target_transfer_tag'}); } iscsi_pdu_packet($pdu, \@template_pdu_text_command, $displayname, $packet); } #------------------------------------------------------------------------------ # iscsi text response (send/receive) #------------------------------------------------------------------------------ sub iscsi_pdu_text_response { my ($pdu, $packet) = @_; my $displayname = "PDU_TEXT_RESPONSE"; my $is_sending = $$pdu{'cmd'} eq $iscsi_def{'CMD_SEND'}; logger("iscsi> pdu $displayname cmd: $$pdu{'cmd'} is_sending: $is_sending"); iscsi_pdu_packet($pdu, \@template_pdu_text_response, $displayname, $packet); } #------------------------------------------------------------------------------ # iscsi logout request (send/receive) #------------------------------------------------------------------------------ sub iscsi_pdu_logout_request { my ($pdu, $packet) = @_; my $displayname = "PDU_LOGOUT_REQUEST"; my $is_sending = $$pdu{'cmd'} eq $iscsi_def{'CMD_SEND'}; logger("iscsi> pdu $displayname cmd: $$pdu{'cmd'} is_sending: $is_sending"); iscsi_pdu_packet($pdu, \@template_pdu_logout_request, $displayname, $packet); } #------------------------------------------------------------------------------ # iscsi logout response (send/receive) #------------------------------------------------------------------------------ sub iscsi_pdu_logout_response { my ($pdu, $packet) = @_; my $displayname = "PDU_LOGOUT_RESPONSE"; my $is_sending = $$pdu{'cmd'} eq $iscsi_def{'CMD_SEND'}; logger("iscsi> pdu $displayname cmd: $$pdu{'cmd'} is_sending: $is_sending"); if ( $is_sending ) { $$pdu{'response'} = 0 if !defined($$pdu{'response'}); } iscsi_pdu_packet($pdu, \@template_pdu_logout_response, $displayname, $packet); } #------------------------------------------------------------------------------ # prepare a pdu packet based on a template or decode a packet based on a template #------------------------------------------------------------------------------ sub iscsi_pdu_packet { my ($pdu, $template, $displayname, $packet) = @_; my $is_sending = $$pdu{'cmd'} eq $iscsi_def{'CMD_SEND'}; if ( $is_sending ) { $$packet = undef; } if ($cfg{'debug'} && $is_sending) { print Dumper $pdu; } # # prepare data segment # my $data_bin; if ( $is_sending ) { foreach my $keyval (@{$$pdu{'keyvals'}}) { $data_bin .= pack('a*x',$keyval); } $$pdu{'data_segment_length'} = length($data_bin); } # # pdu segment # my $parser_length; foreach my $row (@{$template}) { my $field = $$row{'field'}; my $packt = $$row{'packt'}; my $packf = $$row{'packf'}; my $length = $$row{'length'}; if ( $is_sending) { # # pack # my $value = $$pdu{$field}; if ( $field =~ /^padding\d+$/ ) { $value = undef; } if ( defined($packt) ) { $$packet .= pack($packt, $value); } elsif ( $packf eq '3byte' ) { $$packet .= pack_3byte($value); } } else { # # unpack # if ( defined($packt) ) { $$pdu{$field} = unpack_shift($packet, $packt, $length); } elsif ( $packf eq '3byte' ) { $$pdu{$field} = unpack_3byte_shift($packet); } } $parser_length += $length; } logger("iscsi> packet length: ".length($$packet)." parser length: $parser_length"); # # append keyvals segment/parse the keyvals segment # if ( $is_sending ) { $$packet .= $data_bin; } else { my @keyvals = split("\0", $$packet); $$pdu{'keyvals'} = \@keyvals; } # # pad outgoing packet # if ( $is_sending ) { pad_4byte_boundary($packet); } logger("iscsi> packet length: ".length($$packet)." actual with padding and keyvals"); # # save the immediate opcode flag # $$pdu{'opcode_immediate'} = ($$pdu{'opcode'} & $iscsi_def{'OPCODE_IMMEDIATE_DELIVERY'}); if ( !$is_sending ) { # # strip the opcode flag with immediate delivery # $$pdu{'opcode'} &= (0x40 ^ 0xFF); } if ($cfg{'debug'} && $is_sending) { logger("iscsi> packet: $displayname length: ".length($$packet)); hex_dump($$packet); } if ($cfg{'debug'} && !$is_sending) { print Dumper $pdu; } return \$packet; } #------------------------------------------------------------------------------ # unpack a pdu binary data packet and 'shift' what you unpacked #------------------------------------------------------------------------------ sub unpack_shift { my ($data, $template, $length) = @_; my $val = unpack($template, $$data); $$data = substr($$data, $length); return $val; } #------------------------------------------------------------------------------ # pack a null terminated data string #------------------------------------------------------------------------------ sub pack_stringz { my ($data) = @_; return pack('a*x',$data); } #------------------------------------------------------------------------------ # pack 3 byte data segment length #------------------------------------------------------------------------------ sub pack_3byte { my ($val) = @_; return pack('CCC', (($val & 0xff0000) >> 16), (($val & 0x00ff00) >> 8), ($val & 0x0000ff) ); } #------------------------------------------------------------------------------ # unpack 3 byte data segment length #------------------------------------------------------------------------------ sub unpack_3byte { my ($data) = @_; my ($byte3, $byte2, $byte1) = unpack('CCC',$data); return ($byte3 << 16 | $byte2 << 8 | $byte1); } #------------------------------------------------------------------------------ # unpack 3 byte data segment length and unshift data you unpacked #------------------------------------------------------------------------------ sub unpack_3byte_shift { my ($data) = @_; my $val = unpack_3byte($$data); $$data = substr($$data, 3); return $val; } #------------------------------------------------------------------------------ # pad on a 4 byte boundary #------------------------------------------------------------------------------ sub pad_4byte_boundary { my ($data) = @_; my $pad_bytes = 4 - (length($$data) % 4); if ( $pad_bytes < 4 ) { logger("iscis> pad $pad_bytes") if $cfg{'debug'}; $$data .= pack("x$pad_bytes"); } } 1;