|
@@ -24,6 +24,7 @@ import (
|
|
|
"context"
|
|
"context"
|
|
|
"crypto/tls"
|
|
"crypto/tls"
|
|
|
"crypto/x509"
|
|
"crypto/x509"
|
|
|
|
|
+ "encoding/base64"
|
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
"io"
|
|
"io"
|
|
@@ -58,12 +59,19 @@ const (
|
|
|
type RelayConfig struct {
|
|
type RelayConfig struct {
|
|
|
Logger common.Logger
|
|
Logger common.Logger
|
|
|
|
|
|
|
|
- CACertificates []*x509.Certificate
|
|
|
|
|
|
|
+ CACertificates []*x509.Certificate
|
|
|
|
|
+
|
|
|
HostCertificate *tls.Certificate
|
|
HostCertificate *tls.Certificate
|
|
|
|
|
|
|
|
DynamicServerListServiceURL string
|
|
DynamicServerListServiceURL string
|
|
|
|
|
|
|
|
HostID string
|
|
HostID string
|
|
|
|
|
+
|
|
|
|
|
+ // APIParameterValidator is a callback that validates base API metrics.
|
|
|
|
|
+ APIParameterValidator common.APIParameterValidator
|
|
|
|
|
+
|
|
|
|
|
+ // APIParameterValidator is a callback that formats base API metrics.
|
|
|
|
|
+ APIParameterLogFieldFormatter common.APIParameterLogFieldFormatter
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Relay is an intermediary between a DSL client and the DSL backend which
|
|
// Relay is an intermediary between a DSL client and the DSL backend which
|
|
@@ -82,13 +90,13 @@ type Relay struct {
|
|
|
tlsConfig *tls.Config
|
|
tlsConfig *tls.Config
|
|
|
errorResponse []byte
|
|
errorResponse []byte
|
|
|
|
|
|
|
|
- mutex sync.Mutex
|
|
|
|
|
- httpClient *http.Client
|
|
|
|
|
- requestTimeout time.Duration
|
|
|
|
|
- requestRetryCount int
|
|
|
|
|
- serverEntryCache *lrucache.Cache
|
|
|
|
|
- serverEntryCacheDefaultTTL time.Duration
|
|
|
|
|
- serverEntryCacheMaxSize int
|
|
|
|
|
|
|
+ mutex sync.Mutex
|
|
|
|
|
+ httpClient *http.Client
|
|
|
|
|
+ requestTimeout time.Duration
|
|
|
|
|
+ requestRetryCount int
|
|
|
|
|
+ serverEntryCache *lrucache.Cache
|
|
|
|
|
+ serverEntryCacheTTL time.Duration
|
|
|
|
|
+ serverEntryCacheMaxSize int
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// NewRelay creates a new Relay.
|
|
// NewRelay creates a new Relay.
|
|
@@ -184,27 +192,34 @@ func (r *Relay) SetRequestParameters(
|
|
|
// entry caching. When the parameters change, any existing cache is flushed
|
|
// entry caching. When the parameters change, any existing cache is flushed
|
|
|
// and replaced.
|
|
// and replaced.
|
|
|
func (r *Relay) SetCacheParameters(
|
|
func (r *Relay) SetCacheParameters(
|
|
|
- defaultTTL time.Duration,
|
|
|
|
|
|
|
+ TTL time.Duration,
|
|
|
maxSize int) {
|
|
maxSize int) {
|
|
|
|
|
|
|
|
r.mutex.Lock()
|
|
r.mutex.Lock()
|
|
|
defer r.mutex.Unlock()
|
|
defer r.mutex.Unlock()
|
|
|
|
|
|
|
|
if r.serverEntryCache == nil ||
|
|
if r.serverEntryCache == nil ||
|
|
|
- r.serverEntryCacheDefaultTTL != defaultTTL ||
|
|
|
|
|
|
|
+ r.serverEntryCacheTTL != TTL ||
|
|
|
r.serverEntryCacheMaxSize != maxSize {
|
|
r.serverEntryCacheMaxSize != maxSize {
|
|
|
|
|
|
|
|
if r.serverEntryCache != nil {
|
|
if r.serverEntryCache != nil {
|
|
|
r.serverEntryCache.Flush()
|
|
r.serverEntryCache.Flush()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- r.serverEntryCacheDefaultTTL = defaultTTL
|
|
|
|
|
|
|
+ r.serverEntryCacheTTL = TTL
|
|
|
r.serverEntryCacheMaxSize = maxSize
|
|
r.serverEntryCacheMaxSize = maxSize
|
|
|
|
|
|
|
|
- r.serverEntryCache = lrucache.NewWithLRU(
|
|
|
|
|
- r.serverEntryCacheDefaultTTL,
|
|
|
|
|
- 1*time.Minute,
|
|
|
|
|
- r.serverEntryCacheMaxSize)
|
|
|
|
|
|
|
+ if r.serverEntryCacheTTL > 0 {
|
|
|
|
|
+
|
|
|
|
|
+ r.serverEntryCache = lrucache.NewWithLRU(
|
|
|
|
|
+ r.serverEntryCacheTTL,
|
|
|
|
|
+ 1*time.Minute,
|
|
|
|
|
+ r.serverEntryCacheMaxSize)
|
|
|
|
|
+
|
|
|
|
|
+ } else {
|
|
|
|
|
+
|
|
|
|
|
+ r.serverEntryCache = nil
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -273,30 +288,65 @@ func (r *Relay) handleRequest(
|
|
|
"unknown request type %d", relayedRequest.RequestType)
|
|
"unknown request type %d", relayedRequest.RequestType)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // TODO: implement transparent server entry caching.
|
|
|
|
|
|
|
+ // Transparent caching:
|
|
|
//
|
|
//
|
|
|
// For requestTypeGetServerEntries, peek at the RelayedResponse.Response
|
|
// For requestTypeGetServerEntries, peek at the RelayedResponse.Response
|
|
|
// and extract server entries and add to the local cache, keyed by server
|
|
// and extract server entries and add to the local cache, keyed by server
|
|
|
- // entry tag. When the server entry has a specific TTL, use that as the
|
|
|
|
|
- // cache TTL, otherwise using serverEntryCacheDefaultTTL.
|
|
|
|
|
|
|
+ // entry tag.
|
|
|
//
|
|
//
|
|
|
// Peek at RelayedRequest.Request, and if all requested server entries are
|
|
// Peek at RelayedRequest.Request, and if all requested server entries are
|
|
|
// in the cache, serve the request entirely from the local cache.
|
|
// in the cache, serve the request entirely from the local cache.
|
|
|
- // Consider also modifying requests to only fetch server entries that are
|
|
|
|
|
- // not cached.
|
|
|
|
|
//
|
|
//
|
|
|
- // Also handle for changes to server entry version.
|
|
|
|
|
-
|
|
|
|
|
- requestCtx := ctx
|
|
|
|
|
- if requestTimeout > 0 {
|
|
|
|
|
- var requestCancelFunc context.CancelFunc
|
|
|
|
|
- requestCtx, requestCancelFunc = context.WithTimeout(ctx, requestTimeout)
|
|
|
|
|
- defer requestCancelFunc()
|
|
|
|
|
|
|
+ // The backend DSL may enforce a limited time interval in which certain
|
|
|
|
|
+ // server entries can be discovered. This cache doesn't bypass this,
|
|
|
|
|
+ // since DiscoveryServerEntries isn't cached and always passed through to
|
|
|
|
|
+ // the DSL backend. Clients must discover the large, random server entry
|
|
|
|
|
+ // tags via DiscoveryServerEntries within the designated time interval;
|
|
|
|
|
+ // then clients may download the server entries via GetServerEntries at
|
|
|
|
|
+ // any time, and this may be cached.
|
|
|
|
|
+ //
|
|
|
|
|
+ // Limitation: this cache ignores server entry version and may serve a
|
|
|
|
|
+ // version that's older that the latest within the cache TTL.
|
|
|
|
|
+ //
|
|
|
|
|
+ // - Server entry version changes are assumed to be rare.
|
|
|
|
|
+ //
|
|
|
|
|
+ // - The cache will be updated with a new version as soon as
|
|
|
|
|
+ // cacheGetServerEntriesResponse sees it.
|
|
|
|
|
+ //
|
|
|
|
|
+ // - Use a reasonable TTL such as 24h; cache entry TTLs aren't extended on
|
|
|
|
|
+ // hits, so any old version will eventually be removed.
|
|
|
|
|
+ //
|
|
|
|
|
+ // - A more complicated scheme is possible: also peek at
|
|
|
|
|
+ // DiscoverServerEntriesResponses and, for each tag/version pair, if
|
|
|
|
|
+ // the tag is in the cache and the cached entry is an old version,
|
|
|
|
|
+ // delete from the cache. This would require unpacking each server entry.
|
|
|
|
|
+
|
|
|
|
|
+ var response []byte
|
|
|
|
|
+ cachedResponse := false
|
|
|
|
|
+
|
|
|
|
|
+ if relayedRequest.RequestType == requestTypeGetServerEntries {
|
|
|
|
|
+ var err error
|
|
|
|
|
+ response, err = r.getCachedGetServerEntriesResponse(
|
|
|
|
|
+ relayedRequest.Request, clientGeoIPData)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ r.config.Logger.WithTraceFields(common.LogFields{
|
|
|
|
|
+ "error": err.Error(),
|
|
|
|
|
+ }).Warning("DSL: serve cached response failed")
|
|
|
|
|
+ // Proceed with relaying request
|
|
|
|
|
+ }
|
|
|
|
|
+ cachedResponse = err == nil && response != nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- url := fmt.Sprintf("https://%s%s", r.config.DynamicServerListServiceURL, path)
|
|
|
|
|
|
|
+ for i := 0; !cachedResponse; i++ {
|
|
|
|
|
|
|
|
- for i := 0; ; i++ {
|
|
|
|
|
|
|
+ requestCtx := ctx
|
|
|
|
|
+ if requestTimeout > 0 {
|
|
|
|
|
+ var requestCancelFunc context.CancelFunc
|
|
|
|
|
+ requestCtx, requestCancelFunc = context.WithTimeout(ctx, requestTimeout)
|
|
|
|
|
+ defer requestCancelFunc()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ url := fmt.Sprintf("https://%s%s", r.config.DynamicServerListServiceURL, path)
|
|
|
|
|
|
|
|
httpRequest, err := http.NewRequestWithContext(
|
|
httpRequest, err := http.NewRequestWithContext(
|
|
|
requestCtx, "POST", url, bytes.NewBuffer(relayedRequest.Request))
|
|
requestCtx, "POST", url, bytes.NewBuffer(relayedRequest.Request))
|
|
@@ -328,41 +378,186 @@ func (r *Relay) handleRequest(
|
|
|
err = errors.Tracef("unexpected response code: %d", httpResponse.StatusCode)
|
|
err = errors.Tracef("unexpected response code: %d", httpResponse.StatusCode)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- var response []byte
|
|
|
|
|
if err == nil {
|
|
if err == nil {
|
|
|
response, err = io.ReadAll(httpResponse.Body)
|
|
response, err = io.ReadAll(httpResponse.Body)
|
|
|
httpResponse.Body.Close()
|
|
httpResponse.Body.Close()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if err != nil {
|
|
|
|
|
-
|
|
|
|
|
- r.config.Logger.WithTraceFields(common.LogFields{
|
|
|
|
|
- "duration": duration.String(),
|
|
|
|
|
- "error": err.Error(),
|
|
|
|
|
- }).Warning("DSL: service request attempt failed")
|
|
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
|
|
|
- // Retry on network errors.
|
|
|
|
|
- if i < requestRetryCount && ctx.Err() == nil {
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+ if relayedRequest.RequestType == requestTypeGetServerEntries {
|
|
|
|
|
+ err := r.cacheGetServerEntriesResponse(
|
|
|
|
|
+ relayedRequest.Request, response)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ r.config.Logger.WithTraceFields(common.LogFields{
|
|
|
|
|
+ "error": err.Error(),
|
|
|
|
|
+ }).Warning("DSL: cache response failed")
|
|
|
|
|
+ // Proceed with relaying response
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return nil, errors.Tracef("all attempts failed")
|
|
|
|
|
|
|
+ break
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- cborRelayedResponse, err := protocol.CBOREncoding.Marshal(
|
|
|
|
|
- &RelayedResponse{
|
|
|
|
|
- Response: response,
|
|
|
|
|
- })
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, errors.Trace(err)
|
|
|
|
|
|
|
+ r.config.Logger.WithTraceFields(common.LogFields{
|
|
|
|
|
+ "duration": duration.String(),
|
|
|
|
|
+ "error": err.Error(),
|
|
|
|
|
+ }).Warning("DSL: service request attempt failed")
|
|
|
|
|
+
|
|
|
|
|
+ // Retry on network errors.
|
|
|
|
|
+ if i < requestRetryCount && ctx.Err() == nil {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return nil, errors.Tracef("all attempts failed")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cborRelayedResponse, err := protocol.CBOREncoding.Marshal(
|
|
|
|
|
+ &RelayedResponse{
|
|
|
|
|
+ Response: response,
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if len(cborRelayedResponse) > MaxRelayPayloadSize {
|
|
|
|
|
+ return nil, errors.Tracef(
|
|
|
|
|
+ "response size %d exceeds limit %d",
|
|
|
|
|
+ len(cborRelayedResponse), MaxRelayPayloadSize)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return cborRelayedResponse, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (r *Relay) cacheGetServerEntriesResponse(
|
|
|
|
|
+ cborRequest []byte,
|
|
|
|
|
+ cborResponse []byte) error {
|
|
|
|
|
+
|
|
|
|
|
+ if r.serverEntryCacheTTL == 0 {
|
|
|
|
|
+ // Caching is disabled
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var request GetServerEntriesRequest
|
|
|
|
|
+ err := cbor.Unmarshal(cborRequest, &request)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var response GetServerEntriesResponse
|
|
|
|
|
+ err = cbor.Unmarshal(cborResponse, &response)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if len(request.ServerEntryTags) != len(response.SourcedServerEntries) {
|
|
|
|
|
+ return errors.TraceNew("unexpected entry count mismatch")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for i, serverEntryTag := range request.ServerEntryTags {
|
|
|
|
|
+
|
|
|
|
|
+ if response.SourcedServerEntries[i] != nil {
|
|
|
|
|
+
|
|
|
|
|
+ // This will update any existing cached copy of the server entry for
|
|
|
|
|
+ // this tag, in case the server entry version is new. This also
|
|
|
|
|
+ // extends the cache TTL, since the server entry is fresh.
|
|
|
|
|
+
|
|
|
|
|
+ r.serverEntryCache.Set(
|
|
|
|
|
+ string(serverEntryTag),
|
|
|
|
|
+ response.SourcedServerEntries[i],
|
|
|
|
|
+ lrucache.DefaultExpiration)
|
|
|
|
|
+
|
|
|
|
|
+ } else {
|
|
|
|
|
+
|
|
|
|
|
+ // In this case, the DSL backend is indicating that the server
|
|
|
|
|
+ // entry for the requested tag no longer exists, perhaps due to
|
|
|
|
|
+ // server pruning since the DiscoverServerEntries request. This
|
|
|
|
|
+ // is an edge case since DiscoverServerEntries won't return
|
|
|
|
|
+ // invalid tags and so the "nil" value/state isn't cached.
|
|
|
|
|
+
|
|
|
|
|
+ r.serverEntryCache.Delete(string(serverEntryTag))
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if len(cborRelayedResponse) > MaxRelayPayloadSize {
|
|
|
|
|
- return nil, errors.Tracef(
|
|
|
|
|
- "response size %d exceeds limit %d",
|
|
|
|
|
- len(cborRelayedResponse), MaxRelayPayloadSize)
|
|
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (r *Relay) getCachedGetServerEntriesResponse(
|
|
|
|
|
+ cborRequest []byte,
|
|
|
|
|
+ clientGeoIPData common.GeoIPData) ([]byte, error) {
|
|
|
|
|
+
|
|
|
|
|
+ if r.serverEntryCacheTTL == 0 {
|
|
|
|
|
+ // Caching is disabled
|
|
|
|
|
+ return nil, nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var request GetServerEntriesRequest
|
|
|
|
|
+ err := cbor.Unmarshal(cborRequest, &request)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Since we anticipate that most server entries will be cached, allocate
|
|
|
|
|
+ // response slices optimistically.
|
|
|
|
|
+ //
|
|
|
|
|
+ // TODO: check for sufficient cache entries before allocating these
|
|
|
|
|
+ // response slices? Would doubling the cache lookups use less resources
|
|
|
|
|
+ // than unused allocations?
|
|
|
|
|
+
|
|
|
|
|
+ serverEntryTags := make([]string, len(request.ServerEntryTags))
|
|
|
|
|
+
|
|
|
|
|
+ var response GetServerEntriesResponse
|
|
|
|
|
+ response.SourcedServerEntries = make([]*SourcedServerEntry, len(request.ServerEntryTags))
|
|
|
|
|
+
|
|
|
|
|
+ for i, serverEntryTag := range request.ServerEntryTags {
|
|
|
|
|
+ cacheEntry, ok := r.serverEntryCache.Get(string(serverEntryTag))
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+
|
|
|
|
|
+ // The request can't be served from the cache, as some server
|
|
|
|
|
+ // entry tags aren't present. Fall back to a full request to the
|
|
|
|
|
+ // DSL backend.
|
|
|
|
|
+ //
|
|
|
|
|
+ // As a potential future enhancement, consider partially serving
|
|
|
|
|
+ // from the cache, after making a DSL request for just the
|
|
|
|
|
+ // unknown server entries?
|
|
|
|
|
+ return nil, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return cborRelayedResponse, nil
|
|
|
|
|
|
|
+ // The cached entry's TTL is not extended on a hit.
|
|
|
|
|
+
|
|
|
|
|
+ // serverEntryTags are used for logging the request event when served
|
|
|
|
|
+ // from the cache. Use the same same string encoding as
|
|
|
|
|
+ // protocol.GenerateServerEntryTag.
|
|
|
|
|
+ serverEntryTags[i] = base64.StdEncoding.EncodeToString(serverEntryTag)
|
|
|
|
|
+
|
|
|
|
|
+ response.SourcedServerEntries[i] = cacheEntry.(*SourcedServerEntry)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ cborResponse, err := protocol.CBOREncoding.Marshal(&response)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Log the request event. Since this request is server from the relay
|
|
|
|
|
+ // cache, the DSL backend will not see the request and log the event
|
|
|
|
|
+ // itself. This log should match the DSL log format and can be shipped to
|
|
|
|
|
+ // the same log aggregator.
|
|
|
|
|
+
|
|
|
|
|
+ baseParams, err := protocol.DecodePackedAPIParameters(request.BaseAPIParameters)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ err = r.config.APIParameterValidator(baseParams)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, errors.Trace(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logFields := r.config.APIParameterLogFieldFormatter("", clientGeoIPData, baseParams)
|
|
|
|
|
+ logFields["dsl_event"] = "get-server-entries"
|
|
|
|
|
+ logFields["host_id"] = r.config.HostID
|
|
|
|
|
+ logFields["server_entry_tags"] = serverEntryTags
|
|
|
|
|
+ r.config.Logger.LogMetric("dsl", logFields)
|
|
|
|
|
+
|
|
|
|
|
+ return cborResponse, nil
|
|
|
}
|
|
}
|