| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- /*
- * Copyright (c) 2015, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- package psiphon
- import (
- "bytes"
- "crypto/rand"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/url"
- "strings"
- "sync"
- "time"
- "github.com/Psiphon-Inc/crypto/nacl/box"
- "github.com/Psiphon-Inc/goarista/monotime"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
- )
- // MeekConn is based on meek-client.go from Tor and Psiphon:
- //
- // https://gitweb.torproject.org/pluggable-transports/meek.git/blob/HEAD:/meek-client/meek-client.go
- // CC0 1.0 Universal
- //
- // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/go/meek-client/meek-client.go
- const (
- MEEK_PROTOCOL_VERSION = 2
- MEEK_COOKIE_MAX_PADDING = 32
- MAX_SEND_PAYLOAD_LENGTH = 65536
- FULL_RECEIVE_BUFFER_LENGTH = 4194304
- READ_PAYLOAD_CHUNK_LENGTH = 65536
- MIN_POLL_INTERVAL = 100 * time.Millisecond
- MAX_POLL_INTERVAL = 5 * time.Second
- POLL_INTERNAL_MULTIPLIER = 1.5
- MEEK_ROUND_TRIP_RETRY_DEADLINE = 1 * time.Second
- MEEK_ROUND_TRIP_RETRY_DELAY = 50 * time.Millisecond
- MEEK_ROUND_TRIP_TIMEOUT = 20 * time.Second
- )
- // MeekConfig specifies the behavior of a MeekConn
- type MeekConfig struct {
- // DialAddress is the actual network address to dial to establish a
- // connection to the meek server. This may be either a fronted or
- // direct address. The address must be in the form "host:port",
- // where host may be a domain name or IP address.
- DialAddress string
- // UseHTTPS indicates whether to use HTTPS (true) or HTTP (false).
- UseHTTPS bool
- // SNIServerName is the value to place in the TLS SNI server_name
- // field when HTTPS is used.
- SNIServerName string
- // HostHeader is the value to place in the HTTP request Host header.
- HostHeader string
- // TransformedHostName records whether a HostNameTransformer
- // transformation is in effect. This value is used for stats reporting.
- TransformedHostName bool
- // The following values are used to create the obfuscated meek cookie.
- PsiphonServerAddress string
- SessionID string
- MeekCookieEncryptionPublicKey string
- MeekObfuscatedKey string
- }
- // MeekConn is a network connection that tunnels TCP over HTTP and supports "fronting". Meek sends
- // client->server flow in HTTP request bodies and receives server->client flow in HTTP response bodies.
- // Polling is used to achieve full duplex TCP.
- //
- // Fronting is an obfuscation technique in which the connection
- // to a web server, typically a CDN, is indistinguishable from any other HTTPS connection to the generic
- // "fronting domain" -- the HTTP Host header is used to route the requests to the actual destination.
- // See https://trac.torproject.org/projects/tor/wiki/doc/meek for more details.
- //
- // MeekConn also operates in unfronted mode, in which plain HTTP connections are made without routing
- // through a CDN.
- type MeekConn struct {
- url *url.URL
- additionalHeaders map[string]string
- cookie *http.Cookie
- pendingConns *common.Conns
- transport transporter
- mutex sync.Mutex
- isClosed bool
- broadcastClosed chan struct{}
- relayWaitGroup *sync.WaitGroup
- emptyReceiveBuffer chan *bytes.Buffer
- partialReceiveBuffer chan *bytes.Buffer
- fullReceiveBuffer chan *bytes.Buffer
- emptySendBuffer chan *bytes.Buffer
- partialSendBuffer chan *bytes.Buffer
- fullSendBuffer chan *bytes.Buffer
- }
- // transporter is implemented by both http.Transport and upstreamproxy.ProxyAuthTransport.
- type transporter interface {
- CancelRequest(req *http.Request)
- CloseIdleConnections()
- RegisterProtocol(scheme string, rt http.RoundTripper)
- RoundTrip(req *http.Request) (resp *http.Response, err error)
- }
- // 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
- 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.
- var proxyUrl func(*http.Request) (*url.URL, error)
- 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: "/",
- }
- var additionalHeaders map[string]string
- 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,
- }
- }
- 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
- }
- // Close terminates the meek connection. Close waits for the relay processing goroutine
- // to stop and releases HTTP transport resources.
- // A mutex is required to support net.Conn concurrency semantics.
- func (meek *MeekConn) Close() (err error) {
- meek.mutex.Lock()
- isClosed := meek.isClosed
- meek.isClosed = true
- meek.mutex.Unlock()
- if !isClosed {
- close(meek.broadcastClosed)
- meek.pendingConns.CloseAll()
- meek.relayWaitGroup.Wait()
- meek.transport.CloseIdleConnections()
- }
- return nil
- }
- func (meek *MeekConn) closed() bool {
- meek.mutex.Lock()
- isClosed := meek.isClosed
- meek.mutex.Unlock()
- return isClosed
- }
- // Read reads data from the connection.
- // net.Conn Deadlines are ignored. net.Conn concurrency semantics are supported.
- func (meek *MeekConn) Read(buffer []byte) (n int, err error) {
- if meek.closed() {
- return 0, common.ContextError(errors.New("meek connection is closed"))
- }
- // Block until there is received data to consume
- var receiveBuffer *bytes.Buffer
- select {
- case receiveBuffer = <-meek.partialReceiveBuffer:
- case receiveBuffer = <-meek.fullReceiveBuffer:
- case <-meek.broadcastClosed:
- return 0, common.ContextError(errors.New("meek connection has closed"))
- }
- n, err = receiveBuffer.Read(buffer)
- meek.replaceReceiveBuffer(receiveBuffer)
- return n, err
- }
- // Write writes data to the connection.
- // net.Conn Deadlines are ignored. net.Conn concurrency semantics are supported.
- func (meek *MeekConn) Write(buffer []byte) (n int, err error) {
- if meek.closed() {
- return 0, common.ContextError(errors.New("meek connection is closed"))
- }
- // Repeats until all n bytes are written
- n = len(buffer)
- for len(buffer) > 0 {
- // Block until there is capacity in the send buffer
- var sendBuffer *bytes.Buffer
- select {
- case sendBuffer = <-meek.emptySendBuffer:
- case sendBuffer = <-meek.partialSendBuffer:
- case <-meek.broadcastClosed:
- return 0, common.ContextError(errors.New("meek connection has closed"))
- }
- writeLen := MAX_SEND_PAYLOAD_LENGTH - sendBuffer.Len()
- if writeLen > 0 {
- if writeLen > len(buffer) {
- writeLen = len(buffer)
- }
- _, err = sendBuffer.Write(buffer[:writeLen])
- buffer = buffer[writeLen:]
- }
- meek.replaceSendBuffer(sendBuffer)
- }
- return n, err
- }
- // Stub implementation of net.Conn.LocalAddr
- func (meek *MeekConn) LocalAddr() net.Addr {
- return nil
- }
- // Stub implementation of net.Conn.RemoteAddr
- func (meek *MeekConn) RemoteAddr() net.Addr {
- return nil
- }
- // Stub implementation of net.Conn.SetDeadline
- func (meek *MeekConn) SetDeadline(t time.Time) error {
- return common.ContextError(errors.New("not supported"))
- }
- // Stub implementation of net.Conn.SetReadDeadline
- func (meek *MeekConn) SetReadDeadline(t time.Time) error {
- return common.ContextError(errors.New("not supported"))
- }
- // Stub implementation of net.Conn.SetWriteDeadline
- func (meek *MeekConn) SetWriteDeadline(t time.Time) error {
- return common.ContextError(errors.New("not supported"))
- }
- func (meek *MeekConn) replaceReceiveBuffer(receiveBuffer *bytes.Buffer) {
- switch {
- case receiveBuffer.Len() == 0:
- meek.emptyReceiveBuffer <- receiveBuffer
- case receiveBuffer.Len() >= FULL_RECEIVE_BUFFER_LENGTH:
- meek.fullReceiveBuffer <- receiveBuffer
- default:
- meek.partialReceiveBuffer <- receiveBuffer
- }
- }
- func (meek *MeekConn) replaceSendBuffer(sendBuffer *bytes.Buffer) {
- switch {
- case sendBuffer.Len() == 0:
- meek.emptySendBuffer <- sendBuffer
- case sendBuffer.Len() >= MAX_SEND_PAYLOAD_LENGTH:
- meek.fullSendBuffer <- sendBuffer
- default:
- meek.partialSendBuffer <- sendBuffer
- }
- }
- // relay sends and receives tunneled traffic (payload). An HTTP request is
- // triggered when data is in the write queue or at a polling interval.
- // There's a geometric increase, up to a maximum, in the polling interval when
- // no data is exchanged. Only one HTTP request is in flight at a time.
- func (meek *MeekConn) relay() {
- // Note: meek.Close() calls here in relay() are made asynchronously
- // (using goroutines) since Close() will wait on this WaitGroup.
- defer meek.relayWaitGroup.Done()
- interval := MIN_POLL_INTERVAL
- timeout := time.NewTimer(interval)
- sendPayload := make([]byte, MAX_SEND_PAYLOAD_LENGTH)
- for {
- timeout.Reset(interval)
- // Block until there is payload to send or it is time to poll
- var sendBuffer *bytes.Buffer
- select {
- case sendBuffer = <-meek.partialSendBuffer:
- case sendBuffer = <-meek.fullSendBuffer:
- case <-timeout.C:
- // In the polling case, send an empty payload
- case <-meek.broadcastClosed:
- // TODO: timeout case may be selected when broadcastClosed is set?
- return
- }
- sendPayloadSize := 0
- if sendBuffer != nil {
- var err error
- sendPayloadSize, err = sendBuffer.Read(sendPayload)
- meek.replaceSendBuffer(sendBuffer)
- if err != nil {
- NoticeAlert("%s", common.ContextError(err))
- go meek.Close()
- return
- }
- }
- receivedPayload, err := meek.roundTrip(sendPayload[:sendPayloadSize])
- if err != nil {
- NoticeAlert("%s", common.ContextError(err))
- go meek.Close()
- return
- }
- if receivedPayload == nil {
- // In this case, meek.roundTrip encountered broadcastClosed. Exit without error.
- return
- }
- receivedPayloadSize, err := meek.readPayload(receivedPayload)
- if err != nil {
- NoticeAlert("%s", common.ContextError(err))
- go meek.Close()
- return
- }
- if receivedPayloadSize > 0 || sendPayloadSize > 0 {
- interval = 0
- } else if interval == 0 {
- interval = MIN_POLL_INTERVAL
- } else {
- interval = time.Duration(float64(interval) * POLL_INTERNAL_MULTIPLIER)
- if interval >= MAX_POLL_INTERVAL {
- interval = MAX_POLL_INTERVAL
- }
- }
- }
- }
- // readPayload reads the HTTP response in chunks, making the read buffer available
- // to MeekConn.Read() calls after each chunk; the intention is to allow bytes to
- // flow back to the reader as soon as possible instead of buffering the entire payload.
- func (meek *MeekConn) readPayload(receivedPayload io.ReadCloser) (totalSize int64, err error) {
- defer receivedPayload.Close()
- totalSize = 0
- for {
- reader := io.LimitReader(receivedPayload, READ_PAYLOAD_CHUNK_LENGTH)
- // Block until there is capacity in the receive buffer
- var receiveBuffer *bytes.Buffer
- select {
- case receiveBuffer = <-meek.emptyReceiveBuffer:
- case receiveBuffer = <-meek.partialReceiveBuffer:
- case <-meek.broadcastClosed:
- return 0, nil
- }
- // Note: receiveBuffer size may exceed FULL_RECEIVE_BUFFER_LENGTH by up to the size
- // of one received payload. The FULL_RECEIVE_BUFFER_LENGTH value is just a threshold.
- n, err := receiveBuffer.ReadFrom(reader)
- meek.replaceReceiveBuffer(receiveBuffer)
- if err != nil {
- return 0, common.ContextError(err)
- }
- totalSize += n
- if n == 0 {
- break
- }
- }
- return totalSize, nil
- }
- // roundTrip configures and makes the actual HTTP POST request
- func (meek *MeekConn) roundTrip(sendPayload []byte) (io.ReadCloser, error) {
- // The retry mitigates intermittent failures between the client and front/server.
- //
- // Note: Retry will only be effective if entire request failed (underlying transport protocol
- // such as SSH will fail if extra bytes are replayed in either direction due to partial relay
- // success followed by retry).
- // At least one retry is always attempted. We retry when still within a brief deadline and wait
- // for a short time before re-dialing.
- //
- // TODO: in principle, we could retry for min(TUNNEL_WRITE_TIMEOUT, meek-server.MAX_SESSION_STALENESS),
- // i.e., as long as the underlying tunnel has not timed out and as long as the server has not
- // expired the current meek session. Presently not doing this to avoid excessive connection attempts
- // through the first hop. In addition, this will require additional support for timely shutdown.
- retries := uint(0)
- retryDeadline := monotime.Now().Add(MEEK_ROUND_TRIP_RETRY_DEADLINE)
- var err error
- var response *http.Response
- for {
- var request *http.Request
- request, err = http.NewRequest("POST", meek.url.String(), bytes.NewReader(sendPayload))
- if err != nil {
- // Don't retry when can't initialize a Request
- break
- }
- // Don't use the default user agent ("Go 1.1 package http").
- // For now, just omit the header (net/http/request.go: "may be blank to not send the header").
- request.Header.Set("User-Agent", "")
- request.Header.Set("Content-Type", "application/octet-stream")
- for name, value := range meek.additionalHeaders {
- request.Header.Set(name, value)
- }
- request.AddCookie(meek.cookie)
- // The http.Transport.RoundTrip is run in a goroutine to enable cancelling a request in-flight.
- type roundTripResponse struct {
- response *http.Response
- err error
- }
- roundTripResponseChannel := make(chan *roundTripResponse, 1)
- roundTripWaitGroup := new(sync.WaitGroup)
- roundTripWaitGroup.Add(1)
- go func() {
- defer roundTripWaitGroup.Done()
- r, err := meek.transport.RoundTrip(request)
- roundTripResponseChannel <- &roundTripResponse{r, err}
- }()
- select {
- case roundTripResponse := <-roundTripResponseChannel:
- response = roundTripResponse.response
- err = roundTripResponse.err
- case <-meek.broadcastClosed:
- meek.transport.CancelRequest(request)
- return nil, nil
- }
- roundTripWaitGroup.Wait()
- if err == nil {
- break
- }
- if retries >= 1 && monotime.Now().After(retryDeadline) {
- break
- }
- retries += 1
- time.Sleep(MEEK_ROUND_TRIP_RETRY_DELAY)
- }
- if err != nil {
- return nil, common.ContextError(err)
- }
- if response.StatusCode != http.StatusOK {
- return nil, common.ContextError(fmt.Errorf("http request failed %d", response.StatusCode))
- }
- // observe response cookies for meek session key token.
- // Once found it must be used for all consecutive requests made to the server
- for _, c := range response.Cookies() {
- if meek.cookie.Name == c.Name {
- meek.cookie.Value = c.Value
- break
- }
- }
- return response.Body, nil
- }
- type meekCookieData struct {
- ServerAddress string `json:"p"`
- SessionID string `json:"s"`
- MeekProtocolVersion int `json:"v"`
- }
- // makeCookie creates the cookie to be sent with initial meek HTTP request.
- // The purpose of the cookie is to send the following to the server:
- // ServerAddress -- the Psiphon Server address the meek server should relay to
- // SessionID -- the Psiphon session ID (used by meek server to relay geolocation
- // information obtained from the CDN through to the Psiphon Server)
- // MeekProtocolVersion -- tells the meek server that this client understands
- // the latest protocol.
- // The server will create a session using these values and send the session ID
- // back to the client via Set-Cookie header. Client must use that value with
- // all consequent HTTP requests
- // In unfronted meek mode, the cookie is visible over the adversary network, so the
- // cookie is encrypted and obfuscated.
- func makeMeekCookie(meekConfig *MeekConfig) (cookie *http.Cookie, err error) {
- // Make the JSON data
- serverAddress := meekConfig.PsiphonServerAddress
- cookieData := &meekCookieData{
- ServerAddress: serverAddress,
- SessionID: meekConfig.SessionID,
- MeekProtocolVersion: MEEK_PROTOCOL_VERSION,
- }
- serializedCookie, err := json.Marshal(cookieData)
- if err != nil {
- return nil, common.ContextError(err)
- }
- // Encrypt the JSON data
- // NaCl box is used for encryption. The peer public key comes from the server entry.
- // Nonce is always all zeros, and is not sent in the cookie (the server also uses an all-zero nonce).
- // http://nacl.cace-project.eu/box.html:
- // "There is no harm in having the same nonce for different messages if the {sender, receiver} sets are
- // different. This is true even if the sets overlap. For example, a sender can use the same nonce for two
- // different messages if the messages are sent to two different public keys."
- var nonce [24]byte
- var publicKey [32]byte
- decodedPublicKey, err := base64.StdEncoding.DecodeString(meekConfig.MeekCookieEncryptionPublicKey)
- if err != nil {
- return nil, common.ContextError(err)
- }
- copy(publicKey[:], decodedPublicKey)
- ephemeralPublicKey, ephemeralPrivateKey, err := box.GenerateKey(rand.Reader)
- if err != nil {
- return nil, common.ContextError(err)
- }
- box := box.Seal(nil, serializedCookie, &nonce, &publicKey, ephemeralPrivateKey)
- encryptedCookie := make([]byte, 32+len(box))
- copy(encryptedCookie[0:32], ephemeralPublicKey[0:32])
- copy(encryptedCookie[32:], box)
- // Obfuscate the encrypted data
- obfuscator, err := NewClientObfuscator(
- &ObfuscatorConfig{Keyword: meekConfig.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING})
- if err != nil {
- return nil, common.ContextError(err)
- }
- obfuscatedCookie := obfuscator.SendSeedMessage()
- seedLen := len(obfuscatedCookie)
- obfuscatedCookie = append(obfuscatedCookie, encryptedCookie...)
- obfuscator.ObfuscateClientToServer(obfuscatedCookie[seedLen:])
- // Format the HTTP cookie
- // The format is <random letter 'A'-'Z'>=<base64 data>, which is intended to match common cookie formats.
- A := int('A')
- Z := int('Z')
- // letterIndex is integer in range [int('A'), int('Z')]
- letterIndex, err := common.MakeSecureRandomInt(Z - A + 1)
- if err != nil {
- return nil, common.ContextError(err)
- }
- return &http.Cookie{
- Name: string(byte(A + letterIndex)),
- Value: base64.StdEncoding.EncodeToString(obfuscatedCookie)},
- nil
- }
|