Chris Roberts 5a7fd6b302 Inspect guest for supported key types
Updates the SSH communicator to inspect the guest sshd configuration for
supported key types when creating a new key to replace the default
insecure public key. If the guest cannot be inspected, the connection
will be inspected. If the connection cannot be inspected, it will simply
fall back to the original behavior of using an rsa type key.
2024-01-18 11:53:00 -08:00

898 lines
33 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require 'etc'
require 'logger'
require 'pathname'
require 'stringio'
require 'thread'
require 'timeout'
require 'log4r'
require 'net/ssh'
require 'net/ssh/proxy/command'
require 'net/scp'
require 'vagrant/util/ansi_escape_code_remover'
require 'vagrant/util/file_mode'
require 'vagrant/util/keypair'
require 'vagrant/util/platform'
require 'vagrant/util/retryable'
module VagrantPlugins
module CommunicatorSSH
# This class provides communication with the VM via SSH.
class Communicator < Vagrant.plugin("2", :communicator)
READY_COMMAND=""
# Marker for start of PTY enabled command output
PTY_DELIM_START = "bccbb768c119429488cfd109aacea6b5-pty"
# Marker for end of PTY enabled command output
PTY_DELIM_END = "bccbb768c119429488cfd109aacea6b5-pty"
# Marker for start of regular command output
CMD_GARBAGE_MARKER = "41e57d38-b4f7-4e46-9c38-13873d338b86-vagrant-ssh"
# These are the exceptions that we retry because they represent
# errors that are generally fixed from a retry and don't
# necessarily represent immediate failure cases.
SSH_RETRY_EXCEPTIONS = [
Errno::EACCES,
Errno::EADDRINUSE,
Errno::ECONNABORTED,
Errno::ECONNREFUSED,
Errno::ECONNRESET,
Errno::ENETUNREACH,
Errno::EHOSTUNREACH,
Errno::EPIPE,
Net::SSH::Disconnect,
Timeout::Error
]
include Vagrant::Util::ANSIEscapeCodeRemover
include Vagrant::Util::Retryable
def self.match?(machine)
# All machines are currently expected to have SSH.
true
end
def initialize(machine)
@lock = Mutex.new
@machine = machine
@logger = Log4r::Logger.new("vagrant::communication::ssh")
@connection = nil
@inserted_key = false
end
def wait_for_ready(timeout)
Timeout.timeout(timeout) do
# Wait for ssh_info to be ready
ssh_info = nil
while true
ssh_info = @machine.ssh_info
break if ssh_info
sleep(0.5)
end
# Got it! Let the user know what we're connecting to.
if !@ssh_info_notification
@machine.ui.detail("SSH address: #{ssh_info[:host]}:#{ssh_info[:port]}")
@machine.ui.detail("SSH username: #{ssh_info[:username]}")
ssh_auth_type = "private key"
ssh_auth_type = "password" if ssh_info[:password]
@machine.ui.detail("SSH auth method: #{ssh_auth_type}")
@ssh_info_notification = true
end
previous_messages = {}
while true
message = nil
begin
begin
connect(retries: 1)
return true if ready?
rescue Vagrant::Errors::VagrantError => e
@logger.info("SSH not ready: #{e.inspect}")
raise
end
rescue Vagrant::Errors::SSHConnectionTimeout
message = "Connection timeout."
rescue Vagrant::Errors::SSHAuthenticationFailed
message = "Authentication failure."
rescue Vagrant::Errors::SSHDisconnected
message = "Remote connection disconnect."
rescue Vagrant::Errors::SSHConnectionRefused
message = "Connection refused."
rescue Vagrant::Errors::SSHConnectionReset
message = "Connection reset."
rescue Vagrant::Errors::SSHConnectionAborted
message = "Connection aborted."
rescue Vagrant::Errors::SSHHostDown
message = "Host appears down."
rescue Vagrant::Errors::SSHNoRoute
message = "Host unreachable."
rescue Vagrant::Errors::SSHInvalidShell
raise
rescue Vagrant::Errors::SSHKeyTypeNotSupported
raise
rescue Vagrant::Errors::SSHKeyTypeNotSupportedByServer
raise
rescue Vagrant::Errors::SSHKeyBadOwner
raise
rescue Vagrant::Errors::SSHKeyBadPermissions
raise
rescue Vagrant::Errors::SSHInsertKeyUnsupported
raise
rescue Vagrant::Errors::VagrantError => e
# Ignore it, SSH is not ready, some other error.
end
# If we have a message to show, then show it. We don't show
# repeated messages unless they've been repeating longer than
# 10 seconds.
if message
message_at = Time.now.to_f
show_message = true
if previous_messages[message]
show_message = (message_at - previous_messages[message]) > 10.0
end
if show_message
@machine.ui.detail("Warning: #{message} Retrying...")
previous_messages[message] = message_at
end
end
end
end
rescue Timeout::Error
return false
end
def ready?
@logger.debug("Checking whether SSH is ready...")
# Attempt to connect. This will raise an exception if it fails.
begin
connect
@logger.info("SSH is ready!")
rescue Vagrant::Errors::VagrantError => e
# We catch a `VagrantError` which would signal that something went
# wrong expectedly in the `connect`, which means we didn't connect.
@logger.info("SSH not up: #{e.inspect}")
return false
end
# Verify the shell is valid
if execute(self.class.const_get(:READY_COMMAND), error_check: false) != 0
raise Vagrant::Errors::SSHInvalidShell
end
# If we're already attempting to switch out the SSH key, then
# just return that we're ready (for Machine#guest).
@lock.synchronize do
return true if @inserted_key || !machine_config_ssh.insert_key
@inserted_key = true
end
# If we used a password, then insert the insecure key
ssh_info = @machine.ssh_info
return if ssh_info.nil?
insert = ssh_info[:password] && ssh_info[:private_key_path].empty?
ssh_info[:private_key_path].each do |pk|
if insecure_key?(pk)
insert = true
@machine.ui.detail("\n"+I18n.t("vagrant.inserting_insecure_detected"))
break
end
end
if insert
# If we don't have the power to insert/remove keys, then its an error
cap = @machine.guest.capability?(:insert_public_key) &&
@machine.guest.capability?(:remove_public_key)
raise Vagrant::Errors::SSHInsertKeyUnsupported if !cap
key_type = machine_config_ssh.key_type
begin
# If the key type is set to `:auto` check for supported type. Otherwise
# ensure that the key type is supported by the guest
if key_type == :auto
key_type = catch(:key_type) do
begin
Vagrant::Util::Keypair::PREFER_KEY_TYPES.each do |type_name, type|
throw :key_type, type if supports_key_type?(type_name)
end
nil
rescue => err
@logger.warn("Failed to check key types server supports: #{err}")
nil
end
end
@logger.debug("Detected key type for new private key: #{key_type}")
# If no key type was discovered, default to rsa
if key_type.nil?
@logger.debug("Failed to detect supported key type in: #{supported_key_types.join(", ")}")
available_types = supported_key_types.map { |t|
next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t)
"#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})"
}.compact.join(", ")
raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer,
requested_key_type: ":auto",
available_key_types: available_types
end
else
type_name = Vagrant::Util::Keypair::PREFER_KEY_TYPES.key(key_type)
if !supports_key_type?(type_name)
available_types = supported_key_types.map { |t|
next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t)
"#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})"
}.compact.join(", ")
raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer,
requested_key_type: "#{type_name} (#{key_type})",
available_key_types: available_types
end
end
rescue ServerDataError
@logger.warn("failed to load server data for key type check")
if key_type.nil? || key_type == :auto
@logger.warn("defaulting key type to :rsa due to failed server data loading")
key_type = :rsa
end
end
@logger.info("Creating new ssh keypair (type: #{key_type.inspect})")
_pub, priv, openssh = Vagrant::Util::Keypair.create(type: key_type)
@logger.info("Inserting key to avoid password: #{openssh}")
@machine.ui.detail("\n"+I18n.t("vagrant.inserting_random_key"))
@machine.guest.capability(:insert_public_key, openssh)
# Write out the private key in the data dir so that the
# machine automatically picks it up.
@machine.data_dir.join("private_key").open("wb+") do |f|
f.write(priv)
end
# Adjust private key file permissions if host provides capability
if @machine.env.host.capability?(:set_ssh_key_permissions)
@machine.env.host.capability(:set_ssh_key_permissions, @machine.data_dir.join("private_key"))
end
# Remove the old key if it exists
@machine.ui.detail(I18n.t("vagrant.inserting_remove_key"))
@machine.guest.capability(
:remove_public_key,
Vagrant.source_root.join("keys", "vagrant.pub").read.chomp)
# Done, restart.
@machine.ui.detail(I18n.t("vagrant.inserted_key"))
@connection.close
@connection = nil
return ready?
end
# If we reached this point then we successfully connected
true
end
def execute(command, opts=nil, &block)
opts = {
error_check: true,
error_class: Vagrant::Errors::VagrantError,
error_key: :ssh_bad_exit_status,
good_exit: 0,
command: command,
shell: nil,
sudo: false,
force_raw: false
}.merge(opts || {})
opts[:good_exit] = Array(opts[:good_exit])
# Connect via SSH and execute the command in the shell.
stdout = ""
stderr = ""
exit_status = connect do |connection|
shell_opts = {
sudo: opts[:sudo],
shell: opts[:shell],
force_raw: opts[:force_raw]
}
shell_execute(connection, command, **shell_opts) do |type, data|
if type == :stdout
stdout += data
elsif type == :stderr
stderr += data
end
block.call(type, data) if block
end
end
# Check for any errors
if opts[:error_check] && !opts[:good_exit].include?(exit_status)
# The error classes expect the translation key to be _key,
# but that makes for an ugly configuration parameter, so we
# set it here from `error_key`
error_opts = opts.merge(
_key: opts[:error_key],
stdout: stdout,
stderr: stderr
)
raise opts[:error_class], error_opts
end
# Return the exit status
exit_status
end
def sudo(command, opts=nil, &block)
# Run `execute` but with the `sudo` option.
opts = { sudo: true }.merge(opts || {})
execute(command, opts, &block)
end
def download(from, to=nil)
@logger.debug("Downloading: #{from} to #{to}")
scp_connect do |scp|
scp.download!(from, to)
end
end
def test(command, opts=nil)
opts = { error_check: false }.merge(opts || {})
execute(command, opts) == 0
end
def upload(from, to)
@logger.debug("Uploading: #{from} to #{to}")
if File.directory?(from)
if from.end_with?(".")
@logger.debug("Uploading directory contents of: #{from}")
from = from.sub(/\.$/, "")
else
@logger.debug("Uploading full directory container of: #{from}")
to = File.join(to, File.basename(File.expand_path(from)))
end
end
scp_connect do |scp|
uploader = lambda do |path, remote_dest=nil|
if File.directory?(path)
dest = File.join(to, path.sub(/^#{Regexp.escape(from)}/, ""))
create_remote_directory(dest)
Dir.new(path).each do |entry|
next if entry == "." || entry == ".."
full_path = File.join(path, entry)
create_remote_directory(dest)
uploader.call(full_path, dest)
end
else
if remote_dest
dest = File.join(remote_dest, File.basename(path))
else
dest = to
if to.end_with?(File::SEPARATOR)
dest = File.join(to, File.basename(path))
end
end
@logger.debug("Ensuring remote directory exists for destination upload")
create_remote_directory(File.dirname(dest))
@logger.debug("Uploading file #{path} to remote #{dest}")
upload_file = File.open(path, "rb")
begin
scp.upload!(upload_file, dest)
ensure
upload_file.close
end
end
end
uploader.call(from)
end
rescue RuntimeError => e
# Net::SCP raises a runtime error for this so the only way we have
# to really catch this exception is to check the message to see if
# it is something we care about. If it isn't, we re-raise.
raise if e.message !~ /Permission denied/
# Otherwise, it is a permission denied, so let's raise a proper
# exception
raise Vagrant::Errors::SCPPermissionDenied,
from: from.to_s,
to: to.to_s
end
def reset!
if @connection
@connection.close
@connection = nil
end
@ssh_info_notification = true # suppress ssh info output
wait_for_ready(5)
end
def generate_environment_export(env_key, env_value)
template = machine_config_ssh.export_command_template
template.sub("%ENV_KEY%", env_key).sub("%ENV_VALUE%", env_value) + "\n"
end
protected
# Opens an SSH connection and yields it to a block.
def connect(**opts)
if @connection && !@connection.closed?
# There is a chance that the socket is closed despite us checking
# 'closed?' above. To test this we need to send data through the
# socket.
#
# We wrap the check itself in a 5 second timeout because there
# are some cases where this will just hang.
begin
Timeout.timeout(5) do
@connection.exec!("")
end
rescue Exception => e
@logger.info("Connection errored, not re-using. Will reconnect.")
@logger.debug(e.inspect)
@connection = nil
end
# If the @connection is still around, then it is valid,
# and we use it.
if @connection
@logger.debug("Re-using SSH connection.")
return yield @connection if block_given?
return
end
end
# Get the SSH info for the machine, raise an exception if the
# provider is saying that SSH is not ready.
ssh_info = @machine.ssh_info
raise Vagrant::Errors::SSHNotReady if ssh_info.nil?
# Default some options
opts[:retries] = 5 if !opts.key?(:retries)
# Set some valid auth methods. We disable the auth methods that
# we're not using if we don't have the right auth info.
auth_methods = ["none", "hostbased"]
auth_methods << "publickey" if ssh_info[:private_key_path]
auth_methods << "password" if ssh_info[:password]
# Build the options we'll use to initiate the connection via Net::SSH
common_connect_opts = {
auth_methods: auth_methods,
config: false,
forward_agent: ssh_info[:forward_agent],
send_env: ssh_info[:forward_env],
keys_only: ssh_info[:keys_only],
verify_host_key: ssh_info[:verify_host_key],
password: ssh_info[:password],
port: ssh_info[:port],
timeout: ssh_info[:connect_timeout],
user_known_hosts_file: [],
verbose: :debug
}
# Connect to SSH, giving it a few tries
connection = nil
begin
timeout = 60
@logger.info("Attempting SSH connection...")
connection = retryable(tries: opts[:retries], on: SSH_RETRY_EXCEPTIONS) do
Timeout.timeout(timeout) do
begin
# This logger will get the Net-SSH log data for us.
ssh_logger_io = StringIO.new
ssh_logger = Logger.new(ssh_logger_io)
# Setup logging for connections
connect_opts = common_connect_opts.dup
connect_opts[:logger] = ssh_logger
if ssh_info[:private_key_path]
connect_opts[:keys] = ssh_info[:private_key_path]
end
if ssh_info[:proxy_command]
connect_opts[:proxy] = Net::SSH::Proxy::Command.new(ssh_info[:proxy_command])
end
if ssh_info[:config]
connect_opts[:config] = ssh_info[:config]
end
if ssh_info[:remote_user]
connect_opts[:remote_user] = ssh_info[:remote_user]
end
if @machine.config.ssh.keep_alive
connect_opts[:keepalive] = true
connect_opts[:keepalive_interval] = 5
end
@logger.info("Attempting to connect to SSH...")
@logger.info(" - Host: #{ssh_info[:host]}")
@logger.info(" - Port: #{ssh_info[:port]}")
@logger.info(" - Username: #{ssh_info[:username]}")
@logger.info(" - Password? #{!!ssh_info[:password]}")
@logger.info(" - Key Path: #{ssh_info[:private_key_path]}")
@logger.debug(" - connect_opts: #{connect_opts}")
Net::SSH.start(ssh_info[:host], ssh_info[:username], **connect_opts)
ensure
# Make sure we output the connection log
@logger.debug("== Net-SSH connection debug-level log START ==")
@logger.debug(ssh_logger_io.string)
@logger.debug("== Net-SSH connection debug-level log END ==")
end
end
end
rescue Errno::EACCES
# This happens on connect() for unknown reasons yet...
raise Vagrant::Errors::SSHConnectEACCES
rescue Errno::ETIMEDOUT, Timeout::Error
# This happens if we continued to timeout when attempting to connect.
raise Vagrant::Errors::SSHConnectionTimeout
rescue Net::SSH::AuthenticationFailed
# This happens if authentication failed. We wrap the error in our
# own exception.
raise Vagrant::Errors::SSHAuthenticationFailed
rescue Net::SSH::Disconnect
# This happens if the remote server unexpectedly closes the
# connection. This is usually raised when SSH is running on the
# other side but can't properly setup a connection. This is
# usually a server-side issue.
raise Vagrant::Errors::SSHDisconnected
rescue Errno::ECONNREFUSED
# This is raised if we failed to connect the max amount of times
raise Vagrant::Errors::SSHConnectionRefused
rescue Errno::ECONNRESET
# This is raised if we failed to connect the max number of times
# due to an ECONNRESET.
raise Vagrant::Errors::SSHConnectionReset
rescue Errno::ECONNABORTED
# This is raised if we failed to connect the max number of times
# due to an ECONNABORTED
raise Vagrant::Errors::SSHConnectionAborted
rescue Errno::EHOSTDOWN
# This is raised if we get an ICMP DestinationUnknown error.
raise Vagrant::Errors::SSHHostDown
rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
# This is raised if we can't work out how to route traffic.
raise Vagrant::Errors::SSHNoRoute
rescue Net::SSH::Exception => e
# This is an internal error in Net::SSH
raise Vagrant::Errors::NetSSHException, message: e.message
rescue NotImplementedError
# This is raised if a private key type that Net-SSH doesn't support
# is used. Show a nicer error.
raise Vagrant::Errors::SSHKeyTypeNotSupported
end
@connection = connection
@connection_ssh_info = ssh_info
# Yield the connection that is ready to be used and
# return the value of the block
return yield connection if block_given?
end
# The shell wrapper command used in shell_execute defined by
# the sudo and shell options.
def shell_cmd(opts)
sudo = opts[:sudo]
shell = opts[:shell]
# Determine the shell to execute. Prefer the explicitly passed in shell
# over the default configured shell. If we are using `sudo` then we
# need to wrap the shell in a `sudo` call.
cmd = machine_config_ssh.shell
cmd = shell if shell
cmd = machine_config_ssh.sudo_command.gsub("%c", cmd) if sudo
cmd
end
# Executes the command on an SSH connection within a login shell.
def shell_execute(connection, command, **opts)
opts = {
sudo: false,
shell: nil
}.merge(opts)
sudo = opts[:sudo]
@logger.info("Execute: #{command} (sudo=#{sudo.inspect})")
exit_status = nil
# These variables are used to scrub PTY output if we're in a PTY
pty = false
pty_stdout = ""
# Open the channel so we can execute or command
channel = connection.open_channel do |ch|
if machine_config_ssh.pty
ch.request_pty do |ch2, success|
pty = success && command != ""
if success
@logger.debug("pty obtained for connection")
else
@logger.warn("failed to obtain pty, will try to continue anyways")
end
end
end
marker_found = false
data_buffer = ''
stderr_marker_found = false
stderr_data_buffer = ''
ch.exec(shell_cmd(opts)) do |ch2, _|
# Setup the channel callbacks so we can get data and exit status
ch2.on_data do |ch3, data|
# Filter out the clear screen command
data = remove_ansi_escape_codes(data)
if pty
pty_stdout << data
else
if !marker_found
data_buffer << data
marker_index = data_buffer.index(CMD_GARBAGE_MARKER)
if marker_index
marker_found = true
data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size)
data.replace(data_buffer)
data_buffer = nil
end
end
if block_given? && marker_found && !data.empty?
yield :stdout, data
end
end
end
ch2.on_extended_data do |ch3, type, data|
# Filter out the clear screen command
data = remove_ansi_escape_codes(data)
@logger.debug("stderr: #{data}")
if !stderr_marker_found
stderr_data_buffer << data
marker_index = stderr_data_buffer.index(CMD_GARBAGE_MARKER)
if marker_index
stderr_marker_found = true
stderr_data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size)
data.replace(stderr_data_buffer)
stderr_data_buffer = nil
end
end
if block_given? && stderr_marker_found && !data.empty?
yield :stderr, data
end
end
ch2.on_request("exit-status") do |ch3, data|
exit_status = data.read_long
@logger.debug("Exit status: #{exit_status}")
# Close the channel, since after the exit status we're
# probably done. This fixes up issues with hanging.
ch.close
end
# Set the terminal
ch2.send_data(generate_environment_export("TERM", "vt100"))
# Set SSH_AUTH_SOCK if we are in sudo and forwarding agent.
# This is to work around often misconfigured boxes where
# the SSH_AUTH_SOCK env var is not preserved.
if @connection_ssh_info[:forward_agent] && sudo
auth_socket = ""
execute("echo; printf $SSH_AUTH_SOCK") do |type, data|
if type == :stdout
auth_socket += data
end
end
if auth_socket != ""
# Make sure we only read the last line which should be
# the $SSH_AUTH_SOCK env var we printed.
auth_socket = auth_socket.split("\n").last.to_s.chomp
end
if auth_socket == ""
@logger.warn("No SSH_AUTH_SOCK found despite forward_agent being set.")
else
@logger.info("Setting SSH_AUTH_SOCK remotely: #{auth_socket}")
ch2.send_data(generate_environment_export("SSH_AUTH_SOCK", auth_socket))
end
end
# Output the command. If we're using a pty we have to do
# a little dance to make sure we get all the output properly
# without the cruft added from pty mode.
if pty
data = "stty raw -echo\n"
data += generate_environment_export("PS1", "")
data += generate_environment_export("PS2", "")
data += generate_environment_export("PROMPT_COMMAND", "")
data += "printf #{PTY_DELIM_START}\n"
data += "#{command}\n"
data += "exitcode=$?\n"
data += "printf #{PTY_DELIM_END}\n"
data += "exit $exitcode\n"
data = data.force_encoding('ASCII-8BIT')
ch2.send_data(data)
else
ch2.send_data("printf '#{CMD_GARBAGE_MARKER}'\n(>&2 printf '#{CMD_GARBAGE_MARKER}')\n#{command}\n".force_encoding('ASCII-8BIT'))
# Remember to exit or this channel will hang open
ch2.send_data("exit\n")
end
# Send eof to let server know we're done
ch2.eof!
end
end
begin
# Wait for the channel to complete
begin
channel.wait
rescue Errno::ECONNRESET, IOError
@logger.info(
"SSH connection unexpected closed. Assuming reboot or something.")
exit_status = 0
pty = false
rescue Net::SSH::ChannelOpenFailed
raise Vagrant::Errors::SSHChannelOpenFail
rescue Net::SSH::Disconnect
raise Vagrant::Errors::SSHDisconnected
end
end
# If we're in a PTY, we now finally parse the output
if pty
@logger.debug("PTY stdout: #{pty_stdout}")
if !pty_stdout.include?(PTY_DELIM_START) || !pty_stdout.include?(PTY_DELIM_END)
@logger.error("PTY stdout doesn't include delims")
raise Vagrant::Errors::SSHInvalidShell.new
end
data = pty_stdout[/.*#{PTY_DELIM_START}(.*?)#{PTY_DELIM_END}/m, 1]
data ||= ""
@logger.debug("PTY stdout parsed: #{data}")
yield :stdout, data if block_given?
end
if !exit_status
@logger.debug("Exit status: #{exit_status.inspect}")
raise Vagrant::Errors::SSHNoExitStatus
end
# Return the final exit status
return exit_status
end
# Opens an SCP connection and yields it so that you can download
# and upload files.
def scp_connect
# Connect to SCP and yield the SCP object
connect do |connection|
scp = Net::SCP.new(connection)
return yield scp
end
rescue Net::SCP::Error => e
# If we get the exit code of 127, then this means SCP is unavailable.
raise Vagrant::Errors::SCPUnavailable if e.message =~ /\(127\)/
# Otherwise, just raise the error up
raise
end
# This will test whether path is the Vagrant insecure private key.
#
# @param [String] path
def insecure_key?(path)
return false if !path
return false if !File.file?(path)
Dir.glob(Vagrant.source_root.join("keys", "vagrant.key.*")).any? do |source_path|
File.read(path).chomp == File.read(source_path).chomp
end
end
def create_remote_directory(dir)
execute("mkdir -p \"#{dir}\"")
end
def machine_config_ssh
@machine.config.ssh
end
protected
class ServerDataError < StandardError; end
# Check if server supports given key type
#
# @param [String, Symbol] type Key type
# @return [Boolean]
# @note This does not use a stable API and may be subject
# to unexpected breakage on net-ssh updates
def supports_key_type?(type)
if @connection.nil?
raise Vagrant::Errors::SSHNotReady
end
supported_key_types.include?(type.to_s)
end
def supported_key_types
return @supported_key_types if @supported_key_types
if @connection.nil?
raise Vagrant::Errors::SSHNotReady
end
list = ""
result = sudo("sshd -T | grep key", {error_check: false}) do |type, data|
list << data
end
# If the command failed, attempt to extract some supported
# key information from within net-ssh
if result != 0
server_data = @connection.
transport&.
algorithms&.
instance_variable_get(:@server_data)
if server_data.nil?
@logger.warn("No server data available for key type support check")
raise ServerDataError, "no data available"
end
if !server_data.is_a?(Hash)
@logger.warn("Server data is not expected type (expecting Hash, got #{server_data.class})")
raise ServerDataError, "unexpected type encountered (expecting Hash, got #{server_data.class})"
end
@logger.debug("server supported key type list (extracted from connection server info using host key): #{server_data[:host_key]}")
return @supported_key_types = server_data[:host_key]
end
# Convert the options into a Hash for easy access
opts = Hash[*list.split("\n").map{|line| line.split(" ", 2)}.flatten]
# Define the option names to check for in preferred order
# NOTE: pubkeyacceptedkeytypes has been renamed to pubkeyacceptedalgorithms
# ref: https://github.com/openssh/openssh-portable/commit/ee9c0da8035b3168e8e57c1dedc2d1b0daf00eec
["pubkeyacceptedalgorithms", "pubkeyacceptedkeytypes", "hostkeyalgorithms"].each do |opt_name|
next if !opts.key?(opt_name)
@supported_key_types = opts[opt_name].split(",")
@logger.debug("server supported key type list (using #{opt_name}): #{@supported_key_types}")
return @supported_key_types
end
# Still here means unable to determine key types
# so log what information was returned and toss
# and error
@logger.warn("failed to determine supported key types from remote inspection")
@logger.debug("data returned for supported key types remote inspection: #{list.inspect}")
raise ServerDataError, "no data available"
end
end
end
end