// 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 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) } }