net-ssh 7.3.6 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/pull_request_template.md +1 -0
  4. data/.github/workflows/changelog.yml +43 -0
  5. data/.github/workflows/ci-with-docker.yml +0 -3
  6. data/.github/workflows/ci.yml +2 -17
  7. data/.github/workflows/rubocop.yml +3 -1
  8. data/.rubocop.yml +5 -1
  9. data/.rubocop_todo.yml +1 -7
  10. data/CHANGES.txt +82 -7
  11. data/CONTRIBUTING.md +35 -0
  12. data/Dockerfile +3 -3
  13. data/Dockerfile.openssl3 +1 -1
  14. data/Dockerfile.zlib_ng +1 -1
  15. data/README.md +71 -57
  16. data/Rakefile +66 -12
  17. data/appveyor.yml +4 -16
  18. data/changelog.d/README.md +1 -0
  19. data/docker-compose.yml +1 -7
  20. data/lib/net/ssh/authentication/agent.rb +35 -11
  21. data/lib/net/ssh/authentication/ed25519.rb +137 -44
  22. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  23. data/lib/net/ssh/authentication/key_manager.rb +22 -11
  24. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  25. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  26. data/lib/net/ssh/authentication/security_key.rb +71 -0
  27. data/lib/net/ssh/authentication/session.rb +29 -17
  28. data/lib/net/ssh/buffer.rb +6 -1
  29. data/lib/net/ssh/buffered_io.rb +40 -3
  30. data/lib/net/ssh/config.rb +28 -12
  31. data/lib/net/ssh/connection/channel.rb +49 -2
  32. data/lib/net/ssh/connection/session.rb +58 -8
  33. data/lib/net/ssh/errors.rb +24 -6
  34. data/lib/net/ssh/key_factory.rb +7 -1
  35. data/lib/net/ssh/known_hosts.rb +18 -8
  36. data/lib/net/ssh/multiplex/client.rb +162 -0
  37. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  38. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  39. data/lib/net/ssh/multiplex.rb +126 -0
  40. data/lib/net/ssh/prompt.rb +3 -3
  41. data/lib/net/ssh/proxy/jump.rb +40 -2
  42. data/lib/net/ssh/service/forward.rb +35 -24
  43. data/lib/net/ssh/test.rb +2 -1
  44. data/lib/net/ssh/transport/algorithms.rb +88 -33
  45. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  46. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  47. data/lib/net/ssh/transport/cipher_factory.rb +23 -1
  48. data/lib/net/ssh/transport/constants.rb +1 -0
  49. data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
  50. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  51. data/lib/net/ssh/transport/hmac.rb +2 -0
  52. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  53. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  54. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  55. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  56. data/lib/net/ssh/transport/openssl.rb +2 -1
  57. data/lib/net/ssh/transport/packet_stream.rb +12 -0
  58. data/lib/net/ssh/transport/session.rb +43 -45
  59. data/lib/net/ssh/verifiers/always.rb +0 -3
  60. data/lib/net/ssh/version.rb +3 -3
  61. data/lib/net/ssh.rb +141 -58
  62. data/net-ssh.gemspec +5 -8
  63. data/rakelib/changelog.rb +229 -0
  64. data.tar.gz.sig +0 -0
  65. metadata +37 -36
  66. metadata.gz.sig +0 -0
  67. data/Gemfile.noed25519 +0 -12
  68. data/Gemfile.norbnacl +0 -12
@@ -2,6 +2,7 @@ require 'net/ssh/transport/openssl'
2
2
  require 'net/ssh/prompt'
3
3
 
4
4
  require 'net/ssh/authentication/ed25519_loader'
5
+ require 'net/ssh/authentication/security_key'
5
6
 
6
7
  module Net
7
8
  module SSH
@@ -92,7 +93,7 @@ module Net
92
93
  blob = nil
93
94
  begin
94
95
  blob = fields.shift
95
- end while !blob.nil? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match(blob)
96
+ end while !blob.nil? && !public_key_type?(blob)
96
97
  blob = fields.shift
97
98
 
98
99
  raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
@@ -104,6 +105,11 @@ module Net
104
105
 
105
106
  private
106
107
 
