github.com/devops-filetransfer/sshego@v7.0.4+incompatible/sshutil.go (about)

     1  package sshego
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"log"
     9  	"net"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/glycerine/sshego/xendor/github.com/glycerine/xcryptossh"
    14  	"github.com/pquerna/otp"
    15  	"github.com/pquerna/otp/totp"
    16  )
    17  
    18  type kiCliHelp struct {
    19  	passphrase string
    20  	toptUrl    string
    21  }
    22  
    23  // helper assists ssh client with keyboard-interactive
    24  // password and TOPT login. Must match the
    25  // prototype KeyboardInteractiveChallenge.
    26  func (ki *kiCliHelp) helper(ctx context.Context, user string, instruction string, questions []string, echos []bool) ([]string, error) {
    27  	var answers []string
    28  	for _, q := range questions {
    29  		switch q {
    30  		case passwordChallenge: // "password: "
    31  			answers = append(answers, ki.passphrase)
    32  		case gauthChallenge: // "google-authenticator-code: "
    33  			w, err := otp.NewKeyFromURL(strings.TrimSpace(ki.toptUrl))
    34  			panicOn(err)
    35  			code, err := totp.GenerateCode(w.Secret(), time.Now())
    36  			panicOn(err)
    37  			answers = append(answers, code)
    38  		default:
    39  			panic(fmt.Sprintf("unrecognized challenge: '%v'", q))
    40  		}
    41  	}
    42  	return answers, nil
    43  }
    44  
    45  func defaultFileFormat() KnownHostsPersistFormat {
    46  	return KHJson
    47  }
    48  
    49  // HostState recognizes host keys are legitimate or
    50  // impersonated, new, banned, or consitent with
    51  // what we've seen before and so OK.
    52  type HostState int
    53  
    54  // Unknown means we don't have a matching stored host key.
    55  const Unknown HostState = 0
    56  
    57  // Banned means the host has been marked as forbidden.
    58  const Banned HostState = 1
    59  
    60  // KnownOK means the host key matches one we have
    61  // previously allowed.
    62  const KnownOK HostState = 2
    63  
    64  // KnownRecordMismatch means we have a records
    65  // for this IP/host-key, but either the IP or
    66  // the host-key has varied and so it could
    67  // be a Man-in-the-middle attack.
    68  const KnownRecordMismatch HostState = 3
    69  
    70  // AddedNew means the -new flag was given
    71  // and we allowed the addition of a new
    72  // host-key for the first time.
    73  const AddedNew HostState = 4
    74  
    75  func (s HostState) String() string {
    76  	switch s {
    77  	case Unknown:
    78  		return "Unknown"
    79  	case Banned:
    80  		return "Banned"
    81  	case KnownOK:
    82  		return "KnownOK"
    83  	case KnownRecordMismatch:
    84  		return "KnownRecordMismatch"
    85  	case AddedNew:
    86  		return "AddedNew"
    87  	}
    88  	return ""
    89  }
    90  
    91  // HostAlreadyKnown checks the given host details against our
    92  // known hosts file.
    93  func (h *KnownHosts) HostAlreadyKnown(hostname string, remote net.Addr, key ssh.PublicKey, pubBytes []byte, addIfNotKnown bool, allowOneshotConnect bool) (HostState, *ServerPubKey, error) {
    94  	strPubBytes := string(pubBytes)
    95  
    96  	//pp("in HostAlreadyKnown... starting. h=%p, looking up by strPubBytes = '%s'", h, strPubBytes)
    97  
    98  	h.Mut.Lock()
    99  	record, ok := h.Hosts[strPubBytes]
   100  	h.Mut.Unlock()
   101  	p("lookup of h.Hosts[strPubBytes] returned ok=%v, record=%#v", ok, record)
   102  	if ok {
   103  		if record.ServerBanned {
   104  			err := fmt.Errorf("the key '%s' has been marked as banned", strPubBytes)
   105  			p("in HostAlreadyKnown, returning Banned: '%s'", err)
   106  			return Banned, record, err
   107  		}
   108  
   109  		if strings.HasPrefix(hostname, "localhost") || strings.HasPrefix(hostname, "127.0.0.1") {
   110  			// no host checking when coming from localhost
   111  			p("in HostAlreadyKnown, no host checking when coming from localhost, returning KnownOK")
   112  			/*
   113  				if addIfNotKnown {
   114  					msg := fmt.Errorf("error: flag -new given but not needed. Re-run without -new. No host checking on localhost/127.0.0.1. We saw hostname: '%s'", hostname)
   115  					p(msg.Error())
   116  					return KnownOK, record, msg
   117  				}
   118  				return KnownOK, record, nil
   119  			*/
   120  			if addIfNotKnown {
   121  				return h.AddNeeded(addIfNotKnown, allowOneshotConnect, hostname, remote, strPubBytes, key, record)
   122  			}
   123  		}
   124  		if record.Hostname != hostname {
   125  			// check all the SplitHostnames before failing
   126  			found := false
   127  			record.Mut.Lock()
   128  			for hn := range record.SplitHostnames {
   129  				if hn == hostname {
   130  					found = true
   131  					record.Mut.Unlock()
   132  					break
   133  				}
   134  			}
   135  
   136  			if addIfNotKnown {
   137  				return h.AddNeeded(addIfNotKnown, allowOneshotConnect, hostname, remote, strPubBytes, key, record)
   138  			}
   139  			if !found {
   140  				record.Mut.Lock()
   141  				err := fmt.Errorf("hostname mismatch for key '%s': record.Hostname:'%v' in records, hostname:'%s' supplied now. record.SplitHostnames = '%#v", strPubBytes, record.Hostname, hostname, record.SplitHostnames)
   142  				record.Mut.Unlock()
   143  
   144  				//fmt.Printf("\n in HostAlreadyKnown, returning KnownRecordMismatch: '%s'", err)
   145  				return KnownRecordMismatch, record, err
   146  			}
   147  		}
   148  		p("in HostAlreadyKnown, returning KnownOK.")
   149  		if addIfNotKnown {
   150  			msg := fmt.Errorf("error: flag -new given but not needed. Re-run without -new : this is important to prevent MITM attacks; TofuAddIfNotKnown must be false once the server/host is known.")
   151  			p(msg.Error())
   152  			return KnownOK, record, msg
   153  		}
   154  		return KnownOK, record, nil
   155  	}
   156  
   157  	return h.AddNeeded(addIfNotKnown, allowOneshotConnect, hostname, remote, strPubBytes, key, record)
   158  }
   159  
   160  // SSHConnect is the main entry point for the gosshtun library,
   161  // establishing an ssh tunnel between two hosts.
   162  //
   163  // passphrase and toptUrl (one-time password used in challenge/response)
   164  // are optional, but will be offered to the server if set.
   165  //
   166  func (cfg *SshegoConfig) SSHConnect(ctxPar context.Context, h *KnownHosts, username string, keypath string, sshdHost string, sshdPort int64, passphrase string, toptUrl string, halt *ssh.Halter) (sshClient *ssh.Client, nc net.Conn, err error) {
   167  	cfg.Mut.Lock()
   168  	defer cfg.Mut.Unlock()
   169  
   170  	if !cfg.SkipKeepAlive {
   171  		if cfg.KeepAliveEvery <= 0 {
   172  			cfg.KeepAliveEvery = time.Second // default to 1 sec.
   173  		}
   174  	}
   175  
   176  	ctx, cancelctx := context.WithCancel(ctxPar)
   177  	if halt != nil {
   178  		go ssh.MAD(ctx, cancelctx, halt)
   179  	}
   180  
   181  	p("SSHConnect sees sshdHost:port = %s:%v. cfg=%#v", sshdHost, sshdPort, cfg)
   182  	if h == nil {
   183  		panic("h cannot be nil!")
   184  	}
   185  
   186  	// the callback just after key-exchange to validate server is here
   187  	hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
   188  
   189  		pubBytes := ssh.MarshalAuthorizedKey(key)
   190  		fingerprint := ssh.FingerprintSHA256(key)
   191  
   192  		hostStatus, spubkey, err := h.HostAlreadyKnown(hostname, remote, key, pubBytes, cfg.AddIfNotKnown, cfg.TestAllowOneshotConnect)
   193  		//log.Printf("SshegoConfig.SSHConnect(): in hostKeyCallback(), hostStatus: '%s', hostname='%s', remote='%s', key.Type='%s'  server.host.pub.key='%s' and host-key sha256.fingerprint='%s'\n", hostStatus, hostname, remote, key.Type(), pubBytes, fingerprint)
   194  		_ = fingerprint
   195  		//log.Printf("server '%s' has host-key sha256.fingerprint='%s'", hostname, fingerprint)
   196  		h.Mut.Lock()
   197  		h.curStatus = hostStatus
   198  		h.curHost = spubkey
   199  		h.Mut.Unlock()
   200  
   201  		if err != nil {
   202  			// this is strict checking of hosts here, any non-nil error
   203  			// will fail the ssh handshake.
   204  			p("err not nil at line 178 of sshutil.go: '%v'", err)
   205  			return err
   206  		}
   207  
   208  		switch hostStatus {
   209  		case Banned:
   210  			return fmt.Errorf("banned server")
   211  
   212  		case KnownRecordMismatch:
   213  			return fmt.Errorf("known record mismatch")
   214  
   215  		case KnownOK:
   216  			p("in hostKeyCallback(), hostStatus is KnownOK.")
   217  			return nil
   218  
   219  		case Unknown:
   220  			// do we allow?
   221  			return fmt.Errorf("unknown server; could be Man-In-The-Middle attack.  If this is first time setup, you must use -new to allow the new host")
   222  		}
   223  
   224  		return nil
   225  	}
   226  	// end hostKeyCallback closure definition. Has to be a closure to access h.
   227  
   228  	// EMBEDDED SSHD server
   229  	if cfg.EmbeddedSSHd.Addr != "" {
   230  		// only start Esshd if not already:
   231  		if cfg.Esshd == nil {
   232  
   233  			log.Printf("%v starting -esshd with addr: %s",
   234  				cfg.Nickname, cfg.EmbeddedSSHd.Addr)
   235  			err := cfg.EmbeddedSSHd.ParseAddr()
   236  			if err != nil {
   237  				panic(err)
   238  			}
   239  			cfg.NewEsshd()
   240  			go cfg.Esshd.Start(ctx)
   241  		}
   242  	}
   243  
   244  	p("got to direct test. cfg.DirectTcp=%v", cfg.DirectTcp)
   245  	if !cfg.DirectTcp &&
   246  		cfg.RemoteToLocal.Listen.Addr == "" &&
   247  		cfg.LocalToRemote.Listen.Addr == "" {
   248  		//panic("nothing to do?!")
   249  		// when starting an esshd, we just listen,
   250  		// no active outgoing connection.
   251  		return nil, nil, nil
   252  	}
   253  
   254  	if cfg.DirectTcp ||
   255  		cfg.RemoteToLocal.Listen.Addr != "" ||
   256  		cfg.LocalToRemote.Listen.Addr != "" {
   257  
   258  		p("inside direct test")
   259  
   260  		useRSA := true
   261  		var privkey ssh.Signer
   262  		var err error
   263  		// to test that we fail without rsa key,
   264  		// allow submitting auth without it
   265  		// if the keypath == ""
   266  		if keypath == "" {
   267  			useRSA = false
   268  		} else {
   269  			// client forward tunnel with this RSA key
   270  			privkey, err = LoadRSAPrivateKey(keypath)
   271  			if err != nil {
   272  				return nil, nil, fmt.Errorf("error in SshegoConfig.SSHConnect() to '%s@%s:%v', LoadRSAPrivateKey(keypath='%v') errored with: '%v'", username, sshdHost, sshdPort, keypath, err)
   273  			}
   274  		}
   275  
   276  		auth := []ssh.AuthMethod{}
   277  		if useRSA {
   278  			auth = append(auth, ssh.PublicKeys(privkey))
   279  		}
   280  		if passphrase != "" {
   281  			auth = append(auth, ssh.Password(passphrase))
   282  		}
   283  		if toptUrl != "" {
   284  			ans := kiCliHelp{
   285  				passphrase: passphrase,
   286  				toptUrl:    toptUrl,
   287  			}
   288  			auth = append(auth, ssh.KeyboardInteractiveChallenge(ans.helper))
   289  		}
   290  
   291  		cliCfg := &ssh.ClientConfig{
   292  			User:     username,
   293  			HostPort: fmt.Sprintf("%v:%v", sshdHost, sshdPort),
   294  			Auth:     auth,
   295  			// HostKeyCallback, if not nil, is called during the cryptographic
   296  			// handshake to validate the server's host key. A nil HostKeyCallback
   297  			// implies that all host keys are accepted.
   298  			HostKeyCallback: hostKeyCallback,
   299  			Config: ssh.Config{
   300  				Ciphers: getCiphers(),
   301  				Halt:    halt,
   302  			},
   303  		}
   304  		hostport := fmt.Sprintf("%s:%d", sshdHost, sshdPort)
   305  		p("about to ssh.Dial hostport='%s'", hostport)
   306  		sshClient, nc, err = cfg.mySSHDial(ctx, "tcp", hostport, cliCfg, halt)
   307  		p("sshClient back from mySSHDial() = %p, err=%v", sshClient, err)
   308  
   309  		if err != nil {
   310  			p("returning early on %v", err)
   311  			return nil, nil, fmt.Errorf("sshConnect() errored at dial to '%s': '%s' ", hostport, err.Error())
   312  		}
   313  		if sshClient == nil {
   314  			panic("mySSHDial must give us sshClient if err == nil")
   315  		}
   316  		p("sshClient good = %p", sshClient)
   317  
   318  		if cfg.RemoteToLocal.Listen.Addr != "" {
   319  			err = cfg.StartupReverseListener(ctx, sshClient)
   320  			if err != nil {
   321  				return nil, nil, fmt.Errorf("StartupReverseListener failed: %s", err)
   322  			}
   323  		}
   324  		if cfg.LocalToRemote.Listen.Addr != "" {
   325  			err = cfg.StartupForwardListener(ctx, sshClient)
   326  			if err != nil {
   327  				return nil, nil, fmt.Errorf("StartupFowardListener failed: %s", err)
   328  			}
   329  		}
   330  	}
   331  	cfg.Underlying = nc
   332  	cfg.SshClient = sshClient
   333  	return sshClient, nc, nil
   334  }
   335  
   336  // StartupForwardListener is called when a forward tunnel is to
   337  // be listened for.
   338  func (cfg *SshegoConfig) StartupForwardListener(ctx context.Context, sshClientConn *ssh.Client) error {
   339  
   340  	p("sshego: StartupForwardListener: about to listen on %s\n", cfg.LocalToRemote.Listen.Addr)
   341  	ln, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP(cfg.LocalToRemote.Listen.Host), Port: int(cfg.LocalToRemote.Listen.Port)})
   342  	if err != nil {
   343  		return fmt.Errorf("could not -listen on %s: %s", cfg.LocalToRemote.Listen.Addr, err)
   344  	}
   345  
   346  	go func() {
   347  		for {
   348  			p("sshego: about to accept on local port %s\n", cfg.LocalToRemote.Listen.Addr)
   349  			timeoutMillisec := 10000
   350  			err = ln.SetDeadline(time.Now().Add(time.Duration(timeoutMillisec) * time.Millisecond))
   351  			panicOn(err) // TODO handle error
   352  			fromBrowser, err := ln.Accept()
   353  			if err != nil {
   354  				if _, ok := err.(*net.OpError); ok {
   355  					continue
   356  					//break
   357  				}
   358  				p("ln.Accept err = '%s'  aka '%#v'\n", err, err)
   359  				panic(err) // todo handle error
   360  			}
   361  			if !cfg.Quiet {
   362  				log.Printf("sshego: accepted forward connection on %s, forwarding --> to sshd host %s, and thence --> to remote %s\n", cfg.LocalToRemote.Listen.Addr, cfg.SSHdServer.Addr, cfg.LocalToRemote.Remote.Addr)
   363  			}
   364  
   365  			// if you want to collect them...
   366  			//cfg.Fwd = append(cfg.Fwd, NewForward(cfg, sshClientConn, fromBrowser))
   367  			// or just fire and forget...
   368  			NewForward(ctx, cfg, sshClientConn, fromBrowser)
   369  		}
   370  	}()
   371  
   372  	//fmt.Printf("\n returning from SSHConnect().\n")
   373  	return nil
   374  }
   375  
   376  // Fingerprint performs a SHA256 BASE64 fingerprint of the PublicKey, similar to OpenSSH.
   377  // See: https://anongit.mindrot.org/openssh.git/commit/?id=56d1c83cdd1ac
   378  func Fingerprint(k ssh.PublicKey) string {
   379  	hash := sha256.Sum256(k.Marshal())
   380  	r := "SHA256:" + base64.StdEncoding.EncodeToString(hash[:])
   381  	return r
   382  }
   383  
   384  // Forwarder represents one bi-directional forward (sshego to sshd) tcp connection.
   385  type Forwarder struct {
   386  	shovelPair *shovelPair
   387  }
   388  
   389  // NewForward is called to produce a Forwarder structure for each new forward connection.
   390  func NewForward(ctx context.Context, cfg *SshegoConfig, sshClientConn *ssh.Client, fromBrowser net.Conn) *Forwarder {
   391  
   392  	sp := newShovelPair(false)
   393  	sshClientConn.TmpCtx = ctx
   394  	channelToSSHd, err := sshClientConn.Dial("tcp", cfg.LocalToRemote.Remote.Addr)
   395  	if err != nil {
   396  		msg := fmt.Errorf("Remote dial to '%s' error: %s", cfg.LocalToRemote.Remote.Addr, err)
   397  		log.Printf(msg.Error())
   398  		return nil
   399  	}
   400  
   401  	// here is the heart of the ssh-secured tunnel functionality:
   402  	// we start the two shovels that keep traffic flowing
   403  	// in both directions from browser over to sshd:
   404  	// reads on fromBrowser are forwarded to channelToSSHd;
   405  	// reads on channelToSSHd are forwarded to fromBrowser.
   406  
   407  	//sp.DoLog = true
   408  	sp.Start(fromBrowser, channelToSSHd, "fromBrowser<-channelToSSHd", "channelToSSHd<-fromBrowser")
   409  	return &Forwarder{shovelPair: sp}
   410  }
   411  
   412  // Reverse represents one bi-directional (initiated at sshd, tunneled to sshego) tcp connection.
   413  type Reverse struct {
   414  	shovelPair *shovelPair
   415  }
   416  
   417  // StartupReverseListener is called when a reverse tunnel is requested, to listen
   418  // and tunnel those connections.
   419  func (cfg *SshegoConfig) StartupReverseListener(ctx context.Context, sshClientConn *ssh.Client) error {
   420  	p("StartupReverseListener called")
   421  
   422  	addr, err := net.ResolveTCPAddr("tcp", cfg.RemoteToLocal.Listen.Addr)
   423  	if err != nil {
   424  		return err
   425  	}
   426  
   427  	lsn, err := sshClientConn.ListenTCP(ctx, addr)
   428  	if err != nil {
   429  		return err
   430  	}
   431  
   432  	// service "forwarded-tcpip" requests
   433  	go func() {
   434  		for {
   435  			p("sshego: about to accept for remote addr %s\n", cfg.RemoteToLocal.Listen.Addr)
   436  			fromRemote, err := lsn.Accept()
   437  			if err != nil {
   438  				if _, ok := err.(*net.OpError); ok {
   439  					continue
   440  					//break
   441  				}
   442  				p("rev.Lsn.Accept err = '%s'  aka '%#v'\n", err, err)
   443  				panic(err) // TODO handle error
   444  			}
   445  			if !cfg.Quiet {
   446  				log.Printf("sshego: accepted reverse connection from remote on  %s, forwarding to --> to %s\n",
   447  					cfg.RemoteToLocal.Listen.Addr, cfg.RemoteToLocal.Remote.Addr)
   448  			}
   449  			_, err = cfg.StartNewReverse(sshClientConn, fromRemote)
   450  			if err != nil {
   451  				log.Printf("error: StartNewReverse got error '%s'", err)
   452  			}
   453  		}
   454  	}()
   455  	return nil
   456  }
   457  
   458  // StartNewReverse is invoked once per reverse connection made to generate
   459  // a new Reverse structure.
   460  func (cfg *SshegoConfig) StartNewReverse(sshClientConn *ssh.Client, fromRemote net.Conn) (*Reverse, error) {
   461  
   462  	channelToLocalFwd, err := net.Dial("tcp", cfg.RemoteToLocal.Remote.Addr)
   463  	if err != nil {
   464  		msg := fmt.Errorf("Remote dial to '%s' error: %s", cfg.RemoteToLocal.Remote.Addr, err)
   465  		log.Printf(msg.Error())
   466  		return nil, msg
   467  	}
   468  
   469  	sp := newShovelPair(false)
   470  	rev := &Reverse{shovelPair: sp}
   471  	sp.Start(fromRemote, channelToLocalFwd, "fromRemoter<-channelToLocalFwd", "channelToLocalFwd<-fromRemote")
   472  	return rev, nil
   473  }
   474  
   475  func (h *KnownHosts) AddNeeded(addIfNotKnown, allowOneshotConnect bool, hostname string, remote net.Addr, strPubBytes string, key ssh.PublicKey, record *ServerPubKey) (HostState, *ServerPubKey, error) {
   476  	p("top of KnownHosts.AddNeeded(addIfNotKnown=%v, allowOneshotConnect=%v, hostname='%s', remote=%#v)", addIfNotKnown, allowOneshotConnect, hostname, remote)
   477  	if addIfNotKnown {
   478  		record := &ServerPubKey{
   479  			Hostname: hostname,
   480  			remote:   remote,
   481  			//key:      key,
   482  			HumanKey: strPubBytes,
   483  
   484  			// if we are adding to an SSH_KNOWN_HOSTS file, we need these:
   485  			Keytype:                  key.Type(),
   486  			Base64EncodededPublicKey: Base64ofPublicKey(key),
   487  			Comment: fmt.Sprintf("added_by_sshego_on_%v",
   488  				time.Now().Format(time.RFC3339)),
   489  			SplitHostnames: make(map[string]bool),
   490  		}
   491  		//pp("hostname = '%v'", hostname)
   492  		record.AddHostPort(hostname)
   493  
   494  		// host with same key may show up under an IP address and
   495  		// a FQHN, so combine under the key if we see that.
   496  		h.Mut.Lock()
   497  		prior, already := h.Hosts[strPubBytes]
   498  		// unlock below on both arms.
   499  
   500  		if !already {
   501  			//pp("completely new host:port = '%v' -> record: '%#v'", strPubBytes, record)
   502  			h.Hosts[strPubBytes] = record
   503  			h.Mut.Unlock()
   504  			h.Sync()
   505  		} else {
   506  			h.Mut.Unlock()
   507  			// two or more names under the same key.
   508  			//pp("two names under one key, hostname = '%#v'. prior='%#v'\n", hostname, prior)
   509  			prior.AddHostPort(hostname)
   510  			h.Sync()
   511  		}
   512  		if allowOneshotConnect {
   513  			return KnownOK, record, nil
   514  		}
   515  		msg := fmt.Errorf("good: added previously unknown sshd host '%v' with the -new flag. Re-run without -new (or setting TofuAddIfNotKnown=false) now", remote)
   516  		return AddedNew, record, msg
   517  	}
   518  
   519  	p("at end of HostAlreadyKnown/AddNeeded, returning Unknown.")
   520  	return Unknown, record, nil
   521  }
   522  
   523  // client and server cipher chosen here.
   524  func getCiphers() []string {
   525  	return []string{"aes128-gcm@openssh.com"}
   526  	/* available in github.com/glycerine/xcryptossh :
   527  	time for 512MB from SanJose to Amazon EC2 N. Cali,
   528  		"aes128-gcm@openssh.com", 27 seconds, 27 seconds.
   529  		"arcfour256", 24.96 seconds, 31.5 seconds on retry.
   530  		"arcfour128", 30.6 seconds
   531  		"aes128-ctr", 33.4 seconds
   532  		"aes192-ctr", 33.5 seconds
   533  		"aes256-ctr", 34.5 seconds
   534  	*/
   535  }
   536  
   537  func (cfg *SshegoConfig) mySSHDial(ctx context.Context, network, addr string, config *ssh.ClientConfig, halt *ssh.Halter) (*ssh.Client, net.Conn, error) {
   538  	//pp("starting SshegoConfig.mySSHDial().")
   539  	netconn, err := net.DialTimeout(network, addr, config.Timeout)
   540  	if err != nil {
   541  		return nil, nil, err
   542  	}
   543  
   544  	// Close netconn when when get a shutdown request.
   545  	// This close on the underlying TCP connection
   546  	// is essential to unblock some reads deep in
   547  	// the ssh codebash that otherwise won't timeout.
   548  	// Any of three flavors of close work.
   549  	if config.Halt != nil || halt != nil {
   550  		go func() {
   551  			var h1, h2 chan struct{}
   552  			if config.Halt != nil {
   553  				h1 = config.Halt.ReqStopChan()
   554  			}
   555  			if halt != nil {
   556  				h2 = halt.ReqStopChan()
   557  			}
   558  			select {
   559  			case <-h1:
   560  			case <-h2:
   561  			case <-ctx.Done():
   562  			}
   563  			netconn.Close()
   564  		}()
   565  	}
   566  	c, chans, reqs, err := ssh.NewClientConn(ctx, netconn, addr, config)
   567  	if err != nil {
   568  		return nil, nil, err
   569  	}
   570  	cli := cfg.NewSSHClient(ctx, c, chans, reqs, halt)
   571  
   572  	if cfg.KeepAliveEvery > 0 {
   573  		//pp("SshegoConfig.mySSHDial: calling cfg.startKeepalives(): cfg.KeepAliveEvery=%v", cfg.KeepAliveEvery)
   574  		uhp := &UHP{User: config.User, HostPort: config.HostPort}
   575  		err = cfg.startKeepalives(ctx, cfg.KeepAliveEvery, cli, uhp)
   576  	} else {
   577  		//pp("SshegoConfig.mySSHDial: *not* calling cfg.startKeepalives(): cfg.KeepAliveEvery=%v", cfg.KeepAliveEvery)
   578  	}
   579  	return cli, netconn, err
   580  }