// DialMeek returns an initialized meek connection. A meek connection is // an HTTP session which does not depend on an underlying socket connection (although // persistent HTTP connections are used for performance). This function does not // wait for the connection to be "established" before returning. A goroutine // is spawned which will eventually start HTTP polling. // When frontingAddress is not "", fronting is used. This option assumes caller has // already checked server entry capabilities. func DialMeek( serverEntry *ServerEntry, sessionId string, frontingAddress string, config *DialConfig) (meek *MeekConn, err error) { // Configure transport // Note: MeekConn has its own PendingConns to manage the underlying HTTP transport connections, // which may be interrupted on MeekConn.Close(). This code previously used the establishTunnel // pendingConns here, but that was a lifecycle mismatch: we don't want to abort HTTP transport // connections while MeekConn is still in use pendingConns := new(Conns) // Use a copy of DialConfig with the meek pendingConns meekConfig := new(DialConfig) *meekConfig = *config meekConfig.PendingConns = pendingConns var host string var dialer Dialer var proxyUrl func(*http.Request) (*url.URL, error) if frontingAddress != "" { // In this case, host is not what is dialed but is what ends up in the HTTP Host header host = serverEntry.MeekFrontingHost // Custom TLS dialer: // // 1. ignores the HTTP request address and uses the fronting domain // 2. disables SNI -- SNI breaks fronting when used with CDNs that support SNI on the server side. // 3. skips verifying the server cert. // // Reasoning for #3: // // With a TLS MiM attack in place, and server certs verified, we'll fail to connect because the client // will refuse to connect. That's not a successful outcome. // // With a MiM attack in place, and server certs not verified, we'll fail to connect if the MiM is actively // targeting Psiphon and classifying the HTTP traffic by Host header or payload signature. // // However, in the case of a passive MiM that's just recording traffic or an active MiM that's targeting // something other than Psiphon, the client will connect. This is a successful outcome. // // What is exposed to the MiM? The Host header does not contain a Psiphon server IP address, just an // unrelated, randomly generated domain name which cannot be used to block direct connections. The // Psiphon server IP is sent over meek, but it's in the encrypted cookie. // // The payload (user traffic) gets its confidentiality and integrity from the underlying SSH protocol. // So, nothing is leaked to the MiM apart from signatures which could be used to classify the traffic // as Psiphon to possibly block it; but note that not revealing that the client is Psiphon is outside // our threat model; we merely seek to evade mass blocking by taking steps that require progressively // more effort to block. // // There is a subtle attack remaining: an adversary that can MiM some CDNs but not others (and so can // classify Psiphon traffic on some CDNs but not others) may throttle non-MiM CDNs so that our server // selection always chooses tunnels to the MiM CDN (without any server cert verification, we won't // exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after // some short period. This is mitigated by the "impaired" protocol classification mechanism. dialer = NewCustomTLSDialer( &CustomTLSConfig{ Dial: NewTCPDialer(meekConfig), Timeout: meekConfig.ConnectTimeout, FrontingAddr: fmt.Sprintf("%s:%d", frontingAddress, 443), SendServerName: false, SkipVerify: true, UseIndistinguishableTLS: config.UseIndistinguishableTLS, TrustedCACertificatesFilename: config.TrustedCACertificatesFilename, }) } else { // In the unfronted case, host is both what is dialed and what ends up in the HTTP Host header host = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort) if meekConfig.UpstreamProxyUrl != "" { // For unfronted meek, we let the http.Transport handle proxying, as the // target server hostname has to be in the HTTP request line. Also, in this // case, we don't require the proxy to support CONNECT and so we can work // through HTTP proxies that don't support it. url, err := url.Parse(meekConfig.UpstreamProxyUrl) if err != nil { return nil, ContextError(err) } proxyUrl = http.ProxyURL(url) meekConfig.UpstreamProxyUrl = "" } dialer = NewTCPDialer(meekConfig) } // Scheme is always "http". Otherwise http.Transport will try to do another TLS // handshake inside the explicit TLS session (in fronting mode). url := &url.URL{ Scheme: "http", Host: host, Path: "/", } cookie, err := makeCookie(serverEntry, sessionId) if err != nil { return nil, ContextError(err) } httpTransport := &http.Transport{ Proxy: proxyUrl, Dial: dialer, ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT, } var transport transporter if proxyUrl != nil { // Wrap transport with a transport that can perform HTTP proxy auth negotiation transport, err = upstreamproxy.NewProxyAuthTransport(httpTransport) if err != nil { return nil, ContextError(err) } } else { transport = httpTransport } // The main loop of a MeekConn is run in the relay() goroutine. // A MeekConn implements net.Conn concurrency semantics: // "Multiple goroutines may invoke methods on a Conn simultaneously." // // Read() calls and relay() are synchronized by exchanging control of a single // receiveBuffer (bytes.Buffer). This single buffer may be: // - in the emptyReceiveBuffer channel when it is available and empty; // - in the partialReadBuffer channel when it is available and contains data; // - in the fullReadBuffer channel when it is available and full of data; // - "checked out" by relay or Read when they are are writing to or reading from the // buffer, respectively. // relay() will obtain the buffer from either the empty or partial channel but block when // the buffer is full. Read will obtain the buffer from the partial or full channel when // there is data to read but block when the buffer is empty. // Write() calls and relay() are synchronized in a similar way, using a single // sendBuffer. meek = &MeekConn{ frontingAddress: frontingAddress, url: url, cookie: cookie, pendingConns: pendingConns, transport: transport, isClosed: false, broadcastClosed: make(chan struct{}), relayWaitGroup: new(sync.WaitGroup), emptyReceiveBuffer: make(chan *bytes.Buffer, 1), partialReceiveBuffer: make(chan *bytes.Buffer, 1), fullReceiveBuffer: make(chan *bytes.Buffer, 1), emptySendBuffer: make(chan *bytes.Buffer, 1), partialSendBuffer: make(chan *bytes.Buffer, 1), fullSendBuffer: make(chan *bytes.Buffer, 1), } // TODO: benchmark bytes.Buffer vs. built-in append with slices? meek.emptyReceiveBuffer <- new(bytes.Buffer) meek.emptySendBuffer <- new(bytes.Buffer) meek.relayWaitGroup.Add(1) go meek.relay() // Enable interruption if !config.PendingConns.Add(meek) { meek.Close() return nil, ContextError(errors.New("pending connections already closed")) } return meek, nil }
// DialMeek returns an initialized meek connection. A meek connection is // an HTTP session which does not depend on an underlying socket connection (although // persistent HTTP connections are used for performance). This function does not // wait for the connection to be "established" before returning. A goroutine // is spawned which will eventually start HTTP polling. // When frontingAddress is not "", fronting is used. This option assumes caller has // already checked server entry capabilities. func DialMeek( meekConfig *MeekConfig, dialConfig *DialConfig) (meek *MeekConn, err error) { // Configure transport // Note: MeekConn has its own PendingConns to manage the underlying HTTP transport connections, // which may be interrupted on MeekConn.Close(). This code previously used the establishTunnel // pendingConns here, but that was a lifecycle mismatch: we don't want to abort HTTP transport // connections while MeekConn is still in use pendingConns := new(common.Conns) // Use a copy of DialConfig with the meek pendingConns meekDialConfig := new(DialConfig) *meekDialConfig = *dialConfig meekDialConfig.PendingConns = pendingConns var transport transporter var additionalHeaders http.Header var proxyUrl func(*http.Request) (*url.URL, error) if meekConfig.UseHTTPS { // Custom TLS dialer: // // 1. ignores the HTTP request address and uses the fronting domain // 2. optionally disables SNI -- SNI breaks fronting when used with certain CDNs. // 3. skips verifying the server cert. // // Reasoning for #3: // // With a TLS MiM attack in place, and server certs verified, we'll fail to connect because the client // will refuse to connect. That's not a successful outcome. // // With a MiM attack in place, and server certs not verified, we'll fail to connect if the MiM is actively // targeting Psiphon and classifying the HTTP traffic by Host header or payload signature. // // However, in the case of a passive MiM that's just recording traffic or an active MiM that's targeting // something other than Psiphon, the client will connect. This is a successful outcome. // // What is exposed to the MiM? The Host header does not contain a Psiphon server IP address, just an // unrelated, randomly generated domain name which cannot be used to block direct connections. The // Psiphon server IP is sent over meek, but it's in the encrypted cookie. // // The payload (user traffic) gets its confidentiality and integrity from the underlying SSH protocol. // So, nothing is leaked to the MiM apart from signatures which could be used to classify the traffic // as Psiphon to possibly block it; but note that not revealing that the client is Psiphon is outside // our threat model; we merely seek to evade mass blocking by taking steps that require progressively // more effort to block. // // There is a subtle attack remaining: an adversary that can MiM some CDNs but not others (and so can // classify Psiphon traffic on some CDNs but not others) may throttle non-MiM CDNs so that our server // selection always chooses tunnels to the MiM CDN (without any server cert verification, we won't // exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after // some short period. This is mitigated by the "impaired" protocol classification mechanism. dialer := NewCustomTLSDialer(&CustomTLSConfig{ DialAddr: meekConfig.DialAddress, Dial: NewTCPDialer(meekDialConfig), Timeout: meekDialConfig.ConnectTimeout, SNIServerName: meekConfig.SNIServerName, SkipVerify: true, UseIndistinguishableTLS: meekDialConfig.UseIndistinguishableTLS, TrustedCACertificatesFilename: meekDialConfig.TrustedCACertificatesFilename, }) // TODO: wrap in an http.Client and use http.Client.Timeout which actually covers round trip transport = &http.Transport{ Dial: dialer, ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT, } } else { // The dialer ignores address that http.Transport will pass in (derived // from the HTTP request URL) and always dials meekConfig.DialAddress. dialer := func(string, string) (net.Conn, error) { return NewTCPDialer(meekDialConfig)("tcp", meekConfig.DialAddress) } // For HTTP, and when the meekConfig.DialAddress matches the // meekConfig.HostHeader, we let http.Transport handle proxying. // http.Transport will put the the HTTP server address in the HTTP // request line. In this one case, we can use an HTTP proxy that does // not offer CONNECT support. if strings.HasPrefix(meekDialConfig.UpstreamProxyUrl, "http://") && (meekConfig.DialAddress == meekConfig.HostHeader || meekConfig.DialAddress == meekConfig.HostHeader+":80") { url, err := url.Parse(meekDialConfig.UpstreamProxyUrl) if err != nil { return nil, common.ContextError(err) } proxyUrl = http.ProxyURL(url) meekDialConfig.UpstreamProxyUrl = "" // Here, the dialer must use the address that http.Transport // passes in (which will be proxy address). dialer = NewTCPDialer(meekDialConfig) } // TODO: wrap in an http.Client and use http.Client.Timeout which actually covers round trip httpTransport := &http.Transport{ Proxy: proxyUrl, Dial: dialer, ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT, } if proxyUrl != nil { // Wrap transport with a transport that can perform HTTP proxy auth negotiation transport, err = upstreamproxy.NewProxyAuthTransport(httpTransport, meekDialConfig.UpstreamProxyCustomHeaders) if err != nil { return nil, common.ContextError(err) } } else { transport = httpTransport } } // Scheme is always "http". Otherwise http.Transport will try to do another TLS // handshake inside the explicit TLS session (in fronting mode). url := &url.URL{ Scheme: "http", Host: meekConfig.HostHeader, Path: "/", } if meekConfig.UseHTTPS { host, _, err := net.SplitHostPort(meekConfig.DialAddress) if err != nil { return nil, common.ContextError(err) } additionalHeaders = map[string][]string{ "X-Psiphon-Fronting-Address": {host}, } } else { if proxyUrl == nil { additionalHeaders = meekDialConfig.UpstreamProxyCustomHeaders } } cookie, err := makeMeekCookie(meekConfig) if err != nil { return nil, common.ContextError(err) } // The main loop of a MeekConn is run in the relay() goroutine. // A MeekConn implements net.Conn concurrency semantics: // "Multiple goroutines may invoke methods on a Conn simultaneously." // // Read() calls and relay() are synchronized by exchanging control of a single // receiveBuffer (bytes.Buffer). This single buffer may be: // - in the emptyReceiveBuffer channel when it is available and empty; // - in the partialReadBuffer channel when it is available and contains data; // - in the fullReadBuffer channel when it is available and full of data; // - "checked out" by relay or Read when they are are writing to or reading from the // buffer, respectively. // relay() will obtain the buffer from either the empty or partial channel but block when // the buffer is full. Read will obtain the buffer from the partial or full channel when // there is data to read but block when the buffer is empty. // Write() calls and relay() are synchronized in a similar way, using a single // sendBuffer. meek = &MeekConn{ url: url, additionalHeaders: additionalHeaders, cookie: cookie, pendingConns: pendingConns, transport: transport, isClosed: false, broadcastClosed: make(chan struct{}), relayWaitGroup: new(sync.WaitGroup), emptyReceiveBuffer: make(chan *bytes.Buffer, 1), partialReceiveBuffer: make(chan *bytes.Buffer, 1), fullReceiveBuffer: make(chan *bytes.Buffer, 1), emptySendBuffer: make(chan *bytes.Buffer, 1), partialSendBuffer: make(chan *bytes.Buffer, 1), fullSendBuffer: make(chan *bytes.Buffer, 1), } // TODO: benchmark bytes.Buffer vs. built-in append with slices? meek.emptyReceiveBuffer <- new(bytes.Buffer) meek.emptySendBuffer <- new(bytes.Buffer) meek.relayWaitGroup.Add(1) go meek.relay() // Enable interruption if !dialConfig.PendingConns.Add(meek) { meek.Close() return nil, common.ContextError(errors.New("pending connections already closed")) } return meek, nil }