|
|
@@ -101,6 +101,7 @@ type MeekServer struct {
|
|
|
listener net.Listener
|
|
|
listenerTunnelProtocol string
|
|
|
listenerPort int
|
|
|
+ passthroughAddress string
|
|
|
tlsConfig *tris.Config
|
|
|
obfuscatorSeedHistory *obfuscator.SeedHistory
|
|
|
clientHandler func(clientTunnelProtocol string, clientConn net.Conn)
|
|
|
@@ -140,11 +141,14 @@ func NewMeekServer(
|
|
|
|
|
|
bufferPool := NewCachedResponseBufferPool(bufferLength, bufferCount)
|
|
|
|
|
|
+ passthroughAddress := support.Config.TunnelProtocolPassthroughAddresses[listenerTunnelProtocol]
|
|
|
+
|
|
|
meekServer := &MeekServer{
|
|
|
support: support,
|
|
|
listener: listener,
|
|
|
listenerTunnelProtocol: listenerTunnelProtocol,
|
|
|
listenerPort: listenerPort,
|
|
|
+ passthroughAddress: passthroughAddress,
|
|
|
obfuscatorSeedHistory: obfuscator.NewSeedHistory(nil),
|
|
|
clientHandler: clientHandler,
|
|
|
openConns: common.NewConns(),
|
|
|
@@ -157,8 +161,8 @@ func NewMeekServer(
|
|
|
}
|
|
|
|
|
|
if useTLS {
|
|
|
- tlsConfig, err := makeMeekTLSConfig(
|
|
|
- support, isFronted, useObfuscatedSessionTickets)
|
|
|
+ tlsConfig, err := meekServer.makeMeekTLSConfig(
|
|
|
+ isFronted, useObfuscatedSessionTickets)
|
|
|
if err != nil {
|
|
|
return nil, errors.Trace(err)
|
|
|
}
|
|
|
@@ -885,12 +889,13 @@ func (server *MeekServer) getMeekCookiePayload(
|
|
|
&obfuscator.ObfuscatorConfig{
|
|
|
Keyword: server.support.Config.MeekObfuscatedKey,
|
|
|
SeedHistory: server.obfuscatorSeedHistory,
|
|
|
- IrregularLogger: func(clientIP string, logFields common.LogFields) {
|
|
|
+ IrregularLogger: func(clientIP string, err error, logFields common.LogFields) {
|
|
|
logIrregularTunnel(
|
|
|
server.support,
|
|
|
server.listenerTunnelProtocol,
|
|
|
server.listenerPort,
|
|
|
clientIP,
|
|
|
+ errors.Trace(err),
|
|
|
LogFields(logFields))
|
|
|
},
|
|
|
},
|
|
|
@@ -931,94 +936,12 @@ func (server *MeekServer) getMeekCookiePayload(
|
|
|
return payload, nil
|
|
|
}
|
|
|
|
|
|
-type meekSession struct {
|
|
|
- // Note: 64-bit ints used with atomic operations are placed
|
|
|
- // at the start of struct to ensure 64-bit alignment.
|
|
|
- // (https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
|
|
|
- lastActivity int64
|
|
|
- requestCount int64
|
|
|
- metricClientRetries int64
|
|
|
- metricPeakResponseSize int64
|
|
|
- metricPeakCachedResponseSize int64
|
|
|
- metricPeakCachedResponseHitSize int64
|
|
|
- metricCachedResponseMissPosition int64
|
|
|
- lock sync.Mutex
|
|
|
- deleted bool
|
|
|
- clientConn *meekConn
|
|
|
- meekProtocolVersion int
|
|
|
- sessionIDSent bool
|
|
|
- cachedResponse *CachedResponse
|
|
|
-}
|
|
|
-
|
|
|
-func (session *meekSession) touch() {
|
|
|
- atomic.StoreInt64(&session.lastActivity, int64(monotime.Now()))
|
|
|
-}
|
|
|
-
|
|
|
-func (session *meekSession) expired() bool {
|
|
|
- lastActivity := monotime.Time(atomic.LoadInt64(&session.lastActivity))
|
|
|
- return monotime.Since(lastActivity) > MEEK_MAX_SESSION_STALENESS
|
|
|
-}
|
|
|
-
|
|
|
-// delete releases all resources allocated by a session.
|
|
|
-func (session *meekSession) delete(haveLock bool) {
|
|
|
-
|
|
|
- // TODO: close the persistent HTTP client connection, if one exists?
|
|
|
-
|
|
|
- // This final call session.cachedResponse.Reset releases shared resources.
|
|
|
- //
|
|
|
- // This call requires exclusive access. session.lock is be obtained before
|
|
|
- // calling session.cachedResponse.Reset. Once the lock is obtained, no
|
|
|
- // request for this session is being processed concurrently, and pending
|
|
|
- // requests will block at session.lock.
|
|
|
- //
|
|
|
- // This logic assumes that no further session.cachedResponse access occurs,
|
|
|
- // or else resources may deplete (buffers won't be returned to the pool).
|
|
|
- // These requirements are achieved by obtaining the lock, setting
|
|
|
- // session.deleted, and any subsequent request handlers checking
|
|
|
- // session.deleted immediately after obtaining the lock.
|
|
|
- //
|
|
|
- // session.lock.Lock may block for up to MEEK_HTTP_CLIENT_IO_TIMEOUT,
|
|
|
- // the timeout for any active request handler processing a session
|
|
|
- // request.
|
|
|
- //
|
|
|
- // When the lock must be acquired, clientConn.Close is called first, to
|
|
|
- // interrupt any existing request handler blocking on pumpReads or pumpWrites.
|
|
|
-
|
|
|
- session.clientConn.Close()
|
|
|
-
|
|
|
- if !haveLock {
|
|
|
- session.lock.Lock()
|
|
|
- }
|
|
|
-
|
|
|
- // Release all extended buffers back to the pool.
|
|
|
- // session.cachedResponse.Reset is not safe for concurrent calls.
|
|
|
- session.cachedResponse.Reset()
|
|
|
-
|
|
|
- session.deleted = true
|
|
|
-
|
|
|
- if !haveLock {
|
|
|
- session.lock.Unlock()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// GetMetrics implements the common.MetricsSource interface.
|
|
|
-func (session *meekSession) GetMetrics() common.LogFields {
|
|
|
- logFields := make(common.LogFields)
|
|
|
- logFields["meek_client_retries"] = atomic.LoadInt64(&session.metricClientRetries)
|
|
|
- logFields["meek_peak_response_size"] = atomic.LoadInt64(&session.metricPeakResponseSize)
|
|
|
- logFields["meek_peak_cached_response_size"] = atomic.LoadInt64(&session.metricPeakCachedResponseSize)
|
|
|
- logFields["meek_peak_cached_response_hit_size"] = atomic.LoadInt64(&session.metricPeakCachedResponseHitSize)
|
|
|
- logFields["meek_cached_response_miss_position"] = atomic.LoadInt64(&session.metricCachedResponseMissPosition)
|
|
|
- return logFields
|
|
|
-}
|
|
|
-
|
|
|
// makeMeekTLSConfig creates a TLS config for a meek HTTPS listener.
|
|
|
// Currently, this config is optimized for fronted meek where the nature
|
|
|
// of the connection is non-circumvention; it's optimized for performance
|
|
|
// assuming the peer is an uncensored CDN.
|
|
|
-func makeMeekTLSConfig(
|
|
|
- support *SupportServices,
|
|
|
- isFronted, useObfuscatedSessionTickets bool) (*tris.Config, error) {
|
|
|
+func (server *MeekServer) makeMeekTLSConfig(
|
|
|
+ isFronted bool, useObfuscatedSessionTickets bool) (*tris.Config, error) {
|
|
|
|
|
|
certificate, privateKey, err := common.GenerateWebServerCertificate(values.GetHostName())
|
|
|
if err != nil {
|
|
|
@@ -1077,8 +1000,10 @@ func makeMeekTLSConfig(
|
|
|
// See obfuscated session ticket overview
|
|
|
// in NewObfuscatedClientSessionCache.
|
|
|
|
|
|
+ config.UseObfuscatedSessionTickets = true
|
|
|
+
|
|
|
var obfuscatedSessionTicketKey [32]byte
|
|
|
- key, err := hex.DecodeString(support.Config.MeekObfuscatedKey)
|
|
|
+ key, err := hex.DecodeString(server.support.Config.MeekObfuscatedKey)
|
|
|
if err == nil && len(key) != 32 {
|
|
|
err = std_errors.New("invalid obfuscated session key length")
|
|
|
}
|
|
|
@@ -1102,9 +1027,152 @@ func makeMeekTLSConfig(
|
|
|
obfuscatedSessionTicketKey})
|
|
|
}
|
|
|
|
|
|
+ // When configured, initialize passthrough mode, an anti-probing defense.
|
|
|
+ // Clients must prove knowledge of the obfuscated key via a message sent in
|
|
|
+ // the TLS ClientHello random field.
|
|
|
+ //
|
|
|
+ // When clients fail to provide a valid message, the client connection is
|
|
|
+ // relayed to the designated passthrough address, typically another web site.
|
|
|
+ // The entire flow is relayed, including the original ClientHello, so the
|
|
|
+ // client will perform a TLS handshake with the passthrough target.
|
|
|
+ //
|
|
|
+ // Irregular events are logged for invalid client activity.
|
|
|
+
|
|
|
+ if server.passthroughAddress != "" {
|
|
|
+
|
|
|
+ config.PassthroughAddress = server.passthroughAddress
|
|
|
+
|
|
|
+ passthroughKey, err := obfuscator.DeriveTLSPassthroughKey(
|
|
|
+ server.support.Config.MeekObfuscatedKey)
|
|
|
+ if err != nil {
|
|
|
+ return nil, errors.Trace(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ config.PassthroughKey = passthroughKey
|
|
|
+
|
|
|
+ config.PassthroughLogInvalidMessage = func(
|
|
|
+ clientIP string) {
|
|
|
+
|
|
|
+ logIrregularTunnel(
|
|
|
+ server.support,
|
|
|
+ server.listenerTunnelProtocol,
|
|
|
+ server.listenerPort,
|
|
|
+ clientIP,
|
|
|
+ errors.TraceNew("invalid passthrough message"),
|
|
|
+ nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ config.PassthroughHistoryAddNew = func(
|
|
|
+ clientIP string,
|
|
|
+ clientRandom []byte) bool {
|
|
|
+
|
|
|
+ // strictMode is true as, unlike with meek cookies, legitimate meek clients
|
|
|
+ // never retry TLS connections using a previous random value.
|
|
|
+
|
|
|
+ ok, logFields := server.obfuscatorSeedHistory.AddNew(
|
|
|
+ true,
|
|
|
+ clientIP,
|
|
|
+ "client-random",
|
|
|
+ clientRandom)
|
|
|
+
|
|
|
+ if logFields != nil {
|
|
|
+ logIrregularTunnel(
|
|
|
+ server.support,
|
|
|
+ server.listenerTunnelProtocol,
|
|
|
+ server.listenerPort,
|
|
|
+ clientIP,
|
|
|
+ errors.TraceNew("duplicate passthrough message"),
|
|
|
+ LogFields(*logFields))
|
|
|
+ }
|
|
|
+
|
|
|
+ return ok
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
return config, nil
|
|
|
}
|
|
|
|
|
|
+type meekSession struct {
|
|
|
+ // Note: 64-bit ints used with atomic operations are placed
|
|
|
+ // at the start of struct to ensure 64-bit alignment.
|
|
|
+ // (https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
|
|
|
+ lastActivity int64
|
|
|
+ requestCount int64
|
|
|
+ metricClientRetries int64
|
|
|
+ metricPeakResponseSize int64
|
|
|
+ metricPeakCachedResponseSize int64
|
|
|
+ metricPeakCachedResponseHitSize int64
|
|
|
+ metricCachedResponseMissPosition int64
|
|
|
+ lock sync.Mutex
|
|
|
+ deleted bool
|
|
|
+ clientConn *meekConn
|
|
|
+ meekProtocolVersion int
|
|
|
+ sessionIDSent bool
|
|
|
+ cachedResponse *CachedResponse
|
|
|
+}
|
|
|
+
|
|
|
+func (session *meekSession) touch() {
|
|
|
+ atomic.StoreInt64(&session.lastActivity, int64(monotime.Now()))
|
|
|
+}
|
|
|
+
|
|
|
+func (session *meekSession) expired() bool {
|
|
|
+ lastActivity := monotime.Time(atomic.LoadInt64(&session.lastActivity))
|
|
|
+ return monotime.Since(lastActivity) > MEEK_MAX_SESSION_STALENESS
|
|
|
+}
|
|
|
+
|
|
|
+// delete releases all resources allocated by a session.
|
|
|
+func (session *meekSession) delete(haveLock bool) {
|
|
|
+
|
|
|
+ // TODO: close the persistent HTTP client connection, if one exists?
|
|
|
+
|
|
|
+ // This final call session.cachedResponse.Reset releases shared resources.
|
|
|
+ //
|
|
|
+ // This call requires exclusive access. session.lock is be obtained before
|
|
|
+ // calling session.cachedResponse.Reset. Once the lock is obtained, no
|
|
|
+ // request for this session is being processed concurrently, and pending
|
|
|
+ // requests will block at session.lock.
|
|
|
+ //
|
|
|
+ // This logic assumes that no further session.cachedResponse access occurs,
|
|
|
+ // or else resources may deplete (buffers won't be returned to the pool).
|
|
|
+ // These requirements are achieved by obtaining the lock, setting
|
|
|
+ // session.deleted, and any subsequent request handlers checking
|
|
|
+ // session.deleted immediately after obtaining the lock.
|
|
|
+ //
|
|
|
+ // session.lock.Lock may block for up to MEEK_HTTP_CLIENT_IO_TIMEOUT,
|
|
|
+ // the timeout for any active request handler processing a session
|
|
|
+ // request.
|
|
|
+ //
|
|
|
+ // When the lock must be acquired, clientConn.Close is called first, to
|
|
|
+ // interrupt any existing request handler blocking on pumpReads or pumpWrites.
|
|
|
+
|
|
|
+ session.clientConn.Close()
|
|
|
+
|
|
|
+ if !haveLock {
|
|
|
+ session.lock.Lock()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Release all extended buffers back to the pool.
|
|
|
+ // session.cachedResponse.Reset is not safe for concurrent calls.
|
|
|
+ session.cachedResponse.Reset()
|
|
|
+
|
|
|
+ session.deleted = true
|
|
|
+
|
|
|
+ if !haveLock {
|
|
|
+ session.lock.Unlock()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// GetMetrics implements the common.MetricsSource interface.
|
|
|
+func (session *meekSession) GetMetrics() common.LogFields {
|
|
|
+ logFields := make(common.LogFields)
|
|
|
+ logFields["meek_client_retries"] = atomic.LoadInt64(&session.metricClientRetries)
|
|
|
+ logFields["meek_peak_response_size"] = atomic.LoadInt64(&session.metricPeakResponseSize)
|
|
|
+ logFields["meek_peak_cached_response_size"] = atomic.LoadInt64(&session.metricPeakCachedResponseSize)
|
|
|
+ logFields["meek_peak_cached_response_hit_size"] = atomic.LoadInt64(&session.metricPeakCachedResponseHitSize)
|
|
|
+ logFields["meek_cached_response_miss_position"] = atomic.LoadInt64(&session.metricCachedResponseMissPosition)
|
|
|
+ return logFields
|
|
|
+}
|
|
|
+
|
|
|
// makeMeekSessionID creates a new session ID. The variable size is intended to
|
|
|
// frustrate traffic analysis of both plaintext and TLS meek traffic.
|
|
|
func makeMeekSessionID() (string, error) {
|
|
|
@@ -1427,5 +1495,9 @@ func (conn *meekConn) SetWriteDeadline(t time.Time) error {
|
|
|
// MetricsSource.GetMetrics, has a pointer only to this conn, so it calls
|
|
|
// through to the session.
|
|
|
func (conn *meekConn) GetMetrics() common.LogFields {
|
|
|
- return conn.meekSession.GetMetrics()
|
|
|
+ logFields := conn.meekSession.GetMetrics()
|
|
|
+ if conn.meekServer.passthroughAddress != "" {
|
|
|
+ logFields["passthrough_address"] = conn.meekServer.passthroughAddress
|
|
|
+ }
|
|
|
+ return logFields
|
|
|
}
|