Module: Net::SSH::Transport::PacketStream

Includes:
BufferedIo
Defined in:
lib/net/ssh/transport/packet_stream.rb

Overview

A module that builds additional functionality onto the Net::SSH::BufferedIo module. It adds SSH encryption, compression, and packet validation, as per the SSH2 protocol. It also adds an abstraction for polling packets, to allow for both blocking and non-blocking reads.

Constant Summary collapse

PROXY_COMMAND_HOST_IP =

rubocop:disable Metrics/ModuleLength

'<no hostip for proxy command>'.freeze

Instance Attribute Summary collapse

Attributes included from Loggable

#logger

Class Method Summary collapse

Instance Method Summary collapse

Methods included from BufferedIo

#available, #enqueue, #fill, #pending_write?, #read_available, #read_buffer, #send_pending, #wait_for_pending_sends, #write_buffer

Methods included from Loggable

#debug, #error, #fatal, #info, #lwarn

Instance Attribute Details

#clientObject (readonly)

The client state object, which encapsulates the algorithms used to build packets to send to the server.



36
37
38
# File 'lib/net/ssh/transport/packet_stream.rb', line 36

def client
  @client
end

#hintsObject (readonly)

The map of “hints” that can be used to modify the behavior of the packet stream. For instance, when authentication succeeds, an “authenticated” hint is set, which is used to determine whether or not to compress the data when using the “delayed” compression algorithm.



28
29
30
# File 'lib/net/ssh/transport/packet_stream.rb', line 28

def hints
  @hints
end

#serverObject (readonly)

The server state object, which encapsulates the algorithms used to interpret packets coming from the server.



32
33
34
# File 'lib/net/ssh/transport/packet_stream.rb', line 32

def server
  @server
end

Class Method Details

.extended(object) ⇒ Object



20
21
22
# File 'lib/net/ssh/transport/packet_stream.rb', line 20

def self.extended(object)
  object.__send__(:initialize_ssh)
end

Instance Method Details

#available_for_read?Boolean

Returns true if the IO is available for reading, and false otherwise.

Returns:

  • (Boolean)


73
74
75
76
# File 'lib/net/ssh/transport/packet_stream.rb', line 73

def available_for_read?
  result = IO.select([self], nil, nil, 0)
  result && result.first.any?
end

#cleanupObject

Performs any pending cleanup necessary on the IO and its associated state objects. (See State#cleanup).



187
188
189
190
# File 'lib/net/ssh/transport/packet_stream.rb', line 187

def cleanup
  client.cleanup
  server.cleanup
end

#client_nameObject

The name of the client (local) end of the socket, as reported by the socket.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/net/ssh/transport/packet_stream.rb', line 40

def client_name
  @client_name ||= begin
    sockaddr = getsockname
    begin
      Socket.getnameinfo(sockaddr, Socket::NI_NAMEREQD).first
    rescue StandardError
      begin
        Socket.getnameinfo(sockaddr).first
      rescue StandardError
        begin
          Socket.gethostbyname(Socket.gethostname).first
        rescue StandardError
          lwarn { "the client ipaddr/name could not be determined" }
          "unknown"
        end
      end
    end
  end
end

#enqueue_packet(payload) ⇒ Object

Enqueues a packet to be sent, but does not immediately send the packet. The given payload is pre-processed according to the algorithms specified in the client state (compression, cipher, and hmac).



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/net/ssh/transport/packet_stream.rb', line 126

def enqueue_packet(payload) # rubocop:disable Metrics/AbcSize
  # try to compress the packet
  payload = client.compress(payload)

  # the length of the packet, minus the padding
  actual_length = (client.hmac.etm ? 0 : 4) + payload.bytesize + 1

  # compute the padding length
  padding_length = client.block_size - (actual_length % client.block_size)
  padding_length += client.block_size if padding_length < 4

  # compute the packet length (sans the length field itself)
  packet_length = payload.bytesize + padding_length + 1

  if packet_length < 16
    padding_length += client.block_size
    packet_length = payload.bytesize + padding_length + 1
  end

  padding = Array.new(padding_length) { rand(256) }.pack("C*")

  if client.cipher.implicit_mac?
    unencrypted_data = [padding_length, payload, padding].pack("CA*A*")
    message = client.cipher.update_cipher_mac(unencrypted_data, client.sequence_number)
  elsif client.hmac.etm
    debug { "using encrypt-then-mac" }

    # Encrypt padding_length, payload, and padding. Take MAC
    # from the unencrypted packet_lenght and the encrypted
    # data.
    length_data = [packet_length].pack("N")

    unencrypted_data = [padding_length, payload, padding].pack("CA*A*")

    encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher

    mac_data = length_data + encrypted_data

    mac = client.hmac.digest([client.sequence_number, mac_data].pack("NA*"))

    message = mac_data + mac
  else
    unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*")

    mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*"))

    encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher

    message = encrypted_data + mac
  end

  debug { "queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}" }
  enqueue(message)

  client.increment(packet_length)

  self
end

#if_needs_rekey?Boolean

If the IO object requires a rekey operation (as indicated by either its client or server state objects, see State#needs_rekey?), this will yield. Otherwise, this does nothing.

Returns:

  • (Boolean)


195
196
197
198
199
200
201
# File 'lib/net/ssh/transport/packet_stream.rb', line 195

def if_needs_rekey?
  if client.needs_rekey? || server.needs_rekey?
    yield
    client.reset! if client.needs_rekey?
    server.reset! if server.needs_rekey?
  end
end

#next_packet(mode = :nonblock, timeout = nil) ⇒ Object

Returns the next full packet. If the mode parameter is :nonblock (the default), then this will return immediately, whether a packet is available or not, and will return nil if there is no packet ready to be returned. If the mode parameter is :block, then this method will block until a packet is available or timeout seconds have passed.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/net/ssh/transport/packet_stream.rb', line 83

def next_packet(mode = :nonblock, timeout = nil)
  case mode
  when :nonblock then
    packet = poll_next_packet
    return packet if packet

    if available_for_read?
      if fill <= 0
        result = poll_next_packet
        if result.nil?
          raise Net::SSH::Disconnect, "connection closed by remote host"
        else
          return result
        end
      end
    end
    poll_next_packet

  when :block then
    loop do
      packet = poll_next_packet
      return packet if packet

      result = IO.select([self], nil, nil, timeout)
      raise Net::SSH::ConnectionTimeout, "timeout waiting for next packet" unless result
      raise Net::SSH::Disconnect, "connection closed by remote host" if fill <= 0
    end

  else
    raise ArgumentError, "expected :block or :nonblock, got #{mode.inspect}"
  end
end

#peer_ipObject

The IP address of the peer (remote) end of the socket, as reported by the socket.



62
63
64
65
66
67
68
69
70
# File 'lib/net/ssh/transport/packet_stream.rb', line 62

def peer_ip
  @peer_ip ||=
    if respond_to?(:getpeername)
      addr = getpeername
      Socket.getnameinfo(addr, Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV).first
    else
      PROXY_COMMAND_HOST_IP
    end
end

#send_packet(payload) ⇒ Object

Enqueues a packet to be sent, and blocks until the entire packet is sent.



118
119
120
121
# File 'lib/net/ssh/transport/packet_stream.rb', line 118

def send_packet(payload)
  enqueue_packet(payload)
  wait_for_pending_sends
end