net-ssh 7.2.0 → 8.0.0
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.dockerignore +3 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +43 -0
- data/.github/workflows/ci-with-docker.yml +20 -9
- data/.github/workflows/ci.yml +11 -20
- data/.github/workflows/rubocop.yml +3 -1
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +4 -1
- data/CHANGES.txt +114 -0
- data/CONTRIBUTING.md +35 -0
- data/Dockerfile +7 -2
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +29 -0
- data/README.md +76 -57
- data/Rakefile +117 -30
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +2 -8
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +141 -40
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +61 -18
- data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
- data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
- data/lib/net/ssh/authentication/pub_key_fingerprint.rb +1 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +34 -15
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +41 -4
- data/lib/net/ssh/config.rb +30 -13
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +58 -8
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +11 -4
- data/lib/net/ssh/known_hosts.rb +64 -11
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +130 -0
- data/lib/net/ssh/prompt.rb +3 -3
- data/lib/net/ssh/proxy/command.rb +28 -3
- data/lib/net/ssh/proxy/jump.rb +52 -4
- data/lib/net/ssh/service/forward.rb +38 -27
- data/lib/net/ssh/test/extensions.rb +3 -2
- data/lib/net/ssh/test/socket.rb +1 -1
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
- data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
- data/lib/net/ssh/transport/algorithms.rb +96 -34
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +32 -8
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/packet_stream.rb +19 -3
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +60 -46
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/verifiers/always.rb +43 -8
- data/lib/net/ssh/version.rb +2 -2
- data/lib/net/ssh.rb +142 -62
- data/net-ssh-public_cert.pem +19 -18
- data/net-ssh.gemspec +7 -8
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +86 -53
- metadata.gz.sig +1 -1
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
- data/lib/net/ssh/version.rb.old +0 -68
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
|
|
3
|
+
require 'net/ssh/errors'
|
|
4
|
+
require 'net/ssh/connection/session'
|
|
5
|
+
require 'net/ssh/multiplex/client'
|
|
6
|
+
require 'net/ssh/multiplex/control_path'
|
|
7
|
+
require 'net/ssh/multiplex/proxy_transport'
|
|
8
|
+
|
|
9
|
+
module Net
|
|
10
|
+
module SSH
|
|
11
|
+
# Reuse of an existing OpenSSH ControlMaster connection, enabled by the
|
|
12
|
+
# :control_master option of Net::SSH.start. Net::SSH only ever connects to
|
|
13
|
+
# a master someone else started (e.g. with ssh -M or ControlMaster auto);
|
|
14
|
+
# it never becomes one.
|
|
15
|
+
module Multiplex
|
|
16
|
+
# Used for the mux handshake when the :timeout option isn't set.
|
|
17
|
+
DEFAULT_TIMEOUT = 5
|
|
18
|
+
|
|
19
|
+
# Options for setting up a connection, which the master already did
|
|
20
|
+
# with its own settings.
|
|
21
|
+
MASTER_OWNED_OPTIONS = %i[
|
|
22
|
+
agent_socket_factory append_all_supported_algorithms auth_methods
|
|
23
|
+
bind_address check_host_ip compression compression_level encryption
|
|
24
|
+
fingerprint_hash global_known_hosts_file hmac host_key identity_agent
|
|
25
|
+
keepalive keepalive_interval keepalive_maxcount kex key_data keycert_data
|
|
26
|
+
keycerts keys keys_only known_hosts languages minimum_dh_bits no_delay
|
|
27
|
+
non_interactive number_of_password_prompts passphrase password
|
|
28
|
+
password_prompt proxy pubkey_algorithms rekey_blocks_limit rekey_limit
|
|
29
|
+
rekey_packet_limit use_agent user_known_hosts_file verify_host_key
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
VALUES = [:auto, false, nil].freeze
|
|
33
|
+
|
|
34
|
+
def self.validate_options!(options)
|
|
35
|
+
unless VALUES.include?(options[:control_master])
|
|
36
|
+
raise ArgumentError, "control_master must be :auto or false, got #{options[:control_master].inspect}"
|
|
37
|
+
end
|
|
38
|
+
return unless options[:control_master] && options[:forward_agent]
|
|
39
|
+
|
|
40
|
+
raise ArgumentError, "forward_agent can't be used with control_master: the ControlMaster serves agent requests " \
|
|
41
|
+
"from its own agent only if it has ForwardAgent enabled itself, and rejects them otherwise"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns a Connection::Session running through the ControlMaster for
|
|
45
|
+
# this connection, or nil (after logging why) when there is no usable
|
|
46
|
+
# master, in which case the caller connects as usual. +explicit+ are
|
|
47
|
+
# the option names the caller passed to Net::SSH.start. Raises
|
|
48
|
+
# ArgumentError for an invalid ControlPath, as ssh does.
|
|
49
|
+
def self.connect(host_arg, host, user, options, explicit: [])
|
|
50
|
+
logger = options[:logger]
|
|
51
|
+
path = socket_path(host_arg, host, user, options)
|
|
52
|
+
return nil unless path
|
|
53
|
+
|
|
54
|
+
socket = open_proxy(path, options)
|
|
55
|
+
return nil unless socket
|
|
56
|
+
|
|
57
|
+
logger.info { "control_master: using ControlMaster at #{path}" }
|
|
58
|
+
warn_master_owned(logger, explicit)
|
|
59
|
+
begin
|
|
60
|
+
transport = ProxyTransport.new(socket, host: host, control_path: path, options: options)
|
|
61
|
+
Connection::Session.new(transport, session_options(logger, options))
|
|
62
|
+
rescue StandardError
|
|
63
|
+
socket.close
|
|
64
|
+
raise
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ENOENT and ECONNREFUSED just mean no master is running, while the
|
|
69
|
+
# other errors mean a socket is there but can't be used.
|
|
70
|
+
def self.open_proxy(path, options)
|
|
71
|
+
Client.new(path, timeout: options[:timeout] || DEFAULT_TIMEOUT, logger: options[:logger]).connect_proxy
|
|
72
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED => e
|
|
73
|
+
options[:logger].info { "control_master: no ControlMaster at #{path}: #{e.message} (#{e.class})" }
|
|
74
|
+
nil
|
|
75
|
+
rescue Multiplex::Error, SystemCallError, IOError, SocketError => e
|
|
76
|
+
options[:logger].warn { "control_master: not using ControlMaster at #{path}: #{e.message} (#{e.class})" }
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.socket_path(host_arg, host, user, options)
|
|
81
|
+
if Gem.win_platform? || !defined?(UNIXSocket)
|
|
82
|
+
options[:logger].debug { "control_master: UNIX sockets are not available on this platform" }
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
control_path = options[:control_path]
|
|
87
|
+
if control_path.nil? || control_path.to_s == 'none'
|
|
88
|
+
options[:logger].debug { "control_master: no ControlPath set" }
|
|
89
|
+
return nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
expander = ControlPath.new(
|
|
93
|
+
host_arg: host_arg, host: host, port: options[:port] || ProxyTransport::DEFAULT_PORT, remote_user: user,
|
|
94
|
+
jump_host: jump_host(options[:proxy]), host_key_alias: options[:host_key_alias]
|
|
95
|
+
)
|
|
96
|
+
begin
|
|
97
|
+
path = expander.expand(control_path.to_s)
|
|
98
|
+
Socket.sockaddr_un(path)
|
|
99
|
+
rescue ArgumentError => e
|
|
100
|
+
raise ArgumentError, "invalid control_path #{control_path.to_s.inspect}: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
path
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.jump_host(proxy)
|
|
106
|
+
return '' unless defined?(Net::SSH::Proxy::Jump) && proxy.is_a?(Net::SSH::Proxy::Jump)
|
|
107
|
+
|
|
108
|
+
ControlPath.jump_host(proxy.jump_proxies)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.warn_master_owned(logger, explicit)
|
|
112
|
+
bypassed = explicit & MASTER_OWNED_OPTIONS
|
|
113
|
+
return if bypassed.empty?
|
|
114
|
+
|
|
115
|
+
logger.warn { "control_master: ignoring #{bypassed.join(', ')}, the ControlMaster's own settings apply" }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Keepalive is a global request, which the master doesn't allow; it
|
|
119
|
+
# watches its own connection anyway. ForwardAgent from ssh_config is
|
|
120
|
+
# dropped for the reason given in validate_options!.
|
|
121
|
+
def self.session_options(logger, options)
|
|
122
|
+
logger.info { "control_master: keepalive is left to the ControlMaster" } if options[:keepalive]
|
|
123
|
+
logger.info { "control_master: ignoring ForwardAgent, the ControlMaster's own ForwardAgent setting applies" } if options[:forward_agent]
|
|
124
|
+
options.merge(keepalive: false, forward_agent: false)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private_class_method :open_proxy, :socket_path, :jump_host, :warn_master_owned, :session_options
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/net/ssh/prompt.rb
CHANGED
|
@@ -41,9 +41,9 @@ module Net
|
|
|
41
41
|
def ask(prompt, echo = true)
|
|
42
42
|
$stdout.print(prompt)
|
|
43
43
|
$stdout.flush
|
|
44
|
-
ret = $stdin.noecho(&:gets)
|
|
45
|
-
$stdout.print("\n")
|
|
46
|
-
ret
|
|
44
|
+
ret = echo ? $stdin.gets : $stdin.noecho(&:gets)
|
|
45
|
+
$stdout.print("\n") unless echo
|
|
46
|
+
ret.chomp
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# success method will be called when the password was accepted
|
|
@@ -16,6 +16,11 @@ module Net
|
|
|
16
16
|
# ...
|
|
17
17
|
# end
|
|
18
18
|
class Command
|
|
19
|
+
# The command line is run by the shell, so like OpenSSH 9.6 (CVE-2023-51385) we refuse
|
|
20
|
+
# host and user names containing shell metacharacters rather than substituting them.
|
|
21
|
+
INVALID_HOST_NAME = /\A-|[[:space:][:cntrl:]'`"$\\;&<>|(){},]/.freeze
|
|
22
|
+
INVALID_USER_NAME = /\A-|[[:cntrl:]'`";&<>|(){}]|[[:space:]]-|\\\z/.freeze
|
|
23
|
+
|
|
19
24
|
# The command line template
|
|
20
25
|
attr_reader :command_line_template
|
|
21
26
|
|
|
@@ -41,13 +46,13 @@ module Net
|
|
|
41
46
|
command_line = @command_line_template.gsub(/%(.)/) {
|
|
42
47
|
case $1
|
|
43
48
|
when 'h'
|
|
44
|
-
host
|
|
49
|
+
validate_host_name!(host)
|
|
45
50
|
when 'p'
|
|
46
|
-
port
|
|
51
|
+
validate_port!(port)
|
|
47
52
|
when 'r'
|
|
48
53
|
remote_user = connection_options && connection_options[:remote_user]
|
|
49
54
|
if remote_user
|
|
50
|
-
remote_user
|
|
55
|
+
validate_user_name!(remote_user)
|
|
51
56
|
else
|
|
52
57
|
raise ArgumentError, "remote user name not available"
|
|
53
58
|
end
|
|
@@ -117,6 +122,26 @@ module Net
|
|
|
117
122
|
Process.kill('TERM', io.pid)
|
|
118
123
|
Thread.new { io.close }
|
|
119
124
|
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def validate_host_name!(host)
|
|
129
|
+
raise ArgumentError, "host name contains invalid characters: #{host.inspect}" if host.to_s.match?(INVALID_HOST_NAME)
|
|
130
|
+
|
|
131
|
+
host
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_user_name!(user)
|
|
135
|
+
raise ArgumentError, "user name contains invalid characters: #{user.inspect}" if user.to_s.match?(INVALID_USER_NAME)
|
|
136
|
+
|
|
137
|
+
user
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def validate_port!(port)
|
|
141
|
+
raise ArgumentError, "port is not a number: #{port.inspect}" unless port.to_s.match?(/\A\d+\z/)
|
|
142
|
+
|
|
143
|
+
port.to_s
|
|
144
|
+
end
|
|
120
145
|
end
|
|
121
146
|
end
|
|
122
147
|
end
|
data/lib/net/ssh/proxy/jump.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'shellwords'
|
|
1
2
|
require 'uri'
|
|
2
3
|
require 'net/ssh/proxy/command'
|
|
3
4
|
|
|
@@ -14,14 +15,33 @@ module Net
|
|
|
14
15
|
# Net::SSH.start('host', 'user', :proxy => proxy) do |ssh|
|
|
15
16
|
# ...
|
|
16
17
|
# end
|
|
18
|
+
#
|
|
19
|
+
# ssh_config options for the jump host can be given as a Hash via
|
|
20
|
+
# +options:+; each is passed to the ssh command as +-o Key=value+:
|
|
21
|
+
#
|
|
22
|
+
# proxy = Net::SSH::Proxy::Jump.new('user@proxy', options: { 'StrictHostKeyChecking' => 'no' })
|
|
23
|
+
#
|
|
24
|
+
# Options that ssh_config allows more than once (+IdentityFile+,
|
|
25
|
+
# +CertificateFile+ and +SendEnv+) take an Array and are passed once per
|
|
26
|
+
# element; an empty Array passes nothing:
|
|
27
|
+
#
|
|
28
|
+
# proxy = Net::SSH::Proxy::Jump.new('user@proxy', options: { 'IdentityFile' => ['~/.ssh/jump', '~/.ssh/jump_old'] })
|
|
29
|
+
#
|
|
30
|
+
# A +nil+ value, alone or in an Array, raises ArgumentError, as it does
|
|
31
|
+
# for the options passed to Net::SSH.start; omit the option instead.
|
|
32
|
+
#
|
|
33
|
+
# The options apply to the first jump host only; any further hosts in
|
|
34
|
+
# the chain are connected to by that host's own ssh.
|
|
17
35
|
class Jump < Command
|
|
18
36
|
# The jump proxies
|
|
19
37
|
attr_reader :jump_proxies
|
|
20
38
|
|
|
21
39
|
# Create a new socket factory that tunnels via multiple jump proxes as
|
|
22
40
|
# [user@]host[:port].
|
|
23
|
-
def initialize(jump_proxies)
|
|
41
|
+
def initialize(jump_proxies, options: {})
|
|
24
42
|
@jump_proxies = jump_proxies
|
|
43
|
+
@options = options
|
|
44
|
+
validate_options!
|
|
25
45
|
end
|
|
26
46
|
|
|
27
47
|
# Return a new socket connected to the given host and port via the jump
|
|
@@ -34,20 +54,48 @@ module Net
|
|
|
34
54
|
# We cannot build the ProxyCommand template until we know if the :config
|
|
35
55
|
# option was specified during `Net::SSH.start`.
|
|
36
56
|
def build_proxy_command_equivalent(connection_options = nil)
|
|
37
|
-
|
|
57
|
+
extra_jumps = jump_proxies.split(",", 2)[1]
|
|
38
58
|
config = connection_options && connection_options[:config]
|
|
39
|
-
uri =
|
|
59
|
+
uri, = jump_proxies.split(",").map { |jump| validate_jump!(jump) }
|
|
40
60
|
|
|
41
61
|
template = "ssh".dup
|
|
42
62
|
template << " -l #{uri.user}" if uri.user
|
|
43
63
|
template << " -p #{uri.port}" if uri.port
|
|
44
64
|
template << " -J #{extra_jumps}" if extra_jumps
|
|
45
|
-
template << " -F #{config}" if config != true && config
|
|
65
|
+
template << " -F #{escape(config)}" if config != true && config
|
|
66
|
+
@options.each do |key, values|
|
|
67
|
+
Array(values).each { |value| template << " -o #{escape(key)}=#{escape(value)}" }
|
|
68
|
+
end
|
|
46
69
|
template << " -W %h:%p "
|
|
47
70
|
template << uri.host
|
|
48
71
|
|
|
49
72
|
@command_line_template = template
|
|
50
73
|
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Like Net::SSH.start, reject nil option values (alone or in an Array)
|
|
78
|
+
# rather than silently passing nothing or an empty value to ssh.
|
|
79
|
+
def validate_options!
|
|
80
|
+
nil_keys = @options.select { |_key, value| value.nil? || (value.is_a?(Array) && value.include?(nil)) }.keys
|
|
81
|
+
return if nil_keys.empty?
|
|
82
|
+
|
|
83
|
+
raise ArgumentError, "nil is not allowed for #{nil_keys.map(&:inspect).join(', ')} in Net::SSH::Proxy::Jump options; " \
|
|
84
|
+
"omit the option to use the default or ssh_config value"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Escape for the shell, and double any % so it survives the %h/%p
|
|
88
|
+
# substitution done on the template in Command#open.
|
|
89
|
+
def escape(value)
|
|
90
|
+
Shellwords.escape(value).gsub("%", "%%")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_jump!(jump)
|
|
94
|
+
uri = URI.parse("ssh://#{jump}")
|
|
95
|
+
validate_user_name!(uri.user) if uri.user
|
|
96
|
+
validate_host_name!(uri.host)
|
|
97
|
+
uri
|
|
98
|
+
end
|
|
51
99
|
end
|
|
52
100
|
end
|
|
53
101
|
end
|
|
@@ -48,9 +48,9 @@ module Net
|
|
|
48
48
|
# If three arguments are given, it is as if the local bind address is
|
|
49
49
|
# "127.0.0.1", and the rest are applied as above.
|
|
50
50
|
#
|
|
51
|
-
# To request an ephemeral port on the
|
|
52
|
-
# the port number. In all cases, this method will return the port
|
|
53
|
-
# has been assigned.
|
|
51
|
+
# To request an ephemeral port on the local server, provide 0 (zero) for
|
|
52
|
+
# the local port number. In all cases, this method will return the port
|
|
53
|
+
# that has been assigned.
|
|
54
54
|
#
|
|
55
55
|
# ssh.forward.local(1234, "www.capify.org", 80)
|
|
56
56
|
# assigned_port = ssh.forward.local("0.0.0.0", 0, "www.capify.org", 80)
|
|
@@ -320,6 +320,11 @@ module Net
|
|
|
320
320
|
session.listen_to(client)
|
|
321
321
|
channel[:socket] = client
|
|
322
322
|
|
|
323
|
+
# Grant window only for data the local end has accepted, so a slow
|
|
324
|
+
# reader stalls the remote sender instead of filling our memory.
|
|
325
|
+
channel.grant_window_on_consume!
|
|
326
|
+
client.on_sent { |bytes| channel.consumed(bytes) }
|
|
327
|
+
|
|
323
328
|
channel.on_data do |ch, data|
|
|
324
329
|
debug { "data:#{data.length} on #{type} forwarded channel" }
|
|
325
330
|
ch[:socket].enqueue(data)
|
|
@@ -327,36 +332,36 @@ module Net
|
|
|
327
332
|
|
|
328
333
|
channel.on_eof do |ch|
|
|
329
334
|
debug { "eof #{type} on #{type} forwarded channel" }
|
|
330
|
-
|
|
331
|
-
ch[:socket].send_pending
|
|
332
|
-
ch[:socket].shutdown Socket::SHUT_WR
|
|
333
|
-
rescue IOError => e
|
|
334
|
-
if e.message =~ /closed/ then
|
|
335
|
-
debug { "epipe in on_eof => shallowing exception:#{e}" }
|
|
336
|
-
else
|
|
337
|
-
raise
|
|
338
|
-
end
|
|
339
|
-
rescue Errno::EPIPE => e
|
|
340
|
-
debug { "epipe in on_eof => shallowing exception:#{e}" }
|
|
341
|
-
rescue Errno::ENOTCONN => e
|
|
342
|
-
debug { "enotconn in on_eof => shallowing exception:#{e}" }
|
|
343
|
-
end
|
|
335
|
+
ch[:socket].shutdown_write_when_drained
|
|
344
336
|
end
|
|
345
337
|
|
|
346
338
|
channel.on_close do |ch|
|
|
347
339
|
debug { "closing #{type} forwarded channel" }
|
|
348
|
-
ch[:socket]
|
|
349
|
-
session.stop_listening_to(ch[:socket])
|
|
340
|
+
session.close_when_drained(ch[:socket])
|
|
350
341
|
end
|
|
351
342
|
|
|
352
343
|
channel.on_process do |ch|
|
|
353
|
-
|
|
344
|
+
socket = ch[:socket]
|
|
345
|
+
if socket.closed?
|
|
354
346
|
ch.info { "#{type} forwarded connection closed" }
|
|
347
|
+
session.stop_listening_to(socket)
|
|
355
348
|
ch.close
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
349
|
+
else
|
|
350
|
+
if socket.available > 0
|
|
351
|
+
data = socket.read_available(8192)
|
|
352
|
+
ch.debug { "read #{data.length} bytes from client, sending over #{type} forwarded connection" }
|
|
353
|
+
ch.send_data(data)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Like OpenSSH's channel_pre_open, only read more once the remote
|
|
357
|
+
# window can take what is already queued; writes to the socket keep
|
|
358
|
+
# flowing. ch.output is still unsent here because Channel#process
|
|
359
|
+
# runs this block before enqueue_pending_output.
|
|
360
|
+
if ch.output.length + socket.available < ch.remote_window_size.to_i
|
|
361
|
+
session.resume_reading(socket)
|
|
362
|
+
else
|
|
363
|
+
session.pause_reading(socket)
|
|
364
|
+
end
|
|
360
365
|
end
|
|
361
366
|
end
|
|
362
367
|
end
|
|
@@ -388,13 +393,13 @@ module Net
|
|
|
388
393
|
originator_address = packet.read_string
|
|
389
394
|
originator_port = packet.read_long
|
|
390
395
|
|
|
391
|
-
|
|
396
|
+
debug { "REMOTE 0: #{connected_port} #{connected_address} #{originator_address} #{originator_port}" }
|
|
392
397
|
remote = @remote_forwarded_ports[[connected_port, connected_address]]
|
|
393
398
|
if remote.nil?
|
|
394
399
|
raise Net::SSH::ChannelOpenFailed.new(1, "unknown request from remote forwarded connection on #{connected_address}:#{connected_port}")
|
|
395
400
|
end
|
|
396
401
|
|
|
397
|
-
|
|
402
|
+
debug { "REMOTE: #{remote.host} #{remote.port}" }
|
|
398
403
|
client = TCPSocket.new(remote.host, remote.port)
|
|
399
404
|
info { "connected #{connected_address}:#{connected_port} originator #{originator_address}:#{originator_port}" }
|
|
400
405
|
|
|
@@ -405,11 +410,17 @@ module Net
|
|
|
405
410
|
|
|
406
411
|
# The callback used when an auth-agent channel is requested by the server.
|
|
407
412
|
def auth_agent_channel(session, channel, packet)
|
|
413
|
+
# Like OpenSSH, only relay the local agent when forwarding was requested (GHSA-qpg5-fx6p-8cq3).
|
|
414
|
+
unless session.options[:forward_agent] || @agent_forwarded
|
|
415
|
+
error { "server tried agent forwarding, which was not requested; refusing" }
|
|
416
|
+
raise Net::SSH::ChannelOpenFailed.new(1, "agent forwarding was not requested")
|
|
417
|
+
end
|
|
418
|
+
|
|
408
419
|
info { "opening auth-agent channel" }
|
|
409
420
|
channel[:invisible] = true
|
|
410
421
|
|
|
411
422
|
begin
|
|
412
|
-
agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory])
|
|
423
|
+
agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory], session.options[:identity_agent])
|
|
413
424
|
if (agent.socket.is_a? ::IO)
|
|
414
425
|
prepare_client(agent.socket, channel, :agent)
|
|
415
426
|
else
|
|
@@ -57,8 +57,9 @@ module Net
|
|
|
57
57
|
return false unless script.next(:first)
|
|
58
58
|
|
|
59
59
|
if script.next(:first).remote?
|
|
60
|
-
|
|
61
|
-
self.
|
|
60
|
+
current_pos = pos
|
|
61
|
+
self.string = "#{self.string}#{script.next}"
|
|
62
|
+
self.pos = current_pos
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
return true
|
data/lib/net/ssh/test/socket.rb
CHANGED
data/lib/net/ssh/test.rb
CHANGED
|
@@ -73,7 +73,8 @@ module Net
|
|
|
73
73
|
def transport(options = {})
|
|
74
74
|
@transport ||= Net::SSH::Transport::Session.new(
|
|
75
75
|
options[:host] || "localhost",
|
|
76
|
-
options.merge(kex: "test", host_key: "ssh-rsa",
|
|
76
|
+
options.merge(kex: "test", host_key: "ssh-rsa", encryption: "+none", hmac: "+none",
|
|
77
|
+
append_all_supported_algorithms: true, verify_host_key: :never, proxy: socket(options))
|
|
77
78
|
)
|
|
78
79
|
end
|
|
79
80
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'net/ssh/transport/hmac/abstract'
|
|
2
|
+
require 'net/ssh/transport/gcm_cipher'
|
|
3
|
+
|
|
4
|
+
module Net::SSH::Transport
|
|
5
|
+
## Implements the aes128-gcm@openssh cipher
|
|
6
|
+
class AES128_GCM
|
|
7
|
+
extend ::Net::SSH::Transport::GCMCipher
|
|
8
|
+
|
|
9
|
+
## Implicit HMAC, do need to do anything
|
|
10
|
+
class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract
|
|
11
|
+
def aead
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def key_length
|
|
16
|
+
16
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def implicit_mac
|
|
21
|
+
ImplicitHMac.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def algo_name
|
|
25
|
+
'aes-128-gcm'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def name
|
|
29
|
+
'aes128-gcm@openssh.com'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# --- RFC 5647 ---
|
|
34
|
+
# K_LEN AES key length 16 octets
|
|
35
|
+
#
|
|
36
|
+
def self.key_length
|
|
37
|
+
16
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'net/ssh/transport/hmac/abstract'
|
|
2
|
+
require 'net/ssh/transport/gcm_cipher'
|
|
3
|
+
|
|
4
|
+
module Net::SSH::Transport
|
|
5
|
+
## Implements the aes256-gcm@openssh cipher
|
|
6
|
+
class AES256_GCM
|
|
7
|
+
extend ::Net::SSH::Transport::GCMCipher
|
|
8
|
+
|
|
9
|
+
## Implicit HMAC, do need to do anything
|
|
10
|
+
class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract
|
|
11
|
+
def aead
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def key_length
|
|
16
|
+
32
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def implicit_mac
|
|
21
|
+
ImplicitHMac.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def algo_name
|
|
25
|
+
'aes-256-gcm'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def name
|
|
29
|
+
'aes256-gcm@openssh.com'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# --- RFC 5647 ---
|
|
34
|
+
# K_LEN AES key length 32 octets
|
|
35
|
+
#
|
|
36
|
+
def self.key_length
|
|
37
|
+
32
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|