108
+ def public_key_type?(type)
109
+ /^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match?(type) ||
110
+ Net::SSH::Authentication::SecurityKey.understands?(type)
111
+ end
112
+
107
113
  # rubocop:disable Style/Documentation, Lint/DuplicateMethods
108
114
  class KeyType
109
115
  def self.read(key_data, passphrase)
@@ -1,6 +1,5 @@
1
1
  require 'strscan'
2
2
  require 'openssl'
3
- require 'delegate'
4
3
  require 'net/ssh/buffer'
5
4
  require 'net/ssh/authentication/ed25519_loader'
6
5
 
@@ -8,8 +7,11 @@ module Net
8
7
  module SSH
9
8
  module HostKeyEntries
10
9
  # regular public key entry
11
- class PubKey < Delegator
12
- def initialize(key, comment: nil) # rubocop:disable Lint/MissingSuper
10
+ class PubKey
11
+ # The public key itself, e.g. an OpenSSL::PKey::PKey.
12
+ attr_reader :key
13
+
14
+ def initialize(key, comment: nil)
13
15
  @key = key
14
16
  @comment = comment
15
17
  end
@@ -26,14 +28,22 @@ module Net
26
28
  @key.to_blob
27
29
  end
28
30
 
29
- def __getobj__
30
- Kernel.warn("Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated")
31
- @key
32
- end
33
-
34
31
  def matches_key?(server_key)
35
32
  @key.ssh_type == server_key.ssh_type && @key.to_blob == server_key.to_blob
36
33
  end
34
+
35
+ def method_missing(name, *args, &block)
36
+ return super unless @key.respond_to?(name)
37
+
38
+ raise NoMethodError.new(
39
+ "undefined method `#{name}' for #{self.class}; call it on #key, the wrapped public key",
40
+ name
41
+ )
42
+ end
43
+
44
+ def respond_to_missing?(_name, _include_private = false)
45
+ false
46
+ end
37
47
  end
38
48
 
39
49
  # @cert-authority entry
