diff --git a/cache.go b/cache.go index 9df34d5..4af5fa5 100644 --- a/cache.go +++ b/cache.go @@ -27,7 +27,7 @@ func (d *fronted) prepopulateMasquerades(cacheFile string) { } log.Debugf("Attempting to prepopulate masquerades from cache file: %v", cacheFile) - var cachedMasquerades []*masquerade + var cachedMasquerades []*front if err := json.Unmarshal(bytes, &cachedMasquerades); err != nil { log.Errorf("Error reading cached masquerades: %v", err) return @@ -37,7 +37,7 @@ func (d *fronted) prepopulateMasquerades(cacheFile string) { now := time.Now() // update last succeeded status of masquerades based on cached values - for _, m := range d.masquerades { + for _, m := range d.fronts { for _, cm := range cachedMasquerades { sameMasquerade := cm.ProviderID == m.getProviderID() && cm.Domain == m.getDomain() && cm.IpAddress == m.getIpAddress() cachedValueFresh := now.Sub(m.lastSucceeded()) < d.maxAllowedCachedAge @@ -75,7 +75,7 @@ func (d *fronted) maintainCache(cacheFile string) { func (d *fronted) updateCache(cacheFile string) { log.Debugf("Updating cache at %v", cacheFile) - cache := d.masquerades.sortedCopy() + cache := d.fronts.sortedCopy() sizeToSave := len(cache) if d.maxCacheSize < sizeToSave { sizeToSave = d.maxCacheSize @@ -101,8 +101,9 @@ func (d *fronted) updateCache(cacheFile string) { } } -func (d *fronted) closeCache() { +func (d *fronted) Close() { d.closeCacheOnce.Do(func() { close(d.cacheClosed) }) + d.stopCh <- nil } diff --git a/cache_test.go b/cache_test.go index 9b874d1..ba18747 100644 --- a/cache_test.go +++ b/cache_test.go @@ -26,9 +26,9 @@ func TestCaching(t *testing.T) { cloudsackID: NewProvider(nil, "", nil, nil, nil, nil, nil), } - makeDirect := func() *fronted { - d := &fronted{ - masquerades: make(sortedMasquerades, 0, 1000), + makeFronted := func() *fronted { + f := &fronted{ + fronts: make(sortedFronts, 0, 1000), maxAllowedCachedAge: 250 * time.Millisecond, maxCacheSize: 4, cacheSaveInterval: 50 * time.Millisecond, @@ -37,20 +37,20 @@ func TestCaching(t *testing.T) { providers: providers, defaultProviderID: cloudsackID, } - go d.maintainCache(cacheFile) - return d + go f.maintainCache(cacheFile) + return f } now := time.Now() - mb := &masquerade{Masquerade: Masquerade{Domain: "b", IpAddress: "2"}, LastSucceeded: now, ProviderID: testProviderID} - mc := &masquerade{Masquerade: Masquerade{Domain: "c", IpAddress: "3"}, LastSucceeded: now, ProviderID: ""} // defaulted - md := &masquerade{Masquerade: Masquerade{Domain: "d", IpAddress: "4"}, LastSucceeded: now, ProviderID: "sadcloud"} // skipped + mb := &front{Masquerade: Masquerade{Domain: "b", IpAddress: "2"}, LastSucceeded: now, ProviderID: testProviderID} + mc := &front{Masquerade: Masquerade{Domain: "c", IpAddress: "3"}, LastSucceeded: now, ProviderID: ""} // defaulted + md := &front{Masquerade: Masquerade{Domain: "d", IpAddress: "4"}, LastSucceeded: now, ProviderID: "sadcloud"} // skipped - d := makeDirect() - d.masquerades = append(d.masquerades, mb, mc, md) + f := makeFronted() + f.fronts = append(f.fronts, mb, mc, md) - readCached := func() []*masquerade { - var result []*masquerade + readCached := func() []*front { + var result []*front b, err := os.ReadFile(cacheFile) require.NoError(t, err, "Unable to read cache file") err = json.Unmarshal(b, &result) @@ -59,22 +59,22 @@ func TestCaching(t *testing.T) { } // Save the cache - d.markCacheDirty() - time.Sleep(d.cacheSaveInterval * 2) - d.closeCache() + f.markCacheDirty() + time.Sleep(f.cacheSaveInterval * 2) + f.Close() time.Sleep(50 * time.Millisecond) // Reopen cache file and make sure right data was in there - d = makeDirect() - d.prepopulateMasquerades(cacheFile) + f = makeFronted() + f.prepopulateMasquerades(cacheFile) masquerades := readCached() require.Len(t, masquerades, 3, "Wrong number of masquerades read") - for i, expected := range []*masquerade{mb, mc, md} { + for i, expected := range []*front{mb, mc, md} { require.Equal(t, expected.Domain, masquerades[i].Domain, "Wrong masquerade at position %d", i) require.Equal(t, expected.IpAddress, masquerades[i].IpAddress, "Masquerade at position %d has wrong IpAddress", 0) require.Equal(t, expected.ProviderID, masquerades[i].ProviderID, "Masquerade at position %d has wrong ProviderID", 0) require.Equal(t, now.Unix(), masquerades[i].LastSucceeded.Unix(), "Masquerade at position %d has wrong LastSucceeded", 0) } - d.closeCache() + f.Close() } diff --git a/connecting_fronts.go b/connecting_fronts.go index ec4611d..8e2147f 100644 --- a/connecting_fronts.go +++ b/connecting_fronts.go @@ -1,68 +1,53 @@ package fronted import ( - "errors" - "sort" - "sync" - "time" + "context" ) -type connectTimeFront struct { - MasqueradeInterface - connectTime time.Duration +type workingFronts interface { + onConnected(m Front) + connectingFront(context.Context) (Front, error) + size() int } type connectingFronts struct { - fronts []connectTimeFront - //frontsChan chan MasqueradeInterface - sync.RWMutex + // Create a channel of fronts that are connecting. + frontsCh chan Front } // Make sure that connectingFronts is a connectListener var _ workingFronts = &connectingFronts{} // newConnectingFronts creates a new ConnectingFronts struct with an empty slice of Masquerade IPs and domains. -func newConnectingFronts() *connectingFronts { +func newConnectingFronts(size int) *connectingFronts { return &connectingFronts{ - fronts: make([]connectTimeFront, 0), - //frontsChan: make(chan MasqueradeInterface), + // We overallocate the channel to avoid blocking. + frontsCh: make(chan Front, size), } } // AddFront adds a new front to the list of fronts. -func (cf *connectingFronts) onConnected(m MasqueradeInterface, connectTime time.Duration) { - cf.Lock() - defer cf.Unlock() - - cf.fronts = append(cf.fronts, connectTimeFront{ - MasqueradeInterface: m, - connectTime: connectTime, - }) - // Sort fronts by connect time. - sort.Slice(cf.fronts, func(i, j int) bool { - return cf.fronts[i].connectTime < cf.fronts[j].connectTime - }) - //cf.frontsChan <- m +func (cf *connectingFronts) onConnected(m Front) { + cf.frontsCh <- m } -func (cf *connectingFronts) onError(m MasqueradeInterface) { - cf.Lock() - defer cf.Unlock() - - // Remove the front from connecting fronts. - for i, front := range cf.fronts { - if front.MasqueradeInterface == m { - cf.fronts = append(cf.fronts[:i], cf.fronts[i+1:]...) - return +func (cf *connectingFronts) connectingFront(ctx context.Context) (Front, error) { + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case m := <-cf.frontsCh: + // The front may have stopped succeeding since we last checked, + // so only return it if it's still succeeding. + if m.isSucceeding() { + // Add the front back to the channel. + cf.frontsCh <- m + return m, nil + } } } } -func (cf *connectingFronts) workingFront() (MasqueradeInterface, error) { - cf.RLock() - defer cf.RUnlock() - if len(cf.fronts) == 0 { - return nil, errors.New("no fronts available") - } - return cf.fronts[0].MasqueradeInterface, nil +func (cf *connectingFronts) size() int { + return len(cf.frontsCh) } diff --git a/connecting_fronts_test.go b/connecting_fronts_test.go new file mode 100644 index 0000000..23e3fa9 --- /dev/null +++ b/connecting_fronts_test.go @@ -0,0 +1,39 @@ +package fronted + +import ( + "testing" +) + +func TestConnectingFrontsSize(t *testing.T) { + tests := []struct { + name string + setup func() *connectingFronts + expected int + }{ + { + name: "empty channel", + setup: func() *connectingFronts { + return newConnectingFronts(10) + }, + expected: 0, + }, + { + name: "non-empty channel", + setup: func() *connectingFronts { + cf := newConnectingFronts(10) + cf.onConnected(&mockFront{}) + return cf + }, + expected: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cf := tt.setup() + if got := cf.size(); got != tt.expected { + t.Errorf("size() = %d, want %d", got, tt.expected) + } + }) + } +} diff --git a/context.go b/context.go deleted file mode 100644 index 6a8ae6a..0000000 --- a/context.go +++ /dev/null @@ -1,121 +0,0 @@ -package fronted - -import ( - "context" - "crypto/x509" - "fmt" - "net/http" - "time" - - tls "github.com/refraction-networking/utls" - - "github.com/getlantern/eventual/v2" -) - -// Create an interface for the fronting context -type Fronted interface { - UpdateConfig(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string) - NewRoundTripper(timeout time.Duration) (http.RoundTripper, error) - Close() -} - -var defaultContext = newFrontingContext("default") - -// Make sure that the default context is a Fronting -var _ Fronted = defaultContext - -// Configure sets the masquerades to use, the trusted root CAs, and the -// cache file for caching masquerades to set up direct domain fronting -// in the default context. -// -// defaultProviderID is used when a masquerade without a provider is -// encountered (eg in a cache file) -func NewFronted(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string, cacheFile string) (Fronted, error) { - if err := defaultContext.configure(pool, providers, defaultProviderID, cacheFile); err != nil { - return nil, log.Errorf("Error configuring fronting %s context: %s!!", defaultContext.name, err) - } - return defaultContext, nil -} - -func newFrontingContext(name string) *frontingContext { - return &frontingContext{ - name: name, - instance: eventual.NewValue(), - connectingFronts: newConnectingFronts(), - } -} - -type frontingContext struct { - name string - instance eventual.Value - fronted *fronted - connectingFronts *connectingFronts -} - -// UpdateConfig updates the configuration of the fronting context -func (fctx *frontingContext) UpdateConfig(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string) { - fctx.fronted.updateConfig(pool, providers, defaultProviderID) -} - -// configure sets the masquerades to use, the trusted root CAs, and the -// cache file for caching masquerades to set up direct domain fronting. -// defaultProviderID is used when a masquerade without a provider is -// encountered (eg in a cache file) -func (fctx *frontingContext) configure(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string, cacheFile string) error { - return fctx.configureWithHello(pool, providers, defaultProviderID, cacheFile, tls.ClientHelloID{}) -} - -func (fctx *frontingContext) configureWithHello(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string, cacheFile string, clientHelloID tls.ClientHelloID) error { - log.Debugf("Configuring fronted %s context", fctx.name) - - if len(providers) == 0 { - return fmt.Errorf("no fronted providers for %s context", fctx.name) - } - - if _existing, err := fctx.instance.Get(eventual.DontWait); err != nil { - log.Debugf("No existing instance for %s context: %s", fctx.name, err) - } else if _existing != nil { - existing := _existing.(*fronted) - log.Debugf("Closing cache from existing instance for %s context", fctx.name) - existing.closeCache() - } - - var err error - if fctx.fronted, err = newFronted(pool, providers, defaultProviderID, cacheFile, clientHelloID, func(f *fronted) { - log.Debug("Setting fronted instance") - fctx.instance.Set(f) - }, fctx.connectingFronts); err != nil { - return err - } - return nil -} - -// NewFronted creates a new http.RoundTripper that does direct domain fronting. -// If the context isn't configured within the given timeout, this method -// returns nil, false. -func (fctx *frontingContext) NewRoundTripper(timeout time.Duration) (http.RoundTripper, error) { - start := time.Now() - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - instance, err := fctx.instance.Get(ctx) - if err != nil { - return nil, log.Errorf("No DirectHttpClient available within %v for context %s with error %v", timeout, fctx.name, err) - } else { - log.Debugf("DirectHttpClient available for context %s after %v with duration %v", fctx.name, time.Since(start), timeout) - } - return instance.(http.RoundTripper), nil -} - -// Close closes any existing cache file in the default contexxt. -func (fctx *frontingContext) Close() { - _existing, err := fctx.instance.Get(eventual.DontWait) - if err != nil { - log.Errorf("Error getting existing instance for %s context: %s", fctx.name, err) - return - } - if _existing != nil { - existing := _existing.(*fronted) - log.Debugf("Closing cache from existing instance in %s context", fctx.name) - existing.closeCache() - } -} diff --git a/masquerade.go b/front.go similarity index 90% rename from masquerade.go rename to front.go index 0f8dce7..4e50404 100644 --- a/masquerade.go +++ b/front.go @@ -53,7 +53,7 @@ type Masquerade struct { } // Create a masquerade interface for easier testing. -type MasqueradeInterface interface { +type Front interface { dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) // Accessor for the domain of the masquerade @@ -73,9 +73,11 @@ type MasqueradeInterface interface { postCheck(net.Conn, string) bool getProviderID() string + + isSucceeding() bool } -type masquerade struct { +type front struct { Masquerade // lastSucceeded: the most recent time at which this Masquerade succeeded LastSucceeded time.Time @@ -84,7 +86,7 @@ type masquerade struct { mx sync.RWMutex } -func (m *masquerade) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) { +func (m *front) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) { tlsConfig := &tls.Config{ ServerName: m.Domain, RootCAs: rootCAs, @@ -120,7 +122,7 @@ func (m *masquerade) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloI } // postCheck does a post with invalid data to verify domain-fronting works -func (m *masquerade) postCheck(conn net.Conn, testURL string) bool { +func (m *front) postCheck(conn net.Conn, testURL string) bool { client := &http.Client{ Transport: frontedHTTPTransport(conn, true), } @@ -162,55 +164,61 @@ func doCheck(client *http.Client, method string, expectedStatus int, u string) b } // getDomain implements MasqueradeInterface. -func (m *masquerade) getDomain() string { +func (m *front) getDomain() string { return m.Domain } // getIpAddress implements MasqueradeInterface. -func (m *masquerade) getIpAddress() string { +func (m *front) getIpAddress() string { return m.IpAddress } // getProviderID implements MasqueradeInterface. -func (m *masquerade) getProviderID() string { +func (m *front) getProviderID() string { return m.ProviderID } // MarshalJSON marshals masquerade into json -func (m *masquerade) MarshalJSON() ([]byte, error) { +func (m *front) MarshalJSON() ([]byte, error) { m.mx.RLock() defer m.mx.RUnlock() // Type alias for masquerade so that we don't infinitely recurse when marshaling the struct - type alias masquerade + type alias front return json.Marshal((*alias)(m)) } -func (m *masquerade) lastSucceeded() time.Time { +func (m *front) lastSucceeded() time.Time { m.mx.RLock() defer m.mx.RUnlock() return m.LastSucceeded } -func (m *masquerade) setLastSucceeded(t time.Time) { +func (m *front) setLastSucceeded(t time.Time) { m.mx.Lock() defer m.mx.Unlock() m.LastSucceeded = t } -func (m *masquerade) markSucceeded() { +func (m *front) markSucceeded() { m.mx.Lock() defer m.mx.Unlock() m.LastSucceeded = time.Now() } -func (m *masquerade) markFailed() { +func (m *front) markFailed() { m.mx.Lock() defer m.mx.Unlock() m.LastSucceeded = time.Time{} } +func (m *front) isSucceeding() bool { + m.mx.RLock() + defer m.mx.RUnlock() + return m.LastSucceeded.After(time.Time{}) +} + // Make sure that the masquerade struct implements the MasqueradeInterface -var _ MasqueradeInterface = (*masquerade)(nil) +var _ Front = (*front)(nil) // A Direct fronting provider configuration. type Provider struct { @@ -343,11 +351,11 @@ func NewStatusCodeValidator(reject []int) ResponseValidator { } // slice of masquerade sorted by last vetted time -type sortedMasquerades []MasqueradeInterface +type sortedFronts []Front -func (m sortedMasquerades) Len() int { return len(m) } -func (m sortedMasquerades) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -func (m sortedMasquerades) Less(i, j int) bool { +func (m sortedFronts) Len() int { return len(m) } +func (m sortedFronts) Swap(i, j int) { m[i], m[j] = m[j], m[i] } +func (m sortedFronts) Less(i, j int) bool { if m[i].lastSucceeded().After(m[j].lastSucceeded()) { return true } else if m[j].lastSucceeded().After(m[i].lastSucceeded()) { @@ -357,8 +365,8 @@ func (m sortedMasquerades) Less(i, j int) bool { } } -func (m sortedMasquerades) sortedCopy() sortedMasquerades { - c := make(sortedMasquerades, len(m)) +func (m sortedFronts) sortedCopy() sortedFronts { + c := make(sortedFronts, len(m)) copy(c, m) sort.Sort(c) return c diff --git a/masquerade_test.go b/front_test.go similarity index 100% rename from masquerade_test.go rename to front_test.go diff --git a/fronted.go b/fronted.go index aa8d564..f8becc5 100644 --- a/fronted.go +++ b/fronted.go @@ -12,7 +12,6 @@ import ( "net/url" "strings" "sync" - "sync/atomic" "syscall" "time" @@ -33,17 +32,11 @@ var ( log = golog.LoggerFor("fronted") ) -type workingFronts interface { - onConnected(m MasqueradeInterface, connectTime time.Duration) - onError(m MasqueradeInterface) - workingFront() (MasqueradeInterface, error) -} - // fronted identifies working IP address/domain pairings for domain fronting and is // an implementation of http.RoundTripper for the convenience of callers. type fronted struct { certPool *x509.CertPool - masquerades sortedMasquerades + fronts sortedFronts maxAllowedCachedAge time.Duration maxCacheSize int cacheSaveInterval time.Duration @@ -55,31 +48,46 @@ type fronted struct { clientHelloID tls.ClientHelloID workingFronts workingFronts providersMu sync.RWMutex - masqueradesMu sync.RWMutex + frontsMu sync.RWMutex frontedMu sync.RWMutex + stopCh chan interface{} } -func newFronted(pool *x509.CertPool, providers map[string]*Provider, - defaultProviderID, cacheFile string, clientHelloID tls.ClientHelloID, - listener func(f *fronted), workingFronts workingFronts) (*fronted, error) { - if workingFronts == nil { - return nil, fmt.Errorf("workingFronts must not be nil") - } - size := 0 - for _, p := range providers { - size += len(p.Masquerades) - } +// Interface for sending HTTP traffic over domain fronting. +type Fronted interface { + http.RoundTripper - if size == 0 { - return nil, fmt.Errorf("no masquerades found in providers") + // UpdateConfig updates the set of domain fronts to try. + UpdateConfig(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string) + + // Close closes any resources, such as goroutines that are testing fronts. + Close() +} + +// NewFronted sets the domain fronts to use, the trusted root CAs, the fronting providers +// (such as Akamai, Cloudfront, etc), and the cache file for caching fronts to set up +// domain fronting. +// +// defaultProviderID is used when a front without a provider is +// encountered (eg in a cache file) +func NewFronted(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string, cacheFile string, + clientHello tls.ClientHelloID) (Fronted, error) { + log.Debug("Creating new fronted") + // Log method elapsed time + defer func(start time.Time) { + log.Debugf("Creating a new fronted took %v", time.Since(start)) + }(time.Now()) + + if len(providers) == 0 { + return nil, log.Errorf("No providers configured") } - // copy providers providersCopy := copyProviders(providers) + fronts := loadFronts(providersCopy) f := &fronted{ certPool: pool, - masquerades: loadMasquerades(providersCopy, size), + fronts: fronts, maxAllowedCachedAge: defaultMaxAllowedCachedAge, maxCacheSize: defaultMaxCacheSize, cacheSaveInterval: defaultCacheSaveInterval, @@ -87,14 +95,15 @@ func newFronted(pool *x509.CertPool, providers map[string]*Provider, cacheClosed: make(chan interface{}), defaultProviderID: defaultProviderID, providers: providersCopy, - clientHelloID: clientHelloID, - workingFronts: workingFronts, + clientHelloID: clientHello, + workingFronts: newConnectingFronts(len(fronts)), + stopCh: make(chan interface{}), } if cacheFile != "" { f.initCaching(cacheFile) } - f.findWorkingMasquerades(listener) + go f.findWorkingFronts() return f, nil } @@ -107,15 +116,24 @@ func copyProviders(providers map[string]*Provider) map[string]*Provider { return providersCopy } -func loadMasquerades(initial map[string]*Provider, size int) sortedMasquerades { - log.Debugf("Loading candidates for %d providers", len(initial)) +func loadFronts(providers map[string]*Provider) sortedFronts { + log.Debugf("Loading candidates for %d providers", len(providers)) defer log.Debug("Finished loading candidates") - masquerades := make(sortedMasquerades, 0, size) - for key, p := range initial { + // Preallocate the slice to avoid reallocation + size := 0 + for _, p := range providers { + size += len(p.Masquerades) + } + + fronts := make(sortedFronts, size) + + index := 0 + for key, p := range providers { arr := p.Masquerades size := len(arr) + // Shuffle the masquerades to avoid biasing the order in which they are tried // make a shuffled copy of arr // ('inside-out' Fisher-Yates) sh := make([]*Masquerade, size) @@ -126,13 +144,14 @@ func loadMasquerades(initial map[string]*Provider, size int) sortedMasquerades { } for _, c := range sh { - masquerades = append(masquerades, &masquerade{Masquerade: *c, ProviderID: key}) + fronts[index] = &front{Masquerade: *c, ProviderID: key} + index++ } } - return masquerades + return fronts } -func (f *fronted) updateConfig(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string) { +func (f *fronted) UpdateConfig(pool *x509.CertPool, providers map[string]*Provider, defaultProviderID string) { // Make copies just to avoid any concurrency issues with access that may be happening on the // caller side. log.Debug("Updating fronted configuration") @@ -140,7 +159,7 @@ func (f *fronted) updateConfig(pool *x509.CertPool, providers map[string]*Provid f.frontedMu.Lock() defer f.frontedMu.Unlock() f.addProviders(providersCopy) - f.addMasquerades(loadMasquerades(providersCopy, len(providersCopy))) + f.addFronts(loadFronts(providersCopy)) f.defaultProviderID = defaultProviderID f.certPool = pool } @@ -154,14 +173,14 @@ func (f *fronted) addProviders(providers map[string]*Provider) { } } -func (f *fronted) addMasquerades(masquerades sortedMasquerades) { +func (f *fronted) addFronts(fronts sortedFronts) { // Add new masquerades to the existing masquerades slice, but add them at the beginning. - f.masqueradesMu.Lock() - defer f.masqueradesMu.Unlock() - f.masquerades = append(masquerades, f.masquerades...) + f.frontsMu.Lock() + defer f.frontsMu.Unlock() + f.fronts = append(fronts, f.fronts...) } -func (f *fronted) providerFor(m MasqueradeInterface) *Provider { +func (f *fronted) providerFor(m Front) *Provider { pid := m.getProviderID() if pid == "" { pid = f.defaultProviderID @@ -177,7 +196,7 @@ func Vet(m *Masquerade, pool *x509.CertPool, testURL string) bool { maxAllowedCachedAge: defaultMaxAllowedCachedAge, maxCacheSize: defaultMaxCacheSize, } - masq := &masquerade{Masquerade: *m} + masq := &front{Masquerade: *m} conn, _, err := d.doDial(masq) if err != nil { return false @@ -186,61 +205,75 @@ func Vet(m *Masquerade, pool *x509.CertPool, testURL string) bool { return masq.postCheck(conn, testURL) } -// findWorkingMasquerades finds working masquerades by vetting them in batches and in -// parallel. Speed is of the essence here, as without working masquerades, users will +// findWorkingFronts finds working domain fronts by vetting them in batches and in +// parallel. Speed is of the essence here, as without working fronts, users will // be unable to fetch proxy configurations, particularly in the case of a first time // user who does not have proxies cached on disk. -func (f *fronted) findWorkingMasquerades(listener func(f *fronted)) { - // vet masquerades in batches +func (f *fronted) findWorkingFronts() { + // vet fronts in batches const batchSize int = 40 - var successful atomic.Uint32 - // We loop through all of them until we have 4 successful ones. - for i := 0; i < f.masqueradeSize() && successful.Load() < 4; i += batchSize { - f.vetBatch(i, batchSize, &successful, listener) + // Keep looping through all fronts making sure we have working ones. + i := 0 + for { + // Continually loop through the fronts in batches until we have 4 working ones, + // always looping around to the beginning if we reach the end. + // This is important, for example, when the user goes offline and all fronts start failing. + // We want to just keep trying in that case so that we find working fronts as soon as they + // come back online. + if f.workingFronts.size() < 4 { + f.vetBatch(i, batchSize) + i = index(i, batchSize, f.frontSize()) + } else { + select { + case <-f.stopCh: + log.Debug("Stopping parallel dialing") + return + case <-time.After(time.Duration(rand.IntN(12000)) * time.Millisecond): + } + } } } -func (f *fronted) masqueradeSize() int { - f.masqueradesMu.Lock() - defer f.masqueradesMu.Unlock() - return len(f.masquerades) +func index(i, batchSize, size int) int { + return (i + batchSize) % size } -func (f *fronted) masqueradeAt(i int) MasqueradeInterface { - f.masqueradesMu.Lock() - defer f.masqueradesMu.Unlock() - return f.masquerades[i] +func (f *fronted) frontSize() int { + f.frontsMu.Lock() + defer f.frontsMu.Unlock() + return len(f.fronts) } -func (f *fronted) vetBatch(start, batchSize int, successful *atomic.Uint32, listener func(f *fronted)) { +func (f *fronted) frontAt(i int) Front { + f.frontsMu.Lock() + defer f.frontsMu.Unlock() + return f.fronts[i] +} + +func (f *fronted) vetBatch(start, batchSize int) { log.Debugf("Vetting masquerade batch %d-%d", start, start+batchSize) var wg sync.WaitGroup - for i := start; i < start+batchSize && i < f.masqueradeSize(); i++ { + for i := start; i < start+batchSize && i < f.frontSize(); i++ { wg.Add(1) - go func(m MasqueradeInterface) { + go func(m Front) { defer wg.Done() - working, connectTime := f.vetMasquerade(m) + working := f.vetFront(m) if working { - successful.Add(1) - f.workingFronts.onConnected(m, connectTime) - if listener != nil { - go listener(f) - } + f.workingFronts.onConnected(m) } else { - f.workingFronts.onError(m) + m.markFailed() } - }(f.masqueradeAt(i)) + }(f.frontAt(i)) } wg.Wait() } -func (f *fronted) vetMasquerade(m MasqueradeInterface) (bool, time.Duration) { - start := time.Now() - conn, masqueradeGood, err := f.dialMasquerade(m) +func (f *fronted) vetFront(m Front) bool { + conn, masqueradeGood, err := f.dialFront(m) if err != nil { log.Debugf("unexpected error vetting masquerades: %v", err) - return false, time.Since(start) + return false } defer func() { if conn != nil { @@ -252,15 +285,15 @@ func (f *fronted) vetMasquerade(m MasqueradeInterface) (bool, time.Duration) { if provider == nil { log.Debugf("Skipping masquerade with disabled/unknown provider id '%s' not in %v", m.getProviderID(), f.providers) - return false, time.Since(start) + return false } if !masqueradeGood(m.postCheck(conn, provider.TestURL)) { log.Debugf("Unsuccessful vetting with POST request, discarding masquerade") - return false, time.Since(start) + return false } log.Debugf("Successfully vetted one masquerade %v", m.getIpAddress()) - return true, time.Since(start) + return true } // RoundTrip loops through all available masquerades, sorted by the one that most recently @@ -312,24 +345,22 @@ func (f *fronted) RoundTripHijack(req *http.Request) (*http.Response, net.Conn, log.Debugf("Retrying domain-fronted request, pass %d", i) } - m, err := f.workingFronts.workingFront() + m, err := f.workingFronts.connectingFront(req.Context()) if err != nil { // For some reason we have no working fronts. Sleep for a bit and try again. time.Sleep(1 * time.Second) continue } - conn, masqueradeGood, err := f.dialMasquerade(m) + conn, masqueradeGood, err := f.dialFront(m) if err != nil { log.Debugf("Could not dial to %v: %v", m, err) - f.workingFronts.onError(m) continue } resp, conn, err := f.request(req, conn, m, originHost, getBody, masqueradeGood) if err != nil { log.Debugf("Could not complete request: %v", err) - f.workingFronts.onError(m) } else { return resp, conn, nil } @@ -338,7 +369,7 @@ func (f *fronted) RoundTripHijack(req *http.Request) (*http.Response, net.Conn, return nil, nil, op.FailIf(errors.New("could not complete request even with retries")) } -func (f *fronted) request(req *http.Request, conn net.Conn, m MasqueradeInterface, originHost string, getBody func() io.ReadCloser, masqueradeGood func(bool) bool) (*http.Response, net.Conn, error) { +func (f *fronted) request(req *http.Request, conn net.Conn, m Front, originHost string, getBody func() io.ReadCloser, masqueradeGood func(bool) bool) (*http.Response, net.Conn, error) { op := ops.Begin("fronted_request") defer op.End() provider := f.providerFor(m) @@ -389,7 +420,7 @@ func (f *fronted) request(req *http.Request, conn net.Conn, m MasqueradeInterfac return resp, conn, nil } -func (f *fronted) dialMasquerade(m MasqueradeInterface) (net.Conn, func(bool) bool, error) { +func (f *fronted) dialFront(m Front) (net.Conn, func(bool) bool, error) { log.Tracef("Dialing to %v", m) // We do the full TLS connection here because in practice the domains at a given IP @@ -416,7 +447,7 @@ func (f *fronted) dialMasquerade(m MasqueradeInterface) (net.Conn, func(bool) bo return conn, masqueradeGood, err } -func (f *fronted) doDial(m MasqueradeInterface) (net.Conn, bool, error) { +func (f *fronted) doDial(m Front) (net.Conn, bool, error) { op := ops.Begin("dial_masquerade") defer op.End() op.Set("masquerade_domain", m.getDomain()) diff --git a/fronted_test.go b/fronted_test.go index 0a83b9f..a504ca2 100644 --- a/fronted_test.go +++ b/fronted_test.go @@ -15,11 +15,9 @@ import ( "path/filepath" "strconv" "strings" - "sync/atomic" "testing" "time" - "github.com/getlantern/eventual/v2" . "github.com/getlantern/waitforserver" tls "github.com/refraction-networking/utls" "github.com/stretchr/testify/assert" @@ -56,11 +54,8 @@ func TestDirectDomainFrontingWithSNIConfig(t *testing.T) { UseArbitrarySNIs: true, ArbitrarySNIs: []string{"mercadopago.com", "amazon.com.br", "facebook.com", "google.com", "twitter.com", "youtube.com", "instagram.com", "linkedin.com", "whatsapp.com", "netflix.com", "microsoft.com", "yahoo.com", "bing.com", "wikipedia.org", "github.com"}, }) - testContext := newFrontingContext("TestDirectDomainFrontingWithSNIConfig") - testContext.configure(certs, p, "akamai", cacheFile) + transport, err := NewFronted(certs, p, "akamai", cacheFile, tls.HelloChrome_100) - transport, ok := testContext.NewRoundTripper(30 * time.Second) - require.NoError(t, ok) client := &http.Client{ Transport: transport, } @@ -85,11 +80,8 @@ func doTestDomainFronting(t *testing.T, cacheFile string, expectedMasqueradesAtE } certs := trustedCACerts(t) p := testProvidersWithHosts(hosts) - testContext := newFrontingContext("doTestDomainFronting") - testContext.configure(certs, p, testProviderID, cacheFile) - - transport, ok := testContext.NewRoundTripper(30 * time.Second) - require.NoError(t, ok) + transport, err := NewFronted(certs, p, testProviderID, cacheFile, tls.HelloChrome_100) + require.NoError(t, err) client := &http.Client{ Transport: transport, @@ -97,21 +89,19 @@ func doTestDomainFronting(t *testing.T, cacheFile string, expectedMasqueradesAtE } require.True(t, doCheck(client, http.MethodPost, http.StatusAccepted, pingURL)) - transport, ok = testContext.NewRoundTripper(30 * time.Second) - require.NoError(t, ok) + transport, err = NewFronted(certs, p, testProviderID, cacheFile, tls.HelloChrome_100) + require.NoError(t, err) client = &http.Client{ Transport: transport, } require.True(t, doCheck(client, http.MethodGet, http.StatusOK, getURL)) - instance, err := testContext.instance.Get(eventual.DontWait) - require.NoError(t, err) - d := instance.(*fronted) + d := transport.(*fronted) // Check the number of masquerades at the end, waiting until we get the right number masqueradesAtEnd := 0 for i := 0; i < 1000; i++ { - masqueradesAtEnd = len(d.masquerades) + masqueradesAtEnd = len(d.fronts) if masqueradesAtEnd == expectedMasqueradesAtEnd { break } @@ -131,33 +121,6 @@ func TestVet(t *testing.T) { t.Fatal("None of the default masquerades vetted successfully") } -func TestLoadMasquerades(t *testing.T) { - providers := testProviders() - - expected := make(map[Masquerade]bool) - for _, p := range providers { - for _, m := range p.Masquerades { - expected[*m] = true - } - } - - newMasquerades := loadMasquerades(providers, len(expected)) - - d := &fronted{ - masquerades: newMasquerades, - } - - actual := make(map[Masquerade]bool) - count := 0 - for _, m := range d.masquerades { - actual[Masquerade{Domain: m.getDomain(), IpAddress: m.getIpAddress()}] = true - count++ - } - - assert.Equal(t, len(DefaultCloudfrontMasquerades), count, "Unexpected number of candidates") - assert.Equal(t, expected, actual, "Masquerades did not load as expected") -} - func TestHostAliasesBasic(t *testing.T) { headersIn := map[string][]string{ @@ -240,13 +203,8 @@ func TestHostAliasesBasic(t *testing.T) { certs := x509.NewCertPool() certs.AddCert(cloudSack.Certificate()) - testContext := newFrontingContext("TestHostAliasesBasic") - testContext.configure(certs, map[string]*Provider{"cloudsack": p}, "cloudsack", "") + rt, err := NewFronted(certs, map[string]*Provider{"cloudsack": p}, "cloudsack", "", tls.HelloChrome_100) - rt, ok := testContext.NewRoundTripper(30 * time.Second) - if !assert.NoError(t, ok, "failed to obtain direct roundtripper") { - return - } client := &http.Client{Transport: rt} for _, test := range tests { req, err := http.NewRequest(http.MethodGet, test.url, nil) @@ -353,12 +311,8 @@ func TestHostAliasesMulti(t *testing.T) { "sadcloud": p2, } - testContext := newFrontingContext("TestHostAliasesMulti") - testContext.configure(certs, providers, "cloudsack", "") - rt, ok := testContext.NewRoundTripper(30 * time.Second) - if !assert.NoError(t, ok, "failed to obtain direct roundtripper") { - return - } + rt, err := NewFronted(certs, providers, "cloudsack", "", tls.HelloChrome_100) + client := &http.Client{Transport: rt} providerCounts := make(map[string]int) @@ -480,13 +434,9 @@ func TestPassthrough(t *testing.T) { certs := x509.NewCertPool() certs.AddCert(cloudSack.Certificate()) - testContext := newFrontingContext("TestPassthrough") - testContext.configure(certs, map[string]*Provider{"cloudsack": p}, "cloudsack", "") + rt, err := NewFronted(certs, map[string]*Provider{"cloudsack": p}, "cloudsack", "", tls.HelloChrome_100) + require.NoError(t, err) - rt, ok := testContext.NewRoundTripper(30 * time.Second) - if !assert.NoError(t, ok, "failed to obtain direct roundtripper") { - return - } client := &http.Client{Transport: rt} for _, test := range tests { req, err := http.NewRequest(http.MethodGet, test.url, nil) @@ -539,7 +489,7 @@ func TestCustomValidators(t *testing.T) { sadCloudValidator := NewStatusCodeValidator(sadCloudCodes) testURL := "https://abc.forbidden.com/quux" - setup := func(ctx *frontingContext, validator ResponseValidator) { + setup := func(validator ResponseValidator) (Fronted, error) { masq := []*Masquerade{{Domain: "example.com", IpAddress: sadCloudAddr}} alias := map[string]string{ "abc.forbidden.com": "abc.sadcloud.io", @@ -553,7 +503,7 @@ func TestCustomValidators(t *testing.T) { "sadcloud": p, } - ctx.configure(certs, providers, "sadcloud", "") + return NewFronted(certs, providers, "sadcloud", "", tls.HelloChrome_100) } // This error indicates that the validator has discarded all masquerades. @@ -634,10 +584,8 @@ func TestCustomValidators(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - testContext := newFrontingContext(test.name) - setup(testContext, test.validator) - direct, ok := testContext.NewRoundTripper(30 * time.Second) - require.NoError(t, ok) + direct, err := setup(test.validator) + require.NoError(t, err) client := &http.Client{ Transport: direct, } @@ -827,13 +775,13 @@ func TestVerifyPeerCertificate(t *testing.T) { func TestFindWorkingMasquerades(t *testing.T) { tests := []struct { name string - masquerades []*mockMasquerade + masquerades []*mockFront expectedSuccessful int expectedMasquerades int }{ { name: "All successful", - masquerades: []*mockMasquerade{ + masquerades: []*mockFront{ newMockMasquerade("domain1.com", "1.1.1.1", 0, true), newMockMasquerade("domain2.com", "2.2.2.2", 0, true), newMockMasquerade("domain3.com", "3.3.3.3", 0, true), @@ -845,7 +793,7 @@ func TestFindWorkingMasquerades(t *testing.T) { }, { name: "Some successful", - masquerades: []*mockMasquerade{ + masquerades: []*mockFront{ newMockMasquerade("domain1.com", "1.1.1.1", 0, true), newMockMasquerade("domain2.com", "2.2.2.2", 0, false), newMockMasquerade("domain3.com", "3.3.3.3", 0, true), @@ -856,7 +804,7 @@ func TestFindWorkingMasquerades(t *testing.T) { }, { name: "None successful", - masquerades: []*mockMasquerade{ + masquerades: []*mockFront{ newMockMasquerade("domain1.com", "1.1.1.1", 0, false), newMockMasquerade("domain2.com", "2.2.2.2", 0, false), newMockMasquerade("domain3.com", "3.3.3.3", 0, false), @@ -866,8 +814,8 @@ func TestFindWorkingMasquerades(t *testing.T) { }, { name: "Batch processing", - masquerades: func() []*mockMasquerade { - var masquerades []*mockMasquerade + masquerades: func() []*mockFront { + var masquerades []*mockFront for i := 0; i < 50; i++ { masquerades = append(masquerades, newMockMasquerade(fmt.Sprintf("domain%d.com", i), fmt.Sprintf("1.1.1.%d", i), 0, i%2 == 0)) } @@ -879,38 +827,93 @@ func TestFindWorkingMasquerades(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - d := &fronted{} + d := &fronted{ + workingFronts: newConnectingFronts(10), + } d.providers = make(map[string]*Provider) d.providers["testProviderId"] = NewProvider(nil, "", nil, nil, nil, nil, nil) - d.masquerades = make(sortedMasquerades, len(tt.masquerades)) + d.fronts = make(sortedFronts, len(tt.masquerades)) for i, m := range tt.masquerades { - d.masquerades[i] = m + d.fronts[i] = m } - var successful atomic.Uint32 - d.vetBatch(0, 10, &successful, nil) + d.vetBatch(0, 10) tries := 0 - for successful.Load() < uint32(tt.expectedSuccessful) && tries < 100 { + for d.workingFronts.size() < tt.expectedSuccessful && tries < 100 { time.Sleep(30 * time.Millisecond) tries++ } - assert.GreaterOrEqual(t, int(successful.Load()), tt.expectedSuccessful) + assert.GreaterOrEqual(t, d.workingFronts.size(), tt.expectedSuccessful) + }) + } +} + +func TestLoadFronts(t *testing.T) { + providers := map[string]*Provider{ + "provider1": { + Masquerades: []*Masquerade{ + {Domain: "domain1.com", IpAddress: "1.1.1.1"}, + {Domain: "domain2.com", IpAddress: "2.2.2.2"}, + }, + }, + "provider2": { + Masquerades: []*Masquerade{ + {Domain: "domain3.com", IpAddress: "3.3.3.3"}, + {Domain: "domain4.com", IpAddress: "4.4.4.4"}, + }, + }, + } + + expected := map[string]bool{ + "domain1.com": true, + "domain2.com": true, + "domain3.com": true, + "domain4.com": true, + } + + masquerades := loadFronts(providers) + + assert.Equal(t, 4, len(masquerades), "Unexpected number of masquerades loaded") + + for _, m := range masquerades { + assert.True(t, expected[m.getDomain()], "Unexpected masquerade domain: %s", m.getDomain()) + } +} + +func TestIndex(t *testing.T) { + tests := []struct { + i, batchSize, size int + expected int + }{ + {i: 0, batchSize: 10, size: 100, expected: 10}, + {i: 5, batchSize: 10, size: 100, expected: 15}, + {i: 95, batchSize: 10, size: 100, expected: 5}, + {i: 99, batchSize: 10, size: 100, expected: 9}, + {i: 0, batchSize: 5, size: 20, expected: 5}, + {i: 15, batchSize: 5, size: 20, expected: 0}, + {i: 18, batchSize: 5, size: 20, expected: 3}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("i=%d,batchSize=%d,size=%d", test.i, test.batchSize, test.size), func(t *testing.T) { + result := index(test.i, test.batchSize, test.size) + assert.Equal(t, test.expected, result) }) } } // Generate a mock of a MasqueradeInterface with a Dial method that can optionally // return an error after a specified number of milliseconds. -func newMockMasquerade(domain string, ipAddress string, timeout time.Duration, passesCheck bool) *mockMasquerade { +func newMockMasquerade(domain string, ipAddress string, timeout time.Duration, passesCheck bool) *mockFront { return newMockMasqueradeWithLastSuccess(domain, ipAddress, timeout, passesCheck, time.Time{}) } // Generate a mock of a MasqueradeInterface with a Dial method that can optionally // return an error after a specified number of milliseconds. -func newMockMasqueradeWithLastSuccess(domain string, ipAddress string, timeout time.Duration, passesCheck bool, lastSucceededTime time.Time) *mockMasquerade { - return &mockMasquerade{ +func newMockMasqueradeWithLastSuccess(domain string, ipAddress string, timeout time.Duration, passesCheck bool, lastSucceededTime time.Time) *mockFront { + return &mockFront{ Domain: domain, IpAddress: ipAddress, timeout: timeout, @@ -919,7 +922,7 @@ func newMockMasqueradeWithLastSuccess(domain string, ipAddress string, timeout t } } -type mockMasquerade struct { +type mockFront struct { Domain string IpAddress string timeout time.Duration @@ -928,22 +931,27 @@ type mockMasquerade struct { } // setLastSucceeded implements MasqueradeInterface. -func (m *mockMasquerade) setLastSucceeded(succeededTime time.Time) { +func (m *mockFront) setLastSucceeded(succeededTime time.Time) { m.lastSucceededTime = succeededTime } // lastSucceeded implements MasqueradeInterface. -func (m *mockMasquerade) lastSucceeded() time.Time { +func (m *mockFront) lastSucceeded() time.Time { return m.lastSucceededTime } +// isSucceeding implements MasqueradeInterface. +func (m *mockFront) isSucceeding() bool { + return m.lastSucceededTime.After(time.Time{}) +} + // postCheck implements MasqueradeInterface. -func (m *mockMasquerade) postCheck(net.Conn, string) bool { +func (m *mockFront) postCheck(net.Conn, string) bool { return m.passesCheck } // dial implements MasqueradeInterface. -func (m *mockMasquerade) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) { +func (m *mockFront) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) { if m.timeout > 0 { time.Sleep(m.timeout) return nil, errors.New("mock dial error") @@ -953,28 +961,28 @@ func (m *mockMasquerade) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHe } // getDomain implements MasqueradeInterface. -func (m *mockMasquerade) getDomain() string { +func (m *mockFront) getDomain() string { return m.Domain } // getIpAddress implements MasqueradeInterface. -func (m *mockMasquerade) getIpAddress() string { +func (m *mockFront) getIpAddress() string { return m.IpAddress } // getProviderID implements MasqueradeInterface. -func (m *mockMasquerade) getProviderID() string { +func (m *mockFront) getProviderID() string { return "testProviderId" } // markFailed implements MasqueradeInterface. -func (m *mockMasquerade) markFailed() { +func (m *mockFront) markFailed() { } // markSucceeded implements MasqueradeInterface. -func (m *mockMasquerade) markSucceeded() { +func (m *mockFront) markSucceeded() { } // Make sure that the mockMasquerade implements the MasqueradeInterface -var _ MasqueradeInterface = (*mockMasquerade)(nil) +var _ Front = (*mockFront)(nil) diff --git a/go.mod b/go.mod index d0f6eab..8a50f55 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/getlantern/fronted go 1.22.3 require ( - github.com/getlantern/eventual/v2 v2.0.2 github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 github.com/getlantern/keyman v0.0.0-20180207174507-f55e7280e93a github.com/getlantern/netx v0.0.0-20210806160745-b824e2cad607 diff --git a/go.sum b/go.sum index f8b45c6..9450ea9 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,6 @@ github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFB github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= github.com/getlantern/errors v1.0.3 h1:Ne4Ycj7NI1BtSyAfVeAT/DNoxz7/S2BUc3L2Ht1YSHE= github.com/getlantern/errors v1.0.3/go.mod h1:m8C7H1qmouvsGpwQqk/6NUpIVMpfzUPn608aBZDYV04= -github.com/getlantern/eventual/v2 v2.0.2 h1:7b3N2oQBVqSHwm/8u7C8b6W+OkkjgZSmwUc1AdIkrHc= -github.com/getlantern/eventual/v2 v2.0.2/go.mod h1:o1VZHRk8UArBra+pwPSi23WrahBG4qgg4/ew6Mmlq84= github.com/getlantern/fdcount v0.0.0-20190912142506-f89afd7367c4 h1:JdD4XSaT6/j6InM7MT1E4WRvzR8gurxfq53A3ML3B/Q= github.com/getlantern/fdcount v0.0.0-20190912142506-f89afd7367c4/go.mod h1:XZwE+iIlAgr64OFbXKFNCllBwV4wEipPx8Hlo2gZdbM= github.com/getlantern/filepersist v0.0.0-20160317154340-c5f0cd24e799 h1:FhkPUYCQYmoxS02r2GRrIV7dahUIncRl36xzs3/mnjA= diff --git a/test_support.go b/test_support.go index cd615f7..55c936c 100644 --- a/test_support.go +++ b/test_support.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/getlantern/keyman" + tls "github.com/refraction-networking/utls" ) var ( @@ -23,13 +24,13 @@ func ConfigureForTest(t *testing.T) { func ConfigureCachingForTest(t *testing.T, cacheFile string) { certs := trustedCACerts(t) p := testProviders() - NewFronted(certs, p, testProviderID, cacheFile) + NewFronted(certs, p, testProviderID, cacheFile, tls.HelloChrome_100) } func ConfigureHostAlaisesForTest(t *testing.T, hosts map[string]string) { certs := trustedCACerts(t) p := testProvidersWithHosts(hosts) - NewFronted(certs, p, testProviderID, "") + NewFronted(certs, p, testProviderID, "", tls.HelloChrome_100) } func trustedCACerts(t *testing.T) *x509.CertPool {