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-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-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-11-28 01:52:00 +01:00
if 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-11-28 01:52:00 +01:00
if 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-11-28 01:52:00 +01:00
if 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 ) {
remoteName := sshKeywords . SshUser + "@" + xknownhosts . Normalize ( sshKeywords . SshHostName + ":" + sshKeywords . SshPort )
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-11-28 01:52:00 +01:00
conn , err := net . Dial ( "unix" , 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-11-28 01:52:00 +01:00
"publickey" : sshKeywords . SshPubkeyAuthentication ,
"keyboard-interactive" : sshKeywords . SshKbdInteractiveAuthentication && ! sshKeywords . SshBatchMode ,
"password" : sshKeywords . SshPasswordAuthentication && ! 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-11-28 01:52:00 +01:00
networkAddr := sshKeywords . SshHostName + ":" + sshKeywords . SshPort
2024-10-25 21:14:40 +02:00
return & ssh . ClientConfig {
2024-11-28 01:52:00 +01:00
User : sshKeywords . SshUser ,
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-11-28 01:52:00 +01:00
connFlags . SshUser = opts . SSHUser
connFlags . SshHostName = opts . SSHHost
connFlags . SshPort = fmt . Sprintf ( "%d" , opts . SSHPort )
2024-12-05 19:02:07 +01:00
rawName := opts . String ( )
savedKeywords , ok := wconfig . ReadFullConfig ( ) . Connections [ rawName ]
if ! ok {
savedKeywords = wshrpc . ConnKeywords { }
}
sshKeywords , err := combineSshKeywords ( connFlags , sshConfigKeywords , & savedKeywords )
2024-10-25 21:14:40 +02:00
if err != nil {
return nil , debugInfo . JumpNum , ConnectionError { ConnectionDebugInfo : debugInfo , Err : err }
}
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-11-28 01:52:00 +01:00
networkAddr := sshKeywords . SshHostName + ":" + 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
}
2024-12-05 19:02:07 +01:00
func combineSshKeywords ( userProvidedOpts * wshrpc . ConnKeywords , configKeywords * wshrpc . ConnKeywords , savedKeywords * wshrpc . ConnKeywords ) ( * wshrpc . ConnKeywords , error ) {
2024-11-28 01:52:00 +01:00
sshKeywords := & wshrpc . ConnKeywords { }
2024-07-16 03:00:10 +02:00
2024-11-28 01:52:00 +01:00
if userProvidedOpts . SshUser != "" {
sshKeywords . SshUser = userProvidedOpts . SshUser
} else if configKeywords . SshUser != "" {
sshKeywords . SshUser = configKeywords . SshUser
2024-07-16 03:00:10 +02:00
} else {
user , err := user . Current ( )
if err != nil {
return nil , fmt . Errorf ( "failed to get user for ssh: %+v" , err )
}
2024-11-28 01:52:00 +01:00
sshKeywords . SshUser = user . Username
2024-07-16 03:00:10 +02:00
}
// we have to check the host value because of the weird way
// we store the pattern as the hostname for imported remotes
2024-11-28 01:52:00 +01:00
if configKeywords . SshHostName != "" {
sshKeywords . SshHostName = configKeywords . SshHostName
2024-07-16 03:00:10 +02:00
} else {
2024-11-28 01:52:00 +01:00
sshKeywords . SshHostName = userProvidedOpts . SshHostName
2024-07-16 03:00:10 +02:00
}
2024-11-28 01:52:00 +01:00
if userProvidedOpts . SshPort != "0" && userProvidedOpts . SshPort != "22" {
sshKeywords . SshPort = userProvidedOpts . SshPort
} else if configKeywords . SshPort != "" && configKeywords . SshPort != "22" {
sshKeywords . SshPort = configKeywords . SshPort
2024-07-16 03:00:10 +02:00
} else {
2024-11-28 01:52:00 +01:00
sshKeywords . SshPort = "22"
2024-07-16 03:00:10 +02:00
}
2024-12-05 19:02:07 +01:00
// use internal config ones
if savedKeywords != nil {
sshKeywords . SshIdentityFile = append ( sshKeywords . SshIdentityFile , savedKeywords . SshIdentityFile ... )
}
sshKeywords . SshIdentityFile = append ( sshKeywords . SshIdentityFile , userProvidedOpts . SshIdentityFile ... )
sshKeywords . SshIdentityFile = append ( sshKeywords . SshIdentityFile , configKeywords . SshIdentityFile ... )
2024-07-16 03:00:10 +02:00
// these are not officially supported in the waveterm frontend but can be configured
// in ssh config files
2024-11-28 01:52:00 +01:00
sshKeywords . SshBatchMode = configKeywords . SshBatchMode
sshKeywords . SshPubkeyAuthentication = configKeywords . SshPubkeyAuthentication
sshKeywords . SshPasswordAuthentication = configKeywords . SshPasswordAuthentication
sshKeywords . SshKbdInteractiveAuthentication = configKeywords . SshKbdInteractiveAuthentication
sshKeywords . SshPreferredAuthentications = configKeywords . SshPreferredAuthentications
sshKeywords . SshAddKeysToAgent = configKeywords . SshAddKeysToAgent
sshKeywords . SshIdentityAgent = configKeywords . SshIdentityAgent
sshKeywords . SshProxyJump = configKeywords . SshProxyJump
sshKeywords . SshUserKnownHostsFile = configKeywords . SshUserKnownHostsFile
sshKeywords . SshGlobalKnownHostsFile = configKeywords . SshGlobalKnownHostsFile
2024-07-16 03:00:10 +02:00
return sshKeywords , nil
}
// note that a `var == "yes"` will default to false
// but `var != "no"` will default to true
// when given unexpected strings
2024-11-28 01:52:00 +01:00
func findSshConfigKeywords ( hostPattern string ) ( * wshrpc . ConnKeywords , error ) {
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-11-28 01:52:00 +01:00
//config := wconfig.ReadFullConfig()
2024-07-16 03:00:10 +02:00
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-11-28 01:52:00 +01:00
sshKeywords . SshUser = trimquotes . TryTrimQuotes ( userRaw )
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-11-28 01:52:00 +01:00
sshKeywords . SshHostName = trimquotes . TryTrimQuotes ( 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-11-28 01:52:00 +01:00
sshKeywords . SshPort = 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-11-28 01:52:00 +01:00
sshKeywords . SshBatchMode = ( 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-11-28 01:52:00 +01:00
sshKeywords . SshPubkeyAuthentication = ( 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-11-28 01:52:00 +01:00
sshKeywords . SshPasswordAuthentication = ( 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-11-28 01:52:00 +01:00
sshKeywords . SshKbdInteractiveAuthentication = ( 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-11-28 01:52:00 +01:00
sshKeywords . SshAddKeysToAgent = ( 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-11-28 01:52:00 +01:00
sshKeywords . SshIdentityAgent = 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-11-28 01:52:00 +01:00
sshKeywords . SshIdentityAgent = 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" `
SSHPort int ` json:"sshport,omitempty" `
}
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
if opts . SSHPort != 0 {
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
}