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 }