// dialSsh is a helper that builds the transport layers and establishes the SSH connection. // When additional dial configuration is used, DialStats are recorded and returned. // // The net.Conn return value is the value to be removed from pendingConns; additional // layering (ThrottledConn, ActivityMonitoredConn) is applied, but this return value is the // base dial conn. The *ActivityMonitoredConn return value is the layered conn passed into // the ssh.Client. func dialSsh( config *Config, pendingConns *common.Conns, serverEntry *protocol.ServerEntry, selectedProtocol, sessionId string) (*dialResult, error) { // The meek protocols tunnel obfuscated SSH. Obfuscated SSH is layered on top of SSH. // So depending on which protocol is used, multiple layers are initialized. useObfuscatedSsh := false var directTCPDialAddress string var meekConfig *MeekConfig var err error switch selectedProtocol { case protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH: useObfuscatedSsh = true directTCPDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort) case protocol.TUNNEL_PROTOCOL_SSH: directTCPDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshPort) default: useObfuscatedSsh = true meekConfig, err = initMeekConfig(config, serverEntry, selectedProtocol, sessionId) if err != nil { return nil, common.ContextError(err) } } NoticeConnectingServer( serverEntry.IpAddress, serverEntry.Region, selectedProtocol, directTCPDialAddress, meekConfig) // Use an asynchronous callback to record the resolved IP address when // dialing a domain name. Note that DialMeek doesn't immediately // establish any HTTPS connections, so the resolved IP address won't be // reported until during/after ssh session establishment (the ssh traffic // is meek payload). So don't Load() the IP address value until after that // has completed to ensure a result. var resolvedIPAddress atomic.Value resolvedIPAddress.Store("") setResolvedIPAddress := func(IPAddress string) { resolvedIPAddress.Store(IPAddress) } // Create the base transport: meek or direct connection dialConfig := &DialConfig{ UpstreamProxyUrl: config.UpstreamProxyUrl, UpstreamProxyCustomHeaders: config.UpstreamProxyCustomHeaders, ConnectTimeout: time.Duration(*config.TunnelConnectTimeoutSeconds) * time.Second, PendingConns: pendingConns, DeviceBinder: config.DeviceBinder, DnsServerGetter: config.DnsServerGetter, UseIndistinguishableTLS: config.UseIndistinguishableTLS, TrustedCACertificatesFilename: config.TrustedCACertificatesFilename, DeviceRegion: config.DeviceRegion, ResolvedIPCallback: setResolvedIPAddress, } var dialConn net.Conn if meekConfig != nil { dialConn, err = DialMeek(meekConfig, dialConfig) if err != nil { return nil, common.ContextError(err) } } else { dialConn, err = DialTCP(directTCPDialAddress, dialConfig) if err != nil { return nil, common.ContextError(err) } } cleanupConn := dialConn defer func() { // Cleanup on error if cleanupConn != nil { cleanupConn.Close() pendingConns.Remove(cleanupConn) } }() // Activity monitoring is used to measure tunnel duration monitoredConn, err := common.NewActivityMonitoredConn(dialConn, 0, false, nil, nil) if err != nil { return nil, common.ContextError(err) } // Apply throttling (if configured) throttledConn := common.NewThrottledConn(monitoredConn, config.RateLimits) // Add obfuscated SSH layer var sshConn net.Conn = throttledConn if useObfuscatedSsh { sshConn, err = common.NewObfuscatedSshConn( common.OBFUSCATION_CONN_MODE_CLIENT, throttledConn, serverEntry.SshObfuscatedKey) if err != nil { return nil, common.ContextError(err) } } // Now establish the SSH session over the conn transport expectedPublicKey, err := base64.StdEncoding.DecodeString(serverEntry.SshHostKey) if err != nil { return nil, common.ContextError(err) } sshCertChecker := &ssh.CertChecker{ HostKeyFallback: func(addr string, remote net.Addr, publicKey ssh.PublicKey) error { if !bytes.Equal(expectedPublicKey, publicKey.Marshal()) { return common.ContextError(errors.New("unexpected host public key")) } return nil }, } sshPasswordPayload := &protocol.SSHPasswordPayload{ SessionId: sessionId, SshPassword: serverEntry.SshPassword, ClientCapabilities: []string{protocol.CLIENT_CAPABILITY_SERVER_REQUESTS}, } payload, err := json.Marshal(sshPasswordPayload) if err != nil { return nil, common.ContextError(err) } sshClientConfig := &ssh.ClientConfig{ User: serverEntry.SshUsername, Auth: []ssh.AuthMethod{ ssh.Password(string(payload)), }, HostKeyCallback: sshCertChecker.CheckHostKey, } // The ssh session establishment (via ssh.NewClientConn) is wrapped // in a timeout to ensure it won't hang. We've encountered firewalls // that allow the TCP handshake to complete but then send a RST to the // server-side and nothing to the client-side, and if that happens // while ssh.NewClientConn is reading, it may wait forever. The timeout // closes the conn, which interrupts it. // Note: TCP handshake timeouts are provided by TCPConn, and session // timeouts *after* ssh establishment are provided by the ssh keep alive // in operate tunnel. // TODO: adjust the timeout to account for time-elapsed-from-start type sshNewClientResult struct { sshClient *ssh.Client sshRequests <-chan *ssh.Request err error } resultChannel := make(chan *sshNewClientResult, 2) if *config.TunnelConnectTimeoutSeconds > 0 { time.AfterFunc(time.Duration(*config.TunnelConnectTimeoutSeconds)*time.Second, func() { resultChannel <- &sshNewClientResult{nil, nil, errors.New("ssh dial timeout")} }) } go func() { // The following is adapted from ssh.Dial(), here using a custom conn // The sshAddress is passed through to host key verification callbacks; we don't use it. sshAddress := "" sshClientConn, sshChannels, sshRequests, err := ssh.NewClientConn( sshConn, sshAddress, sshClientConfig) var sshClient *ssh.Client if err == nil { sshClient = ssh.NewClient(sshClientConn, sshChannels, nil) } resultChannel <- &sshNewClientResult{sshClient, sshRequests, err} }() result := <-resultChannel if result.err != nil { return nil, common.ContextError(result.err) } var dialStats *TunnelDialStats if dialConfig.UpstreamProxyUrl != "" || meekConfig != nil { dialStats = &TunnelDialStats{} if dialConfig.UpstreamProxyUrl != "" { // Note: UpstreamProxyUrl should have parsed correctly in the dial proxyURL, err := url.Parse(dialConfig.UpstreamProxyUrl) if err == nil { dialStats.UpstreamProxyType = proxyURL.Scheme } dialStats.UpstreamProxyCustomHeaderNames = make([]string, 0) for name, _ := range dialConfig.UpstreamProxyCustomHeaders { dialStats.UpstreamProxyCustomHeaderNames = append(dialStats.UpstreamProxyCustomHeaderNames, name) } } if meekConfig != nil { dialStats.MeekDialAddress = meekConfig.DialAddress dialStats.MeekResolvedIPAddress = resolvedIPAddress.Load().(string) dialStats.MeekSNIServerName = meekConfig.SNIServerName dialStats.MeekHostHeader = meekConfig.HostHeader dialStats.MeekTransformedHostName = meekConfig.TransformedHostName } NoticeConnectedTunnelDialStats(serverEntry.IpAddress, dialStats) } cleanupConn = nil // Note: dialConn may be used to close the underlying network connection // but should not be used to perform I/O as that would interfere with SSH // (and also bypasses throttling). return &dialResult{ dialConn: dialConn, monitoredConn: monitoredConn, sshClient: result.sshClient, sshRequests: result.sshRequests, dialStats: dialStats}, nil }
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. }