summaryrefslogtreecommitdiff
path: root/lib/net/ssh/transport/packet_stream.rb
blob: 94660965c8ce844b3169f5d5dda17cb04be059c1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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
115
116
117
118
119
120
121
122
123
124
125
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
require 'net/ssh/buffered_io'
require 'net/ssh/errors'
require 'net/ssh/packet'
require 'net/ssh/transport/cipher_factory'
require 'net/ssh/transport/hmac'
require 'net/ssh/transport/state'

module Net
  module SSH
    module Transport
      # 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.
      module PacketStream
        PROXY_COMMAND_HOST_IP = '<no hostip for proxy command>'.freeze

        include BufferedIo

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

        # 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.
        attr_reader :hints

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

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

        # The name of the client (local) end of the socket, as reported by the
        # socket.
        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

        # The IP address of the peer (remote) end of the socket, as reported by
        # the socket.
        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

        # Returns true if the IO is available for reading, and false otherwise.
        def available_for_read?
          result = IO.select([self], nil, nil, 0)
          result && result.first.any?
        end

        # 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.
        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

        # Enqueues a packet to be sent, and blocks until the entire packet is
        # sent.
        def send_packet(payload)
          enqueue_packet(payload)
          wait_for_pending_sends
        end

        # 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).
        def enqueue_packet(payload)
          # 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.name == "chacha20-poly1305@openssh.com"
            unencrypted_data = [padding_length, payload, padding].pack("CA*A*")
            message = client.cipher.update_cipher_mac(unencrypted_data, client.sequence_number)
          else
            if 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
          end

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

          client.increment(packet_length)

          self
        end

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

        # 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.
        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

        protected

        # Called when this module is used to extend an object. It initializes
        # the states and generally prepares the object for use as a packet stream.
        def initialize_ssh
          @hints  = {}
          @server = State.new(self, :server)
          @client = State.new(self, :client)
          @packet = nil
          initialize_buffered_io
        end

        # Tries to read the next packet. If there is insufficient data to read
        # an entire packet, this returns immediately, otherwise the packet is
        # read, post-processed according to the cipher, hmac, and compression
        # algorithms specified in the server state object, and returned as a
        # new Packet object.
        # rubocop:disable Metrics/AbcSize
        def poll_next_packet
          aad_length = server.hmac.etm ? 4 : 0

          if @packet.nil?
            minimum = server.block_size < 4 ? 4 : server.block_size
            return nil if available < minimum + aad_length

            data = read_available(minimum + aad_length)

            # decipher it
            if server.cipher.name == "chacha20-poly1305@openssh.com"
              @packet_length = server.cipher.read_length(data[0...4], server.sequence_number)
              @packet = Net::SSH::Buffer.new()
              @mac_data = data
            else
              if server.hmac.etm
                @packet_length = data.unpack("N").first
                @mac_data = data
                @packet = Net::SSH::Buffer.new(server.update_cipher(data[aad_length..-1]))
              else
                @packet = Net::SSH::Buffer.new(server.update_cipher(data))
                @packet_length = @packet.read_long
              end
            end
          end

          need = @packet_length + 4 - aad_length - server.block_size
          raise Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0

          if server.cipher.name == "chacha20-poly1305@openssh.com"
            return nil if available < need + server.cipher.mac_length
          else
            return nil if available < need + server.hmac.mac_length
          end

          if need > 0
            # read the remainder of the packet and decrypt it.
            data = read_available(need)
            @mac_data += data if server.hmac.etm || (server.cipher.name == "chacha20-poly1305@openssh.com")
            if server.cipher.name != "chacha20-poly1305@openssh.com"
              @packet.append(server.update_cipher(data))
            end
          end

          if server.cipher.name != "chacha20-poly1305@openssh.com"
            # get the hmac from the tail of the packet (if one exists), and
            # then validate it.
            real_hmac = read_available(server.hmac.mac_length) || ""

            @packet.append(server.final_cipher)
            padding_length = @packet.read_byte

            payload = @packet.read(@packet_length - padding_length - 1)
            

            my_computed_hmac = if server.hmac.etm
                               server.hmac.digest([server.sequence_number, @mac_data].pack("NA*"))
                             else
                               server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
                             end
            raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac
          else
            real_hmac = read_available(server.cipher.mac_length) || ""
            @packet = Net::SSH::Buffer.new(server.cipher.read_and_mac(@mac_data, real_hmac, server.sequence_number))
            padding_length = @packet.read_byte
            payload = @packet.read(@packet_length - padding_length - 1)
          end
          # try to decompress the payload, in case compression is active
          payload = server.decompress(payload)

          debug { "received packet nr #{server.sequence_number} type #{payload.getbyte(0)} len #{@packet_length}" }

          server.increment(@packet_length)
          @packet = nil

          return Packet.new(payload)
        end
      end
      # rubocop:enable Metrics/AbcSize
    end
  end
end