2017-10-29 09:45:21 +00:00
package hosts
import (
"fmt"
"net"
"net/http"
2018-07-11 15:22:38 +00:00
"strings"
2018-03-27 21:07:16 +00:00
"time"
2017-10-29 09:45:21 +00:00
2020-07-11 16:24:19 +00:00
"k8s.io/client-go/transport"
v3 "github.com/rancher/rke/types"
2017-10-29 09:45:21 +00:00
"golang.org/x/crypto/ssh"
)
2018-03-27 21:07:16 +00:00
const (
2018-07-10 19:21:27 +00:00
DockerDialerTimeout = 50
2018-03-27 21:07:16 +00:00
)
2017-12-16 03:38:15 +00:00
type DialerFactory func ( h * Host ) ( func ( network , address string ) ( net . Conn , error ) , error )
2017-12-11 18:28:08 +00:00
2017-12-16 03:38:15 +00:00
type dialer struct {
2018-03-06 00:52:43 +00:00
signer ssh . Signer
sshKeyString string
2019-01-28 13:54:00 +00:00
sshCertString string
2018-03-06 00:52:43 +00:00
sshAddress string
username string
netConn string
dockerSocket string
useSSHAgentAuth bool
2018-05-08 22:30:50 +00:00
bastionDialer * dialer
2017-10-29 09:45:21 +00:00
}
2018-11-07 23:54:08 +00:00
type DialersOptions struct {
DockerDialerFactory DialerFactory
LocalConnDialerFactory DialerFactory
2019-08-19 17:53:15 +00:00
K8sWrapTransport transport . WrapperFunc
2018-11-07 23:54:08 +00:00
}
2019-08-19 17:53:15 +00:00
func GetDialerOptions ( d , l DialerFactory , w transport . WrapperFunc ) DialersOptions {
2018-11-07 23:54:08 +00:00
return DialersOptions {
DockerDialerFactory : d ,
LocalConnDialerFactory : l ,
K8sWrapTransport : w ,
}
}
2018-02-26 21:27:51 +00:00
func newDialer ( h * Host , kind string ) ( * dialer , error ) {
2018-05-08 22:30:50 +00:00
// Check for Bastion host connection
var bastionDialer * dialer
if len ( h . BastionHost . Address ) > 0 {
bastionDialer = & dialer {
sshAddress : fmt . Sprintf ( "%s:%s" , h . BastionHost . Address , h . BastionHost . Port ) ,
username : h . BastionHost . User ,
sshKeyString : h . BastionHost . SSHKey ,
2019-01-28 13:54:00 +00:00
sshCertString : h . BastionHost . SSHCert ,
2018-05-08 22:30:50 +00:00
netConn : "tcp" ,
useSSHAgentAuth : h . SSHAgentAuth ,
}
2018-08-28 19:44:06 +00:00
if bastionDialer . sshKeyString == "" && ! bastionDialer . useSSHAgentAuth {
2018-06-25 19:01:02 +00:00
var err error
bastionDialer . sshKeyString , err = privateKeyPath ( h . BastionHost . SSHKeyPath )
if err != nil {
return nil , err
}
2019-01-28 13:54:00 +00:00
if bastionDialer . sshCertString == "" && len ( h . BastionHost . SSHCertPath ) > 0 {
bastionDialer . sshCertString , err = certificatePath ( h . BastionHost . SSHCertPath )
if err != nil {
return nil , err
}
}
2018-05-08 22:30:50 +00:00
}
}
2017-12-16 03:38:15 +00:00
dialer := & dialer {
2018-03-06 00:52:43 +00:00
sshAddress : fmt . Sprintf ( "%s:%s" , h . Address , h . Port ) ,
username : h . User ,
dockerSocket : h . DockerSocket ,
sshKeyString : h . SSHKey ,
2019-01-28 13:54:00 +00:00
sshCertString : h . SSHCert ,
2018-03-06 00:52:43 +00:00
netConn : "unix" ,
useSSHAgentAuth : h . SSHAgentAuth ,
2018-05-08 22:30:50 +00:00
bastionDialer : bastionDialer ,
2017-12-11 18:28:08 +00:00
}
2017-10-29 09:45:21 +00:00
2018-08-28 19:44:06 +00:00
if dialer . sshKeyString == "" && ! dialer . useSSHAgentAuth {
2018-06-25 19:01:02 +00:00
var err error
dialer . sshKeyString , err = privateKeyPath ( h . SSHKeyPath )
if err != nil {
return nil , err
}
2019-01-28 13:54:00 +00:00
if dialer . sshCertString == "" && len ( h . SSHCertPath ) > 0 {
dialer . sshCertString , err = certificatePath ( h . SSHCertPath )
if err != nil {
return nil , err
}
}
2017-12-19 22:18:27 +00:00
}
2018-02-26 21:27:51 +00:00
switch kind {
case "network" , "health" :
dialer . netConn = "tcp"
}
if len ( dialer . dockerSocket ) == 0 {
dialer . dockerSocket = "/var/run/docker.sock"
2017-12-19 22:18:27 +00:00
}
2018-02-26 21:27:51 +00:00
return dialer , nil
}
func SSHFactory ( h * Host ) ( func ( network , address string ) ( net . Conn , error ) , error ) {
dialer , err := newDialer ( h , "docker" )
return dialer . Dial , err
}
func LocalConnFactory ( h * Host ) ( func ( network , address string ) ( net . Conn , error ) , error ) {
dialer , err := newDialer ( h , "network" )
return dialer . Dial , err
2017-12-19 22:18:27 +00:00
}
func ( d * dialer ) DialDocker ( network , addr string ) ( net . Conn , error ) {
2018-02-26 21:27:51 +00:00
return d . Dial ( network , addr )
}
func ( d * dialer ) DialLocalConn ( network , addr string ) ( net . Conn , error ) {
return d . Dial ( network , addr )
}
func ( d * dialer ) Dial ( network , addr string ) ( net . Conn , error ) {
2018-05-08 22:30:50 +00:00
var conn * ssh . Client
var err error
if d . bastionDialer != nil {
conn , err = d . getBastionHostTunnelConn ( )
} else {
conn , err = d . getSSHTunnelConnection ( )
}
2017-10-29 09:45:21 +00:00
if err != nil {
2018-07-11 15:22:38 +00:00
if strings . Contains ( err . Error ( ) , "no key found" ) {
return nil , fmt . Errorf ( "Unable to access node with address [%s] using SSH. Please check if the configured key or specified key file is a valid SSH Private Key. Error: %v" , d . sshAddress , err )
} else if strings . Contains ( err . Error ( ) , "no supported methods remain" ) {
return nil , fmt . Errorf ( "Unable to access node with address [%s] using SSH. Please check if you are able to SSH to the node using the specified SSH Private Key and if you have configured the correct SSH username. Error: %v" , d . sshAddress , err )
} else if strings . Contains ( err . Error ( ) , "cannot decode encrypted private keys" ) {
return nil , fmt . Errorf ( "Unable to access node with address [%s] using SSH. Using encrypted private keys is only supported using ssh-agent. Please configure the option `ssh_agent_auth: true` in the configuration file or use --ssh-agent-auth as a parameter when running RKE. This will use the `SSH_AUTH_SOCK` environment variable. Error: %v" , d . sshAddress , err )
2018-07-27 18:36:33 +00:00
} else if strings . Contains ( err . Error ( ) , "operation timed out" ) {
return nil , fmt . Errorf ( "Unable to access node with address [%s] using SSH. Please check if the node is up and is accepting SSH connections or check network policies and firewall rules. Error: %v" , d . sshAddress , err )
2018-07-11 15:22:38 +00:00
}
2018-02-26 21:27:51 +00:00
return nil , fmt . Errorf ( "Failed to dial ssh using address [%s]: %v" , d . sshAddress , err )
2017-10-29 09:45:21 +00:00
}
2018-02-26 21:27:51 +00:00
// Docker Socket....
if d . netConn == "unix" {
addr = d . dockerSocket
network = d . netConn
2017-10-29 09:45:21 +00:00
}
2018-02-26 21:27:51 +00:00
remote , err := conn . Dial ( network , addr )
2017-10-29 09:45:21 +00:00
if err != nil {
2018-07-20 18:39:57 +00:00
if strings . Contains ( err . Error ( ) , "connect failed" ) {
return nil , fmt . Errorf ( "Unable to access the service on %s. The service might be still starting up. Error: %v" , addr , err )
} else if strings . Contains ( err . Error ( ) , "administratively prohibited" ) {
return nil , fmt . Errorf ( "Unable to access the Docker socket (%s). Please check if the configured user can execute `docker ps` on the node, and if the SSH server version is at least version 6.7 or higher. If you are using RedHat/CentOS, you can't use the user `root`. Please refer to the documentation for more instructions. Error: %v" , addr , err )
}
return nil , fmt . Errorf ( "Failed to dial to %s: %v" , addr , err )
2017-10-29 09:45:21 +00:00
}
return remote , err
}
2017-12-16 03:38:15 +00:00
2018-02-26 21:27:51 +00:00
func ( d * dialer ) getSSHTunnelConnection ( ) ( * ssh . Client , error ) {
2019-01-28 13:54:00 +00:00
cfg , err := getSSHConfig ( d . username , d . sshKeyString , d . sshCertString , d . useSSHAgentAuth )
2017-12-19 22:18:27 +00:00
if err != nil {
return nil , fmt . Errorf ( "Error configuring SSH: %v" , err )
}
// Establish connection with SSH server
2018-02-26 21:27:51 +00:00
return ssh . Dial ( "tcp" , d . sshAddress , cfg )
2017-12-19 22:18:27 +00:00
}
2017-12-16 03:38:15 +00:00
func ( h * Host ) newHTTPClient ( dialerFactory DialerFactory ) ( * http . Client , error ) {
2018-02-26 21:27:51 +00:00
factory := dialerFactory
if factory == nil {
2017-12-16 03:38:15 +00:00
factory = SSHFactory
}
dialer , err := factory ( h )
if err != nil {
return nil , err
}
2018-03-27 21:07:16 +00:00
dockerDialerTimeout := time . Second * DockerDialerTimeout
2017-12-16 03:38:15 +00:00
return & http . Client {
Transport : & http . Transport {
2018-03-27 21:07:16 +00:00
Dial : dialer ,
TLSHandshakeTimeout : dockerDialerTimeout ,
IdleConnTimeout : dockerDialerTimeout ,
ResponseHeaderTimeout : dockerDialerTimeout ,
2017-12-16 03:38:15 +00:00
} ,
} , nil
}
2018-05-08 22:30:50 +00:00
func ( d * dialer ) getBastionHostTunnelConn ( ) ( * ssh . Client , error ) {
2019-01-28 13:54:00 +00:00
bastionCfg , err := getSSHConfig ( d . bastionDialer . username , d . bastionDialer . sshKeyString , d . bastionDialer . sshCertString , d . bastionDialer . useSSHAgentAuth )
2018-05-08 22:30:50 +00:00
if err != nil {
return nil , fmt . Errorf ( "Error configuring SSH for bastion host [%s]: %v" , d . bastionDialer . sshAddress , err )
}
bastionClient , err := ssh . Dial ( "tcp" , d . bastionDialer . sshAddress , bastionCfg )
if err != nil {
return nil , fmt . Errorf ( "Failed to connect to the bastion host [%s]: %v" , d . bastionDialer . sshAddress , err )
}
conn , err := bastionClient . Dial ( d . bastionDialer . netConn , d . sshAddress )
if err != nil {
return nil , fmt . Errorf ( "Failed to connect to the host [%s]: %v" , d . sshAddress , err )
}
2019-01-28 13:54:00 +00:00
cfg , err := getSSHConfig ( d . username , d . sshKeyString , d . sshCertString , d . useSSHAgentAuth )
2018-05-08 22:30:50 +00:00
if err != nil {
return nil , fmt . Errorf ( "Error configuring SSH for host [%s]: %v" , d . sshAddress , err )
}
newClientConn , channels , sshRequest , err := ssh . NewClientConn ( conn , d . sshAddress , cfg )
if err != nil {
return nil , fmt . Errorf ( "Failed to establish new ssh client conn [%s]: %v" , d . sshAddress , err )
}
return ssh . NewClient ( newClientConn , channels , sshRequest ) , nil
}
2019-08-19 17:53:15 +00:00
func BastionHostWrapTransport ( bastionHost v3 . BastionHost ) ( transport . WrapperFunc , error ) {
2018-05-08 22:30:50 +00:00
bastionDialer := & dialer {
sshAddress : fmt . Sprintf ( "%s:%s" , bastionHost . Address , bastionHost . Port ) ,
username : bastionHost . User ,
sshKeyString : bastionHost . SSHKey ,
2019-01-28 13:54:00 +00:00
sshCertString : bastionHost . SSHCert ,
2018-05-08 22:30:50 +00:00
netConn : "tcp" ,
useSSHAgentAuth : bastionHost . SSHAgentAuth ,
}
2019-09-26 14:06:40 +00:00
if bastionDialer . sshKeyString == "" && ! bastionDialer . useSSHAgentAuth {
2018-06-25 19:01:02 +00:00
var err error
bastionDialer . sshKeyString , err = privateKeyPath ( bastionHost . SSHKeyPath )
if err != nil {
return nil , err
}
2019-01-28 13:54:00 +00:00
}
if bastionDialer . sshCertString == "" && len ( bastionHost . SSHCertPath ) > 0 {
var err error
bastionDialer . sshCertString , err = certificatePath ( bastionHost . SSHCertPath )
if err != nil {
return nil , err
}
2018-05-08 22:30:50 +00:00
}
return func ( rt http . RoundTripper ) http . RoundTripper {
if ht , ok := rt . ( * http . Transport ) ; ok {
ht . DialContext = nil
ht . DialTLS = nil
ht . Dial = bastionDialer . Dial
}
return rt
2018-06-25 19:01:02 +00:00
} , nil
2018-05-08 22:30:50 +00:00
}