@@ -0,0 +1,162 @@
1
+ require 'io/wait'
2
+ require 'socket'
3
+
4
+ require 'net/ssh/buffer'
5
+ require 'net/ssh/errors'
6
+ require 'net/ssh/loggable'
7
+
8
+ module Net
9
+ module SSH
10
+ module Multiplex
11
+ # Raised when the ControlMaster socket can't be used, e.g. the master
12
+ # refused the request or spoke an unexpected protocol.
13
+ class Error < Net::SSH::Exception; end
14
+
15
+ # Connects to an OpenSSH ControlMaster's UNIX socket and switches it to
16
+ # proxy mode (MUX_C_PROXY, OpenSSH 7.4 and later), after which the socket
17
+ # carries plain SSH connection-protocol packets. See PROTOCOL.mux and
18
+ # mux.c in OpenSSH.
19
+ class Client
20
+ include Loggable
21
+
22
+ MUX_MSG_HELLO = 0x00000001
23
+ MUX_C_PROXY = 0x1000000f
24
+ MUX_S_PERMISSION_DENIED = 0x80000002
25
+ MUX_S_FAILURE = 0x80000003
26
+ MUX_S_PROXY = 0x8000000f
27
+
28
+ PROTOCOL_VERSION = 4
29
+
30
+ # Mux control messages are small; this only guards against reading a
31
+ # bogus length from something that isn't a ControlMaster.
32
+ MAX_MESSAGE_SIZE = 256 * 1024
33
+
34
+ attr_reader :path
35
+
36
+ def initialize(path, timeout:, logger: nil)
37
+ self.logger = logger
38
+ @path = path
39
+ @timeout = timeout
40
+ end
41
+
42
+ # Performs the HELLO and PROXY exchange and returns the connected
43
+ # socket, positioned at the first proxied packet. Raises
44
+ # Multiplex::Error, SystemCallError or IOError if the master can't be
45
+ # used; the socket is closed in that case.
46
+ def connect_proxy
47
+ @deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
48
+ @socket = open_socket
49
+ verify_peer
50
+ hello
51
+ request_proxy
52
+ @socket
53
+ rescue StandardError
54
+ @socket&.close
55
+ raise
56
+ end
57
+
58
+ private
59
+
60
+ def open_socket
61
+ socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM)
62
+ address = Socket.sockaddr_un(path)
63
+ loop do
64
+ result = socket.connect_nonblock(address, exception: false)
65
+ break if result == 0
66
+
67
+ wait_for(:writable, socket)
68
+ rescue Errno::EISCONN
69
+ break
70
+ end
71
+ socket
72
+ rescue StandardError
73
+ socket&.close
74
+ raise
75
+ end
76
+
77
+ # The master itself rejects clients with a different uid (channels.c,
78
+ # getpeereid); checking here also keeps us off a socket someone else
79
+ # planted at the ControlPath.
80
+ def verify_peer
81
+ euid = begin
82
+ @socket.getpeereid.first
83
+ rescue NotImplementedError
84
+ File.stat(path).uid
85
+ end
86
+ raise Error, "#{path} is owned by uid #{euid}, not #{Process.euid}" unless euid == Process.euid
87
+ end
88
+
89
+ def hello
90
+ type, body = read_message
91
+ raise Error, "expected MUX_MSG_HELLO, got 0x#{type.to_s(16)}" unless type == MUX_MSG_HELLO
92
+
93
+ version = body.read_long
94
+ raise Error, "unsupported mux protocol version #{version}" unless version == PROTOCOL_VERSION
95
+
96
+ # Any extensions the master advertises (OpenSSH 10.3 sends "info")
97
+ # are optional, so the rest of the body is ignored.
98
+ write_message(MUX_MSG_HELLO, :long, PROTOCOL_VERSION)
99
+ debug { "mux hello exchanged with #{path}" }
100
+ end
101
+
102
+ def request_proxy
103
+ request_id = 1
104
+ write_message(MUX_C_PROXY, :long, request_id)
105
+ type, body = read_message
106
+ case type
107
+ when MUX_S_PROXY
108
+ reply_id = body.read_long
109
+ raise Error, "MUX_S_PROXY for request #{reply_id}, expected #{request_id}" unless reply_id == request_id
110
+ when MUX_S_PERMISSION_DENIED, MUX_S_FAILURE
111
+ body.read_long
112
+ raise Error, "master refused proxy request: #{body.read_string}"
113
+ else
114
+ raise Error, "unexpected reply 0x#{type.to_s(16)} to MUX_C_PROXY"
115
+ end
116
+ end
117
+
118
+ def write_message(type, *args)
119
+ body = Net::SSH::Buffer.from(:long, type, *args).to_s
120
+ data = [body.bytesize].pack('N') + body
121
+ until data.empty?
122
+ written = @socket.write_nonblock(data, exception: false)
123
+ if written == :wait_writable
124
+ wait_for(:writable, @socket)
125
+ else
126
+ data = data.byteslice(written..-1)
127
+ end
128
+ end
129
+ end
130
+
131
+ def read_message
132
+ length = read_exactly(4).unpack1('N')
133
+ raise Error, "mux message length #{length} out of range" unless length.between?(4, MAX_MESSAGE_SIZE)
134
+
135
+ body = Net::SSH::Buffer.new(read_exactly(length))
136
+ [body.read_long, body]
137
+ end
138
+
139
+ # Reads exactly +count+ bytes so nothing after the PROXY reply, which
140
+ # already belongs to the packet stream, is consumed here.
141
+ def read_exactly(count)
142
+ data = String.new(encoding: Encoding::BINARY)
143
+ while data.bytesize < count
144
+ chunk = @socket.read_nonblock(count - data.bytesize, exception: false)
145
+ case chunk
146
+ when :wait_readable then wait_for(:readable, @socket)
147
+ when nil then raise Error, "#{path} closed the connection during the mux handshake"
148
+ else data << chunk
149
+ end
150
+ end
151
+ data
152
+ end
153
+
154
+ def wait_for(direction, socket)
155
+ remaining = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
+ ready = remaining > 0 && (direction == :readable ? socket.wait_readable(remaining) : socket.wait_writable(remaining))
157
+ raise Error, "timed out talking to #{path}" unless ready
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,120 @@
1
+ require 'digest/sha1'
2
+ require 'ipaddr'
3
+ require 'etc'
4
+ require 'socket'
5
+
6
+ module Net
7
+ module SSH
8
+ module Multiplex
9
+ # Expands a ControlPath the way OpenSSH's ssh does: a leading ~ first,
10
+ # then ${ENV} and the % tokens below in a single pass (ssh.c,
11
+ # DEFAULT_CLIENT_PERCENT_EXPAND_ARGS in sshconnect.h, misc.c
12
+ # percent_dollar_expand).
13
+ #
14
+ # %% a literal %
15
+ # %C SHA1 hex of %l%h%p%r%j
16
+ # %d local home directory
17
+ # %h remote host name, after HostName
18
+ # %i local uid
19
+ # %j host of the last ProxyJump hop, or empty
20
+ # %k the host key alias, or the host name as given
21
+ # %L local host name up to the first dot
22
+ # %l local host name
23
+ # %n the host name as given
24
+ # %p remote port
25
+ # %r remote user
26
+ # %u local user
27
+ class ControlPath
28
+ # +host_arg+ is the host as given, +host+ the one connected to after
29
+ # HostName.
30
+ def initialize(host_arg:, host:, port:, remote_user:, jump_host: '', host_key_alias: nil) # rubocop:disable Metrics/ParameterLists
31
+ @host_arg = host_arg.to_s
32
+ # ssh lowercases host names, but not addresses, before expanding.
33
+ @host = address?(host.to_s) ? host.to_s : host.to_s.downcase
34
+ @port = port.to_s
35
+ @remote_user = remote_user.to_s
36
+ @jump_host = jump_host.to_s
37
+ @host_key_alias = host_key_alias
38
+ end
39
+
40
+ # Raises ArgumentError for an unknown % token, an unset ${ENV}
41
+ # variable or an unknown ~user, which make ssh refuse the ControlPath
42
+ # too.
43
+ def expand(path)
44
+ expand_tokens(expand_tilde(path))
45
+ end
46
+
47
+ def tokens
48
+ @tokens ||= begin
49
+ pw = Etc.getpwuid(Process.uid)
50
+ local_host = Socket.gethostname
51
+ {
52
+ 'C' => Digest::SHA1.hexdigest("#{local_host}#{@host}#{@port}#{@remote_user}#{@jump_host}"),
53
+ 'd' => pw.dir,
54
+ 'h' => @host,
55
+ 'i' => Process.uid.to_s,
56
+ 'j' => @jump_host,
57
+ 'k' => @host_key_alias || @host_arg,
58
+ 'L' => local_host.split('.', 2).first,
59
+ 'l' => local_host,
60
+ 'n' => @host_arg,
61
+ 'p' => @port,
62
+ 'r' => @remote_user,
63
+ 'u' => pw.name
64
+ }
65
+ end
66
+ end
67
+
68
+ # The host ssh uses for %j: the last hop of a ProxyJump list, without
69
+ # user or port (readconf.c, parse_jump).
70
+ def self.jump_host(proxy_jump)
71
+ last = proxy_jump.to_s.split(',').last.to_s.strip.delete_prefix('ssh://')
72
+ last = last.split('@').last.to_s
73
+ if last.start_with?('[')
74
+ last[1...last.index(']').to_i]
75
+ else
76
+ last.split(':').first.to_s
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # ssh uses the home directory from the password database, not $HOME
83
+ # (misc.c, tilde_expand).
84
+ def expand_tilde(path)
85
+ return path unless path.start_with?('~')
86
+
87
+ user, rest = path[1..].split('/', 2)
88
+ home = user.nil? || user.empty? ? Etc.getpwuid(Process.uid).dir : Etc.getpwnam(user).dir
89
+
90
+ rest.nil? ? home : File.join(home, rest)
91
+ end
92
+
93
+ def expand_tokens(path)
94
+ path.gsub(/\$\{([^}]*)\}|%(.?)|\$\{.*/m) do
95
+ if Regexp.last_match(1)
96
+ name = Regexp.last_match(1)
97
+ raise ArgumentError, "empty ${} variable" if name.empty?
98
+
99
+ ENV.fetch(name) { raise ArgumentError, "variable ${#{name}} is not set" }
100
+ elsif Regexp.last_match(2)
101
+ token = Regexp.last_match(2)
102
+ next '%' if token == '%'
103
+
104
+ tokens.fetch(token) { raise ArgumentError, "unknown token %#{token}" }
105
+ else
106
+ raise ArgumentError, "unterminated ${"
107
+ end
108
+ end
109
+ end
110
+
111
+ def address?(host)
112
+ IPAddr.new(host)
113
+ true
114
+ rescue IPAddr::Error
115
+ false
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,218 @@
1
+ require 'io/wait'
2
+
3
+ require 'net/ssh/buffer'
4
+ require 'net/ssh/buffered_io'
5
+ require 'net/ssh/errors'
6
+ require 'net/ssh/loggable'
7
+ require 'net/ssh/packet'
8
+ require 'net/ssh/connection/constants'
9
+
10
+ module Net
11
+ module SSH
12
+ module Multiplex
13
+ # Packet framing on a ControlMaster socket in proxy mode: the usual
14
+ # uint32 packet_length and byte padding_length, but no padding, MAC,
15
+ # encryption or compression. The master parses everything after the
16
+ # padding_length byte as payload (channel_proxy_downstream in OpenSSH's
17
+ # channels.c), so padding must not be sent.
18
+ module PacketSocket
19
+ include BufferedIo
20
+
21
+ # OpenSSH's PACKET_MAX_SIZE.
22
+ MAX_PACKET_SIZE = 256 * 1024
23
+
24
+ def self.extended(object) # :nodoc:
25
+ object.__send__(:initialize_buffered_io)
26
+ end
27
+
28
+ def available_for_read?
29
+ !wait_readable(0).nil?
30
+ end
31
+
32
+ # Same contract as Net::SSH::Transport::PacketStream#next_packet.
33
+ def next_packet(mode = :nonblock, timeout = nil)
34
+ case mode
35
+ when :nonblock
36
+ packet = poll_next_packet
37
+ return packet if packet
38
+
39
+ if available_for_read? && fill <= 0
40
+ packet = poll_next_packet
41
+ raise Net::SSH::Disconnect, "connection closed by ControlMaster" if packet.nil?
42
+
43
+ return packet
44
+ end
45
+ poll_next_packet
46
+ when :block
47
+ loop do
48
+ packet = poll_next_packet
49
+ return packet if packet
50
+
51
+ raise Net::SSH::ConnectionTimeout, "timeout waiting for next packet" unless wait_readable(timeout)
52
+ raise Net::SSH::Disconnect, "connection closed by ControlMaster" if fill <= 0
53
+ end
54
+ else
55
+ raise ArgumentError, "expected :block or :nonblock, got #{mode.inspect}"
56
+ end
57
+ end
58
+
59
+ def enqueue_packet(payload)
60
+ data = payload.to_s
61
+ enqueue([data.bytesize + 1, 0].pack('NC') + data.b)
62
+ end
63
+
64
+ def send_packet(payload)
65
+ enqueue_packet(payload)
66
+ wait_for_pending_sends
67
+ end
68
+
69
+ private
70
+
71
+ def poll_next_packet
72
+ return nil if available < 4
73
+
74
+ start = input.position
75
+ length = input.read_long
76
+ raise Net::SSH::Exception, "invalid packet length #{length} from ControlMaster" unless length.between?(1, MAX_PACKET_SIZE)
77
+
78
+ if available < length
79
+ input.position = start
80
+ return nil
81
+ end
82
+
83
+ data = input.read(length)
84
+ input.consume!
85
+ padding = data.getbyte(0)
86
+ raise Net::SSH::Exception, "invalid padding length #{padding} from ControlMaster" if padding > length - 2
87
+
88
+ Net::SSH::Packet.new(data.byteslice(1, length - 1 - padding))
89
+ end
90
+ end
91
+
92
+ # Stands in for Net::SSH::Transport::Session underneath a
93
+ # Net::SSH::Connection::Session when connected through an OpenSSH
94
+ # ControlMaster in proxy mode. Key exchange, rekeying, host key
95
+ # verification and authentication all belong to the master, so the
96
+ # matching transport methods are no-ops or raise
97
+ # NotSupportedOverControlMaster.
98
+ class ProxyTransport
99
+ include Loggable
100
+ include Net::SSH::Connection::Constants
101
+
102
+ DEFAULT_PORT = 22
103
+
104
+ # Channel requests whose follow-up channel the server opens towards
105
+ # the master, which serves it from its own agent or X display only if
106
+ # it has ForwardAgent or ForwardX11 enabled itself, and rejects it
107
+ # otherwise (clientloop.c, client_input_channel_open).
108
+ UNSUPPORTED_CHANNEL_REQUESTS = %w[auth-agent-req@openssh.com auth-agent-req x11-req].freeze
109
+
110
+ attr_reader :host, :port, :socket, :options
111
+
112
+ # The ControlMaster socket path this transport is connected through.
113
+ attr_reader :control_path
114
+
115
+ def initialize(socket, host:, control_path:, options: {})
116
+ self.logger = options[:logger]
117
+ @host = host
118
+ @port = options[:port] || DEFAULT_PORT
119
+ @options = options
120
+ @control_path = control_path
121
+ @socket = socket
122
+ @socket.extend(PacketSocket)
123
+ @socket.logger = logger
124
+ end
125
+
126
+ def host_as_string
127
+ @host_as_string ||= port == DEFAULT_PORT ? host.to_s : "[#{host}]:#{port}"
128
+ end
129
+
130
+ def peer
131
+ @peer ||= { ip: nil, port: port.to_i, host: host, canonized: host_as_string }
132
+ end
133
+
134
+ def closed?
135
+ socket.closed?
136
+ end
137
+
138
+ def close
139
+ socket.close unless closed?
140
+ end
141
+
142
+ def shutdown!
143
+ error { "forcing connection closed" }
144
+ close
145
+ end
146
+
147
+ def next_message
148
+ poll_message(:block)
149
+ end
150
+
151
+ def poll_message(mode = :nonblock, *)
152
+ socket.next_packet(mode, options[:timeout])
153
+ end
154
+
155
+ def send_message(message)
156
+ socket.send_packet(check_supported(message.to_s))
157
+ end
158
+
159
+ def enqueue_message(message)
160
+ socket.enqueue_packet(check_supported(message.to_s))
161
+ end
162
+
163
+ # The master rekeys its own connection.
164
+ def rekey!; end
165
+
166
+ def rekey_as_needed; end
167
+
168
+ def hint(*); end
169
+
170
+ def server_version
171
+ not_supported("the server version")
172
+ end
173
+
174
+ def algorithms
175
+ not_supported("the negotiated algorithms")
176
+ end
177
+
178
+ def host_keys
179
+ not_supported("the host keys")
180
+ end
181
+
182
+ def host_key_verifier
183
+ not_supported("host key verification")
184
+ end
185
+
186
+ private
187
+
188
+ def not_supported(what)
189
+ raise NotSupportedOverControlMaster, "#{what} belong(s) to the ControlMaster at #{control_path}"
190
+ end
191
+
192
+ # The master kills the proxy connection on any global request other
193
+ # than tcpip-forward, and consumes the reply to that one itself, so
194
+ # its success or failure and an allocated port 0 can't be observed
195
+ # (channels.c channel_proxy_downstream, clientloop.c
196
+ # client_global_request_reply).
197
+ def check_supported(data)
198
+ case data.getbyte(0)
199
+ when GLOBAL_REQUEST
200
+ request = Net::SSH::Buffer.new(data.byteslice(1..-1)).read_string
201
+ if request == 'tcpip-forward'
202
+ raise NotSupportedOverControlMaster, "remote forwarding (tcpip-forward) is not supported over a ControlMaster: " \
203
+ "the master consumes the reply, so success and the allocated port can't be observed"
204
+ end
205
+
206
+ raise NotSupportedOverControlMaster, "global request #{request} is not supported over a ControlMaster"
207
+ when CHANNEL_REQUEST
208
+ request = Net::SSH::Buffer.new(data.byteslice(5..-1)).read_string
209
+ if UNSUPPORTED_CHANNEL_REQUESTS.include?(request)
210
+ raise NotSupportedOverControlMaster, "channel request #{request} is not supported over a ControlMaster"
211
+ end
212
+ end
213
+ data
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end