📄 ssh2_msg.c
字号:
long waiting for the rehandshake to complete before sending
new data because the lack of WINDOW_ADJUSTs (in an
implementation that sends these with almost every packet, as
most do) will screw up flow control and lead to deadlock.
This problem got so bad that as of 2.4.0 the ssh.com
implementation would detect OpenSSH (the other main
implementation at the time) and disable the rehandshake when
it was talking to it, but it may not do this for other
implementations.
To avoid falling into this hole, or at least to fail
obviously when the two sides can't agree on how to handle the
layering mismatch problem, we report a rehandshake request as
an error. Trying to handle it properly results in hard-to-
diagnose errors (it depends on what the layers are doing at
the time of the problem), typically some bad-packet error
when the other side tries to interpret a connection-layer
packet as part of the rehandshake, or when the two sides
disagree on when to switch keys and it decrypts with the
wrong keys and gets a garbled packet type */
retExt( sessionInfoPtr, CRYPT_ERROR_BADDATA,
"Unexpected KEXINIT request received" );
case SSH2_MSG_CHANNEL_DATA:
case SSH2_MSG_CHANNEL_EXTENDED_DATA:
case SSH2_MSG_CHANNEL_REQUEST:
case SSH2_MSG_CHANNEL_WINDOW_ADJUST:
case SSH2_MSG_CHANNEL_EOF:
case SSH2_MSG_CHANNEL_CLOSE:
/* All channel-specific messages end up here */
channelNo = readUint32( stream );
if( cryptStatusError( channelNo ) )
/* We can't send an error response to a channel request at
this point both because we haven't got to the response-
required flag yet and because SSH doesn't provide a
mechanism for returning an error response without an
accompanying channel number. The best that we can do is
to quietly ignore the packet */
retExt( sessionInfoPtr, CRYPT_ERROR_BADDATA,
"Invalid channel-specific packet type %d",
sshInfo->packetType );
if( channelNo != getCurrentChannelNo( sessionInfoPtr, \
CHANNEL_READ ) )
{
/* It's a request on something other than the current
channel, try and select the new channel */
status = selectChannel( sessionInfoPtr, channelNo,
CHANNEL_READ );
if( cryptStatusError( status ) )
{
/* As before for error handling */
retExt( sessionInfoPtr, CRYPT_ERROR_BADDATA,
"Invalid channel number %ld in channel-specific "
"packet type %d, current channel "
"is %ld", channelNo,
sshInfo->packetType, prevChannelNo );
}
}
break;
default:
{
BYTE buffer[ 16 ];
/* We got something unexpected, throw an exception in the debug
version and let the caller know the details */
assert( NOTREACHED );
status = sread( stream, buffer, 8 );
if( cryptStatusError( status ) )
/* There's not enough data present to dump the start of the
packet, provide a more generic response */
retExt( sessionInfoPtr, CRYPT_ERROR_BADDATA,
"Unexpected control packet type %d received",
sshInfo->packetType );
retExt( sessionInfoPtr, CRYPT_ERROR_BADDATA,
"Unexpected control packet type %d received, beginning "
"%02X %02X %02X %02X %02X %02X %02X %02X",
sshInfo->packetType,
buffer[ 0 ], buffer[ 1 ], buffer[ 2 ], buffer[ 3 ],
buffer[ 4 ], buffer[ 5 ], buffer[ 6 ], buffer[ 7 ] );
}
}
/* From here on we're processing a channel-specific message that applies
to the currently selected channel */
switch( sshInfo->packetType )
{
case SSH2_MSG_CHANNEL_DATA:
case SSH2_MSG_CHANNEL_EXTENDED_DATA:
{
const int streamPos = stell( stream );
const BOOLEAN hasWindowBug = \
( sessionInfoPtr->protocolFlags & SSH_PFLAG_WINDOWBUG );
long length;
int windowCount;
/* Get the payload length and make sure that it's
(approximately) valid. More exact checking will be done
by the caller (which is why we reset the stream position
to allow it to be re-read), all that we're really interested
in here is that the length is approximately valid for window-
adjust calculation purposes */
length = readUint32( stream );
sseek( stream, streamPos );
if( length < 0 || length > sessionInfoPtr->receiveBufSize )
retExt( sessionInfoPtr, CRYPT_ERROR_BADDATA,
"Invalid data packet payload length %d, should be "
"0...%d", length, sessionInfoPtr->receiveBufSize );
/* These are messages that consume window space, adjust the data
window and communicate changes to the other side if necessary.
See the comment in sendChannelOpen() for the reason for the
window size handling */
getChannelExtAttribute( sessionInfoPtr,
SSH_ATTRIBUTE_WINDOWCOUNT,
NULL, &windowCount );
windowCount += length;
if( windowCount > MAX_WINDOW_SIZE - \
sessionInfoPtr->sendBufSize || hasWindowBug )
{
/* Send the window adjust to the remote system:
byte SSH2_MSG_CHANNEL_WINDOW_ADJUST
uint32 channel
uint32 bytes_to_add
We ignore any possible error code from the packet send
because we're supposed to be processing a read and not a
write at this point, the write is only required by SSH's
braindamaged flow-control handling */
enqueueChannelData( sessionInfoPtr,
SSH2_MSG_CHANNEL_WINDOW_ADJUST,
channelNo, hasWindowBug ? \
length : MAX_WINDOW_SIZE );
/* We've reset the window, start again from zero */
windowCount = 0;
}
setChannelExtAttribute( sessionInfoPtr,
SSH_ATTRIBUTE_WINDOWCOUNT,
NULL, windowCount );
/* If it's a standard data packet, we're done */
if( sshInfo->packetType == SSH2_MSG_CHANNEL_DATA )
return( CRYPT_OK );
/* The extended data message is used for out-of-band data sent
over a channel, specifically output sent to stderr from a
shell command. What to do with this is somewhat uncertain,
the only possible action that we could take apart from just
ignoring it is to convert it back to in-band data. However,
something running a shell command may not expect to get
anything returned in this manner (see the comment for the
port-forwarding channel open in the client-side channel-open
code for more on this), so for now we just ignore it and
assume that the user will rely on results sent as in-band
data. This should be fairly safe since this message type
seems to be rarely (if ever) used, so apps will function
without it */
return( OK_SPECIAL );
}
case SSH2_MSG_CHANNEL_REQUEST:
status = processChannelRequest( sessionInfoPtr, stream,
prevChannelNo );
if( cryptStatusError( status ) && status != OK_SPECIAL )
return( status );
return( OK_SPECIAL );
case SSH2_MSG_CHANNEL_WINDOW_ADJUST:
/* Another noop-equivalent (but a very performance-affecting
one) */
return( OK_SPECIAL );
case SSH2_MSG_CHANNEL_EOF:
/* According to the SSH docs the EOF packet is mostly a courtesy
notification, however many implementations seem to use a
channel EOF in place of a close before sending a disconnect
message */
return( OK_SPECIAL );
case SSH2_MSG_CHANNEL_CLOSE:
/* The peer has closed their side of the channel, if our side
isn't already closed (in other words if this message isn't
a response to a close that we sent), close our side as well */
if( getChannelStatus( sessionInfoPtr, channelNo ) == CHANNEL_BOTH )
status = sendChannelClose( sessionInfoPtr, channelNo,
CHANNEL_BOTH, TRUE );
else
/* We've already closed our side of the channel, delete it */
status = deleteChannel( sessionInfoPtr, channelNo,
CHANNEL_BOTH, TRUE );
/* If this wasn't the last channel, we're done */
if( status != OK_SPECIAL )
return( OK_SPECIAL );
/* We've closed the last channel, indicate that the overall
connection is now closed. This behaviour isn't mentioned in
the spec, but it seems to be the standard way of handling
things, particularly for the most common case where
channel == session */
sessionInfoPtr->flags |= SESSION_SENDCLOSED;
retExt( sessionInfoPtr, CRYPT_ERROR_COMPLETE,
"Remote system closed last remaining SSH channel" );
}
assert( NOTREACHED );
return( CRYPT_ERROR ); /* Get rid of compiler warning */
}
/* Close a channel */
int closeChannel( SESSION_INFO *sessionInfoPtr,
const BOOLEAN closeLastChannel )
{
READSTATE_INFO readInfo;
const int currWriteChannelNo = \
getCurrentChannelNo( sessionInfoPtr, CHANNEL_WRITE );
int status;
/* If we've already sent the final channel-close message in response to
getting a final close notification from the peer, all that's left
to do is to disconnect the session */
if( sessionInfoPtr->flags & SESSION_SENDCLOSED )
{
sNetDisconnect( &sessionInfoPtr->stream );
return( CRYPT_OK );
}
/* Normally we can keep closing open channels until we hit the last one
whereupon we close the overall session, however if we're closing a
single identified channel we can't automatically close the whole
session as a side-effect of closing the single channel */
if( !closeLastChannel && currWriteChannelNo == UNUSED_CHANNEL_NO )
retExt( sessionInfoPtr, CRYPT_ERROR_NOTINITED,
"No current channel information available to close "
"channel" );
/* If there's no channel open, close the session with a session
disconnect rather than a channel close:
byte SSH2_MSG_DISCONNECT
uint32 reason_code = SSH2_DISCONNECT_CONNECTION_LOST
string description = ""
string language_tag = ""
The spec doesn't explain what the reason codes actually mean, but
SSH2_DISCONNECT_CONNECTION_LOST seems to be the least inappropriate
disconnect reason at this point */
if( currWriteChannelNo == UNUSED_CHANNEL_NO )
{
status = enqueueResponse( sessionInfoPtr, SSH2_MSG_DISCONNECT, 3,
SSH2_DISCONNECT_CONNECTION_LOST, 0, 0,
CRYPT_UNUSED );
if( cryptStatusOK( status ) )
sendEnqueuedResponse( sessionInfoPtr, CRYPT_UNUSED );
sessionInfoPtr->flags |= SESSION_SENDCLOSED;
sNetDisconnect( &sessionInfoPtr->stream );
return( CRYPT_OK );
}
/* Close the write side of the channel, the complete close will be done
when the other side acknowledges our close. If this isn't the last
open channel, the response to our close will be handled as part of
normal packet processing and we're done */
status = sendChannelClose( sessionInfoPtr, currWriteChannelNo,
CHANNEL_WRITE, closeLastChannel );
if( status != OK_SPECIAL )
{
/* If this is the last remaining channel, we similarly can't close
it */
if( status == CRYPT_ERROR_PERMISSION )
retExt( sessionInfoPtr, CRYPT_ERROR_PERMISSION,
"Cannot close last remaining channel without closing "
"the overall session" );
return( CRYPT_OK );
}
/* It's the last open channel, close down the session */
status = sendCloseNotification( sessionInfoPtr, NULL, 0 );
if( cryptStatusError( status ) || \
( sessionInfoPtr->protocolFlags & SESSION_SENDCLOSED ) )
{
/* There's a problem at the network level or the other side has
already closed the session, close the network link and exit */
sNetDisconnect( &sessionInfoPtr->stream );
return( CRYPT_OK );
}
/* If there's not enough room in the receive buffer to read at least 1K
of packet data, we can't try anything further */
if( sessionInfoPtr->receiveBufSize - sessionInfoPtr->receiveBufEnd < \
min( sessionInfoPtr->pendingPacketRemaining, 1024 ) )
{
sNetDisconnect( &sessionInfoPtr->stream );
return( CRYPT_OK );
}
/* Read back the other side's channel close. This is somewhat messy
since the other side could decide that it still wants to send us
arbitrary amounts of data (the spec is rather vague about how urgent
a channel close is, the general idea among implementors seems to be
that you should let output drain before you close your side, but
if you're in the middle of sending a 2GB file that's a lot of output
to drain). This can also be complicated by implementation-specific
quirks, for example OpenSSH may hang more or less indefinitely if
there's output coming from a background process on the server. This
is because of a rather obscure race condition that would occur if it
exited immediately in which the SSH server gets the SIGCHLD from the
(local) background process exiting before it's written all of its
data to the (local) pipe connecting it to the SSH server, so it
closes the (remote) SSH channel/connection before the last piece of
data comes over the (local) pipe. Because the server won't close the
(remote) SSH connection until it's certain that the (local) process
has written all of its data, and it'll never get the EOF over the
pipe, it hangs forever. This is a piece of Unix plumbing arcana that
doesn't really concern us, so again just exiting after a short wait
is the best response.
Since we're about to shut down the session anyway, we try
to read a basic channel close ack from the other side, if there's
anything more than that we drop it. This is complicated somewhat by
the fact that what we're doing here is something that's normally
handled by the high-level read code in sess_rw.c. What we implement
here is the absolute minimum needed to clear the stream
(sendCloseNotification() has set the necessary (small) nonzero
timeout for us) */
status = sessionInfoPtr->readHeaderFunction( sessionInfoPtr, &readInfo );
if( !cryptStatusError( status ) )
{
/* Adjust the packet info for the packet header data that was just
read */
sessionInfoPtr->receiveBufEnd += status;
sessionInfoPtr->pendingPacketPartialLength = status;
sessionInfoPtr->pendingPacketRemaining -= status;
if( sessionInfoPtr->pendingPacketRemaining <= 512 )
{
const int bytesLeft = sessionInfoPtr->receiveBufSize - \
sessionInfoPtr->receiveBufEnd;
/* We got a packet and it's probably the channel close ack, read
it */
status = sread( &sessionInfoPtr->stream,
sessionInfoPtr->receiveBuffer + \
sessionInfoPtr->receiveBufEnd,
min( sessionInfoPtr->pendingPacketRemaining, \
bytesLeft ) );
}
}
sNetDisconnect( &sessionInfoPtr->stream );
return( CRYPT_OK );
}
#endif /* USE_SSH2 */
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -