2024-07-19 00:21:33 +02:00
// Copyright 2024, Command Line Inc.
2024-07-16 03:00:10 +02:00
// SPDX-License-Identifier: Apache-2.0
package remote
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"fmt"
"log"
2024-10-25 21:14:40 +02:00
"math"
2024-07-16 03:00:10 +02:00
"net"
"os"
2024-09-06 22:19:38 +02:00
"os/exec"
2024-07-16 03:00:10 +02:00
"os/user"
"path/filepath"
"strings"
2024-10-28 04:35:19 +01:00
"sync"
2024-07-19 00:21:33 +02:00
"time"
2024-07-16 03:00:10 +02:00
"github.com/kevinburke/ssh_config"
2024-08-28 22:18:43 +02:00
"github.com/skeema/knownhosts"
2024-12-19 20:46:35 +01:00
"github.com/wavetermdev/waveterm/pkg/panichandler"
2024-09-06 22:19:38 +02:00
"github.com/wavetermdev/waveterm/pkg/trimquotes"
2024-09-05 23:25:45 +02:00
"github.com/wavetermdev/waveterm/pkg/userinput"
2024-09-06 22:19:38 +02:00
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
2024-12-24 00:12:14 +01:00
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
2024-09-05 23:25:45 +02:00
"github.com/wavetermdev/waveterm/pkg/wavebase"
2024-12-05 19:02:07 +01:00
"github.com/wavetermdev/waveterm/pkg/wconfig"
2024-11-28 01:52:00 +01:00
"github.com/wavetermdev/waveterm/pkg/wshrpc"
2024-07-16 03:00:10 +02:00
"golang.org/x/crypto/ssh"
2024-09-06 22:19:38 +02:00
"golang.org/x/crypto/ssh/agent"
2024-08-28 22:18:43 +02:00
xknownhosts "golang.org/x/crypto/ssh/knownhosts"
2024-07-16 03:00:10 +02:00
)
2024-10-25 21:14:40 +02:00
const SshProxyJumpMaxDepth = 10
2024-10-28 04:35:19 +01:00
var waveSshConfigUserSettingsInternal * ssh_config . UserSettings
var configUserSettingsOnce = & sync . Once { }
func WaveSshConfigUserSettings ( ) * ssh_config . UserSettings {
configUserSettingsOnce . Do ( func ( ) {
waveSshConfigUserSettingsInternal = ssh_config . DefaultUserSettings
waveSshConfigUserSettingsInternal . IgnoreMatchDirective = true
} )
return waveSshConfigUserSettingsInternal
}
2024-07-16 03:00:10 +02:00
type UserInputCancelError struct {
Err error
}
2024-08-28 22:18:43 +02:00
type HostKeyAlgorithms = func ( hostWithPort string ) ( algos [ ] string )
2024-07-16 03:00:10 +02:00
func ( uice UserInputCancelError ) Error ( ) string {
return uice . Err . Error ( )
}
2024-10-25 21:14:40 +02:00
type ConnectionDebugInfo struct {
CurrentClient * ssh . Client
NextOpts * SSHOpts
JumpNum int32
}
type ConnectionError struct {
* ConnectionDebugInfo
Err error
}
func ( ce ConnectionError ) Error ( ) string {
if ce . CurrentClient == nil {
return fmt . Sprintf ( "Connecting to %+#v, Error: %v" , ce . NextOpts , ce . Err )
}
return fmt . Sprintf ( "Connecting from %v to %+#v (jump number %d), Error: %v" , ce . CurrentClient , ce . NextOpts , ce . JumpNum , ce . Err )
}
2024-07-16 03:00:10 +02:00
// This exists to trick the ssh library into continuing to try
// different public keys even when the current key cannot be
// properly parsed
func createDummySigner ( ) ( [ ] ssh . Signer , error ) {
dummyKey , err := rsa . GenerateKey ( rand . Reader , 2048 )
if err != nil {
return nil , err
}
dummySigner , err := ssh . NewSignerFromKey ( dummyKey )
if err != nil {
return nil , err
}
return [ ] ssh . Signer { dummySigner } , nil
}
// This is a workaround to only process one identity file at a time,
// even if they have passphrases. It must be combined with retryable
// authentication to work properly
//
// Despite returning an array of signers, we only ever provide one since
// it allows proper user interaction in between attempts
//
// A significant number of errors end up returning dummy values as if
// they were successes. An error in this function prevents any other
// keys from being attempted. But if there's an error because of a dummy
// file, the library can still try again with a new key.
2024-11-28 01:52:00 +01:00
func createPublicKeyCallback ( connCtx context . Context , sshKeywords * wshrpc . ConnKeywords , authSockSignersExt [ ] ssh . Signer , agentClient agent . ExtendedAgent , debugInfo * ConnectionDebugInfo ) func ( ) ( [ ] ssh . Signer , error ) {
2024-07-16 03:00:10 +02:00
var identityFiles [ ] string
existingKeys := make ( map [ string ] [ ] byte )
// checking the file early prevents us from needing to send a
// dummy signer if there's a problem with the signer
2024-11-28 01:52:00 +01:00
for _ , identityFile := range sshKeywords . SshIdentityFile {
2024-09-25 03:24:39 +02:00
filePath , err := wavebase . ExpandHomeDir ( identityFile )
if err != nil {
continue
}
privateKey , err := os . ReadFile ( filePath )
2024-07-16 03:00:10 +02:00
if err != nil {
// skip this key and try with the next
continue
}
existingKeys [ identityFile ] = privateKey
identityFiles = append ( identityFiles , identityFile )
}
// require pointer to modify list in closure
identityFilesPtr := & identityFiles
2024-09-06 22:19:38 +02:00
var authSockSigners [ ] ssh . Signer
authSockSigners = append ( authSockSigners , authSockSignersExt ... )
authSockSignersPtr := & authSockSigners
2024-07-16 03:00:10 +02:00
return func ( ) ( [ ] ssh . Signer , error ) {
2024-09-06 22:19:38 +02:00
// try auth sock
if len ( * authSockSignersPtr ) != 0 {
authSockSigner := ( * authSockSignersPtr ) [ 0 ]
* authSockSignersPtr = ( * authSockSignersPtr ) [ 1 : ]
return [ ] ssh . Signer { authSockSigner } , nil
}
2024-07-16 03:00:10 +02:00
if len ( * identityFilesPtr ) == 0 {
2024-10-25 21:14:40 +02:00
return nil , ConnectionError { ConnectionDebugInfo : debugInfo , Err : fmt . Errorf ( "no identity files remaining" ) }
2024-07-16 03:00:10 +02:00
}
identityFile := ( * identityFilesPtr ) [ 0 ]
* identityFilesPtr = ( * identityFilesPtr ) [ 1 : ]
privateKey , ok := existingKeys [ identityFile ]
if ! ok {
log . Printf ( "error with existingKeys, this should never happen" )
// skip this key and try with the next
return createDummySigner ( )
}
2024-09-06 22:19:38 +02:00
unencryptedPrivateKey , err := ssh . ParseRawPrivateKey ( privateKey )
2024-07-16 03:00:10 +02:00
if err == nil {
2024-09-06 22:19:38 +02:00
signer , err := ssh . NewSignerFromKey ( unencryptedPrivateKey )
if err == nil {
2024-12-24 00:12:14 +01:00
if utilfn . SafeDeref ( sshKeywords . SshAddKeysToAgent ) && agentClient != nil {
2024-09-06 22:19:38 +02:00
agentClient . Add ( agent . AddedKey {
PrivateKey : unencryptedPrivateKey ,
} )
}
2024-10-25 21:14:40 +02:00
return [ ] ssh . Signer { signer } , nil
2024-09-06 22:19:38 +02:00
}
2024-07-16 03:00:10 +02:00
}
2024-09-06 22:58:27 +02:00
if _ , ok := err . ( * ssh . PassphraseMissingError ) ; ! ok {
// skip this key and try with the next
return createDummySigner ( )
}
2024-07-16 03:00:10 +02:00
// batch mode deactivates user input
2024-12-24 00:12:14 +01:00
if utilfn . SafeDeref ( sshKeywords . SshBatchMode ) {
2024-07-16 03:00:10 +02:00
// skip this key and try with the next
return createDummySigner ( )
}
2024-07-19 00:21:33 +02:00
request := & userinput . UserInputRequest {
ResponseType : "text" ,
QueryText : fmt . Sprintf ( "Enter passphrase for the SSH key: %s" , identityFile ) ,
Title : "Publickey Auth + Passphrase" ,
}
ctx , cancelFn := context . WithTimeout ( connCtx , 60 * time . Second )
defer cancelFn ( )
response , err := userinput . GetUserInput ( ctx , request )
if err != nil {
// this is an error where we actually do want to stop
// trying keys
2024-10-25 21:14:40 +02:00
return nil , ConnectionError { ConnectionDebugInfo : debugInfo , Err : UserInputCancelError { Err : err } }
2024-07-19 00:21:33 +02:00
}
2024-09-06 22:19:38 +02:00
unencryptedPrivateKey , err = ssh . ParseRawPrivateKeyWithPassphrase ( privateKey , [ ] byte ( [ ] byte ( response . Text ) ) )
2024-07-19 00:21:33 +02:00
if err != nil {
// skip this key and try with the next
return createDummySigner ( )
}
2024-09-06 22:19:38 +02:00
signer , err := ssh . NewSignerFromKey ( unencryptedPrivateKey )
if err != nil {
// skip this key and try with the next
return createDummySigner ( )
}
2024-12-24 00:12:14 +01:00
if utilfn . SafeDeref ( sshKeywords . SshAddKeysToAgent ) && agentClient != nil {
2024-09-06 22:19:38 +02:00
agentClient . Add ( agent . AddedKey {
PrivateKey : unencryptedPrivateKey ,
} )
}
2024-10-25 21:14:40 +02:00
return [ ] ssh . Signer { signer } , nil
2024-07-16 03:00:10 +02:00
}
}
2024-10-25 21:14:40 +02:00
func createInteractivePasswordCallbackPrompt ( connCtx context . Context , remoteDisplayName string , debugInfo * ConnectionDebugInfo ) func ( ) ( secret string , err error ) {
2024-07-16 03:00:10 +02:00
return func ( ) ( secret string , err error ) {
2024-07-19 00:21:33 +02:00
ctx , cancelFn := context . WithTimeout ( connCtx , 60 * time . Second )
defer cancelFn ( )
queryText := fmt . Sprintf (
"Password Authentication requested from connection \n" +
"%s\n\n" +
"Password:" , remoteDisplayName )
request := & userinput . UserInputRequest {
ResponseType : "text" ,
QueryText : queryText ,
Markdown : true ,
Title : "Password Authentication" ,
}
response , err := userinput . GetUserInput ( ctx , request )
if err != nil {
2024-10-25 21:14:40 +02:00
return "" , ConnectionError { ConnectionDebugInfo : debugInfo , Err : err }
2024-07-19 00:21:33 +02:00
}
return response . Text , nil
2024-07-16 03:00:10 +02:00
}
}
2024-10-25 21:14:40 +02:00
func createInteractiveKbdInteractiveChallenge ( connCtx context . Context , remoteName string , debugInfo * ConnectionDebugInfo ) func ( name , instruction string , questions [ ] string , echos [ ] bool ) ( answers [ ] string , err error ) {
2024-07-16 03:00:10 +02:00
return func ( name , instruction string , questions [ ] string , echos [ ] bool ) ( answers [ ] string , err error ) {
if len ( questions ) != len ( echos ) {
return nil , fmt . Errorf ( "bad response from server: questions has len %d, echos has len %d" , len ( questions ) , len ( echos ) )
}
for i , question := range questions {
echo := echos [ i ]
answer , err := promptChallengeQuestion ( connCtx , question , echo , remoteName )
if err != nil {
2024-10-25 21:14:40 +02:00
return nil , ConnectionError { ConnectionDebugInfo : debugInfo , Err : err }
2024-07-16 03:00:10 +02:00
}
answers = append ( answers , answer )
}
return answers , nil
}
}
func promptChallengeQuestion ( connCtx context . Context , question string , echo bool , remoteName string ) ( answer string , err error ) {
// limited to 15 seconds for some reason. this should be investigated more
// in the future
2024-07-19 00:21:33 +02:00
ctx , cancelFn := context . WithTimeout ( connCtx , 60 * time . Second )
defer cancelFn ( )
queryText := fmt . Sprintf (
"Keyboard Interactive Authentication requested from connection \n" +
"%s\n\n" +
"%s" , remoteName , question )
request := & userinput . UserInputRequest {
ResponseType : "text" ,
QueryText : queryText ,
Markdown : true ,
Title : "Keyboard Interactive Authentication" ,
PublicText : echo ,
}
response , err := userinput . GetUserInput ( ctx , request )
if err != nil {
return "" , err
}
return response . Text , nil
2024-07-16 03:00:10 +02:00
}
func openKnownHostsForEdit ( knownHostsFilename string ) ( * os . File , error ) {
path , _ := filepath . Split ( knownHostsFilename )
err := os . MkdirAll ( path , 0700 )
if err != nil {
return nil , err
}
return os . OpenFile ( knownHostsFilename , os . O_APPEND | os . O_WRONLY | os . O_CREATE , 0644 )
}
2024-07-19 00:21:33 +02:00
func writeToKnownHosts ( knownHostsFile string , newLine string , getUserVerification func ( ) ( * userinput . UserInputResponse , error ) ) error {
2024-07-16 03:00:10 +02:00
if getUserVerification == nil {
2024-07-19 00:21:33 +02:00
getUserVerification = func ( ) ( * userinput . UserInputResponse , error ) {
return & userinput . UserInputResponse {
2024-07-16 03:00:10 +02:00
Type : "confirm" ,
Confirm : true ,
} , nil
}
}
path , _ := filepath . Split ( knownHostsFile )
err := os . MkdirAll ( path , 0700 )
if err != nil {
return err
}
f , err := os . OpenFile ( knownHostsFile , os . O_APPEND | os . O_WRONLY | os . O_CREATE , 0644 )
if err != nil {
return err
}
// do not close writeable files with defer
// this file works, so let's ask the user for permission
response , err := getUserVerification ( )
if err != nil {
f . Close ( )
return UserInputCancelError { Err : err }
}
if ! response . Confirm {
f . Close ( )
return UserInputCancelError { Err : fmt . Errorf ( "canceled by the user" ) }
}
_ , err = f . WriteString ( newLine + "\n" )
if err != nil {
f . Close ( )
return err
}
return f . Close ( )
}
2024-07-19 00:21:33 +02:00
func createUnknownKeyVerifier ( knownHostsFile string , hostname string , remote string , key ssh . PublicKey ) func ( ) ( * userinput . UserInputResponse , error ) {
2024-07-16 03:00:10 +02:00
base64Key := base64 . StdEncoding . EncodeToString ( key . Marshal ( ) )
queryText := fmt . Sprintf (
"The authenticity of host '%s (%s)' can't be established " +
"as it **does not exist in any checked known_hosts files**. " +
"The host you are attempting to connect to provides this %s key: \n" +
"%s.\n\n" +
"**Would you like to continue connecting?** If so, the key will be permanently " +
"added to the file %s " +
"to protect from future man-in-the-middle attacks." , hostname , remote , key . Type ( ) , base64Key , knownHostsFile )
2024-07-19 00:21:33 +02:00
request := & userinput . UserInputRequest {
2024-07-16 03:00:10 +02:00
ResponseType : "confirm" ,
QueryText : queryText ,
Markdown : true ,
Title : "Known Hosts Key Missing" ,
}
2024-07-19 00:21:33 +02:00
return func ( ) ( * userinput . UserInputResponse , error ) {
2024-07-16 03:00:10 +02:00
ctx , cancelFn := context . WithTimeout ( context . Background ( ) , 60 * time . Second )
defer cancelFn ( )
2024-11-28 01:52:00 +01:00
resp , err := userinput . GetUserInput ( ctx , request )
if err != nil {
return nil , err
}
if ! resp . Confirm {
return nil , fmt . Errorf ( "user selected no" )
}
return resp , nil
2024-07-16 03:00:10 +02:00
}
}
2024-07-19 00:21:33 +02:00
func createMissingKnownHostsVerifier ( knownHostsFile string , hostname string , remote string , key ssh . PublicKey ) func ( ) ( * userinput . UserInputResponse , error ) {
2024-07-16 03:00:10 +02:00
base64Key := base64 . StdEncoding . EncodeToString ( key . Marshal ( ) )
queryText := fmt . Sprintf (
"The authenticity of host '%s (%s)' can't be established " +
"as **no known_hosts files could be found**. " +
"The host you are attempting to connect to provides this %s key: \n" +
"%s.\n\n" +
"**Would you like to continue connecting?** If so: \n" +
"- %s will be created \n" +
"- the key will be added to %s\n\n" +
"This will protect from future man-in-the-middle attacks." , hostname , remote , key . Type ( ) , base64Key , knownHostsFile , knownHostsFile )
2024-07-19 00:21:33 +02:00
request := & userinput . UserInputRequest {
2024-07-16 03:00:10 +02:00
ResponseType : "confirm" ,
QueryText : queryText ,
Markdown : true ,
Title : "Known Hosts File Missing" ,
}
2024-07-19 00:21:33 +02:00
return func ( ) ( * userinput . UserInputResponse , error ) {
2024-07-16 03:00:10 +02:00
ctx , cancelFn := context . WithTimeout ( context . Background ( ) , 60 * time . Second )
defer cancelFn ( )
2024-11-28 01:52:00 +01:00
resp , err := userinput . GetUserInput ( ctx , request )
if err != nil {
return nil , err
}
if ! resp . Confirm {
return nil , fmt . Errorf ( "user selected no" )
}
return resp , nil
2024-07-16 03:00:10 +02:00
}
}
func lineContainsMatch ( line [ ] byte , matches [ ] [ ] byte ) bool {
for _ , match := range matches {
if bytes . Contains ( line , match ) {
return true
}
}
return false
}
2024-11-28 01:52:00 +01:00
func createHostKeyCallback ( sshKeywords * wshrpc . ConnKeywords ) ( ssh . HostKeyCallback , HostKeyAlgorithms , error ) {
globalKnownHostsFiles := sshKeywords . SshGlobalKnownHostsFile
userKnownHostsFiles := sshKeywords . SshUserKnownHostsFile
2024-07-16 03:00:10 +02:00
osUser , err := user . Current ( )
if err != nil {
2024-08-28 22:18:43 +02:00
return nil , nil , err
2024-07-16 03:00:10 +02:00
}
var unexpandedKnownHostsFiles [ ] string
if osUser . Username == "root" {
unexpandedKnownHostsFiles = globalKnownHostsFiles
} else {
unexpandedKnownHostsFiles = append ( userKnownHostsFiles , globalKnownHostsFiles ... )
}
var knownHostsFiles [ ] string
for _ , filename := range unexpandedKnownHostsFiles {
2024-09-25 03:24:39 +02:00
filePath , err := wavebase . ExpandHomeDir ( filename )
if err != nil {
continue
}
knownHostsFiles = append ( knownHostsFiles , filePath )
2024-07-16 03:00:10 +02:00
}
// there are no good known hosts files
if len ( knownHostsFiles ) == 0 {
2024-08-28 22:18:43 +02:00
return nil , nil , fmt . Errorf ( "no known_hosts files provided by ssh. defaults are overridden" )
2024-07-16 03:00:10 +02:00
}
var unreadableFiles [ ] string
// the library we use isn't very forgiving about files that are formatted
// incorrectly. if a problem file is found, it is removed from our list
// and we try again
var basicCallback ssh . HostKeyCallback
2024-08-28 22:18:43 +02:00
var hostKeyAlgorithms HostKeyAlgorithms
2024-07-16 03:00:10 +02:00
for basicCallback == nil && len ( knownHostsFiles ) > 0 {
2024-08-28 22:18:43 +02:00
keyDb , err := knownhosts . NewDB ( knownHostsFiles ... )
2024-07-16 03:00:10 +02:00
if serr , ok := err . ( * os . PathError ) ; ok {
badFile := serr . Path
unreadableFiles = append ( unreadableFiles , badFile )
var okFiles [ ] string
for _ , filename := range knownHostsFiles {
if filename != badFile {
okFiles = append ( okFiles , filename )
}
}
if len ( okFiles ) >= len ( knownHostsFiles ) {
2024-08-28 22:18:43 +02:00
return nil , nil , fmt . Errorf ( "problem file (%s) doesn't exist. this should not be possible" , badFile )
2024-07-16 03:00:10 +02:00
}
knownHostsFiles = okFiles
} else if err != nil {
// TODO handle obscure problems if possible
2024-08-28 22:18:43 +02:00
return nil , nil , fmt . Errorf ( "known_hosts formatting error: %+v" , err )
} else {
basicCallback = keyDb . HostKeyCallback ( )
hostKeyAlgorithms = keyDb . HostKeyAlgorithms
2024-07-16 03:00:10 +02:00
}
}
if basicCallback == nil {
basicCallback = func ( hostname string , remote net . Addr , key ssh . PublicKey ) error {
2024-08-28 22:18:43 +02:00
return & xknownhosts . KeyError { }
}
// need to return nil here to avoid null pointer from attempting to call
// the one provided by the db if nothing was found
hostKeyAlgorithms = func ( hostWithPort string ) ( algos [ ] string ) {
return nil
2024-07-16 03:00:10 +02:00
}
}
waveHostKeyCallback := func ( hostname string , remote net . Addr , key ssh . PublicKey ) error {
err := basicCallback ( hostname , remote , key )
if err == nil {
// success
return nil
2024-08-28 22:18:43 +02:00
} else if _ , ok := err . ( * xknownhosts . RevokedError ) ; ok {
2024-07-16 03:00:10 +02:00
// revoked credentials are refused outright
return err
2024-08-28 22:18:43 +02:00
} else if _ , ok := err . ( * xknownhosts . KeyError ) ; ! ok {
2024-07-16 03:00:10 +02:00
// this is an unknown error (note the !ok is opposite of usual)
return err
}
2024-08-28 22:18:43 +02:00
serr , _ := err . ( * xknownhosts . KeyError )
2024-07-16 03:00:10 +02:00
if len ( serr . Want ) == 0 {
// the key was not found
// try to write to a file that could be read
2024-07-19 00:21:33 +02:00
err := fmt . Errorf ( "placeholder, should not be returned" ) // a null value here can cause problems with empty slice
for _ , filename := range knownHostsFiles {
2024-08-28 22:18:43 +02:00
newLine := xknownhosts . Line ( [ ] string { xknownhosts . Normalize ( hostname ) } , key )
2024-07-19 00:21:33 +02:00
getUserVerification := createUnknownKeyVerifier ( filename , hostname , remote . String ( ) , key )
err = writeToKnownHosts ( filename , newLine , getUserVerification )
if err == nil {
break
}
if serr , ok := err . ( UserInputCancelError ) ; ok {
return serr
2024-07-16 03:00:10 +02:00
}
2024-07-19 00:21:33 +02:00
}
// try to write to a file that could not be read (file likely doesn't exist)
// should catch cases where there is no known_hosts file
if err != nil {
for _ , filename := range unreadableFiles {
2024-08-28 22:18:43 +02:00
newLine := xknownhosts . Line ( [ ] string { xknownhosts . Normalize ( hostname ) } , key )
2024-07-19 00:21:33 +02:00
getUserVerification := createMissingKnownHostsVerifier ( filename , hostname , remote . String ( ) , key )
err = writeToKnownHosts ( filename , newLine , getUserVerification )
if err == nil {
knownHostsFiles = [ ] string { filename }
break
}
if serr , ok := err . ( UserInputCancelError ) ; ok {
return serr
}
2024-07-16 03:00:10 +02:00
}
2024-07-19 00:21:33 +02:00
}
if err != nil {
return fmt . Errorf ( "unable to create new knownhost key: %e" , err )
}
2024-07-16 03:00:10 +02:00
} else {
// the key changed
correctKeyFingerprint := base64 . StdEncoding . EncodeToString ( key . Marshal ( ) )
var bulletListKnownHosts [ ] string
for _ , knownHostName := range knownHostsFiles {
withBulletPoint := "- " + knownHostName
bulletListKnownHosts = append ( bulletListKnownHosts , withBulletPoint )
}
var offendingKeysFmt [ ] string
for _ , badKey := range serr . Want {
formattedKey := "- " + base64 . StdEncoding . EncodeToString ( badKey . Key . Marshal ( ) )
offendingKeysFmt = append ( offendingKeysFmt , formattedKey )
}
// todo
2024-09-30 21:32:22 +02:00
errorMsg := fmt . Sprintf ( "**WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!**\n\n" +
2024-07-16 03:00:10 +02:00
"If this is not expected, it is possible that someone could be trying to " +
"eavesdrop on you via a man-in-the-middle attack. " +
"Alternatively, the host you are connecting to may have changed its key. " +
"The %s key sent by the remote hist has the fingerprint: \n" +
"%s\n\n" +
"If you are sure this is correct, please update your known_hosts files to " +
"remove the lines with the offending before trying to connect again. \n" +
"**Known Hosts Files** \n" +
"%s\n\n" +
"**Offending Keys** \n" +
"%s" , key . Type ( ) , correctKeyFingerprint , strings . Join ( bulletListKnownHosts , " \n" ) , strings . Join ( offendingKeysFmt , " \n" ) )
2024-10-25 21:14:40 +02:00
2024-09-30 21:32:22 +02:00
log . Print ( errorMsg )
2024-07-16 03:00:10 +02:00
//update := scbus.MakeUpdatePacket()
// create update into alert message
//send update via bus?
return fmt . Errorf ( "remote host identification has changed" )
}
2024-08-28 22:18:43 +02:00
updatedCallback , err := xknownhosts . New ( knownHostsFiles ... )
2024-07-16 03:00:10 +02:00
if err != nil {
return err
}
// try one final time
return updatedCallback ( hostname , remote , key )
}
2024-08-28 22:18:43 +02:00
return waveHostKeyCallback , hostKeyAlgorithms , nil
2024-07-16 03:00:10 +02:00
}
2024-11-28 01:52:00 +01:00
func createClientConfig ( connCtx context . Context , sshKeywords * wshrpc . ConnKeywords , debugInfo * ConnectionDebugInfo ) ( * ssh . ClientConfig , error ) {
2024-12-24 00:12:14 +01:00
chosenUser := utilfn . SafeDeref ( sshKeywords . SshUser )
chosenHostName := utilfn . SafeDeref ( sshKeywords . SshHostName )
chosenPort := utilfn . SafeDeref ( sshKeywords . SshPort )
remoteName := chosenUser + xknownhosts . Normalize ( chosenHostName + ":" + chosenPort )
2024-07-16 03:00:10 +02:00
2024-09-06 22:19:38 +02:00
var authSockSigners [ ] ssh . Signer
var agentClient agent . ExtendedAgent
2024-12-24 00:12:14 +01:00
conn , err := net . Dial ( "unix" , utilfn . SafeDeref ( sshKeywords . SshIdentityAgent ) )
2024-09-06 22:19:38 +02:00
if err != nil {
log . Printf ( "Failed to open Identity Agent Socket: %v" , err )
2024-07-16 03:00:10 +02:00
} else {
2024-09-06 22:19:38 +02:00
agentClient = agent . NewClient ( conn )
authSockSigners , _ = agentClient . Signers ( )
2024-07-16 03:00:10 +02:00
}
2024-10-25 21:14:40 +02:00
publicKeyCallback := ssh . PublicKeysCallback ( createPublicKeyCallback ( connCtx , sshKeywords , authSockSigners , agentClient , debugInfo ) )
keyboardInteractive := ssh . KeyboardInteractive ( createInteractiveKbdInteractiveChallenge ( connCtx , remoteName , debugInfo ) )
passwordCallback := ssh . PasswordCallback ( createInteractivePasswordCallbackPrompt ( connCtx , remoteName , debugInfo ) )
2024-09-06 22:19:38 +02:00
2024-07-16 03:00:10 +02:00
// exclude gssapi-with-mic and hostbased until implemented
authMethodMap := map [ string ] ssh . AuthMethod {
2024-11-28 01:52:00 +01:00
"publickey" : ssh . RetryableAuthMethod ( publicKeyCallback , len ( sshKeywords . SshIdentityFile ) + len ( authSockSigners ) ) ,
2024-09-06 22:19:38 +02:00
"keyboard-interactive" : ssh . RetryableAuthMethod ( keyboardInteractive , 1 ) ,
"password" : ssh . RetryableAuthMethod ( passwordCallback , 1 ) ,
2024-07-16 03:00:10 +02:00
}
2024-09-06 22:19:38 +02:00
// note: batch mode turns off interactive input
2024-07-16 03:00:10 +02:00
authMethodActiveMap := map [ string ] bool {
2024-12-24 00:12:14 +01:00
"publickey" : utilfn . SafeDeref ( sshKeywords . SshPubkeyAuthentication ) ,
"keyboard-interactive" : utilfn . SafeDeref ( sshKeywords . SshKbdInteractiveAuthentication ) && ! utilfn . SafeDeref ( sshKeywords . SshBatchMode ) ,
"password" : utilfn . SafeDeref ( sshKeywords . SshPasswordAuthentication ) && ! utilfn . SafeDeref ( sshKeywords . SshBatchMode ) ,
2024-07-16 03:00:10 +02:00
}
var authMethods [ ] ssh . AuthMethod
2024-11-28 01:52:00 +01:00
for _ , authMethodName := range sshKeywords . SshPreferredAuthentications {
2024-07-16 03:00:10 +02:00
authMethodActive , ok := authMethodActiveMap [ authMethodName ]
if ! ok || ! authMethodActive {
continue
}
authMethod , ok := authMethodMap [ authMethodName ]
if ! ok {
continue
}
authMethods = append ( authMethods , authMethod )
}
2024-10-25 21:14:40 +02:00
hostKeyCallback , hostKeyAlgorithms , err := createHostKeyCallback ( sshKeywords )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
networkAddr := chosenHostName + ":" + chosenPort
2024-10-25 21:14:40 +02:00
return & ssh . ClientConfig {
2024-12-24 00:12:14 +01:00
User : chosenUser ,
2024-08-28 22:18:43 +02:00
Auth : authMethods ,
HostKeyCallback : hostKeyCallback ,
HostKeyAlgorithms : hostKeyAlgorithms ( networkAddr ) ,
2024-10-25 21:14:40 +02:00
} , nil
}
func connectInternal ( ctx context . Context , networkAddr string , clientConfig * ssh . ClientConfig , currentClient * ssh . Client ) ( * ssh . Client , error ) {
var clientConn net . Conn
var err error
if currentClient == nil {
d := net . Dialer { Timeout : clientConfig . Timeout }
clientConn , err = d . DialContext ( ctx , "tcp" , networkAddr )
if err != nil {
return nil , err
}
} else {
clientConn , err = currentClient . DialContext ( ctx , "tcp" , networkAddr )
if err != nil {
return nil , err
}
}
c , chans , reqs , err := ssh . NewClientConn ( clientConn , networkAddr , clientConfig )
if err != nil {
return nil , err
}
return ssh . NewClient ( c , chans , reqs ) , nil
}
2024-11-28 01:52:00 +01:00
func ConnectToClient ( connCtx context . Context , opts * SSHOpts , currentClient * ssh . Client , jumpNum int32 , connFlags * wshrpc . ConnKeywords ) ( * ssh . Client , int32 , error ) {
2024-10-25 21:14:40 +02:00
debugInfo := & ConnectionDebugInfo {
CurrentClient : currentClient ,
NextOpts : opts ,
JumpNum : jumpNum ,
}
if jumpNum > SshProxyJumpMaxDepth {
return nil , jumpNum , ConnectionError { ConnectionDebugInfo : debugInfo , Err : fmt . Errorf ( "ProxyJump %d exceeds Wave's max depth of %d" , jumpNum , SshProxyJumpMaxDepth ) }
}
// todo print final warning if logging gets turned off
sshConfigKeywords , err := findSshConfigKeywords ( opts . SSHHost )
if err != nil {
return nil , debugInfo . JumpNum , ConnectionError { ConnectionDebugInfo : debugInfo , Err : err }
}
2024-12-24 00:12:14 +01:00
parsedKeywords := & wshrpc . ConnKeywords { }
if opts . SSHUser != "" {
parsedKeywords . SshUser = & opts . SSHUser
}
if opts . SSHPort != "" {
parsedKeywords . SshPort = & opts . SSHPort
}
2024-11-28 01:52:00 +01:00
2024-12-05 19:02:07 +01:00
rawName := opts . String ( )
2024-12-24 00:12:14 +01:00
fullConfig := wconfig . ReadFullConfig ( )
internalSshConfigKeywords , ok := fullConfig . Connections [ rawName ]
2024-12-05 19:02:07 +01:00
if ! ok {
2024-12-24 00:12:14 +01:00
internalSshConfigKeywords = wshrpc . ConnKeywords { }
2024-12-05 19:02:07 +01:00
}
2024-12-24 00:12:14 +01:00
// cascade order:
// ssh config -> (optional) internal config -> specified flag keywords -> parsed keywords
partialMerged := sshConfigKeywords
if internalSshConfigKeywords . ConnOverrideConfig {
partialMerged = mergeKeywords ( partialMerged , & internalSshConfigKeywords )
2024-10-25 21:14:40 +02:00
}
2024-12-24 00:12:14 +01:00
partialMerged = mergeKeywords ( partialMerged , connFlags )
sshKeywords := mergeKeywords ( partialMerged , parsedKeywords )
// handle these separately since
// - they append
// - since they append, the order is reversed
// - there is no reason to not include the internal config
// - they are never part of the parsedKeywords
sshKeywords . SshIdentityFile = append ( sshKeywords . SshIdentityFile , connFlags . SshIdentityFile ... )
sshKeywords . SshIdentityFile = append ( sshKeywords . SshIdentityFile , internalSshConfigKeywords . SshIdentityFile ... )
sshKeywords . SshIdentityFile = append ( sshKeywords . SshIdentityFile , sshConfigKeywords . SshIdentityFile ... )
2024-10-25 21:14:40 +02:00
2024-11-28 01:52:00 +01:00
for _ , proxyName := range sshKeywords . SshProxyJump {
2024-10-25 21:14:40 +02:00
proxyOpts , err := ParseOpts ( proxyName )
if err != nil {
return nil , debugInfo . JumpNum , ConnectionError { ConnectionDebugInfo : debugInfo , Err : err }
}
// ensure no overflow (this will likely never happen)
if jumpNum < math . MaxInt32 {
jumpNum += 1
}
2024-11-28 01:52:00 +01:00
// do not apply supplied keywords to proxies - ssh config must be used for that
debugInfo . CurrentClient , jumpNum , err = ConnectToClient ( connCtx , proxyOpts , debugInfo . CurrentClient , jumpNum , & wshrpc . ConnKeywords { } )
2024-10-25 21:14:40 +02:00
if err != nil {
// do not add a context on a recursive call
// (this can cause a recursive nested context that's arbitrarily deep)
return nil , jumpNum , err
}
2024-07-16 03:00:10 +02:00
}
2024-10-25 21:14:40 +02:00
clientConfig , err := createClientConfig ( connCtx , sshKeywords , debugInfo )
if err != nil {
return nil , debugInfo . JumpNum , ConnectionError { ConnectionDebugInfo : debugInfo , Err : err }
}
2024-12-24 00:12:14 +01:00
networkAddr := utilfn . SafeDeref ( sshKeywords . SshHostName ) + ":" + utilfn . SafeDeref ( sshKeywords . SshPort )
2024-10-25 21:14:40 +02:00
client , err := connectInternal ( connCtx , networkAddr , clientConfig , debugInfo . CurrentClient )
if err != nil {
return client , debugInfo . JumpNum , ConnectionError { ConnectionDebugInfo : debugInfo , Err : err }
}
return client , debugInfo . JumpNum , nil
2024-07-16 03:00:10 +02:00
}
// note that a `var == "yes"` will default to false
// but `var != "no"` will default to true
// when given unexpected strings
2024-12-19 20:46:35 +01:00
func findSshConfigKeywords ( hostPattern string ) ( connKeywords * wshrpc . ConnKeywords , outErr error ) {
defer func ( ) {
err := panichandler . PanicHandler ( "sshclient:find-ssh-config-keywords" )
if err != nil {
outErr = err
}
} ( )
2024-10-28 04:35:19 +01:00
WaveSshConfigUserSettings ( ) . ReloadConfigs ( )
2024-11-28 01:52:00 +01:00
sshKeywords := & wshrpc . ConnKeywords { }
2024-07-16 03:00:10 +02:00
var err error
2024-10-28 04:35:19 +01:00
userRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "User" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
userClean := trimquotes . TryTrimQuotes ( userRaw )
if userClean == "" {
userDetails , err := user . Current ( )
if err != nil {
return nil , err
}
userClean = userDetails . Username
}
sshKeywords . SshUser = & userClean
2024-07-16 03:00:10 +02:00
2024-10-28 04:35:19 +01:00
hostNameRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "HostName" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
// manually implementing default HostName here as it is not handled by ssh_config library
hostNameProcessed := trimquotes . TryTrimQuotes ( hostNameRaw )
if hostNameProcessed == "" {
sshKeywords . SshHostName = & hostPattern
} else {
sshKeywords . SshHostName = & hostNameRaw
}
2024-07-16 03:00:10 +02:00
2024-10-28 04:35:19 +01:00
portRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "Port" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshPort = utilfn . Ptr ( trimquotes . TryTrimQuotes ( portRaw ) )
2024-07-16 03:00:10 +02:00
2024-10-28 04:35:19 +01:00
identityFileRaw := WaveSshConfigUserSettings ( ) . GetAll ( hostPattern , "IdentityFile" )
2024-09-06 22:19:38 +02:00
for i := 0 ; i < len ( identityFileRaw ) ; i ++ {
identityFileRaw [ i ] = trimquotes . TryTrimQuotes ( identityFileRaw [ i ] )
}
2024-11-28 01:52:00 +01:00
sshKeywords . SshIdentityFile = identityFileRaw
2024-07-16 03:00:10 +02:00
2024-10-28 04:35:19 +01:00
batchModeRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "BatchMode" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshBatchMode = utilfn . Ptr ( strings . ToLower ( trimquotes . TryTrimQuotes ( batchModeRaw ) ) == "yes" )
2024-07-16 03:00:10 +02:00
// we currently do not support host-bound or unbound but will use yes when they are selected
2024-10-28 04:35:19 +01:00
pubkeyAuthenticationRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "PubkeyAuthentication" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshPubkeyAuthentication = utilfn . Ptr ( strings . ToLower ( trimquotes . TryTrimQuotes ( pubkeyAuthenticationRaw ) ) != "no" )
2024-07-16 03:00:10 +02:00
2024-10-28 04:35:19 +01:00
passwordAuthenticationRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "PasswordAuthentication" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshPasswordAuthentication = utilfn . Ptr ( strings . ToLower ( trimquotes . TryTrimQuotes ( passwordAuthenticationRaw ) ) != "no" )
2024-07-16 03:00:10 +02:00
2024-10-28 04:35:19 +01:00
kbdInteractiveAuthenticationRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "KbdInteractiveAuthentication" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshKbdInteractiveAuthentication = utilfn . Ptr ( strings . ToLower ( trimquotes . TryTrimQuotes ( kbdInteractiveAuthenticationRaw ) ) != "no" )
2024-07-16 03:00:10 +02:00
// these are parsed as a single string and must be separated
// these are case sensitive in openssh so they are here too
2024-10-28 04:35:19 +01:00
preferredAuthenticationsRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "PreferredAuthentications" )
2024-07-16 03:00:10 +02:00
if err != nil {
return nil , err
}
2024-11-28 01:52:00 +01:00
sshKeywords . SshPreferredAuthentications = strings . Split ( trimquotes . TryTrimQuotes ( preferredAuthenticationsRaw ) , "," )
2024-10-28 04:35:19 +01:00
addKeysToAgentRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "AddKeysToAgent" )
2024-09-06 22:19:38 +02:00
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshAddKeysToAgent = utilfn . Ptr ( strings . ToLower ( trimquotes . TryTrimQuotes ( addKeysToAgentRaw ) ) == "yes" )
2024-09-06 22:19:38 +02:00
2024-10-28 04:35:19 +01:00
identityAgentRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "IdentityAgent" )
2024-09-06 22:19:38 +02:00
if err != nil {
return nil , err
}
if identityAgentRaw == "" {
shellPath := shellutil . DetectLocalShellPath ( )
authSockCommand := exec . Command ( shellPath , "-c" , "echo ${SSH_AUTH_SOCK}" )
sshAuthSock , err := authSockCommand . Output ( )
if err == nil {
2024-09-25 03:24:39 +02:00
agentPath , err := wavebase . ExpandHomeDir ( trimquotes . TryTrimQuotes ( strings . TrimSpace ( string ( sshAuthSock ) ) ) )
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshIdentityAgent = utilfn . Ptr ( agentPath )
2024-09-06 22:19:38 +02:00
} else {
log . Printf ( "unable to find SSH_AUTH_SOCK: %v\n" , err )
}
} else {
2024-09-25 03:24:39 +02:00
agentPath , err := wavebase . ExpandHomeDir ( trimquotes . TryTrimQuotes ( identityAgentRaw ) )
if err != nil {
return nil , err
}
2024-12-24 00:12:14 +01:00
sshKeywords . SshIdentityAgent = utilfn . Ptr ( agentPath )
2024-09-06 22:19:38 +02:00
}
2024-07-16 03:00:10 +02:00
2024-10-28 04:35:19 +01:00
proxyJumpRaw , err := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "ProxyJump" )
2024-10-25 21:14:40 +02:00
if err != nil {
return nil , err
}
proxyJumpSplit := strings . Split ( proxyJumpRaw , "," )
for _ , proxyJumpName := range proxyJumpSplit {
proxyJumpName = strings . TrimSpace ( proxyJumpName )
if proxyJumpName == "" || strings . ToLower ( proxyJumpName ) == "none" {
continue
}
2024-11-28 01:52:00 +01:00
sshKeywords . SshProxyJump = append ( sshKeywords . SshProxyJump , proxyJumpName )
2024-10-25 21:14:40 +02:00
}
2024-10-28 04:35:19 +01:00
rawUserKnownHostsFile , _ := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "UserKnownHostsFile" )
2024-11-28 01:52:00 +01:00
sshKeywords . SshUserKnownHostsFile = strings . Fields ( rawUserKnownHostsFile ) // TODO - smarter splitting escaped spaces and quotes
2024-10-28 04:35:19 +01:00
rawGlobalKnownHostsFile , _ := WaveSshConfigUserSettings ( ) . GetStrict ( hostPattern , "GlobalKnownHostsFile" )
2024-11-28 01:52:00 +01:00
sshKeywords . SshGlobalKnownHostsFile = strings . Fields ( rawGlobalKnownHostsFile ) // TODO - smarter splitting escaped spaces and quotes
2024-10-25 21:14:40 +02:00
2024-07-16 03:00:10 +02:00
return sshKeywords , nil
}
type SSHOpts struct {
SSHHost string ` json:"sshhost" `
SSHUser string ` json:"sshuser" `
2024-12-24 00:12:14 +01:00
SSHPort string ` json:"sshport,omitempty" `
2024-07-16 03:00:10 +02:00
}
2024-08-17 20:21:25 +02:00
func ( opts SSHOpts ) String ( ) string {
2024-08-24 03:12:40 +02:00
stringRepr := ""
if opts . SSHUser != "" {
stringRepr = opts . SSHUser + "@"
}
stringRepr = stringRepr + opts . SSHHost
2024-12-24 00:12:14 +01:00
if opts . SSHPort != "22" && opts . SSHPort != "" {
2024-08-24 03:12:40 +02:00
stringRepr = stringRepr + ":" + fmt . Sprint ( opts . SSHPort )
2024-08-17 20:21:25 +02:00
}
2024-08-24 03:12:40 +02:00
return stringRepr
2024-08-17 20:21:25 +02:00
}
2024-12-24 00:12:14 +01:00
func mergeKeywords ( oldKeywords * wshrpc . ConnKeywords , newKeywords * wshrpc . ConnKeywords ) * wshrpc . ConnKeywords {
if oldKeywords == nil {
oldKeywords = & wshrpc . ConnKeywords { }
}
if newKeywords == nil {
return oldKeywords
}
outKeywords := * oldKeywords
if newKeywords . SshHostName != nil {
outKeywords . SshHostName = newKeywords . SshHostName
}
if newKeywords . SshUser != nil {
outKeywords . SshUser = newKeywords . SshUser
}
if newKeywords . SshPort != nil {
outKeywords . SshPort = newKeywords . SshPort
}
// skip identityfile (handled separately due to different behavior)
if newKeywords . SshBatchMode != nil {
outKeywords . SshBatchMode = newKeywords . SshBatchMode
}
if newKeywords . SshPubkeyAuthentication != nil {
outKeywords . SshPubkeyAuthentication = newKeywords . SshPubkeyAuthentication
}
if newKeywords . SshPasswordAuthentication != nil {
outKeywords . SshPasswordAuthentication = newKeywords . SshPasswordAuthentication
}
if newKeywords . SshKbdInteractiveAuthentication != nil {
outKeywords . SshKbdInteractiveAuthentication = newKeywords . SshKbdInteractiveAuthentication
}
if newKeywords . SshPreferredAuthentications != nil {
outKeywords . SshPreferredAuthentications = newKeywords . SshPreferredAuthentications
}
if newKeywords . SshAddKeysToAgent != nil {
outKeywords . SshAddKeysToAgent = newKeywords . SshAddKeysToAgent
}
if newKeywords . SshIdentityAgent != nil {
outKeywords . SshIdentityAgent = newKeywords . SshIdentityAgent
}
if newKeywords . SshProxyJump != nil {
outKeywords . SshProxyJump = newKeywords . SshProxyJump
}
if newKeywords . SshUserKnownHostsFile != nil {
outKeywords . SshUserKnownHostsFile = newKeywords . SshUserKnownHostsFile
}
if newKeywords . SshGlobalKnownHostsFile != nil {
outKeywords . SshGlobalKnownHostsFile = newKeywords . SshGlobalKnownHostsFile
}
return & outKeywords
}