├── .github └── workflows │ └── test.yaml ├── LICENSE ├── README.md ├── cache.go ├── cache_test.go ├── connected_roundtripper.go ├── default_masquerades.go ├── download_config.sh ├── front.go ├── front_test.go ├── fronted.go ├── fronted.yaml.gz ├── fronted_test.go ├── go.mod ├── go.sum ├── test_support.go └── updateFrontedConfig.bash /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 18 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: "go.mod" 22 | - name: Install go go-ctrf-json-reporter 23 | run: go install github.com/ctrf-io/go-ctrf-json-reporter/cmd/go-ctrf-json-reporter@latest 24 | - name: Run tests 25 | run: go test -json -race -tags="headless" -coverprofile=profile.cov ./... | go-ctrf-json-reporter -output ctrf-report.json 26 | - name: Upload test results 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: ctrf-report 30 | path: ctrf-report.json 31 | - name: Publish Test Report 32 | uses: ctrf-io/github-test-reporter@v1 33 | with: 34 | report-path: 'ctrf-report.json' 35 | if: always() 36 | - name: Publish Test Summary Results 37 | run: npx github-actions-ctrf ctrf-report.json 38 | - name: Install goveralls 39 | run: go install github.com/mattn/goveralls@latest 40 | - name: Send coverage 41 | env: 42 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | run: goveralls -coverprofile=profile.cov -service=github 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014 Brave New Software Project, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fronted [![Coverage Status](https://coveralls.io/repos/getlantern/fronted/badge.png)](https://coveralls.io/r/getlantern/fronted) [![GoDoc](https://godoc.org/github.com/getlantern/fronted?status.png)](http://godoc.org/github.com/getlantern/fronted) 2 | ========== 3 | To install: 4 | 5 | `go get github.com/getlantern/fronted` 6 | 7 | For docs: 8 | 9 | `godoc github.com/getlantern/fronted` 10 | 11 | See [ddftool](https://github.com/getlantern/ddftool) for more details on how to generate and tests fronting domains for the supported CDNs. 12 | 13 | [!NOTE] 14 | Since the masquerade domains and IP addresses can change, tests might fail and they need to be updated. You can basically ping some of the masquerade domains (from `default_masquerade.go`) and update the IPs accordingly. 15 | 16 | To generate an updated domain fronting configuration file, just run: 17 | 18 | ``` 19 | ./updateFrontedConfig.bash 20 | ``` -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | ) 9 | 10 | func (f *fronted) initCaching(cacheFile string) { 11 | f.prepopulateFronts(cacheFile) 12 | go f.maintainCache(cacheFile) 13 | } 14 | 15 | func (f *fronted) prepopulateFronts(cacheFile string) { 16 | bytes, err := os.ReadFile(cacheFile) 17 | if os.IsNotExist(err) { 18 | if err = os.MkdirAll(filepath.Dir(cacheFile), 0755); err != nil { 19 | log.Errorf("Error creating cache directory: %v", err) 20 | return 21 | } 22 | } 23 | if err != nil { 24 | log.Errorf("Error reading cache file: %v", err) 25 | return 26 | } 27 | 28 | if len(bytes) == 0 { 29 | // This can happen if the file is empty or just not there 30 | log.Debug("ignorable error: Cache file is empty") 31 | return 32 | } 33 | 34 | log.Debugf("Attempting to prepopulate masquerades from cache file: %v", cacheFile) 35 | var cachedFronts []*front 36 | if err := json.Unmarshal(bytes, &cachedFronts); err != nil { 37 | log.Errorf("Error reading cached masquerades: %v", err) 38 | return 39 | } 40 | 41 | log.Debugf("Cache contained %d masquerades", len(cachedFronts)) 42 | now := time.Now() 43 | 44 | // update last succeeded status of masquerades based on cached values 45 | for _, fr := range f.fronts.fronts { 46 | for _, cf := range cachedFronts { 47 | sameFront := cf.ProviderID == fr.getProviderID() && cf.Domain == fr.getDomain() && cf.IpAddress == fr.getIpAddress() 48 | cachedValueFresh := now.Sub(fr.lastSucceeded()) < f.maxAllowedCachedAge 49 | if sameFront && cachedValueFresh { 50 | fr.setLastSucceeded(cf.LastSucceeded) 51 | } 52 | } 53 | } 54 | } 55 | 56 | func (f *fronted) markCacheDirty() { 57 | select { 58 | case f.cacheDirty <- nil: 59 | // okay 60 | default: 61 | // already dirty 62 | } 63 | } 64 | 65 | func (f *fronted) maintainCache(cacheFile string) { 66 | for { 67 | select { 68 | case <-f.cacheClosed: 69 | return 70 | case <-time.After(f.cacheSaveInterval): 71 | select { 72 | case <-f.cacheClosed: 73 | return 74 | case <-f.cacheDirty: 75 | f.updateCache(cacheFile) 76 | } 77 | } 78 | } 79 | } 80 | 81 | func (f *fronted) updateCache(cacheFile string) { 82 | log.Debugf("Updating cache at %v", cacheFile) 83 | cache := f.fronts.sortedCopy() 84 | sizeToSave := len(cache) 85 | if f.maxCacheSize < sizeToSave { 86 | sizeToSave = f.maxCacheSize 87 | } 88 | b, err := json.Marshal(cache[:sizeToSave]) 89 | if err != nil { 90 | log.Errorf("Unable to marshal cache to JSON: %v", err) 91 | return 92 | } 93 | err = os.WriteFile(cacheFile, b, 0644) 94 | if err != nil { 95 | log.Errorf("Unable to save cache to disk: %v", err) 96 | // Log the directory of the cache file and if it exists for debugging purposes 97 | parent := filepath.Dir(cacheFile) 98 | // check if the parent directory exists 99 | if _, err := os.Stat(parent); err == nil { 100 | // parent directory exists 101 | log.Debugf("Parent directory of cache file exists: %v", parent) 102 | } else { 103 | // parent directory does not exist 104 | log.Debugf("Parent directory of cache file does not exist: %v", parent) 105 | } 106 | } else { 107 | log.Debugf("Cache saved to disk") 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestCaching(t *testing.T) { 15 | dir, err := os.MkdirTemp("", "direct_test") 16 | if !assert.NoError(t, err, "Unable to create temp dir") { 17 | return 18 | } 19 | defer os.RemoveAll(dir) 20 | cacheFile := filepath.Join(dir, "cachefile.1") 21 | 22 | cloudsackID := "cloudsack" 23 | 24 | providers := map[string]*Provider{ 25 | testProviderID: NewProvider(nil, "", nil, nil, nil, nil, ""), 26 | cloudsackID: NewProvider(nil, "", nil, nil, nil, nil, ""), 27 | } 28 | 29 | log.Debug("Creating fronted") 30 | makeFronted := func() *fronted { 31 | f := &fronted{ 32 | fronts: newThreadSafeFronts(1000), 33 | maxAllowedCachedAge: 250 * time.Millisecond, 34 | maxCacheSize: 4, 35 | cacheSaveInterval: 50 * time.Millisecond, 36 | cacheDirty: make(chan interface{}, 1), 37 | cacheClosed: make(chan interface{}), 38 | providers: providers, 39 | stopCh: make(chan interface{}, 10), 40 | defaultProviderID: cloudsackID, 41 | } 42 | go f.maintainCache(cacheFile) 43 | return f 44 | } 45 | 46 | now := time.Now() 47 | mb := &front{Masquerade: Masquerade{Domain: "b", IpAddress: "2"}, LastSucceeded: now, ProviderID: testProviderID} 48 | mc := &front{Masquerade: Masquerade{Domain: "c", IpAddress: "3"}, LastSucceeded: now, ProviderID: ""} // defaulted 49 | md := &front{Masquerade: Masquerade{Domain: "d", IpAddress: "4"}, LastSucceeded: now, ProviderID: "sadcloud"} // skipped 50 | 51 | f := makeFronted() 52 | 53 | log.Debug("Adding fronts") 54 | f.fronts.fronts = append(f.fronts.fronts, mb, mc, md) 55 | 56 | readCached := func() []*front { 57 | log.Debug("Reading cached fronts") 58 | var result []*front 59 | b, err := os.ReadFile(cacheFile) 60 | require.NoError(t, err, "Unable to read cache file") 61 | err = json.Unmarshal(b, &result) 62 | require.NoError(t, err, "Unable to unmarshal cache file") 63 | return result 64 | } 65 | 66 | // Save the cache 67 | f.markCacheDirty() 68 | 69 | time.Sleep(f.cacheSaveInterval * 2) 70 | f.Close() 71 | 72 | time.Sleep(50 * time.Millisecond) 73 | 74 | log.Debug("Reopening fronted") 75 | // Reopen cache file and make sure right data was in there 76 | f = makeFronted() 77 | f.prepopulateFronts(cacheFile) 78 | masquerades := readCached() 79 | require.Len(t, masquerades, 3, "Wrong number of masquerades read") 80 | for i, expected := range []*front{mb, mc, md} { 81 | require.Equal(t, expected.Domain, masquerades[i].Domain, "Wrong masquerade at position %d", i) 82 | require.Equal(t, expected.IpAddress, masquerades[i].IpAddress, "Masquerade at position %d has wrong IpAddress", 0) 83 | require.Equal(t, expected.ProviderID, masquerades[i].ProviderID, "Masquerade at position %d has wrong ProviderID", 0) 84 | require.Equal(t, now.Unix(), masquerades[i].LastSucceeded.Unix(), "Masquerade at position %d has wrong LastSucceeded", 0) 85 | } 86 | f.Close() 87 | } 88 | -------------------------------------------------------------------------------- /connected_roundtripper.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | 12 | "github.com/getlantern/ops" 13 | ) 14 | 15 | type connectedRoundTripper struct { 16 | front Front 17 | net.Conn 18 | provider *Provider 19 | } 20 | 21 | func newConnectedRoundTripper(fr Front, conn net.Conn, provider *Provider) connectedRoundTripper { 22 | return connectedRoundTripper{ 23 | front: fr, 24 | Conn: conn, 25 | provider: provider, 26 | } 27 | } 28 | 29 | // Also implements http.RoundTripper 30 | func (crt connectedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 31 | op := ops.Begin("fronted_request") 32 | defer op.End() 33 | originHost := req.URL.Hostname() 34 | frontedHost := crt.provider.Lookup(originHost) 35 | if frontedHost == "" { 36 | // this error is not the masquerade's fault in particular 37 | // so it is returned as good. 38 | crt.Conn.Close() 39 | crt.front.markWithResult(true) 40 | err := fmt.Errorf("no domain fronting mapping for '%s'. Please add it to provider_map.yaml or equivalent for %s", 41 | crt.front.getProviderID(), originHost) 42 | op.FailIf(err) 43 | return nil, err 44 | } 45 | log.Debugf("Translated origin %s -> %s for provider %s...", originHost, frontedHost, crt.front.getProviderID()) 46 | 47 | reqi, err := withDomainFront(req, frontedHost, req.Body) 48 | if err != nil { 49 | return nil, op.FailIf(log.Errorf("Failed to copy http request with origin translated to %v?: %v", frontedHost, err)) 50 | } 51 | disableKeepAlives := true 52 | if strings.EqualFold(reqi.Header.Get("Connection"), "upgrade") { 53 | disableKeepAlives = false 54 | } 55 | 56 | tr := connectedConnHTTPTransport(crt.Conn, disableKeepAlives) 57 | resp, err := tr.RoundTrip(reqi) 58 | if err != nil { 59 | log.Debugf("Could not complete request: %v", err) 60 | crt.front.markWithResult(false) 61 | return nil, err 62 | } 63 | 64 | err = crt.provider.ValidateResponse(resp) 65 | if err != nil { 66 | log.Debugf("Response did not validate for origin %s -> %s for provider %s with error: %v", originHost, frontedHost, crt.front.getProviderID(), err) 67 | resp.Body.Close() 68 | crt.front.markWithResult(false) 69 | return nil, err 70 | } 71 | 72 | crt.front.markWithResult(true) 73 | log.Debug("Request completed successfully") 74 | return resp, nil 75 | } 76 | 77 | // connectedConnHTTPTransport uses a preconnected connection to the CDN to make HTTP requests. 78 | // This uses the pre-established connection to the CDN on the fronting domain. 79 | func connectedConnHTTPTransport(conn net.Conn, disableKeepAlives bool) http.RoundTripper { 80 | return &connectedTransport{ 81 | Transport: http.Transport{ 82 | Dial: func(network, addr string) (net.Conn, error) { 83 | return conn, nil 84 | }, 85 | TLSHandshakeTimeout: 20 * time.Second, 86 | DisableKeepAlives: disableKeepAlives, 87 | IdleConnTimeout: 70 * time.Second, 88 | }, 89 | } 90 | } 91 | 92 | // connectedTransport is a wrapper struct enabling us to modify the protocol of outgoing 93 | // requests to make them all HTTP instead of potentially HTTPS, which breaks our particular 94 | // implemenation of direct domain fronting. 95 | type connectedTransport struct { 96 | http.Transport 97 | } 98 | 99 | func (ct *connectedTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 100 | defer func(op ops.Op) { op.End() }(ops.Begin("direct_transport_roundtrip")) 101 | 102 | // The connection is already encrypted by domain fronting. We need to rewrite URLs starting 103 | // with "https://" to "http://", lest we get an error for doubling up on TLS. 104 | 105 | // The RoundTrip interface requires that we not modify the memory in the request, so we just 106 | // create a copy. 107 | norm := new(http.Request) 108 | *norm = *req // includes shallow copies of maps, but okay 109 | norm.URL = new(url.URL) 110 | *norm.URL = *req.URL 111 | norm.URL.Scheme = "http" 112 | return ct.Transport.RoundTrip(norm) 113 | } 114 | 115 | func withDomainFront(req *http.Request, frontedHost string, body io.ReadCloser) (*http.Request, error) { 116 | urlCopy := *req.URL 117 | urlCopy.Host = frontedHost 118 | r, err := http.NewRequestWithContext(req.Context(), req.Method, urlCopy.String(), body) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | for k, vs := range req.Header { 124 | if !strings.EqualFold(k, "Host") { 125 | v := make([]string, len(vs)) 126 | copy(v, vs) 127 | r.Header[k] = v 128 | } 129 | } 130 | return r, nil 131 | } 132 | -------------------------------------------------------------------------------- /download_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl -O https://raw.githubusercontent.com/getlantern/lantern-binaries/refs/heads/main/fronted.yaml.gz 3 | -------------------------------------------------------------------------------- /front.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "crypto/sha256" 5 | "crypto/x509" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "sort" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/getlantern/netx" 18 | "github.com/getlantern/ops" 19 | "github.com/getlantern/tlsdialer/v3" 20 | tls "github.com/refraction-networking/utls" 21 | ) 22 | 23 | const ( 24 | NumWorkers = 10 // number of worker goroutines for verifying 25 | ) 26 | 27 | var ( 28 | defaultValidator = NewStatusCodeValidator([]int{403}) 29 | ) 30 | 31 | // CA represents a certificate authority 32 | type CA struct { 33 | CommonName string 34 | Cert string // PEM-encoded 35 | } 36 | 37 | // Masquerade contains the data for a single masquerade host, including 38 | // the domain and the root CA. 39 | type Masquerade struct { 40 | // Domain: the domain to use for domain fronting 41 | Domain string 42 | 43 | // IpAddress: pre-resolved ip address to use instead of Domain (if 44 | // available) 45 | IpAddress string 46 | 47 | // SNI: the SNI to use for this masquerade 48 | SNI string 49 | 50 | // VerifyHostname is used for checking if the certificate for a given hostname is valid. 51 | // This is used for verifying if the peer certificate for the hostnames that are being fronted are valid. 52 | VerifyHostname *string 53 | } 54 | 55 | // Create a masquerade interface for easier testing. 56 | type Front interface { 57 | dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) 58 | 59 | // Accessor for the domain of the masquerade 60 | getDomain() string 61 | 62 | // Accessor for the IP address of the masquerade 63 | getIpAddress() string 64 | 65 | markSucceeded() 66 | 67 | markFailed() 68 | 69 | lastSucceeded() time.Time 70 | 71 | setLastSucceeded(time.Time) 72 | 73 | verifyWithPost(net.Conn, string) bool 74 | 75 | getProviderID() string 76 | 77 | isSucceeding() bool 78 | 79 | markWithResult(bool) bool 80 | 81 | markCacheDirty() 82 | } 83 | 84 | type front struct { 85 | Masquerade 86 | // lastSucceeded: the most recent time at which this Masquerade succeeded 87 | LastSucceeded time.Time 88 | // id of DirectProvider that this masquerade is provided by 89 | ProviderID string 90 | mx sync.RWMutex 91 | cacheDirty chan interface{} 92 | } 93 | 94 | func newFront(m *Masquerade, providerID string, cacheDirty chan interface{}) Front { 95 | return &front{ 96 | Masquerade: *m, 97 | ProviderID: providerID, 98 | LastSucceeded: time.Time{}, 99 | cacheDirty: cacheDirty, 100 | } 101 | } 102 | func (fr *front) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) { 103 | tlsConfig := &tls.Config{ 104 | ServerName: fr.Domain, 105 | RootCAs: rootCAs, 106 | } 107 | dialTimeout := 5 * time.Second 108 | addr := fr.IpAddress 109 | var sendServerNameExtension bool 110 | if fr.SNI != "" { 111 | sendServerNameExtension = true 112 | tlsConfig.ServerName = fr.SNI 113 | tlsConfig.InsecureSkipVerify = true 114 | tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error { 115 | var verifyHostname string 116 | if fr.VerifyHostname != nil { 117 | verifyHostname = *fr.VerifyHostname 118 | } 119 | return verifyPeerCertificate(rawCerts, rootCAs, verifyHostname) 120 | } 121 | } 122 | dialer := &tlsdialer.Dialer{ 123 | DoDial: netx.DialTimeout, 124 | Timeout: dialTimeout, 125 | SendServerName: sendServerNameExtension, 126 | Config: tlsConfig, 127 | ClientHelloID: clientHelloID, 128 | } 129 | _, _, err := net.SplitHostPort(addr) 130 | if err != nil { 131 | // If there is no port, we default to 443 132 | addr = net.JoinHostPort(addr, "443") 133 | } 134 | return dialer.Dial("tcp", addr) 135 | } 136 | 137 | // verifyWithPost does a post with invalid data to verify domain-fronting works 138 | func (fr *front) verifyWithPost(conn net.Conn, testURL string) bool { 139 | client := &http.Client{ 140 | Transport: connectedConnHTTPTransport(conn, true), 141 | } 142 | return doCheck(client, http.MethodPost, http.StatusAccepted, testURL) 143 | } 144 | 145 | func doCheck(client *http.Client, method string, expectedStatus int, u string) bool { 146 | op := ops.Begin("check_masquerade") 147 | defer op.End() 148 | 149 | isPost := method == http.MethodPost 150 | var requestBody io.Reader 151 | if isPost { 152 | requestBody = strings.NewReader("a") 153 | } 154 | req, _ := http.NewRequest(method, u, requestBody) 155 | if isPost { 156 | req.Header.Set("Content-Type", "application/json") 157 | } 158 | resp, err := client.Do(req) 159 | if err != nil { 160 | op.FailIf(err) 161 | log.Debugf("Unsuccessful vetting with %v request, discarding masquerade: %v", method, err) 162 | return false 163 | } 164 | if resp.Body != nil { 165 | io.Copy(io.Discard, resp.Body) 166 | resp.Body.Close() 167 | } 168 | if resp.StatusCode != expectedStatus { 169 | op.Set("response_status", resp.StatusCode) 170 | op.Set("expected_status", expectedStatus) 171 | msg := fmt.Sprintf("Unexpected response status vetting masquerade, expected %d got %d: %v", expectedStatus, resp.StatusCode, resp.Status) 172 | op.FailIf(errors.New(msg)) 173 | log.Debug(msg) 174 | return false 175 | } 176 | return true 177 | } 178 | 179 | // getDomain implements MasqueradeInterface. 180 | func (fr *front) getDomain() string { 181 | return fr.Domain 182 | } 183 | 184 | // getIpAddress implements MasqueradeInterface. 185 | func (fr *front) getIpAddress() string { 186 | return fr.IpAddress 187 | } 188 | 189 | // getProviderID implements MasqueradeInterface. 190 | func (fr *front) getProviderID() string { 191 | return fr.ProviderID 192 | } 193 | 194 | // MarshalJSON marshals masquerade into json 195 | func (fr *front) MarshalJSON() ([]byte, error) { 196 | fr.mx.RLock() 197 | defer fr.mx.RUnlock() 198 | // Type alias for masquerade so that we don't infinitely recurse when marshaling the struct 199 | type alias front 200 | return json.Marshal((*alias)(fr)) 201 | } 202 | 203 | func (fr *front) lastSucceeded() time.Time { 204 | fr.mx.RLock() 205 | defer fr.mx.RUnlock() 206 | return fr.LastSucceeded 207 | } 208 | 209 | func (fr *front) setLastSucceeded(t time.Time) { 210 | fr.mx.Lock() 211 | defer fr.mx.Unlock() 212 | fr.LastSucceeded = t 213 | } 214 | 215 | func (fr *front) markSucceeded() { 216 | fr.mx.Lock() 217 | defer fr.mx.Unlock() 218 | fr.LastSucceeded = time.Now() 219 | } 220 | 221 | func (fr *front) markFailed() { 222 | fr.mx.Lock() 223 | defer fr.mx.Unlock() 224 | fr.LastSucceeded = time.Time{} 225 | } 226 | 227 | func (fr *front) isSucceeding() bool { 228 | fr.mx.RLock() 229 | defer fr.mx.RUnlock() 230 | return fr.LastSucceeded.After(time.Time{}) 231 | } 232 | 233 | // Make sure that the masquerade struct implements the MasqueradeInterface 234 | var _ Front = (*front)(nil) 235 | 236 | // A Direct fronting provider configuration. 237 | type Provider struct { 238 | // Specific hostname mappings used for this provider. 239 | // remaps certain requests to provider specific host names. 240 | HostAliases map[string]string 241 | 242 | // Allow unaliased pass-through of hostnames 243 | // matching these patterns. 244 | // eg "*.cloudfront.net" for cloudfront provider 245 | // would permit all .cloudfront.net domains to 246 | // pass through without alias. Only suffix 247 | // patterns and exact matches are supported. 248 | PassthroughPatterns []string 249 | 250 | // Url used to vet masquerades for this provider 251 | TestURL string 252 | Masquerades []*Masquerade 253 | 254 | // VerifyHostname is used for checking if the certificate for a given hostname is valid. 255 | // This attribute is only being defined here so it can be sent to the masquerade struct later. 256 | VerifyHostname *string 257 | 258 | // FrontingSNIs is a map of country code the the SNI config to use for that country. 259 | FrontingSNIs map[string]*SNIConfig 260 | } 261 | 262 | type SNIConfig struct { 263 | UseArbitrarySNIs bool 264 | ArbitrarySNIs []string 265 | } 266 | 267 | // Create a Provider with the given details 268 | func NewProvider(hosts map[string]string, testURL string, masquerades []*Masquerade, passthrough []string, frontingSNIs map[string]*SNIConfig, verifyHostname *string, countryCode string) *Provider { 269 | p := &Provider{ 270 | HostAliases: make(map[string]string), 271 | TestURL: testURL, 272 | Masquerades: make([]*Masquerade, 0, len(masquerades)), 273 | PassthroughPatterns: make([]string, 0, len(passthrough)), 274 | VerifyHostname: verifyHostname, 275 | FrontingSNIs: frontingSNIs, 276 | } 277 | for k, v := range hosts { 278 | p.HostAliases[strings.ToLower(k)] = v 279 | } 280 | 281 | var config *SNIConfig 282 | if countryCode != "" { 283 | var ok bool 284 | config, ok = frontingSNIs[countryCode] 285 | if !ok { 286 | config = frontingSNIs["default"] 287 | } 288 | } 289 | for _, m := range masquerades { 290 | sni := generateSNI(config, m) 291 | p.Masquerades = append(p.Masquerades, &Masquerade{Domain: m.Domain, IpAddress: m.IpAddress, SNI: sni, VerifyHostname: verifyHostname}) 292 | } 293 | p.PassthroughPatterns = append(p.PassthroughPatterns, passthrough...) 294 | return p 295 | } 296 | 297 | // generateSNI generates a SNI for the given domain and ip address 298 | func generateSNI(config *SNIConfig, m *Masquerade) string { 299 | if config != nil && m != nil && config.UseArbitrarySNIs && len(config.ArbitrarySNIs) > 0 { 300 | // Ensure that we use a consistent SNI for a given combination of IP address and SNI set 301 | hash := sha256.New() 302 | hash.Write([]byte(m.IpAddress)) 303 | checksum := int(hash.Sum(nil)[0]) 304 | // making sure checksum is positive 305 | if checksum < 0 { 306 | checksum = -checksum 307 | } 308 | return config.ArbitrarySNIs[checksum%len(config.ArbitrarySNIs)] 309 | } 310 | return "" 311 | } 312 | 313 | // Lookup the host alias for the given hostname for this provider 314 | func (p *Provider) Lookup(hostname string) string { 315 | // only consider the host porition if given a port as well. 316 | if h, _, err := net.SplitHostPort(hostname); err == nil { 317 | hostname = h 318 | } 319 | hostname = strings.ToLower(hostname) 320 | if alias := p.HostAliases[hostname]; alias != "" { 321 | return alias 322 | } 323 | 324 | for _, pt := range p.PassthroughPatterns { 325 | pt = strings.ToLower(pt) 326 | if strings.HasPrefix(pt, "*.") && strings.HasSuffix(hostname, pt[1:]) { 327 | return hostname 328 | } else if pt == hostname { 329 | return hostname 330 | } 331 | } 332 | 333 | return "" 334 | } 335 | 336 | // Validate a fronted response. Returns an error if the 337 | // response failed to reach the origin, eg if the request 338 | // was rejected by the provider. 339 | func (p *Provider) ValidateResponse(res *http.Response) error { 340 | return defaultValidator(res) 341 | } 342 | 343 | // A validator for fronted responses. Returns an error if the 344 | // response failed to reach the origin, eg if the request 345 | // was rejected by the provider. 346 | type ResponseValidator func(*http.Response) error 347 | 348 | // Create a new ResponseValidator that rejects any response with 349 | // a given list of http status codes. 350 | func NewStatusCodeValidator(reject []int) ResponseValidator { 351 | bad := make(map[int]bool) 352 | for _, code := range reject { 353 | bad[code] = true 354 | } 355 | return func(res *http.Response) error { 356 | if bad[res.StatusCode] { 357 | return fmt.Errorf("response status %d: %v", res.StatusCode, res.Status) 358 | } 359 | return nil 360 | } 361 | } 362 | 363 | type threadSafeFronts struct { 364 | fronts sortedFronts 365 | mx sync.RWMutex 366 | } 367 | 368 | func newThreadSafeFronts(size int) *threadSafeFronts { 369 | return &threadSafeFronts{ 370 | fronts: make(sortedFronts, 0, size), 371 | mx: sync.RWMutex{}, 372 | } 373 | } 374 | 375 | func (tsf *threadSafeFronts) sortedCopy() sortedFronts { 376 | tsf.mx.RLock() 377 | defer tsf.mx.RUnlock() 378 | c := make(sortedFronts, len(tsf.fronts)) 379 | copy(c, tsf.fronts) 380 | sort.Sort(c) 381 | return c 382 | } 383 | 384 | func (tsf *threadSafeFronts) addFronts(newFronts ...Front) { 385 | tsf.mx.Lock() 386 | defer tsf.mx.Unlock() 387 | tsf.fronts = append(tsf.fronts, newFronts...) 388 | } 389 | 390 | func (tsf *threadSafeFronts) frontSize() int { 391 | tsf.mx.RLock() 392 | defer tsf.mx.RUnlock() 393 | return len(tsf.fronts) 394 | } 395 | 396 | func (tsf *threadSafeFronts) frontAt(i int) Front { 397 | tsf.mx.RLock() 398 | defer tsf.mx.RUnlock() 399 | return tsf.fronts[i] 400 | } 401 | 402 | // slice of masquerade sorted by last vetted time 403 | type sortedFronts []Front 404 | 405 | func (m sortedFronts) Len() int { return len(m) } 406 | func (m sortedFronts) Swap(i, j int) { m[i], m[j] = m[j], m[i] } 407 | func (m sortedFronts) Less(i, j int) bool { 408 | if m[i].lastSucceeded().After(m[j].lastSucceeded()) { 409 | return true 410 | } else if m[j].lastSucceeded().After(m[i].lastSucceeded()) { 411 | return false 412 | } else { 413 | return m[i].getIpAddress() < m[j].getIpAddress() 414 | } 415 | } 416 | 417 | func (fr *front) markCacheDirty() { 418 | select { 419 | case fr.cacheDirty <- nil: 420 | // okay 421 | default: 422 | // already dirty 423 | } 424 | } 425 | 426 | func (fr *front) markWithResult(good bool) bool { 427 | if good { 428 | fr.markSucceeded() 429 | } else { 430 | fr.markFailed() 431 | } 432 | fr.markCacheDirty() 433 | return good 434 | } 435 | -------------------------------------------------------------------------------- /front_test.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewProvider(t *testing.T) { 10 | verifyHostname := "verifyHostname.com" 11 | var tests = []struct { 12 | name string 13 | givenHosts map[string]string 14 | givenTestURL string 15 | givenMasquerades []*Masquerade 16 | givenPassthrough []string 17 | //givenSNIConfig *SNIConfig 18 | givenFrontingSNIs map[string]*SNIConfig 19 | givenVerifyHostname *string 20 | assert func(t *testing.T, actual *Provider) 21 | }{ 22 | { 23 | name: "should return a new provider without host aliases, masquerades and passthrough", 24 | givenHosts: map[string]string{}, 25 | givenTestURL: "http://test.com", 26 | assert: func(t *testing.T, actual *Provider) { 27 | assert.Empty(t, actual.HostAliases) 28 | assert.Empty(t, actual.Masquerades) 29 | assert.Empty(t, actual.PassthroughPatterns) 30 | assert.Equal(t, "http://test.com", actual.TestURL) 31 | //assert.Nil(t, actual.Validator) 32 | assert.Nil(t, actual.FrontingSNIs) 33 | }, 34 | }, 35 | { 36 | name: "should return a new provider with host aliases, masquerades and passthrough", 37 | givenHosts: map[string]string{"host1": "alias1", "host2": "alias2"}, 38 | givenTestURL: "http://test.com", 39 | givenMasquerades: []*Masquerade{{Domain: "domain1", IpAddress: "127.0.0.1"}}, 40 | givenPassthrough: []string{"passthrough1", "passthrough2"}, 41 | givenFrontingSNIs: map[string]*SNIConfig{ 42 | "test": &SNIConfig{ 43 | UseArbitrarySNIs: true, 44 | ArbitrarySNIs: []string{"sni1.com", "sni2.com"}, 45 | }, 46 | }, 47 | givenVerifyHostname: &verifyHostname, 48 | assert: func(t *testing.T, actual *Provider) { 49 | assert.Equal(t, "http://test.com", actual.TestURL) 50 | assert.Equal(t, "alias1", actual.HostAliases["host1"]) 51 | assert.Equal(t, "alias2", actual.HostAliases["host2"]) 52 | assert.Equal(t, 1, len(actual.Masquerades)) 53 | assert.Equal(t, "domain1", actual.Masquerades[0].Domain) 54 | assert.Equal(t, "127.0.0.1", actual.Masquerades[0].IpAddress) 55 | assert.Equal(t, "sni1.com", actual.Masquerades[0].SNI) 56 | assert.Equal(t, verifyHostname, *actual.Masquerades[0].VerifyHostname) 57 | assert.Equal(t, 2, len(actual.PassthroughPatterns)) 58 | }, 59 | }, 60 | } 61 | for _, tt := range tests { 62 | tt := tt 63 | t.Run(tt.name, func(t *testing.T) { 64 | actual := NewProvider(tt.givenHosts, tt.givenTestURL, tt.givenMasquerades, tt.givenPassthrough, tt.givenFrontingSNIs, tt.givenVerifyHostname, "test") 65 | tt.assert(t, actual) 66 | }) 67 | } 68 | } 69 | 70 | func TestGenerateSNI(t *testing.T) { 71 | emptyMasquerade := new(Masquerade) 72 | var tests = []struct { 73 | name string 74 | assert func(t *testing.T, actual string) 75 | givenConfig *SNIConfig 76 | givenMasquerade *Masquerade 77 | }{ 78 | { 79 | name: "should return a empty string when given SNI config is nil", 80 | givenConfig: nil, 81 | givenMasquerade: emptyMasquerade, 82 | assert: func(t *testing.T, actual string) { 83 | assert.Empty(t, actual) 84 | }, 85 | }, 86 | { 87 | name: "should return a empty string when given SNI config is not nil and UseArbitrarySNIs is false", 88 | givenConfig: &SNIConfig{ 89 | UseArbitrarySNIs: false, 90 | }, 91 | givenMasquerade: emptyMasquerade, 92 | assert: func(t *testing.T, actual string) { 93 | assert.Empty(t, actual) 94 | }, 95 | }, 96 | { 97 | name: "should return a empty SNI when the list of arbitrary SNIs is empty", 98 | givenConfig: &SNIConfig{ 99 | UseArbitrarySNIs: true, 100 | ArbitrarySNIs: []string{}, 101 | }, 102 | givenMasquerade: &Masquerade{ 103 | IpAddress: "1.1.1.1", 104 | Domain: "randomdomain.net", 105 | }, 106 | assert: func(t *testing.T, actual string) { 107 | assert.Empty(t, actual) 108 | }, 109 | }, 110 | { 111 | name: "should return a SNI when given SNI config is not nil and UseArbitrarySNIs is true", 112 | givenConfig: &SNIConfig{ 113 | UseArbitrarySNIs: true, 114 | ArbitrarySNIs: []string{"sni1.com", "sni2.com"}, 115 | }, 116 | givenMasquerade: &Masquerade{ 117 | IpAddress: "1.1.1.1", 118 | Domain: "randomdomain.net", 119 | }, 120 | assert: func(t *testing.T, actual string) { 121 | assert.NotEmpty(t, actual) 122 | }, 123 | }, 124 | } 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | actual := generateSNI(tt.givenConfig, tt.givenMasquerade) 128 | tt.assert(t, actual) 129 | }) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /fronted.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "crypto/x509" 8 | "embed" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "math/rand/v2" 13 | "net" 14 | "net/http" 15 | "os" 16 | "path/filepath" 17 | "runtime/debug" 18 | "strings" 19 | "sync" 20 | "sync/atomic" 21 | "syscall" 22 | "time" 23 | 24 | "github.com/goccy/go-yaml" 25 | tls "github.com/refraction-networking/utls" 26 | 27 | "github.com/getlantern/golog" 28 | "github.com/getlantern/keepcurrent" 29 | "github.com/getlantern/ops" 30 | 31 | "github.com/alitto/pond/v2" 32 | ) 33 | 34 | const ( 35 | defaultMaxAllowedCachedAge = 24 * time.Hour 36 | defaultMaxCacheSize = 1000 37 | defaultCacheSaveInterval = 5 * time.Second 38 | maxTries = 6 39 | ) 40 | 41 | var ( 42 | log = golog.LoggerFor("fronted") 43 | defaultFrontedProviderID = "cloudfront" 44 | ) 45 | 46 | // fronted identifies working IP address/domain pairings for domain fronting and is 47 | // an implementation of http.RoundTripper for the convenience of callers. 48 | type fronted struct { 49 | certPool atomic.Value 50 | fronts *threadSafeFronts 51 | maxAllowedCachedAge time.Duration 52 | maxCacheSize int 53 | cacheFile string 54 | cacheSaveInterval time.Duration 55 | cacheDirty chan interface{} 56 | cacheClosed chan interface{} 57 | closeCacheOnce sync.Once 58 | defaultProviderID string 59 | providers map[string]*Provider 60 | clientHelloID tls.ClientHelloID 61 | 62 | providersMu sync.RWMutex 63 | frontsMu sync.RWMutex 64 | stopCh chan interface{} 65 | crawlOnce sync.Once 66 | stopped atomic.Bool 67 | countryCode string 68 | httpClient *http.Client 69 | configURL string 70 | frontsCh chan Front 71 | panicListener func(string) 72 | } 73 | 74 | // Interface for sending HTTP traffic over domain fronting. 75 | type Fronted interface { 76 | NewConnectedRoundTripper(ctx context.Context, addr string) (http.RoundTripper, error) 77 | 78 | // onNewFrontsConfig updates the set of domain fronts to try from a YAML configuration. 79 | onNewFrontsConfig(yml []byte) 80 | 81 | // onNewFronts updates the set of domain fronts to try. 82 | onNewFronts(pool *x509.CertPool, providers map[string]*Provider) 83 | 84 | // Close closes any resources, such as goroutines that are testing fronts. 85 | Close() 86 | } 87 | 88 | //go:generate ./download_config.sh 89 | //go:embed fronted.yaml.gz 90 | var embedFS embed.FS 91 | 92 | // Option is a functional option type that allows us to configure the fronted client. 93 | type Option func(*fronted) 94 | 95 | // NewFronted creates a new Fronted instance with the given cache file. 96 | // At this point it does not have the actual IPs, domains, etc of the fronts to try. 97 | // defaultProviderID is used when a front without a provider is encountered (eg in a cache file) 98 | func NewFronted(options ...Option) Fronted { 99 | log.Debug("Creating new fronted") 100 | 101 | f := &fronted{ 102 | certPool: atomic.Value{}, 103 | fronts: newThreadSafeFronts(0), 104 | maxAllowedCachedAge: defaultMaxAllowedCachedAge, 105 | maxCacheSize: defaultMaxCacheSize, 106 | cacheSaveInterval: defaultCacheSaveInterval, 107 | cacheDirty: make(chan any, 1), 108 | cacheClosed: make(chan any), 109 | providers: make(map[string]*Provider), 110 | // We can and should update this as new ClientHellos become available in utls. 111 | clientHelloID: tls.HelloChrome_131, 112 | stopCh: make(chan any, 10), 113 | defaultProviderID: defaultFrontedProviderID, 114 | httpClient: http.DefaultClient, 115 | configURL: "", 116 | frontsCh: make(chan Front, 4000), 117 | panicListener: func(msg string) { log.Errorf("Panic in fronted: %v", msg) }, 118 | } 119 | 120 | for _, opt := range options { 121 | opt(f) 122 | } 123 | if f.cacheFile == "" { 124 | f.cacheFile = defaultCacheFilePath() 125 | } 126 | 127 | f.initCaching(f.cacheFile) 128 | f.readFrontsFromEmbeddedConfig() 129 | f.keepCurrent() 130 | 131 | return f 132 | } 133 | 134 | // WithHTTPClient sets the HTTP client to use for fetching the fronted configuration. For example, the client 135 | // could be censorship-resistant in some way. 136 | func WithHTTPClient(httpClient *http.Client) Option { 137 | return func(f *fronted) { 138 | f.httpClient = httpClient 139 | } 140 | } 141 | 142 | // WithCacheFile sets the file to use for caching domains that have successfully connected. 143 | func WithCacheFile(file string) Option { 144 | return func(f *fronted) { 145 | f.cacheFile = file 146 | } 147 | } 148 | 149 | // WithCountryCode sets the country code to use for fronting, which is particularly relevant for the 150 | // SNI to use when connecting to the fronting domain. 151 | func WithCountryCode(cc string) Option { 152 | return func(f *fronted) { 153 | f.countryCode = cc 154 | } 155 | } 156 | 157 | // WithConfigURL sets the URL from which to continually fetch updated domain fronting configurations. 158 | func WithConfigURL(configURL string) Option { 159 | return func(f *fronted) { 160 | f.configURL = configURL 161 | } 162 | } 163 | 164 | // WithPanicListener sets a listener for panics that occur in the fronted. 165 | func WithPanicListener(panicListener func(string)) Option { 166 | return func(f *fronted) { 167 | f.panicListener = panicListener 168 | } 169 | } 170 | 171 | func defaultCacheFilePath() string { 172 | if dir, err := os.UserConfigDir(); err != nil { 173 | log.Errorf("Unable to get user config dir: %v", err) 174 | // Use the temporary directory. 175 | return mkdirall(os.TempDir(), "domainfronting", "fronted_cache.json") 176 | } else { 177 | return mkdirall(dir, "domainfronting", "fronted_cache.json") 178 | } 179 | } 180 | 181 | func mkdirall(base, path, fileName string) string { 182 | path = filepath.Join(base, path) 183 | if err := os.MkdirAll(path, 0o700); err != nil { 184 | log.Errorf("Unable to create directory %v: %v", path, err) 185 | } 186 | return filepath.Join(path, fileName) 187 | } 188 | 189 | // keepCurrent fetches the fronted configuration from the given URL and keeps it up 190 | // to date by fetching it periodically. 191 | func (f *fronted) keepCurrent() { 192 | if f.configURL == "" { 193 | log.Debug("No config URL provided -- not updating fronting configuration") 194 | return 195 | } 196 | 197 | log.Debugf("Updating fronted configuration from URL %v", f.configURL) 198 | source := keepcurrent.FromWebWithClient(f.configURL, f.httpClient) 199 | chDB := make(chan []byte) 200 | dest := keepcurrent.ToChannel(chDB) 201 | 202 | runner := keepcurrent.NewWithValidator( 203 | f.validator(), 204 | source, 205 | dest, 206 | ) 207 | 208 | go func() { 209 | // Recover from panics and log them 210 | defer func() { 211 | if r := recover(); r != nil { 212 | f.panicListener(fmt.Sprintf("Panic waiting for fronts %v with stack: %v", r, debug.Stack())) 213 | } 214 | }() 215 | for data := range chDB { 216 | log.Debug("Received new fronted configuration") 217 | f.onNewFrontsConfig(data) 218 | } 219 | }() 220 | 221 | runner.Start(12 * time.Hour) 222 | } 223 | 224 | func (f *fronted) validator() func([]byte) error { 225 | return func(data []byte) error { 226 | _, _, err := processYaml(data) 227 | if err != nil { 228 | return err 229 | } 230 | return nil 231 | } 232 | } 233 | 234 | func (f *fronted) readFrontsFromEmbeddedConfig() { 235 | yml, err := embedFS.ReadFile("fronted.yaml.gz") 236 | if err != nil { 237 | log.Debugf("Failed to read smart dialer config %v", err) 238 | return 239 | } 240 | f.onNewFrontsConfig(yml) 241 | } 242 | 243 | func (f *fronted) onNewFrontsConfig(gzippedYaml []byte) { 244 | pool, providers, err := processYaml(gzippedYaml) 245 | if err != nil { 246 | log.Errorf("Failed to process fronted config: %v", err) 247 | return 248 | } 249 | f.onNewFronts(pool, providers) 250 | } 251 | 252 | // onNewFronts sets the domain fronts to use, the trusted root CAs and the fronting providers 253 | // (such as Akamai, Cloudfront, etc) 254 | func (f *fronted) onNewFronts(pool *x509.CertPool, providers map[string]*Provider) { 255 | // Make copies just to avoid any concurrency issues with access that may be happening on the 256 | // caller side. 257 | log.Debug("Updating fronted configuration") 258 | if len(providers) == 0 { 259 | log.Errorf("No providers configured") 260 | return 261 | } 262 | providersCopy := copyProviders(providers, f.countryCode) 263 | f.addProviders(providersCopy) 264 | f.fronts.addFronts(loadFronts(providersCopy, f.cacheDirty)...) 265 | f.certPool.Store(pool) 266 | 267 | // The goroutine for finding working fronts runs forever, so only start it once. 268 | f.crawlOnce.Do(func() { 269 | go func() { 270 | defer func() { 271 | if r := recover(); r != nil { 272 | f.panicListener(fmt.Sprintf("Panic finding working fronts %v with stack: %v", r, debug.Stack())) 273 | } 274 | }() 275 | f.findWorkingFronts() 276 | }() 277 | }) 278 | } 279 | 280 | // findWorkingFronts finds working domain fronts by testing them using a worker pool. Speed 281 | // is of the essence here, as without working fronts, users will 282 | // be unable to fetch proxy configurations, particularly in the case of a first time 283 | // user who does not have proxies cached on disk. 284 | func (f *fronted) findWorkingFronts() { 285 | // Keep looping through all fronts making sure we have working ones. 286 | for { 287 | // Continually loop through the fronts until we have 4 working ones. 288 | // This is important, for example, when the user goes offline and all fronts start failing. 289 | // We want to just keep trying in that case so that we find working fronts as soon as they 290 | // come back online. 291 | if !f.hasEnoughWorkingFronts() { 292 | // Note that trying all fronts takes awhile, as it only completes when we either 293 | // have enough working fronts, or we've tried all of them. 294 | log.Debug("findWorkingFronts::Trying all fronts") 295 | f.tryAllFronts() 296 | log.Debug("findWorkingFronts::Tried all fronts") 297 | 298 | // Sleep to avoid spinning infinitely in the case where we don't even know of fronts 299 | // to try, for example. 300 | time.Sleep(1 * time.Second) 301 | } else { 302 | log.Trace("findWorkingFronts::Enough working fronts...sleeping") 303 | select { 304 | case <-f.stopCh: 305 | log.Debug("findWorkingFronts::Stopping parallel dialing") 306 | return 307 | case <-time.After(time.Duration(randRange(6, 12)) * time.Second): 308 | // Run again after a random time between 0 and 12 seconds 309 | } 310 | } 311 | } 312 | } 313 | 314 | // onConnected adds a working front to the channel of working fronts. 315 | func (f *fronted) onConnected(fr Front) { 316 | f.frontsCh <- fr 317 | } 318 | 319 | func (f *fronted) tryAllFronts() { 320 | // Find working fronts using a worker pool of goroutines. 321 | pool := pond.NewPool(40) 322 | 323 | // Submit all fronts to the worker pool. 324 | for i := 0; i < f.fronts.frontSize(); i++ { 325 | m := f.fronts.frontAt(i) 326 | pool.Submit(func() { 327 | if f.isStopped() { 328 | return 329 | } 330 | if f.hasEnoughWorkingFronts() { 331 | // We have enough working fronts, so no need to continue. 332 | // log.Debug("Enough working fronts...ignoring task") 333 | return 334 | } 335 | working := f.vetFront(m) 336 | if working { 337 | f.onConnected(m) 338 | } else { 339 | m.markFailed() 340 | } 341 | }) 342 | } 343 | 344 | // Stop the pool and wait for all submitted tasks to complete 345 | pool.StopAndWait() 346 | } 347 | 348 | func (f *fronted) hasEnoughWorkingFronts() bool { 349 | return len(f.frontsCh) >= 4 350 | } 351 | 352 | func (f *fronted) vetFront(fr Front) bool { 353 | conn, err := f.dialFront(fr) 354 | if err != nil { 355 | log.Debugf("unexpected error vetting masquerades: %v", err) 356 | return false 357 | } 358 | defer func() { 359 | if conn != nil { 360 | conn.Close() 361 | } 362 | }() 363 | 364 | provider := f.providerFor(fr) 365 | if provider == nil { 366 | log.Debugf("Skipping masquerade with disabled/unknown provider id '%s' not in %v", 367 | fr.getProviderID(), f.providers) 368 | return false 369 | } 370 | if !fr.markWithResult(fr.verifyWithPost(conn, provider.TestURL)) { 371 | log.Debugf("Unsuccessful vetting with POST request, discarding masquerade") 372 | return false 373 | } 374 | 375 | return true 376 | } 377 | 378 | func (f *fronted) NewConnectedRoundTripper(ctx context.Context, addr string) (http.RoundTripper, error) { 379 | for range 6 { 380 | select { 381 | case <-ctx.Done(): 382 | return nil, ctx.Err() 383 | // Add a case for the stop channel being called 384 | case <-f.stopCh: 385 | return nil, errors.New("fronted stopped") 386 | case fr := <-f.frontsCh: 387 | // The front may have stopped succeeding since we last checked, 388 | // so only return it if it's still succeeding. 389 | if !fr.isSucceeding() { 390 | continue 391 | } 392 | provider := f.providerFor(fr) 393 | if provider == nil { 394 | log.Debugf("Skipping masquerade with disabled/unknown provider '%s'", fr.getProviderID()) 395 | fr.markWithResult(false) 396 | continue 397 | } 398 | 399 | conn, err := f.dialFront(fr) 400 | if err != nil { 401 | log.Debugf("Could not dial to %v: %v", fr, err) 402 | fr.markWithResult(false) 403 | continue 404 | } 405 | fr.markWithResult(true) 406 | // Add the front back to the channel. 407 | f.frontsCh <- fr 408 | 409 | return newConnectedRoundTripper(fr, conn, provider), err 410 | } 411 | } 412 | return nil, fmt.Errorf("could not connect to any front") 413 | } 414 | 415 | func (f *fronted) dialFront(fr Front) (net.Conn, error) { 416 | log.Tracef("Dialing to %v", fr) 417 | 418 | // We do the full TLS connection here because in practice the domains at a given IP 419 | // address can change frequently on CDNs, so the certificate may not match what 420 | // we expect. 421 | conn, retriable, err := f.doDial(fr) 422 | if err == nil { 423 | return conn, err 424 | } else if !retriable { 425 | log.Debugf("Dropping masquerade: non retryable error: %v", err) 426 | fr.markWithResult(false) 427 | } 428 | return conn, err 429 | } 430 | 431 | func (f *fronted) doDial(fr Front) (net.Conn, bool, error) { 432 | op := ops.Begin("dial_masquerade") 433 | defer op.End() 434 | op.Set("masquerade_domain", fr.getDomain()) 435 | op.Set("masquerade_ip", fr.getIpAddress()) 436 | op.Set("masquerade_provider", fr.getProviderID()) 437 | 438 | var conn net.Conn 439 | var err error 440 | retriable := false 441 | // A nil cert pool will just use the system's root CAs. 442 | pool, typeCorrect := f.certPool.Load().(*x509.CertPool) 443 | if !typeCorrect || pool == nil { 444 | pool, err = x509.SystemCertPool() 445 | if err != nil { 446 | return nil, retriable, fmt.Errorf("failed to load system cert pool: %w", err) 447 | } 448 | } 449 | conn, err = fr.dial(pool, f.clientHelloID) 450 | if err != nil { 451 | if !isNetworkUnreachable(err) { 452 | op.FailIf(err) 453 | } 454 | log.Debugf("Could not dial to %v, %v", fr.getIpAddress(), err) 455 | // Don't re-add this candidate if it's any certificate error, as that 456 | // will just keep failing and will waste connections. We can't access the underlying 457 | // error at this point so just look for "certificate" and "handshake". 458 | if strings.Contains(err.Error(), "certificate") || strings.Contains(err.Error(), "handshake") { 459 | log.Debugf("Not re-adding candidate that failed on error '%v'", err.Error()) 460 | retriable = false 461 | } else { 462 | log.Debugf("Unexpected error dialing, keeping masquerade: %v", err) 463 | retriable = true 464 | } 465 | } 466 | return conn, retriable, err 467 | } 468 | 469 | func isNetworkUnreachable(err error) bool { 470 | var opErr *net.OpError 471 | if errors.As(err, &opErr) { 472 | // The following error verifications look for errors that generally happen at Linux/Unix devices 473 | if errors.Is(opErr.Err, syscall.ENETUNREACH) || errors.Is(opErr.Err, syscall.EHOSTUNREACH) { 474 | return true 475 | } 476 | 477 | // The string verification errors use a broader approach with errors from windows and also linux/unix devices 478 | errMsg := opErr.Err.Error() 479 | if strings.Contains(errMsg, "network is unreachable") || 480 | strings.Contains(errMsg, "no route to host") || 481 | strings.Contains(errMsg, "unreachable network") || 482 | strings.Contains(errMsg, "unreachable host") { 483 | return true 484 | } 485 | } 486 | return false 487 | } 488 | 489 | func verifyPeerCertificate(rawCerts [][]byte, roots *x509.CertPool, domain string) error { 490 | if len(rawCerts) == 0 { 491 | return fmt.Errorf("no certificates presented") 492 | } 493 | cert, err := x509.ParseCertificate(rawCerts[0]) 494 | if err != nil { 495 | return fmt.Errorf("unable to parse certificate: %w", err) 496 | } 497 | 498 | opts := []x509.VerifyOptions{generateVerifyOptions(roots, domain)} 499 | for i := range rawCerts { 500 | if i == 0 { 501 | continue 502 | } 503 | crt, err := x509.ParseCertificate(rawCerts[i]) 504 | if err != nil { 505 | return fmt.Errorf("unable to parse intermediate certificate: %w", err) 506 | } 507 | 508 | for _, opt := range opts { 509 | opt.Intermediates.AddCert(crt) 510 | } 511 | } 512 | 513 | var verificationErrors error 514 | for _, opt := range opts { 515 | _, err := cert.Verify(opt) 516 | if err != nil { 517 | verificationErrors = errors.Join(verificationErrors, err) 518 | } 519 | } 520 | 521 | if verificationErrors != nil { 522 | return fmt.Errorf("certificate verification failed: %w", verificationErrors) 523 | } 524 | 525 | return nil 526 | } 527 | 528 | func generateVerifyOptions(roots *x509.CertPool, domain string) x509.VerifyOptions { 529 | return x509.VerifyOptions{ 530 | Roots: roots, 531 | CurrentTime: time.Now(), 532 | DNSName: domain, 533 | Intermediates: x509.NewCertPool(), 534 | } 535 | } 536 | 537 | func randRange(min, max int) int { 538 | return rand.IntN(max-min) + min 539 | } 540 | 541 | func (f *fronted) Close() { 542 | f.stopped.Store(true) 543 | f.closeCacheOnce.Do(func() { 544 | close(f.cacheClosed) 545 | }) 546 | f.stopCh <- nil 547 | } 548 | 549 | func (f *fronted) isStopped() bool { 550 | return f.stopped.Load() 551 | } 552 | 553 | func copyProviders(providers map[string]*Provider, countryCode string) map[string]*Provider { 554 | providersCopy := make(map[string]*Provider, len(providers)) 555 | for key, p := range providers { 556 | providersCopy[key] = NewProvider(p.HostAliases, p.TestURL, p.Masquerades, p.PassthroughPatterns, p.FrontingSNIs, p.VerifyHostname, countryCode) 557 | } 558 | return providersCopy 559 | } 560 | 561 | func loadFronts(providers map[string]*Provider, cacheDirty chan interface{}) []Front { 562 | log.Debugf("Loading candidates for %d providers", len(providers)) 563 | defer log.Debug("Finished loading candidates") 564 | 565 | // Preallocate the slice to avoid reallocation 566 | size := 0 567 | for _, p := range providers { 568 | size += len(p.Masquerades) 569 | } 570 | 571 | fronts := make([]Front, size) 572 | 573 | // Note that map iteration order is random, so the order of the providers is automatically randomized. 574 | index := 0 575 | for key, p := range providers { 576 | arr := p.Masquerades 577 | size := len(arr) 578 | 579 | // Shuffle the masquerades to avoid biasing the order in which they are tried 580 | // make a shuffled copy of arr 581 | // ('inside-out' Fisher-Yates) 582 | sh := make([]*Masquerade, size) 583 | for i := 0; i < size; i++ { 584 | j := rand.IntN(i + 1) // 0 <= j <= i 585 | sh[i] = sh[j] 586 | sh[j] = arr[i] 587 | } 588 | 589 | for _, c := range sh { 590 | fronts[index] = newFront(c, key, cacheDirty) 591 | index++ 592 | } 593 | } 594 | return fronts 595 | } 596 | 597 | func (f *fronted) addProviders(providers map[string]*Provider) { 598 | // Add new providers to the existing providers map, overwriting any existing ones. 599 | f.providersMu.Lock() 600 | defer f.providersMu.Unlock() 601 | for key, p := range providers { 602 | f.providers[key] = p 603 | } 604 | } 605 | 606 | func (f *fronted) providerFor(m Front) *Provider { 607 | pid := m.getProviderID() 608 | if pid == "" { 609 | pid = f.defaultProviderID 610 | } 611 | return f.providers[pid] 612 | } 613 | 614 | // Vet vets the specified Masquerade, verifying certificate using the given CertPool. 615 | // This is used in genconfig. 616 | func Vet(m *Masquerade, pool *x509.CertPool, testURL string) bool { 617 | d := &fronted{ 618 | certPool: atomic.Value{}, 619 | maxAllowedCachedAge: defaultMaxAllowedCachedAge, 620 | maxCacheSize: defaultMaxCacheSize, 621 | panicListener: func(msg string) { log.Errorf("Panic in fronted: %v", msg) }, 622 | } 623 | d.certPool.Store(pool) 624 | masq := &front{Masquerade: *m} 625 | conn, _, err := d.doDial(masq) 626 | if err != nil { 627 | return false 628 | } 629 | defer conn.Close() 630 | return masq.verifyWithPost(conn, testURL) 631 | } 632 | 633 | func processYaml(gzippedYaml []byte) (*x509.CertPool, map[string]*Provider, error) { 634 | r, gzipErr := gzip.NewReader(bytes.NewReader(gzippedYaml)) 635 | if gzipErr != nil { 636 | log.Errorf("Failed to create gzip reader %v", gzipErr) 637 | // Wrap the error 638 | return nil, nil, fmt.Errorf("failed to create gzip reader: %w", gzipErr) 639 | } 640 | yml, err := io.ReadAll(r) 641 | if err != nil { 642 | log.Errorf("Failed to read gzipped file %v", err) 643 | return nil, nil, fmt.Errorf("failed to read gzipped file: %w", err) 644 | } 645 | path, err := yaml.PathString("$.providers") 646 | if err != nil { 647 | log.Errorf("Failed to create providers path %v", err) 648 | return nil, nil, fmt.Errorf("failed to create providers path: %w", err) 649 | } 650 | providers := make(map[string]*Provider) 651 | pathErr := path.Read(bytes.NewReader(yml), &providers) 652 | if pathErr != nil { 653 | log.Errorf("Failed to read providers %v", pathErr) 654 | return nil, nil, fmt.Errorf("failed to read providers: %w", pathErr) 655 | } 656 | 657 | trustedCAsPath, err := yaml.PathString("$.trustedcas") 658 | if err != nil { 659 | log.Errorf("Failed to create trusted CA path %v", err) 660 | return nil, nil, fmt.Errorf("failed to create trusted CA path: %w", err) 661 | } 662 | var trustedCAs []*CA 663 | trustedCAsErr := trustedCAsPath.Read(bytes.NewReader(yml), &trustedCAs) 664 | if trustedCAsErr != nil { 665 | log.Errorf("Failed to read trusted CAs %v", trustedCAsErr) 666 | return nil, nil, fmt.Errorf("failed to read trusted CAs: %w", trustedCAsErr) 667 | } 668 | pool := x509.NewCertPool() 669 | for _, ca := range trustedCAs { 670 | pool.AppendCertsFromPEM([]byte(ca.Cert)) 671 | } 672 | return pool, providers, nil 673 | } 674 | -------------------------------------------------------------------------------- /fronted.yaml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getlantern/fronted/07e542f20deadfa68cc2d0da67f1e498282455b1/fronted.yaml.gz -------------------------------------------------------------------------------- /fronted_test.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/http/httptest" 13 | "net/http/httputil" 14 | "net/url" 15 | "os" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | "testing" 20 | "time" 21 | 22 | . "github.com/getlantern/waitforserver" 23 | tls "github.com/refraction-networking/utls" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestConfigUpdating(t *testing.T) { 29 | f := NewFronted( 30 | WithConfigURL("https://media.githubusercontent.com/media/getlantern/fronted/refs/heads/main/fronted.yaml.gz"), 31 | WithCountryCode("cn"), 32 | ) 33 | time.Sleep(1 * time.Second) 34 | 35 | // Try to hit raw.githubusercontent.com 36 | rt, err := f.NewConnectedRoundTripper(context.Background(), "") 37 | require.NoError(t, err) 38 | client := &http.Client{ 39 | Transport: rt, 40 | } 41 | resp, err := client.Get("https://raw.githubusercontent.com/getlantern/fronted/main/fronted.yaml.gz") 42 | require.NoError(t, err) 43 | require.Equal(t, http.StatusOK, resp.StatusCode) 44 | 45 | // Read the full response body 46 | bod, err := io.ReadAll(resp.Body) 47 | require.NoError(t, err) 48 | require.Greater(t, len(bod), 0) 49 | resp.Body.Close() 50 | } 51 | 52 | func TestYamlParsing(t *testing.T) { 53 | // Disable this if we're running in CI because the file is using git lfs and will just be a pointer. 54 | if os.Getenv("GITHUB_ACTIONS") == "true" { 55 | t.Skip("Skipping test in GitHub Actions because the file is using git lfs and will be a pointer") 56 | } 57 | yamlFile, err := os.ReadFile("fronted.yaml.gz") 58 | require.NoError(t, err) 59 | pool, providers, err := processYaml(yamlFile) 60 | require.NoError(t, err) 61 | require.NotNil(t, pool) 62 | require.NotNil(t, providers) 63 | 64 | // Make sure there are some providers 65 | assert.Greater(t, len(providers), 0) 66 | } 67 | 68 | func TestDirectDomainFrontingWithoutSNIConfig(t *testing.T) { 69 | dir, err := os.MkdirTemp("", "direct_test") 70 | require.NoError(t, err, "Unable to create temp dir") 71 | defer os.RemoveAll(dir) 72 | cacheFile := filepath.Join(dir, "cachefile.2") 73 | 74 | log.Debug("Testing direct domain fronting without SNI config") 75 | doTestDomainFronting(t, cacheFile, 10) 76 | time.Sleep(defaultCacheSaveInterval * 2) 77 | // Then try again, this time reusing the existing cacheFile but a corrupted version 78 | corruptMasquerades(cacheFile) 79 | log.Debug("Testing direct domain fronting without SNI config again") 80 | doTestDomainFronting(t, cacheFile, 10) 81 | } 82 | 83 | func TestDirectDomainFrontingWithSNIConfig(t *testing.T) { 84 | dir, err := os.MkdirTemp("", "direct_test") 85 | require.NoError(t, err, "Unable to create temp dir") 86 | defer os.RemoveAll(dir) 87 | cacheFile := filepath.Join(dir, "cachefile.3") 88 | 89 | getURL := "https://config.example.com/global.yaml.gz" 90 | getHost := "config.example.com" 91 | getFrontedHost := "globalconfig.dsa.akamai.getiantem.org" 92 | 93 | hosts := map[string]string{ 94 | getHost: getFrontedHost, 95 | } 96 | certs := trustedCACerts(t) 97 | p := testAkamaiProvidersWithHosts(hosts, &SNIConfig{ 98 | UseArbitrarySNIs: true, 99 | 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"}, 100 | }) 101 | defaultFrontedProviderID = "akamai" 102 | transport := NewFronted(WithCacheFile(cacheFile)) 103 | transport.onNewFronts(certs, p) 104 | 105 | client := &http.Client{ 106 | Transport: newTransportFromDialer(transport), 107 | } 108 | require.True(t, doCheck(client, http.MethodGet, http.StatusOK, getURL)) 109 | } 110 | 111 | func newTransportFromDialer(f Fronted) http.RoundTripper { 112 | rt, _ := f.NewConnectedRoundTripper(context.Background(), "") 113 | return rt 114 | } 115 | 116 | func doTestDomainFronting(t *testing.T, cacheFile string, expectedMasqueradesAtEnd int) int { 117 | getURL := "https://config.example.com/global.yaml.gz" 118 | getHost := "config.example.com" 119 | getFrontedHost := "d24ykmup0867cj.cloudfront.net" 120 | 121 | pingHost := "ping.example.com" 122 | pu, err := url.Parse(pingTestURL) 123 | require.NoError(t, err) 124 | pingFrontedHost := pu.Hostname() 125 | pu.Host = pingHost 126 | pingURL := pu.String() 127 | 128 | hosts := map[string]string{ 129 | pingHost: pingFrontedHost, 130 | getHost: getFrontedHost, 131 | } 132 | certs := trustedCACerts(t) 133 | p := testProvidersWithHosts(hosts) 134 | defaultFrontedProviderID = testProviderID 135 | transport := NewFronted(WithCacheFile(cacheFile)) 136 | transport.onNewFronts(certs, p) 137 | 138 | rt := newTransportFromDialer(transport) 139 | client := &http.Client{ 140 | Transport: rt, 141 | Timeout: 5 * time.Second, 142 | } 143 | require.True(t, doCheck(client, http.MethodPost, http.StatusAccepted, pingURL)) 144 | 145 | defaultFrontedProviderID = testProviderID 146 | transport = NewFronted(WithCacheFile(cacheFile)) 147 | transport.onNewFronts(certs, p) 148 | client = &http.Client{ 149 | Transport: newTransportFromDialer(transport), 150 | } 151 | require.True(t, doCheck(client, http.MethodGet, http.StatusOK, getURL)) 152 | 153 | d := transport.(*fronted) 154 | 155 | // Check the number of masquerades at the end, waiting until we get the right number 156 | masqueradesAtEnd := 0 157 | for i := 0; i < 1000; i++ { 158 | masqueradesAtEnd = len(d.fronts.fronts) 159 | if masqueradesAtEnd == expectedMasqueradesAtEnd { 160 | break 161 | } 162 | time.Sleep(30 * time.Millisecond) 163 | } 164 | require.GreaterOrEqual(t, masqueradesAtEnd, expectedMasqueradesAtEnd) 165 | return masqueradesAtEnd 166 | } 167 | 168 | func TestVet(t *testing.T) { 169 | pool := trustedCACerts(t) 170 | for _, m := range testMasquerades { 171 | if Vet(m, pool, pingTestURL) { 172 | return 173 | } 174 | } 175 | t.Fatal("None of the default masquerades vetted successfully") 176 | } 177 | 178 | func TestHostAliasesBasic(t *testing.T) { 179 | 180 | headersIn := map[string][]string{ 181 | "X-Foo-Bar": {"Quux", "Baz"}, 182 | "X-Bar-Foo": {"XYZ"}, 183 | "X-Quux": {""}, 184 | } 185 | headersOut := map[string][]string{ 186 | "X-Foo-Bar": {"Quux", "Baz"}, 187 | "X-Bar-Foo": {"XYZ"}, 188 | "X-Quux": {""}, 189 | "Connection": {"close"}, 190 | "User-Agent": {"Go-http-client/1.1"}, 191 | "Accept-Encoding": {"gzip"}, 192 | } 193 | 194 | tests := []struct { 195 | url string 196 | headers map[string][]string 197 | expectedResult CDNResult 198 | expectedStatus int 199 | }{ 200 | { 201 | "http://abc.forbidden.com/foo/bar", 202 | headersIn, 203 | CDNResult{"abc.cloudsack.biz", "/foo/bar", "", "cloudsack", headersOut}, 204 | http.StatusAccepted, 205 | }, 206 | { 207 | "https://abc.forbidden.com/bar?x=y&z=w", 208 | headersIn, 209 | CDNResult{"abc.cloudsack.biz", "/bar", "x=y&z=w", "cloudsack", headersOut}, 210 | http.StatusAccepted, 211 | }, 212 | { 213 | "http://def.forbidden.com:12345/foo", 214 | headersIn, 215 | CDNResult{"def.cloudsack.biz", "/foo", "", "cloudsack", headersOut}, 216 | http.StatusAccepted, 217 | }, 218 | { 219 | "https://def.forbidden.com/bar?x=y&z=w", 220 | headersIn, 221 | CDNResult{"def.cloudsack.biz", "/bar", "x=y&z=w", "cloudsack", headersOut}, 222 | http.StatusAccepted, 223 | }, 224 | } 225 | 226 | errtests := []struct { 227 | url string 228 | expectedError string 229 | }{ 230 | { 231 | "http://fff.cloudsack.biz/foo", 232 | `Get "http://fff.cloudsack.biz/foo": could not complete request even with retries`, 233 | }, 234 | { 235 | "http://fff.cloudsack.biz:1234/bar?x=y&z=w", 236 | `Get "http://fff.cloudsack.biz:1234/bar?x=y&z=w": could not complete request even with retries`, 237 | }, 238 | { 239 | "https://www.google.com", 240 | `Get "https://www.google.com": could not complete request even with retries`, 241 | }, 242 | } 243 | 244 | cloudSack, cloudSackAddr, err := newCDN("cloudsack", "cloudsack.biz") 245 | if !assert.NoError(t, err, "failed to start cloudsack cdn") { 246 | return 247 | } 248 | defer cloudSack.Close() 249 | 250 | masq := []*Masquerade{{Domain: "example.com", IpAddress: cloudSackAddr}} 251 | alias := map[string]string{ 252 | "abc.forbidden.com": "abc.cloudsack.biz", 253 | "def.forbidden.com": "def.cloudsack.biz", 254 | } 255 | p := NewProvider(alias, "https://ttt.cloudsack.biz/ping", masq, nil, nil, nil, "") 256 | 257 | certs := x509.NewCertPool() 258 | certs.AddCert(cloudSack.Certificate()) 259 | 260 | defaultFrontedProviderID = "cloudsack" 261 | rt := NewFronted() 262 | 263 | rt.onNewFronts(certs, map[string]*Provider{"cloudsack": p}) 264 | for _, test := range tests { 265 | client := &http.Client{Transport: newTransportFromDialer(rt)} 266 | req, err := http.NewRequest(http.MethodGet, test.url, nil) 267 | if !assert.NoError(t, err) { 268 | return 269 | } 270 | 271 | for k, v := range test.headers { 272 | req.Header[k] = v 273 | } 274 | resp, err := client.Do(req) 275 | if !assert.NoError(t, err, "Request %s failed", test.url) { 276 | continue 277 | } 278 | assert.Equal(t, test.expectedStatus, resp.StatusCode) 279 | if !assert.NotNil(t, resp.Body) { 280 | continue 281 | } 282 | 283 | var result CDNResult 284 | data, err := io.ReadAll(resp.Body) 285 | if !assert.NoError(t, err) { 286 | continue 287 | } 288 | 289 | err = json.Unmarshal(data, &result) 290 | if !assert.NoError(t, err) { 291 | continue 292 | } 293 | assert.Equal(t, test.expectedResult, result) 294 | } 295 | 296 | for _, test := range errtests { 297 | client := &http.Client{Transport: newTransportFromDialer(rt)} 298 | resp, err := client.Get(test.url) 299 | assert.EqualError(t, err, test.expectedError) 300 | assert.Nil(t, resp) 301 | 302 | } 303 | } 304 | 305 | func TestHostAliasesMulti(t *testing.T) { 306 | 307 | tests := []struct { 308 | url string 309 | expectedStatus int 310 | expectedPath string 311 | expectedQuery string 312 | expectedHosts []string 313 | }{ 314 | { 315 | "http://abc.forbidden.com/foo/bar", 316 | http.StatusAccepted, 317 | "/foo/bar", 318 | "", 319 | []string{ 320 | "abc.cloudsack.biz", 321 | "abc.sadcloud.io", 322 | }, 323 | }, 324 | { 325 | "http://def.forbidden.com/bar?x=y&z=w", 326 | http.StatusAccepted, 327 | "/bar", 328 | "x=y&z=w", 329 | []string{ 330 | "def.cloudsack.biz", 331 | "def.sadcloud.io", 332 | }, 333 | }, 334 | } 335 | 336 | sadCloud, sadCloudAddr, err := newCDN("sadcloud", "sadcloud.io") 337 | if !assert.NoError(t, err, "failed to start sadcloud cdn") { 338 | return 339 | } 340 | defer sadCloud.Close() 341 | 342 | cloudSack, cloudSackAddr, err := newCDN("cloudsack", "cloudsack.biz") 343 | if !assert.NoError(t, err, "failed to start cloudsack cdn") { 344 | return 345 | } 346 | defer cloudSack.Close() 347 | 348 | masq1 := []*Masquerade{{Domain: "example.com", IpAddress: cloudSackAddr}} 349 | alias1 := map[string]string{ 350 | "abc.forbidden.com": "abc.cloudsack.biz", 351 | "def.forbidden.com": "def.cloudsack.biz", 352 | } 353 | p1 := NewProvider(alias1, "https://ttt.cloudsack.biz/ping", masq1, nil, nil, nil, "") 354 | 355 | masq2 := []*Masquerade{{Domain: "example.com", IpAddress: sadCloudAddr}} 356 | alias2 := map[string]string{ 357 | "abc.forbidden.com": "abc.sadcloud.io", 358 | "def.forbidden.com": "def.sadcloud.io", 359 | } 360 | p2 := NewProvider(alias2, "https://ttt.sadcloud.io/ping", masq2, nil, nil, nil, "") 361 | 362 | certs := x509.NewCertPool() 363 | certs.AddCert(cloudSack.Certificate()) 364 | certs.AddCert(sadCloud.Certificate()) 365 | 366 | providers := map[string]*Provider{ 367 | "cloudsack": p1, 368 | "sadcloud": p2, 369 | } 370 | 371 | defaultFrontedProviderID = "cloudsack" 372 | rt := NewFronted() 373 | rt.onNewFronts(certs, providers) 374 | 375 | providerCounts := make(map[string]int) 376 | 377 | for i := 0; i < 10; i++ { 378 | for _, test := range tests { 379 | client := &http.Client{Transport: newTransportFromDialer(rt)} 380 | resp, err := client.Get(test.url) 381 | if !assert.NoError(t, err, "Request %s failed", test.url) { 382 | continue 383 | } 384 | assert.Equal(t, test.expectedStatus, resp.StatusCode) 385 | if !assert.NotNil(t, resp.Body) { 386 | continue 387 | } 388 | 389 | var result CDNResult 390 | data, err := io.ReadAll(resp.Body) 391 | if !assert.NoError(t, err) { 392 | continue 393 | } 394 | 395 | err = json.Unmarshal(data, &result) 396 | if !assert.NoError(t, err) { 397 | continue 398 | } 399 | assert.Contains(t, test.expectedHosts, result.Host) 400 | assert.Equal(t, test.expectedQuery, result.Query) 401 | assert.Equal(t, test.expectedPath, result.Path) 402 | 403 | providerCounts[result.Provider] += 1 404 | } 405 | } 406 | 407 | assert.True(t, providerCounts["cloudsack"]+providerCounts["sadcloud"] > 2) 408 | } 409 | 410 | func TestPassthrough(t *testing.T) { 411 | headersIn := map[string][]string{ 412 | "X-Foo-Bar": {"Quux", "Baz"}, 413 | "X-Bar-Foo": {"XYZ"}, 414 | "X-Quux": {""}, 415 | } 416 | headersOut := map[string][]string{ 417 | "X-Foo-Bar": {"Quux", "Baz"}, 418 | "X-Bar-Foo": {"XYZ"}, 419 | "X-Quux": {""}, 420 | "Connection": {"close"}, 421 | "User-Agent": {"Go-http-client/1.1"}, 422 | "Accept-Encoding": {"gzip"}, 423 | } 424 | 425 | tests := []struct { 426 | url string 427 | headers map[string][]string 428 | expectedResult CDNResult 429 | expectedStatus int 430 | }{ 431 | { 432 | "http://fff.ok.cloudsack.biz/foo", 433 | headersIn, 434 | CDNResult{"fff.ok.cloudsack.biz", "/foo", "", "cloudsack", headersOut}, 435 | http.StatusAccepted, 436 | }, 437 | { 438 | "http://abc.cloudsack.biz/bar", 439 | headersIn, 440 | CDNResult{"abc.cloudsack.biz", "/bar", "", "cloudsack", headersOut}, 441 | http.StatusAccepted, 442 | }, 443 | { 444 | "http://XYZ.ZyZ.OK.CloudSack.BiZ/bar", 445 | headersIn, 446 | CDNResult{"xyz.zyz.ok.cloudsack.biz", "/bar", "", "cloudsack", headersOut}, 447 | http.StatusAccepted, 448 | }, 449 | } 450 | 451 | errtests := []struct { 452 | url string 453 | expectedError string 454 | }{ 455 | { 456 | "http://www.notok.cloudsack.biz", 457 | `Get "http://www.notok.cloudsack.biz": could not complete request even with retries`, 458 | }, 459 | { 460 | "http://ok.cloudsack.biz", 461 | `Get "http://ok.cloudsack.biz": could not complete request even with retries`, 462 | }, 463 | { 464 | "http://www.abc.cloudsack.biz", 465 | `Get "http://www.abc.cloudsack.biz": could not complete request even with retries`, 466 | }, 467 | { 468 | "http://noabc.cloudsack.biz", 469 | `Get "http://noabc.cloudsack.biz": could not complete request even with retries`, 470 | }, 471 | { 472 | "http://cloudsack.biz", 473 | `Get "http://cloudsack.biz": could not complete request even with retries`, 474 | }, 475 | { 476 | "https://www.google.com", 477 | `Get "https://www.google.com": could not complete request even with retries`, 478 | }, 479 | } 480 | 481 | cloudSack, cloudSackAddr, err := newCDN("cloudsack", "cloudsack.biz") 482 | if !assert.NoError(t, err, "failed to start cloudsack cdn") { 483 | return 484 | } 485 | defer cloudSack.Close() 486 | 487 | masq := []*Masquerade{{Domain: "example.com", IpAddress: cloudSackAddr}} 488 | alias := map[string]string{} 489 | passthrough := []string{"*.ok.cloudsack.biz", "abc.cloudsack.biz"} 490 | p := NewProvider(alias, "https://ttt.cloudsack.biz/ping", masq, passthrough, nil, nil, "") 491 | 492 | certs := x509.NewCertPool() 493 | certs.AddCert(cloudSack.Certificate()) 494 | 495 | defaultFrontedProviderID = "cloudsack" 496 | rt := NewFronted() 497 | rt.onNewFronts(certs, map[string]*Provider{"cloudsack": p}) 498 | 499 | for _, test := range tests { 500 | client := &http.Client{Transport: newTransportFromDialer(rt)} 501 | req, err := http.NewRequest(http.MethodGet, test.url, nil) 502 | if !assert.NoError(t, err) { 503 | return 504 | } 505 | 506 | for k, v := range test.headers { 507 | req.Header[k] = v 508 | } 509 | resp, err := client.Do(req) 510 | if !assert.NoError(t, err, "Request %s failed", test.url) { 511 | continue 512 | } 513 | assert.Equal(t, test.expectedStatus, resp.StatusCode) 514 | if !assert.NotNil(t, resp.Body) { 515 | continue 516 | } 517 | 518 | var result CDNResult 519 | data, err := io.ReadAll(resp.Body) 520 | if !assert.NoError(t, err) { 521 | continue 522 | } 523 | 524 | err = json.Unmarshal(data, &result) 525 | if !assert.NoError(t, err) { 526 | continue 527 | } 528 | assert.Equal(t, test.expectedResult, result) 529 | } 530 | 531 | for _, test := range errtests { 532 | client := &http.Client{Transport: newTransportFromDialer(rt)} 533 | resp, err := client.Get(test.url) 534 | assert.EqualError(t, err, test.expectedError) 535 | assert.Nil(t, resp) 536 | 537 | } 538 | } 539 | 540 | const ( 541 | // set this header to an integer to force response status code 542 | CDNForceFail = "X-CDN-Force-Fail" 543 | ) 544 | 545 | type CDNResult struct { 546 | Host, Path, Query, Provider string 547 | Headers map[string][]string 548 | } 549 | 550 | func newCDN(providerID, domain string) (*httptest.Server, string, error) { 551 | allowedSuffix := fmt.Sprintf(".%s", domain) 552 | srv := httptest.NewTLSServer( 553 | http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 554 | dump, err := httputil.DumpRequest(req, true) 555 | if err != nil { 556 | log.Errorf("Failed to dump request: %s", err) 557 | } else { 558 | log.Debugf("(%s) CDN Request: %s", domain, dump) 559 | } 560 | 561 | forceFail := req.Header.Get(CDNForceFail) 562 | 563 | vhost := req.Host 564 | if vhost == domain || strings.HasSuffix(vhost, allowedSuffix) && forceFail == "" { 565 | log.Debugf("accepting request host=%s ff=%s", vhost, forceFail) 566 | body, _ := json.Marshal(&CDNResult{ 567 | Host: vhost, 568 | Path: req.URL.Path, 569 | Query: req.URL.RawQuery, 570 | Provider: providerID, 571 | Headers: req.Header, 572 | }) 573 | rw.WriteHeader(http.StatusAccepted) 574 | rw.Write(body) 575 | } else { 576 | log.Debugf("(%s) Rejecting request with host = %q ff=%s allowed=%s", domain, vhost, forceFail, allowedSuffix) 577 | errorCode := http.StatusForbidden 578 | if forceFail != "" { 579 | errorCode, err = strconv.Atoi(forceFail) 580 | if err != nil { 581 | errorCode = http.StatusInternalServerError 582 | } 583 | log.Debugf("Forcing status code to %d", errorCode) 584 | } 585 | rw.WriteHeader(errorCode) 586 | } 587 | })) 588 | addr := srv.Listener.Addr().String() 589 | log.Debugf("Waiting for origin server at %s...", addr) 590 | if err := WaitForServer("tcp", addr, 10*time.Second); err != nil { 591 | return nil, "", err 592 | } 593 | log.Debugf("Started %s CDN", domain) 594 | return srv, addr, nil 595 | } 596 | 597 | func corruptMasquerades(cacheFile string) { 598 | log.Debug("Corrupting masquerades") 599 | data, err := os.ReadFile(cacheFile) 600 | if err != nil { 601 | log.Error(err) 602 | return 603 | } 604 | masquerades := make([]map[string]interface{}, 0) 605 | err = json.Unmarshal(data, &masquerades) 606 | if err != nil { 607 | log.Error(err) 608 | return 609 | } 610 | log.Debugf("Number of masquerades to corrupt: %d", len(masquerades)) 611 | for _, masquerade := range masquerades { 612 | domain := masquerade["Domain"] 613 | ip := masquerade["IpAddress"] 614 | ipParts := strings.Split(ip.(string), ".") 615 | part0, _ := strconv.Atoi(ipParts[0]) 616 | ipParts[0] = strconv.Itoa(part0 + 1) 617 | masquerade["IpAddress"] = strings.Join(ipParts, ".") 618 | log.Debugf("Corrupted masquerade %v", domain) 619 | } 620 | messedUp, err := json.Marshal(masquerades) 621 | if err != nil { 622 | return 623 | } 624 | os.WriteFile(cacheFile, messedUp, 0644) 625 | } 626 | 627 | func TestVerifyPeerCertificate(t *testing.T) { 628 | // raw certs generated by printing the received rawCerts from TestDirectDomainFrontingWithSNIConfig 629 | rawCerts := [][]byte{{48, 130, 6, 78, 48, 130, 5, 54, 160, 3, 2, 1, 2, 2, 16, 11, 14, 250, 105, 152, 72, 112, 146, 165, 214, 78, 192, 231, 165, 110, 242, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 48, 79, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 21, 48, 19, 6, 3, 85, 4, 10, 19, 12, 68, 105, 103, 105, 67, 101, 114, 116, 32, 73, 110, 99, 49, 41, 48, 39, 6, 3, 85, 4, 3, 19, 32, 68, 105, 103, 105, 67, 101, 114, 116, 32, 84, 76, 83, 32, 82, 83, 65, 32, 83, 72, 65, 50, 53, 54, 32, 50, 48, 50, 48, 32, 67, 65, 49, 48, 30, 23, 13, 50, 52, 48, 52, 49, 56, 48, 48, 48, 48, 48, 48, 90, 23, 13, 50, 53, 48, 52, 49, 57, 50, 51, 53, 57, 53, 57, 90, 48, 121, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 22, 48, 20, 6, 3, 85, 4, 8, 19, 13, 77, 97, 115, 115, 97, 99, 104, 117, 115, 101, 116, 116, 115, 49, 18, 48, 16, 6, 3, 85, 4, 7, 19, 9, 67, 97, 109, 98, 114, 105, 100, 103, 101, 49, 34, 48, 32, 6, 3, 85, 4, 10, 19, 25, 65, 107, 97, 109, 97, 105, 32, 84, 101, 99, 104, 110, 111, 108, 111, 103, 105, 101, 115, 44, 32, 73, 110, 99, 46, 49, 26, 48, 24, 6, 3, 85, 4, 3, 19, 17, 97, 50, 52, 56, 46, 101, 46, 97, 107, 97, 109, 97, 105, 46, 110, 101, 116, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 5, 224, 177, 69, 53, 250, 80, 142, 150, 138, 229, 168, 82, 249, 163, 196, 35, 150, 140, 182, 86, 208, 48, 132, 211, 49, 12, 169, 58, 148, 19, 105, 223, 193, 88, 236, 160, 208, 199, 150, 32, 252, 119, 75, 85, 5, 247, 130, 138, 242, 186, 184, 107, 67, 177, 230, 40, 36, 104, 131, 178, 228, 231, 148, 163, 130, 3, 197, 48, 130, 3, 193, 48, 31, 6, 3, 85, 29, 35, 4, 24, 48, 22, 128, 20, 183, 107, 162, 234, 168, 170, 132, 140, 121, 234, 180, 218, 15, 152, 178, 197, 149, 118, 185, 244, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 115, 183, 92, 115, 61, 0, 51, 82, 107, 67, 69, 86, 236, 116, 51, 65, 161, 9, 34, 162, 48, 110, 6, 3, 85, 29, 17, 4, 103, 48, 101, 130, 17, 97, 50, 52, 56, 46, 101, 46, 97, 107, 97, 109, 97, 105, 46, 110, 101, 116, 130, 15, 42, 46, 97, 107, 97, 109, 97, 105, 122, 101, 100, 46, 110, 101, 116, 130, 23, 42, 46, 97, 107, 97, 109, 97, 105, 122, 101, 100, 45, 115, 116, 97, 103, 105, 110, 103, 46, 110, 101, 116, 130, 14, 42, 46, 97, 107, 97, 109, 97, 105, 104, 100, 46, 110, 101, 116, 130, 22, 42, 46, 97, 107, 97, 109, 97, 105, 104, 100, 45, 115, 116, 97, 103, 105, 110, 103, 46, 110, 101, 116, 48, 62, 6, 3, 85, 29, 32, 4, 55, 48, 53, 48, 51, 6, 6, 103, 129, 12, 1, 2, 2, 48, 41, 48, 39, 6, 8, 43, 6, 1, 5, 5, 7, 2, 1, 22, 27, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 47, 67, 80, 83, 48, 14, 6, 3, 85, 29, 15, 1, 1, 255, 4, 4, 3, 2, 3, 136, 48, 29, 6, 3, 85, 29, 37, 4, 22, 48, 20, 6, 8, 43, 6, 1, 5, 5, 7, 3, 1, 6, 8, 43, 6, 1, 5, 5, 7, 3, 2, 48, 129, 143, 6, 3, 85, 29, 31, 4, 129, 135, 48, 129, 132, 48, 64, 160, 62, 160, 60, 134, 58, 104, 116, 116, 112, 58, 47, 47, 99, 114, 108, 51, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 47, 68, 105, 103, 105, 67, 101, 114, 116, 84, 76, 83, 82, 83, 65, 83, 72, 65, 50, 53, 54, 50, 48, 50, 48, 67, 65, 49, 45, 52, 46, 99, 114, 108, 48, 64, 160, 62, 160, 60, 134, 58, 104, 116, 116, 112, 58, 47, 47, 99, 114, 108, 52, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 47, 68, 105, 103, 105, 67, 101, 114, 116, 84, 76, 83, 82, 83, 65, 83, 72, 65, 50, 53, 54, 50, 48, 50, 48, 67, 65, 49, 45, 52, 46, 99, 114, 108, 48, 127, 6, 8, 43, 6, 1, 5, 5, 7, 1, 1, 4, 115, 48, 113, 48, 36, 6, 8, 43, 6, 1, 5, 5, 7, 48, 1, 134, 24, 104, 116, 116, 112, 58, 47, 47, 111, 99, 115, 112, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 48, 73, 6, 8, 43, 6, 1, 5, 5, 7, 48, 2, 134, 61, 104, 116, 116, 112, 58, 47, 47, 99, 97, 99, 101, 114, 116, 115, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 47, 68, 105, 103, 105, 67, 101, 114, 116, 84, 76, 83, 82, 83, 65, 83, 72, 65, 50, 53, 54, 50, 48, 50, 48, 67, 65, 49, 45, 49, 46, 99, 114, 116, 48, 12, 6, 3, 85, 29, 19, 1, 1, 255, 4, 2, 48, 0, 48, 130, 1, 125, 6, 10, 43, 6, 1, 4, 1, 214, 121, 2, 4, 2, 4, 130, 1, 109, 4, 130, 1, 105, 1, 103, 0, 118, 0, 78, 117, 163, 39, 92, 154, 16, 195, 56, 91, 108, 212, 223, 63, 82, 235, 29, 240, 224, 142, 27, 141, 105, 192, 177, 250, 100, 177, 98, 154, 57, 223, 0, 0, 1, 142, 241, 217, 223, 134, 0, 0, 4, 3, 0, 71, 48, 69, 2, 33, 0, 182, 60, 198, 96, 136, 128, 205, 139, 42, 82, 117, 248, 90, 158, 186, 210, 179, 163, 225, 68, 48, 33, 54, 42, 66, 129, 205, 220, 227, 47, 241, 24, 2, 32, 47, 50, 19, 81, 103, 101, 88, 38, 67, 79, 20, 225, 232, 59, 123, 77, 100, 243, 60, 99, 22, 213, 169, 109, 122, 35, 153, 88, 59, 40, 193, 180, 0, 118, 0, 125, 89, 30, 18, 225, 120, 42, 123, 28, 97, 103, 124, 94, 253, 248, 208, 135, 92, 20, 160, 78, 149, 158, 185, 3, 47, 217, 14, 140, 46, 121, 184, 0, 0, 1, 142, 241, 217, 223, 135, 0, 0, 4, 3, 0, 71, 48, 69, 2, 33, 0, 236, 206, 233, 76, 152, 193, 240, 13, 15, 141, 73, 58, 88, 53, 123, 217, 228, 185, 26, 35, 9, 53, 191, 231, 1, 223, 99, 28, 200, 188, 2, 47, 2, 32, 39, 67, 173, 42, 123, 38, 247, 178, 220, 3, 89, 37, 218, 105, 45, 249, 17, 111, 222, 84, 173, 197, 17, 26, 177, 217, 193, 163, 221, 229, 129, 134, 0, 117, 0, 230, 210, 49, 99, 64, 119, 140, 193, 16, 65, 6, 215, 113, 185, 206, 193, 210, 64, 246, 150, 132, 134, 251, 186, 135, 50, 29, 253, 30, 55, 142, 80, 0, 0, 1, 142, 241, 217, 223, 156, 0, 0, 4, 3, 0, 70, 48, 68, 2, 32, 63, 238, 16, 71, 200, 160, 240, 218, 87, 96, 100, 137, 184, 151, 189, 202, 191, 140, 193, 138, 110, 83, 166, 225, 152, 192, 33, 228, 72, 60, 146, 9, 2, 32, 20, 216, 203, 133, 251, 181, 154, 237, 126, 11, 120, 77, 219, 28, 73, 93, 254, 23, 141, 52, 195, 145, 216, 145, 16, 187, 133, 16, 140, 184, 135, 183, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 3, 130, 1, 1, 0, 98, 147, 27, 116, 164, 135, 78, 19, 1, 11, 53, 227, 221, 49, 154, 147, 19, 174, 118, 228, 188, 90, 81, 60, 70, 72, 54, 95, 222, 204, 55, 191, 171, 254, 126, 228, 34, 208, 165, 74, 135, 252, 133, 131, 205, 71, 216, 124, 81, 208, 146, 28, 219, 168, 108, 81, 76, 30, 114, 121, 71, 134, 116, 156, 58, 85, 38, 176, 202, 33, 124, 189, 155, 252, 217, 111, 116, 7, 83, 186, 149, 7, 7, 127, 39, 167, 50, 69, 97, 162, 65, 90, 234, 59, 114, 92, 19, 87, 118, 143, 216, 97, 192, 226, 95, 230, 244, 208, 237, 199, 7, 3, 99, 108, 69, 214, 95, 36, 69, 116, 75, 195, 254, 18, 207, 11, 34, 253, 237, 248, 127, 152, 29, 58, 131, 49, 178, 141, 72, 111, 11, 151, 30, 3, 56, 6, 6, 156, 45, 103, 3, 25, 210, 95, 235, 109, 29, 45, 59, 21, 36, 81, 146, 160, 165, 185, 201, 100, 150, 126, 160, 230, 126, 128, 222, 243, 49, 119, 188, 163, 162, 98, 153, 174, 185, 234, 44, 226, 102, 184, 207, 2, 193, 66, 77, 199, 39, 219, 64, 44, 145, 6, 207, 52, 237, 50, 200, 55, 253, 21, 208, 124, 150, 3, 136, 196, 70, 121, 86, 75, 41, 76, 71, 193, 94, 73, 151, 255, 164, 127, 129, 242, 35, 125, 80, 24, 21, 121, 184, 18, 224, 212, 70, 58, 206, 122, 34, 250, 119, 203, 84, 55, 11, 9, 221, 103}, {48, 130, 4, 190, 48, 130, 3, 166, 160, 3, 2, 1, 2, 2, 16, 6, 216, 217, 4, 213, 88, 67, 70, 246, 138, 47, 167, 84, 34, 126, 196, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 48, 97, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 21, 48, 19, 6, 3, 85, 4, 10, 19, 12, 68, 105, 103, 105, 67, 101, 114, 116, 32, 73, 110, 99, 49, 25, 48, 23, 6, 3, 85, 4, 11, 19, 16, 119, 119, 119, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 49, 32, 48, 30, 6, 3, 85, 4, 3, 19, 23, 68, 105, 103, 105, 67, 101, 114, 116, 32, 71, 108, 111, 98, 97, 108, 32, 82, 111, 111, 116, 32, 67, 65, 48, 30, 23, 13, 50, 49, 48, 52, 49, 52, 48, 48, 48, 48, 48, 48, 90, 23, 13, 51, 49, 48, 52, 49, 51, 50, 51, 53, 57, 53, 57, 90, 48, 79, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 21, 48, 19, 6, 3, 85, 4, 10, 19, 12, 68, 105, 103, 105, 67, 101, 114, 116, 32, 73, 110, 99, 49, 41, 48, 39, 6, 3, 85, 4, 3, 19, 32, 68, 105, 103, 105, 67, 101, 114, 116, 32, 84, 76, 83, 32, 82, 83, 65, 32, 83, 72, 65, 50, 53, 54, 32, 50, 48, 50, 48, 32, 67, 65, 49, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 193, 75, 179, 101, 71, 112, 188, 221, 79, 88, 219, 236, 156, 237, 195, 102, 229, 31, 49, 19, 84, 173, 74, 102, 70, 31, 44, 10, 236, 100, 7, 229, 46, 220, 220, 185, 10, 32, 237, 223, 227, 196, 208, 158, 154, 169, 122, 29, 130, 136, 229, 17, 86, 219, 30, 159, 88, 194, 81, 231, 44, 52, 13, 46, 210, 146, 225, 86, 203, 241, 121, 95, 179, 187, 135, 202, 37, 3, 123, 154, 82, 65, 102, 16, 96, 79, 87, 19, 73, 240, 232, 55, 103, 131, 223, 231, 211, 75, 103, 76, 34, 81, 166, 223, 14, 153, 16, 237, 87, 81, 116, 38, 226, 125, 199, 202, 98, 46, 19, 27, 127, 35, 136, 37, 83, 111, 193, 52, 88, 0, 139, 132, 255, 248, 190, 167, 88, 73, 34, 123, 150, 173, 162, 136, 155, 21, 188, 160, 124, 223, 233, 81, 168, 213, 176, 237, 55, 226, 54, 180, 130, 75, 98, 181, 73, 154, 236, 199, 103, 214, 227, 62, 245, 227, 214, 18, 94, 68, 241, 191, 113, 66, 125, 88, 132, 3, 128, 177, 129, 1, 250, 249, 202, 50, 187, 180, 142, 39, 135, 39, 197, 43, 116, 212, 168, 214, 151, 222, 195, 100, 249, 202, 206, 83, 162, 86, 188, 120, 23, 142, 73, 3, 41, 174, 251, 73, 79, 164, 21, 185, 206, 242, 92, 25, 87, 109, 107, 121, 167, 43, 162, 39, 32, 19, 181, 208, 61, 64, 211, 33, 48, 7, 147, 234, 153, 245, 2, 3, 1, 0, 1, 163, 130, 1, 130, 48, 130, 1, 126, 48, 18, 6, 3, 85, 29, 19, 1, 1, 255, 4, 8, 48, 6, 1, 1, 255, 2, 1, 0, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 183, 107, 162, 234, 168, 170, 132, 140, 121, 234, 180, 218, 15, 152, 178, 197, 149, 118, 185, 244, 48, 31, 6, 3, 85, 29, 35, 4, 24, 48, 22, 128, 20, 3, 222, 80, 53, 86, 209, 76, 187, 102, 240, 163, 226, 27, 27, 195, 151, 178, 61, 209, 85, 48, 14, 6, 3, 85, 29, 15, 1, 1, 255, 4, 4, 3, 2, 1, 134, 48, 29, 6, 3, 85, 29, 37, 4, 22, 48, 20, 6, 8, 43, 6, 1, 5, 5, 7, 3, 1, 6, 8, 43, 6, 1, 5, 5, 7, 3, 2, 48, 118, 6, 8, 43, 6, 1, 5, 5, 7, 1, 1, 4, 106, 48, 104, 48, 36, 6, 8, 43, 6, 1, 5, 5, 7, 48, 1, 134, 24, 104, 116, 116, 112, 58, 47, 47, 111, 99, 115, 112, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 48, 64, 6, 8, 43, 6, 1, 5, 5, 7, 48, 2, 134, 52, 104, 116, 116, 112, 58, 47, 47, 99, 97, 99, 101, 114, 116, 115, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 47, 68, 105, 103, 105, 67, 101, 114, 116, 71, 108, 111, 98, 97, 108, 82, 111, 111, 116, 67, 65, 46, 99, 114, 116, 48, 66, 6, 3, 85, 29, 31, 4, 59, 48, 57, 48, 55, 160, 53, 160, 51, 134, 49, 104, 116, 116, 112, 58, 47, 47, 99, 114, 108, 51, 46, 100, 105, 103, 105, 99, 101, 114, 116, 46, 99, 111, 109, 47, 68, 105, 103, 105, 67, 101, 114, 116, 71, 108, 111, 98, 97, 108, 82, 111, 111, 116, 67, 65, 46, 99, 114, 108, 48, 61, 6, 3, 85, 29, 32, 4, 54, 48, 52, 48, 11, 6, 9, 96, 134, 72, 1, 134, 253, 108, 2, 1, 48, 7, 6, 5, 103, 129, 12, 1, 1, 48, 8, 6, 6, 103, 129, 12, 1, 2, 1, 48, 8, 6, 6, 103, 129, 12, 1, 2, 2, 48, 8, 6, 6, 103, 129, 12, 1, 2, 3, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 3, 130, 1, 1, 0, 128, 50, 206, 94, 11, 221, 110, 90, 13, 10, 175, 225, 214, 132, 203, 192, 142, 250, 133, 112, 237, 218, 93, 179, 12, 247, 43, 117, 64, 254, 133, 10, 250, 243, 49, 120, 183, 112, 75, 26, 137, 88, 186, 128, 189, 243, 107, 29, 233, 126, 207, 11, 186, 88, 156, 89, 212, 144, 211, 253, 108, 253, 208, 152, 109, 183, 113, 130, 91, 207, 109, 11, 90, 9, 208, 123, 222, 196, 67, 216, 42, 164, 222, 158, 65, 38, 95, 187, 143, 153, 203, 221, 174, 225, 168, 111, 159, 135, 254, 116, 183, 31, 27, 32, 171, 177, 79, 198, 245, 103, 93, 93, 155, 60, 233, 255, 105, 247, 97, 108, 214, 217, 243, 253, 54, 198, 171, 3, 136, 118, 210, 75, 46, 117, 134, 227, 252, 216, 85, 125, 38, 194, 17, 119, 223, 62, 2, 182, 124, 243, 171, 123, 122, 134, 54, 111, 184, 247, 216, 147, 113, 207, 134, 223, 115, 48, 250, 123, 171, 237, 42, 89, 200, 66, 132, 59, 17, 23, 26, 82, 243, 201, 14, 20, 125, 162, 91, 114, 103, 186, 113, 237, 87, 71, 102, 197, 184, 2, 74, 101, 52, 94, 139, 208, 42, 60, 32, 156, 81, 153, 76, 231, 82, 158, 247, 107, 17, 43, 13, 146, 126, 29, 232, 138, 235, 54, 22, 67, 135, 234, 42, 99, 191, 117, 63, 235, 222, 196, 3, 187, 10, 60, 247, 48, 239, 235, 175, 76, 252, 139, 54, 16, 115, 62, 243, 164}} 630 | 631 | var tests = []struct { 632 | name string 633 | givenRawCerts [][]byte 634 | givenRoots *x509.CertPool 635 | givenVerifyHost string 636 | assert func(t *testing.T, err error) 637 | }{ 638 | { 639 | name: "should return no certificates present when not providing rawCerts", 640 | givenRawCerts: make([][]byte, 0), 641 | assert: func(t *testing.T, err error) { 642 | if assert.Error(t, err) { 643 | assert.ErrorContains(t, err, "no certificates presented") 644 | } 645 | }, 646 | }, 647 | { 648 | name: "should return an error when providing invalid first rawCert", 649 | givenRawCerts: [][]byte{{}}, 650 | assert: func(t *testing.T, err error) { 651 | if assert.Error(t, err) { 652 | assert.ErrorContains(t, err, "unable to parse certificate") 653 | } 654 | }, 655 | }, 656 | { 657 | name: "should return an error when unable to load intermediate certificates", 658 | givenRawCerts: [][]byte{rawCerts[0], {}}, 659 | assert: func(t *testing.T, err error) { 660 | if assert.Error(t, err) { 661 | assert.ErrorContains(t, err, "unable to parse intermediate certificate") 662 | } 663 | }, 664 | }, 665 | { 666 | name: "should return an error when failing to verify the certificate for the given verifyHost", 667 | givenRawCerts: rawCerts, 668 | givenRoots: trustedCACerts(t), 669 | givenVerifyHost: "cloudfront.net", 670 | assert: func(t *testing.T, err error) { 671 | if assert.Error(t, err) { 672 | assert.ErrorContains(t, err, "certificate verification failed") 673 | } 674 | }, 675 | }, 676 | { 677 | name: "should succeed when providing valid rawCerts, roots, domain and sni", 678 | givenRawCerts: rawCerts, 679 | givenRoots: trustedCACerts(t), 680 | givenVerifyHost: "potato.akamaihd.net", 681 | assert: func(t *testing.T, err error) { 682 | assert.NoError(t, err) 683 | }, 684 | }, 685 | { 686 | name: "should succeed when providing valid rawCerts, roots even without verifying the host", 687 | givenRawCerts: rawCerts, 688 | givenRoots: trustedCACerts(t), 689 | givenVerifyHost: "", 690 | assert: func(t *testing.T, err error) { 691 | assert.NoError(t, err) 692 | }, 693 | }, 694 | } 695 | for _, tt := range tests { 696 | t.Run(tt.name, func(t *testing.T) { 697 | err := verifyPeerCertificate(tt.givenRawCerts, tt.givenRoots, tt.givenVerifyHost) 698 | tt.assert(t, err) 699 | }) 700 | } 701 | } 702 | 703 | func TestFindWorkingMasquerades(t *testing.T) { 704 | tests := []struct { 705 | name string 706 | masquerades []*mockFront 707 | expectedSuccessful int 708 | expectedMasquerades int 709 | }{ 710 | { 711 | name: "All successful", 712 | masquerades: []*mockFront{ 713 | newMockFront("domain1.com", "1.1.1.1", 0, true), 714 | newMockFront("domain2.com", "2.2.2.2", 0, true), 715 | newMockFront("domain3.com", "3.3.3.3", 0, true), 716 | newMockFront("domain4.com", "4.4.4.4", 0, true), 717 | newMockFront("domain1.com", "1.1.1.1", 0, true), 718 | newMockFront("domain1.com", "1.1.1.1", 0, true), 719 | }, 720 | expectedSuccessful: 4, 721 | }, 722 | { 723 | name: "Some successful", 724 | masquerades: []*mockFront{ 725 | newMockFront("domain1.com", "1.1.1.1", 0, true), 726 | newMockFront("domain2.com", "2.2.2.2", 0, false), 727 | newMockFront("domain3.com", "3.3.3.3", 0, true), 728 | newMockFront("domain4.com", "4.4.4.4", 0, false), 729 | newMockFront("domain1.com", "1.1.1.1", 0, true), 730 | }, 731 | expectedSuccessful: 2, 732 | }, 733 | { 734 | name: "None successful", 735 | masquerades: []*mockFront{ 736 | newMockFront("domain1.com", "1.1.1.1", 0, false), 737 | newMockFront("domain2.com", "2.2.2.2", 0, false), 738 | newMockFront("domain3.com", "3.3.3.3", 0, false), 739 | newMockFront("domain4.com", "4.4.4.4", 0, false), 740 | }, 741 | expectedSuccessful: 0, 742 | }, 743 | { 744 | name: "Batch processing", 745 | masquerades: func() []*mockFront { 746 | var masquerades []*mockFront 747 | for i := 0; i < 50; i++ { 748 | masquerades = append(masquerades, newMockFront(fmt.Sprintf("domain%d.com", i), fmt.Sprintf("1.1.1.%d", i), 0, i%2 == 0)) 749 | } 750 | return masquerades 751 | }(), 752 | expectedSuccessful: 4, 753 | }, 754 | } 755 | 756 | for _, tt := range tests { 757 | t.Run(tt.name, func(t *testing.T) { 758 | f := &fronted{ 759 | stopCh: make(chan interface{}, 10), 760 | frontsCh: make(chan Front, 10), 761 | } 762 | f.providers = make(map[string]*Provider) 763 | f.providers["testProviderId"] = NewProvider(nil, "", nil, nil, nil, nil, "") 764 | //f.fronts = make(sortedFronts, len(tt.masquerades)) 765 | f.fronts = newThreadSafeFronts(len(tt.masquerades)) 766 | for i, m := range tt.masquerades { 767 | f.fronts.fronts[i] = m 768 | } 769 | 770 | f.tryAllFronts() 771 | 772 | tries := 0 773 | for len(f.frontsCh) < tt.expectedSuccessful && tries < 100 { 774 | time.Sleep(30 * time.Millisecond) 775 | tries++ 776 | } 777 | 778 | assert.GreaterOrEqual(t, len(f.frontsCh), tt.expectedSuccessful) 779 | }) 780 | } 781 | } 782 | 783 | func TestLoadFronts(t *testing.T) { 784 | providers := map[string]*Provider{ 785 | "provider1": { 786 | Masquerades: []*Masquerade{ 787 | {Domain: "domain1.com", IpAddress: "1.1.1.1"}, 788 | {Domain: "domain2.com", IpAddress: "2.2.2.2"}, 789 | }, 790 | }, 791 | "provider2": { 792 | Masquerades: []*Masquerade{ 793 | {Domain: "domain3.com", IpAddress: "3.3.3.3"}, 794 | {Domain: "domain4.com", IpAddress: "4.4.4.4"}, 795 | }, 796 | }, 797 | } 798 | 799 | expected := map[string]bool{ 800 | "domain1.com": true, 801 | "domain2.com": true, 802 | "domain3.com": true, 803 | "domain4.com": true, 804 | } 805 | 806 | // Create the cache dirty channel 807 | cacheDirty := make(chan interface{}, 10) 808 | masquerades := loadFronts(providers, cacheDirty) 809 | 810 | assert.Equal(t, 4, len(masquerades), "Unexpected number of masquerades loaded") 811 | 812 | for _, m := range masquerades { 813 | assert.True(t, expected[m.getDomain()], "Unexpected masquerade domain: %s", m.getDomain()) 814 | } 815 | } 816 | 817 | func TestRandRange(t *testing.T) { 818 | tests := []struct { 819 | min, max int 820 | }{ 821 | {1, 10}, 822 | {5, 15}, 823 | {0, 100}, 824 | {-10, 10}, 825 | {50, 60}, 826 | } 827 | 828 | for _, tt := range tests { 829 | t.Run(fmt.Sprintf("min=%d,max=%d", tt.min, tt.max), func(t *testing.T) { 830 | for i := 0; i < 100; i++ { 831 | result := randRange(tt.min, tt.max) 832 | assert.GreaterOrEqual(t, result, tt.min) 833 | assert.Less(t, result, tt.max) 834 | } 835 | }) 836 | } 837 | } 838 | 839 | // Generate a mock of a MasqueradeInterface with a Dial method that can optionally 840 | // return an error after a specified number of milliseconds. 841 | func newMockFront(domain string, ipAddress string, timeout time.Duration, passesCheck bool) *mockFront { 842 | return newMockFrontWithLastSuccess(domain, ipAddress, timeout, passesCheck, time.Time{}) 843 | } 844 | 845 | // Generate a mock of a MasqueradeInterface with a Dial method that can optionally 846 | // return an error after a specified number of milliseconds. 847 | func newMockFrontWithLastSuccess(domain string, ipAddress string, timeout time.Duration, passesCheck bool, lastSucceededTime time.Time) *mockFront { 848 | return &mockFront{ 849 | Domain: domain, 850 | IpAddress: ipAddress, 851 | timeout: timeout, 852 | passesCheck: passesCheck, 853 | lastSucceededTime: lastSucceededTime, 854 | } 855 | } 856 | 857 | type mockFront struct { 858 | Domain string 859 | IpAddress string 860 | timeout time.Duration 861 | passesCheck bool 862 | lastSucceededTime time.Time 863 | } 864 | 865 | // setLastSucceeded implements MasqueradeInterface. 866 | func (m *mockFront) setLastSucceeded(succeededTime time.Time) { 867 | m.lastSucceededTime = succeededTime 868 | } 869 | 870 | // lastSucceeded implements MasqueradeInterface. 871 | func (m *mockFront) lastSucceeded() time.Time { 872 | return m.lastSucceededTime 873 | } 874 | 875 | // isSucceeding implements MasqueradeInterface. 876 | func (m *mockFront) isSucceeding() bool { 877 | return m.lastSucceededTime.After(time.Time{}) 878 | } 879 | 880 | // verifyWithPost implements MasqueradeInterface. 881 | func (m *mockFront) verifyWithPost(net.Conn, string) bool { 882 | return m.passesCheck 883 | } 884 | 885 | // dial implements MasqueradeInterface. 886 | func (m *mockFront) dial(rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID) (net.Conn, error) { 887 | if m.timeout > 0 { 888 | time.Sleep(m.timeout) 889 | return nil, errors.New("mock dial error") 890 | } 891 | m.lastSucceededTime = time.Now() 892 | return &net.TCPConn{}, nil 893 | } 894 | 895 | // getDomain implements MasqueradeInterface. 896 | func (m *mockFront) getDomain() string { 897 | return m.Domain 898 | } 899 | 900 | // getIpAddress implements MasqueradeInterface. 901 | func (m *mockFront) getIpAddress() string { 902 | return m.IpAddress 903 | } 904 | 905 | // getProviderID implements MasqueradeInterface. 906 | func (m *mockFront) getProviderID() string { 907 | return "testProviderId" 908 | } 909 | 910 | // markFailed implements MasqueradeInterface. 911 | func (m *mockFront) markFailed() { 912 | 913 | } 914 | 915 | // markSucceeded implements MasqueradeInterface. 916 | func (m *mockFront) markSucceeded() { 917 | } 918 | 919 | // markCacheDirty implements MasqueradeInterface. 920 | func (m *mockFront) markCacheDirty() { 921 | } 922 | 923 | func (m *mockFront) markWithResult(good bool) bool { 924 | if good { 925 | m.markSucceeded() 926 | } else { 927 | m.markFailed() 928 | } 929 | m.markCacheDirty() 930 | return good 931 | } 932 | 933 | // Make sure that the mockMasquerade implements the MasqueradeInterface 934 | var _ Front = (*mockFront)(nil) 935 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/getlantern/fronted 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/alitto/pond/v2 v2.1.5 9 | github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 10 | github.com/getlantern/keepcurrent v0.0.0-20240126172110-2e0264ca385d 11 | github.com/getlantern/keyman v0.0.0-20200819205636-76fef27c39f1 12 | github.com/getlantern/netx v0.0.0-20240814210628-0984f52e2d18 13 | github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 14 | github.com/getlantern/tlsdialer/v3 v3.0.3 15 | github.com/getlantern/waitforserver v1.0.1 16 | github.com/goccy/go-yaml v1.15.13 17 | github.com/refraction-networking/utls v1.7.1 18 | github.com/stretchr/testify v1.10.0 19 | ) 20 | 21 | require ( 22 | github.com/andybalholm/brotli v1.0.6 // indirect 23 | github.com/cloudflare/circl v1.5.0 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect 26 | github.com/getlantern/byteexec v0.0.0-20170405023437-4cfb26ec74f4 // indirect 27 | github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect 28 | github.com/getlantern/elevate v0.0.0-20200430163644-2881a121236d // indirect 29 | github.com/getlantern/errors v1.0.3 // indirect 30 | github.com/getlantern/filepersist v0.0.0-20160317154340-c5f0cd24e799 // indirect 31 | github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect 32 | github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect 33 | github.com/getlantern/iptool v0.0.0-20230112135223-c00e863b2696 // indirect 34 | github.com/getlantern/mtime v0.0.0-20200417132445-23682092d1f7 // indirect 35 | github.com/go-logr/logr v1.4.1 // indirect 36 | github.com/go-logr/stdr v1.2.2 // indirect 37 | github.com/go-stack/stack v1.8.1 // indirect 38 | github.com/golang/snappy v0.0.4 // indirect 39 | github.com/klauspost/compress v1.17.4 // indirect 40 | github.com/klauspost/pgzip v1.2.5 // indirect 41 | github.com/mholt/archiver/v3 v3.5.1 // indirect 42 | github.com/nwaples/rardecode v1.1.0 // indirect 43 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 44 | github.com/pierrec/lz4/v4 v4.1.15 // indirect 45 | github.com/pmezard/go-difflib v1.0.0 // indirect 46 | github.com/rogpeppe/go-internal v1.13.1 // indirect 47 | github.com/ulikunitz/xz v0.5.10 // indirect 48 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 49 | go.opentelemetry.io/otel v1.19.0 // indirect 50 | go.opentelemetry.io/otel/metric v1.19.0 // indirect 51 | go.opentelemetry.io/otel/trace v1.19.0 // indirect 52 | go.uber.org/multierr v1.11.0 // indirect 53 | go.uber.org/zap v1.26.0 // indirect 54 | golang.org/x/crypto v0.36.0 // indirect 55 | golang.org/x/sys v0.31.0 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alitto/pond/v2 v2.1.5 h1:2pp/KAPcb02NSpHsjjnxnrTDzogMLsq+vFf/L0DB84A= 2 | github.com/alitto/pond/v2 v2.1.5/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= 3 | github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 4 | github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= 5 | github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 6 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 7 | github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= 8 | github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= 13 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= 14 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 15 | github.com/getlantern/byteexec v0.0.0-20170405023437-4cfb26ec74f4 h1:Nqmy8i81dzokjNHpyOg24gnQBeGRF7D51m8HmBRNn0Y= 16 | github.com/getlantern/byteexec v0.0.0-20170405023437-4cfb26ec74f4/go.mod h1:4WCQkaCIwta0KlF9bQZA1jYqp8bzIS2PeCqjnef8nZ8= 17 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= 18 | github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA= 19 | github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo= 20 | github.com/getlantern/elevate v0.0.0-20180207094634-c2e2e4901072/go.mod h1:T4VB2POK13lsPLFV98WJQrL7gAXYD9TyJxBU2P8c8p4= 21 | github.com/getlantern/elevate v0.0.0-20200430163644-2881a121236d h1:o0EHYAq7u9/umRZE0PpJ00GYQvxPxVUvtoDkUca2guQ= 22 | github.com/getlantern/elevate v0.0.0-20200430163644-2881a121236d/go.mod h1:+nYKXAqGigcDHB3as7WikMzg3eIHzGUbLnBKOCBJeUE= 23 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= 24 | github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= 25 | github.com/getlantern/errors v1.0.3 h1:Ne4Ycj7NI1BtSyAfVeAT/DNoxz7/S2BUc3L2Ht1YSHE= 26 | github.com/getlantern/errors v1.0.3/go.mod h1:m8C7H1qmouvsGpwQqk/6NUpIVMpfzUPn608aBZDYV04= 27 | github.com/getlantern/fdcount v0.0.0-20190912142506-f89afd7367c4 h1:JdD4XSaT6/j6InM7MT1E4WRvzR8gurxfq53A3ML3B/Q= 28 | github.com/getlantern/fdcount v0.0.0-20190912142506-f89afd7367c4/go.mod h1:XZwE+iIlAgr64OFbXKFNCllBwV4wEipPx8Hlo2gZdbM= 29 | github.com/getlantern/filepersist v0.0.0-20160317154340-c5f0cd24e799 h1:FhkPUYCQYmoxS02r2GRrIV7dahUIncRl36xzs3/mnjA= 30 | github.com/getlantern/filepersist v0.0.0-20160317154340-c5f0cd24e799/go.mod h1:8DGAx0LNUfXNnEH+fXI0s3OCBA/351kZCiz/8YSK3i8= 31 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= 32 | github.com/getlantern/golog v0.0.0-20210606115803-bce9f9fe5a5f/go.mod h1:ZyIjgH/1wTCl+B+7yH1DqrWp6MPJqESmwmEQ89ZfhvA= 33 | github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0= 34 | github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU= 35 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= 36 | github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU= 37 | github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM= 38 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= 39 | github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE= 40 | github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A= 41 | github.com/getlantern/iptool v0.0.0-20210721034953-519bf8ce0147/go.mod h1:hfspzdRcvJ130tpTPL53/L92gG0pFtvQ6ln35ppwhHE= 42 | github.com/getlantern/iptool v0.0.0-20230112135223-c00e863b2696 h1:D7wbL2Ww6QN5SblEDMiQcFulqz2jgcvawKaNBTzHLvQ= 43 | github.com/getlantern/iptool v0.0.0-20230112135223-c00e863b2696/go.mod h1:hfspzdRcvJ130tpTPL53/L92gG0pFtvQ6ln35ppwhHE= 44 | github.com/getlantern/keepcurrent v0.0.0-20240126172110-2e0264ca385d h1:2/9rPC1xT+jWBnAe4mD6Q0LWkByFYGcTiKsmDWbv2T4= 45 | github.com/getlantern/keepcurrent v0.0.0-20240126172110-2e0264ca385d/go.mod h1:enUAvxkJ15QUtTKOKoO9WJV2L5u33P8YmqkC+iu8iT4= 46 | github.com/getlantern/keyman v0.0.0-20200819205636-76fef27c39f1 h1:8qNXKWQBgmMfaXXTNfYs1D6i42etSjvwxfCSlmvHHr8= 47 | github.com/getlantern/keyman v0.0.0-20200819205636-76fef27c39f1/go.mod h1:FMf0g72BHs14jVcD8i8ubEk4sMB6JdidBn67d44i3ws= 48 | github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848 h1:2MhMMVBTnaHrst6HyWFDhwQCaJ05PZuOv1bE2gN8WFY= 49 | github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848/go.mod h1:+F5GJ7qGpQ03DBtcOEyQpM30ix4BLswdaojecFtsdy8= 50 | github.com/getlantern/mtime v0.0.0-20200417132445-23682092d1f7 h1:03J6Cb42EG06lHgpOFGm5BOax4qFqlSbSeKO2RGrj2g= 51 | github.com/getlantern/mtime v0.0.0-20200417132445-23682092d1f7/go.mod h1:GfzwugvtH7YcmNIrHHizeyImsgEdyL88YkdnK28B14c= 52 | github.com/getlantern/netx v0.0.0-20211206143627-7ccfeb739cbd/go.mod h1:WEXF4pfIfnHBUAKwLa4DW7kcEINtG6wjUkbL2btwXZQ= 53 | github.com/getlantern/netx v0.0.0-20240814210628-0984f52e2d18 h1:I5xFq/HkvWGUPysqC8LQH9oks1WaM9BpcB+fjmvMRic= 54 | github.com/getlantern/netx v0.0.0-20240814210628-0984f52e2d18/go.mod h1:4WkWbHy7Mqri9lxpLFN6dOU5nUy3kyNCpHxSRQZocv0= 55 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= 56 | github.com/getlantern/ops v0.0.0-20200403153110-8476b16edcd6/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= 57 | github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= 58 | github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag= 59 | github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534/go.mod h1:ZsLfOY6gKQOTyEcPYNA9ws5/XHZQFroxqCOhHjGcs9Y= 60 | github.com/getlantern/tlsdialer/v3 v3.0.3 h1:OXzzAqO8YojBOu2Kk8wquX2zbFmgJjji41RpaT6knLg= 61 | github.com/getlantern/tlsdialer/v3 v3.0.3/go.mod h1:hwA0X81pnrgx7GEwddaGWSxqr6eLBm7A0rrUMK2J7KY= 62 | github.com/getlantern/waitforserver v1.0.1 h1:xBjqJ3GgEk9JMWnDgRSiNHXINi6Lv2tGNjJR0hCkHFY= 63 | github.com/getlantern/waitforserver v1.0.1/go.mod h1:K1oSA8lNKgQ9iC00OFpMfMNm4UMrsxoGCdHf0NT9LGs= 64 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 65 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 66 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 67 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 68 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 69 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 70 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 71 | github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= 72 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 73 | github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg= 74 | github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 75 | github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 76 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 77 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 78 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 79 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 80 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 81 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 82 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 83 | github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 84 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 85 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 86 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 87 | github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= 88 | github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 89 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 90 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 91 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 92 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 93 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 94 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 95 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 96 | github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= 97 | github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= 98 | github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= 99 | github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= 100 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= 101 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 102 | github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 103 | github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= 104 | github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 105 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 106 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 107 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 108 | github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0= 109 | github.com/refraction-networking/utls v1.7.1 h1:dxg+jla3uocgN8HtX+ccwDr68uCBBO3qLrkZUbqkcw0= 110 | github.com/refraction-networking/utls v1.7.1/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ= 111 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 112 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 115 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 116 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 117 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 118 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 119 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 120 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 121 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 122 | github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 123 | github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 124 | github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= 125 | github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 126 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 127 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 128 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 129 | go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= 130 | go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= 131 | go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= 132 | go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= 133 | go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= 134 | go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= 135 | go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= 136 | go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= 137 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 138 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 139 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 140 | go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 141 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 142 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 143 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 144 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 145 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 146 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 147 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 148 | golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 149 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 150 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 151 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 152 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 153 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 154 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 155 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 156 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 157 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 158 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 161 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 166 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 167 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 168 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 169 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 170 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 171 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 172 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 173 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 174 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 175 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 176 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 177 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 178 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 179 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 180 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 181 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 182 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 183 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 185 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 186 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 187 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 188 | -------------------------------------------------------------------------------- /test_support.go: -------------------------------------------------------------------------------- 1 | package fronted 2 | 3 | import ( 4 | "crypto/x509" 5 | "testing" 6 | 7 | "github.com/getlantern/keyman" 8 | ) 9 | 10 | var ( 11 | testProviderID = "cloudfront" 12 | pingTestURL = "https://d157vud77ygy87.cloudfront.net/ping" 13 | testHosts = map[string]string(nil) 14 | testMasquerades = DefaultCloudfrontMasquerades 15 | ) 16 | 17 | // ConfigureForTest configures fronted for testing using default masquerades and 18 | // certificate authorities. 19 | func ConfigureForTest(t *testing.T) Fronted { 20 | return ConfigureCachingForTest(t, "") 21 | } 22 | 23 | func ConfigureCachingForTest(t *testing.T, cacheFile string) Fronted { 24 | certs := trustedCACerts(t) 25 | p := testProviders() 26 | defaultFrontedProviderID = testProviderID 27 | f := NewFronted(WithCacheFile(cacheFile)) 28 | f.onNewFronts(certs, p) 29 | return f 30 | } 31 | 32 | func ConfigureHostAlaisesForTest(t *testing.T, hosts map[string]string) Fronted { 33 | certs := trustedCACerts(t) 34 | p := testProvidersWithHosts(hosts) 35 | defaultFrontedProviderID = testProviderID 36 | f := NewFronted() 37 | f.onNewFronts(certs, p) 38 | return f 39 | } 40 | 41 | func trustedCACerts(t *testing.T) *x509.CertPool { 42 | certs := make([]string, 0, len(DefaultTrustedCAs)) 43 | for _, ca := range DefaultTrustedCAs { 44 | certs = append(certs, ca.Cert) 45 | } 46 | pool, err := keyman.PoolContainingCerts(certs...) 47 | if err != nil { 48 | log.Errorf("Could not create pool %v", err) 49 | t.Fatalf("Unable to set up cert pool") 50 | } 51 | return pool 52 | } 53 | 54 | func testProviders() map[string]*Provider { 55 | return map[string]*Provider{ 56 | testProviderID: NewProvider(testHosts, pingTestURL, testMasquerades, nil, nil, nil, ""), 57 | } 58 | } 59 | 60 | func testProvidersWithHosts(hosts map[string]string) map[string]*Provider { 61 | return map[string]*Provider{ 62 | testProviderID: NewProvider(hosts, pingTestURL, testMasquerades, nil, nil, nil, ""), 63 | } 64 | } 65 | func testAkamaiProvidersWithHosts(hosts map[string]string, sniConfig *SNIConfig) map[string]*Provider { 66 | frontingSNIs := map[string]*SNIConfig{ 67 | "test": sniConfig, 68 | } 69 | return map[string]*Provider{ 70 | "akamai": NewProvider(hosts, "https://fronted-ping.dsa.akamai.getiantem.org/ping", DefaultAkamaiMasquerades, nil, frontingSNIs, nil, "test"), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /updateFrontedConfig.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | # Check if yq is installed and install it if not 5 | if ! command -v yq &> /dev/null 6 | then 7 | echo "yq could not be found, installing it now" 8 | # If we're on MacOS, use brew to install yq 9 | if [[ "$OSTYPE" == "darwin"* ]]; then 10 | brew install yq 11 | # If we're on Linux, use apt-get to install yq 12 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 13 | sudo apt-get install yq 14 | else 15 | echo "Unsupported OS" 16 | exit 1 17 | fi 18 | fi 19 | 20 | cd ../lantern-binaries 21 | echo `pwd` 22 | echo "trustedcas:" > fronted.yaml 23 | curl https://globalconfig.flashlightproxy.com/global.yaml.gz | gunzip | yq '.trustedcas' >> fronted.yaml 24 | curl https://globalconfig.flashlightproxy.com/global.yaml.gz | gunzip | yq '.client.fronted' >> fronted.yaml 25 | 26 | # Compress the generated file 27 | gzip -c fronted.yaml > fronted.yaml.gz 28 | 29 | # If the generated file is different from the current one, commit and push the changes 30 | if ! git diff --quiet fronted.yaml.gz; then 31 | git add fronted.yaml.gz 32 | git commit -m "Update fronted.yaml.gz" 33 | git push 34 | else 35 | echo "No changes detected in fronted.yaml.gz" 36 | fi --------------------------------------------------------------------------------