func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.Conn) { sshServer.registerAcceptedClient(tunnelProtocol) defer sshServer.unregisterAcceptedClient(tunnelProtocol) geoIPData := sshServer.support.GeoIPService.Lookup( common.IPAddressFromAddr(clientConn.RemoteAddr())) sshClient := newSshClient(sshServer, tunnelProtocol, geoIPData) // Set initial traffic rules, pre-handshake, based on currently known info. sshClient.setTrafficRules() // Wrap the base client connection with an ActivityMonitoredConn which will // terminate the connection if no data is received before the deadline. This // timeout is in effect for the entire duration of the SSH connection. Clients // must actively use the connection or send SSH keep alive requests to keep // the connection active. Writes are not considered reliable activity indicators // due to buffering. activityConn, err := common.NewActivityMonitoredConn( clientConn, SSH_CONNECTION_READ_DEADLINE, false, nil) if err != nil { clientConn.Close() log.WithContextFields(LogFields{"error": err}).Error("NewActivityMonitoredConn failed") return } clientConn = activityConn // Further wrap the connection in a rate limiting ThrottledConn. throttledConn := common.NewThrottledConn(clientConn, sshClient.rateLimits()) clientConn = throttledConn // Run the initial [obfuscated] SSH handshake in a goroutine so we can both // respect shutdownBroadcast and implement a specific handshake timeout. // The timeout is to reclaim network resources in case the handshake takes // too long. type sshNewServerConnResult struct { conn net.Conn sshConn *ssh.ServerConn channels <-chan ssh.NewChannel requests <-chan *ssh.Request err error } resultChannel := make(chan *sshNewServerConnResult, 2) if SSH_HANDSHAKE_TIMEOUT > 0 { time.AfterFunc(time.Duration(SSH_HANDSHAKE_TIMEOUT), func() { resultChannel <- &sshNewServerConnResult{err: errors.New("ssh handshake timeout")} }) } go func(conn net.Conn) { sshServerConfig := &ssh.ServerConfig{ PasswordCallback: sshClient.passwordCallback, AuthLogCallback: sshClient.authLogCallback, ServerVersion: sshServer.support.Config.SSHServerVersion, } sshServerConfig.AddHostKey(sshServer.sshHostKey) result := &sshNewServerConnResult{} // Wrap the connection in an SSH deobfuscator when required. if common.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) { // Note: NewObfuscatedSshConn blocks on network I/O // TODO: ensure this won't block shutdown conn, result.err = psiphon.NewObfuscatedSshConn( psiphon.OBFUSCATION_CONN_MODE_SERVER, conn, sshServer.support.Config.ObfuscatedSSHKey) if result.err != nil { result.err = common.ContextError(result.err) } } if result.err == nil { result.sshConn, result.channels, result.requests, result.err = ssh.NewServerConn(conn, sshServerConfig) } resultChannel <- result }(clientConn) var result *sshNewServerConnResult select { case result = <-resultChannel: case <-sshServer.shutdownBroadcast: // Close() will interrupt an ongoing handshake // TODO: wait for goroutine to exit before returning? clientConn.Close() return } if result.err != nil { clientConn.Close() // This is a Debug log due to noise. The handshake often fails due to I/O // errors as clients frequently interrupt connections in progress when // client-side load balancing completes a connection to a different server. log.WithContextFields(LogFields{"error": result.err}).Debug("handshake failed") return } sshClient.Lock() sshClient.sshConn = result.sshConn sshClient.activityConn = activityConn sshClient.throttledConn = throttledConn sshClient.Unlock() if !sshServer.registerEstablishedClient(sshClient) { clientConn.Close() log.WithContext().Warning("register failed") return } defer sshServer.unregisterEstablishedClient(sshClient.sessionID) sshClient.runClient(result.channels, result.requests) // Note: sshServer.unregisterClient calls sshClient.Close(), // which also closes underlying transport Conn. }
func TestObfuscatedSSHConn(t *testing.T) { keyword, _ := MakeRandomStringHex(32) serverAddress := "127.0.0.1:2222" listener, err := net.Listen("tcp", serverAddress) if err != nil { t.Fatalf("Listen failed: %s", err) } rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("GenerateKey failed: %s", err) } hostKey, err := ssh.NewSignerFromKey(rsaKey) if err != nil { t.Fatalf("NewSignerFromKey failed: %s", err) } sshCertChecker := &ssh.CertChecker{ HostKeyFallback: func(addr string, remote net.Addr, publicKey ssh.PublicKey) error { if !bytes.Equal(hostKey.PublicKey().Marshal(), publicKey.Marshal()) { return errors.New("unexpected host public key") } return nil }, } result := make(chan error, 1) go func() { conn, err := listener.Accept() if err == nil { conn, err = NewObfuscatedSshConn( OBFUSCATION_CONN_MODE_SERVER, conn, keyword) } if err == nil { config := &ssh.ServerConfig{ NoClientAuth: true, } config.AddHostKey(hostKey) _, _, _, err = ssh.NewServerConn(conn, config) } if err != nil { select { case result <- err: default: } } }() go func() { conn, err := net.DialTimeout("tcp", serverAddress, 5*time.Second) if err == nil { conn, err = NewObfuscatedSshConn( OBFUSCATION_CONN_MODE_CLIENT, conn, keyword) } if err == nil { config := &ssh.ClientConfig{ HostKeyCallback: sshCertChecker.CheckHostKey, } _, _, _, err = ssh.NewClientConn(conn, "", config) } // Sends nil on success select { case result <- err: default: } }() err = <-result if err != nil { t.Fatalf("obfuscated SSH handshake failed: %s", err) } }