// selectFrontingParameters is a helper which selects/generates meek fronting // parameters where the server entry provides multiple options or patterns. func selectFrontingParameters( serverEntry *ServerEntry) (frontingAddress, frontingHost string, err error) { if len(serverEntry.MeekFrontingAddressesRegex) > 0 { // Generate a front address based on the regex. frontingAddress, err = regen.Generate(serverEntry.MeekFrontingAddressesRegex) if err != nil { return "", "", ContextError(err) } } else { // Randomly select, for this connection attempt, one front address for // fronting-capable servers. if len(serverEntry.MeekFrontingAddresses) == 0 { return "", "", ContextError(errors.New("MeekFrontingAddresses is empty")) } index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingAddresses)) if err != nil { return "", "", ContextError(err) } frontingAddress = serverEntry.MeekFrontingAddresses[index] } if len(serverEntry.MeekFrontingHosts) > 0 { index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingHosts)) if err != nil { return "", "", ContextError(err) } frontingHost = serverEntry.MeekFrontingHosts[index] } else { // Backwards compatibility case frontingHost = serverEntry.MeekFrontingHost } return }
// dialSsh is a helper that builds the transport layers and establishes the SSH connection func dialSsh( config *Config, pendingConns *Conns, serverEntry *ServerEntry, selectedProtocol, sessionId string) (conn net.Conn, sshClient *ssh.Client, err 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. port := 0 useMeek := false useFronting := false useObfuscatedSsh := false switch selectedProtocol { case TUNNEL_PROTOCOL_FRONTED_MEEK: useMeek = true useFronting = true useObfuscatedSsh = true case TUNNEL_PROTOCOL_UNFRONTED_MEEK: useMeek = true useObfuscatedSsh = true port = serverEntry.SshObfuscatedPort case TUNNEL_PROTOCOL_OBFUSCATED_SSH: useObfuscatedSsh = true port = serverEntry.SshObfuscatedPort case TUNNEL_PROTOCOL_SSH: port = serverEntry.SshPort } frontingAddress := "" if useFronting { if len(serverEntry.MeekFrontingAddressesRegex) > 0 { // Generate a front address based on the regex. frontingAddress, err = regen.Generate(serverEntry.MeekFrontingAddressesRegex) if err != nil { return nil, nil, ContextError(err) } } else { // Randomly select, for this connection attempt, one front address for // fronting-capable servers. if len(serverEntry.MeekFrontingAddresses) == 0 { return nil, nil, ContextError(errors.New("MeekFrontingAddresses is empty")) } index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingAddresses)) if err != nil { return nil, nil, ContextError(err) } frontingAddress = serverEntry.MeekFrontingAddresses[index] } } NoticeConnectingServer( serverEntry.IpAddress, serverEntry.Region, selectedProtocol, frontingAddress) // Create the base transport: meek or direct connection dialConfig := &DialConfig{ UpstreamProxyUrl: config.UpstreamProxyUrl, ConnectTimeout: TUNNEL_CONNECT_TIMEOUT, PendingConns: pendingConns, DeviceBinder: config.DeviceBinder, DnsServerGetter: config.DnsServerGetter, UseIndistinguishableTLS: config.UseIndistinguishableTLS, TrustedCACertificatesFilename: config.TrustedCACertificatesFilename, } if useMeek { conn, err = DialMeek(serverEntry, sessionId, frontingAddress, dialConfig) if err != nil { return nil, nil, ContextError(err) } } else { conn, err = DialTCP(fmt.Sprintf("%s:%d", serverEntry.IpAddress, port), dialConfig) if err != nil { return nil, nil, ContextError(err) } } cleanupConn := conn defer func() { // Cleanup on error if err != nil { cleanupConn.Close() } }() // Add obfuscated SSH layer var sshConn net.Conn sshConn = conn if useObfuscatedSsh { sshConn, err = NewObfuscatedSshConn(conn, serverEntry.SshObfuscatedKey) if err != nil { return nil, nil, ContextError(err) } } // Now establish the SSH session over the sshConn transport expectedPublicKey, err := base64.StdEncoding.DecodeString(serverEntry.SshHostKey) if err != nil { return nil, nil, ContextError(err) } sshCertChecker := &ssh.CertChecker{ HostKeyFallback: func(addr string, remote net.Addr, publicKey ssh.PublicKey) error { if !bytes.Equal(expectedPublicKey, publicKey.Marshal()) { return ContextError(errors.New("unexpected host public key")) } return nil }, } sshPasswordPayload, err := json.Marshal( struct { SessionId string `json:"SessionId"` SshPassword string `json:"SshPassword"` }{sessionId, serverEntry.SshPassword}) if err != nil { return nil, nil, ContextError(err) } sshClientConfig := &ssh.ClientConfig{ User: serverEntry.SshUsername, Auth: []ssh.AuthMethod{ ssh.Password(string(sshPasswordPayload)), }, 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 err error } resultChannel := make(chan *sshNewClientResult, 2) time.AfterFunc(TUNNEL_CONNECT_TIMEOUT, func() { resultChannel <- &sshNewClientResult{nil, errors.New("ssh dial timeout")} }) go func() { // The folowing 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, sshChans, sshReqs, err := ssh.NewClientConn(sshConn, sshAddress, sshClientConfig) var sshClient *ssh.Client if err == nil { sshClient = ssh.NewClient(sshClientConn, sshChans, sshReqs) } resultChannel <- &sshNewClientResult{sshClient, err} }() result := <-resultChannel if result.err != nil { return nil, nil, ContextError(result.err) } return conn, result.sshClient, nil }