diff --git a/config/client_config.go b/config/client_config.go index 4ba9fecc8..b71e2e9e5 100644 --- a/config/client_config.go +++ b/config/client_config.go @@ -43,7 +43,7 @@ type ProviderConfig struct { Validator *ValidatorConfig PassthroughPatterns []string FrontingSNIs map[string]*fronted.SNIConfig - VerifyHostname *string + VerifyHostname *string `yaml:"verifyHostname,omitempty"` } // returns a fronted.ResponseValidator specified by the diff --git a/genconfig/genconfig b/genconfig/genconfig index b34a87821..56d26fc68 100755 --- a/genconfig/genconfig +++ b/genconfig/genconfig @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:738c2ad88d714e9cfeb9e61869be7ebb850412bc5c8702217e3e6a40cf382768 -size 34839312 +oid sha256:33bad90db6008dc8deaa61998b380c830bdb88b5f46bd59b88f2441da4a4336b +size 34832016 diff --git a/genconfig/genconfig.go b/genconfig/genconfig.go index 27f8b1323..8309fc7fe 100644 --- a/genconfig/genconfig.go +++ b/genconfig/genconfig.go @@ -1,6 +1,8 @@ package main import ( + "bytes" + "context" "crypto/x509" _ "embed" "encoding/base64" @@ -59,34 +61,48 @@ var ( blacklist = make(filter) proxiedSites = make(filter) - ftVersion string +) + +type ConfigGenerator struct { + ftVersion string - inputCh = make(chan string) - masqueradesCh = make(chan *masquerade) + inputCh chan string + masqueradesCh chan *masquerade wg sync.WaitGroup - providers map[string]*provider // supported fronting providers -) + Providers map[string]*provider // supported fronting providers +} + +func NewConfigGenerator() *ConfigGenerator { + return &ConfigGenerator{ + inputCh: make(chan string), + masqueradesCh: make(chan *masquerade), + Providers: loadMapping(), + wg: sync.WaitGroup{}, + } +} //go:embed provider_map.yaml var mappingData []byte -func populateProviders() { +func loadMapping() map[string]*provider { var mapping map[string]ProviderConfig err := yaml.Unmarshal(mappingData, &mapping) if err != nil { - panic(fmt.Sprintf("Mapping file is invalid: %v", err)) + panic(fmt.Errorf("mapping file is invalid: %w", err)) } - providers = make(map[string]*provider) + providers := make(map[string]*provider) for name, p := range mapping { - providers[name] = newProvider(p.Ping, p.Mapping, p.FrontingSNIs, &config.ValidatorConfig{RejectStatus: []int{p.RejectStatus}}) + providers[name] = newProvider(p.Ping, p.Mapping, p.FrontingSNIs, &config.ValidatorConfig{RejectStatus: []int{p.RejectStatus}}, p.VerifyHostname) } + return providers } type ProviderConfig struct { - Ping string `yaml:"ping"` - RejectStatus int `yaml:"rejectStatus"` - Mapping map[string]string `yaml:"mapping"` - FrontingSNIs map[string]*fronted.SNIConfig `yaml:"frontingsnis"` + Ping string `yaml:"ping"` + RejectStatus int `yaml:"rejectStatus"` + Mapping map[string]string `yaml:"mapping"` + FrontingSNIs map[string]*fronted.SNIConfig `yaml:"frontingsnis"` + VerifyHostname *string `yaml:"verifyHostname,omitempty"` } type filter map[string]bool @@ -106,21 +122,23 @@ type castat struct { } type provider struct { - HostAliases map[string]string - TestURL string - Masquerades []*masquerade - Validator *config.ValidatorConfig - Enabled bool - FrontingSNIs map[string]*fronted.SNIConfig + HostAliases map[string]string + TestURL string + Masquerades []*masquerade + Validator *config.ValidatorConfig + Enabled bool + FrontingSNIs map[string]*fronted.SNIConfig + VerifyHostname *string } -func newProvider(testURL string, hosts map[string]string, frontingSNIs map[string]*fronted.SNIConfig, validator *config.ValidatorConfig) *provider { +func newProvider(testURL string, hosts map[string]string, frontingSNIs map[string]*fronted.SNIConfig, validator *config.ValidatorConfig, verifyHostname *string) *provider { return &provider{ - HostAliases: hosts, - TestURL: testURL, - Masquerades: make([]*masquerade, 0), - Validator: validator, - FrontingSNIs: frontingSNIs, + HostAliases: hosts, + TestURL: testURL, + Masquerades: make([]*masquerade, 0), + Validator: validator, + FrontingSNIs: frontingSNIs, + VerifyHostname: verifyHostname, } } @@ -135,6 +153,24 @@ func (ss *stringsFlag) Set(value string) error { return nil } +func (c *ConfigGenerator) GenerateConfig(ctx context.Context, yamlTmpl string, masquerades []string, proxiedSites, blacklist filter, numberOfWorkers int, minFreq float64, minMasquerades, maxMasquerades int) ([]byte, error) { + if err := c.loadFtVersion(); err != nil { + return nil, err + } + + go c.feedMasquerades() + cas, masqs := c.coalesceMasquerades() + if err := c.vetAndAssignMasquerades(cas, masqs, minMasquerades, maxMasquerades, numberOfWorkers); err != nil { + return nil, err + } + + model, err := c.buildModel("cloud.yaml", cas) + if err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + return generateTemplate(model, yamlTmpl) +} + func main() { flag.Var(&masqueradesInFiles, "masquerades", "Path to file containing list of masquerades to use, with one space-separated 'ip domain provider' set per line (e.g. masquerades.txt)") flag.Var(&enabledProviders, "enable-provider", "Enable fronting provider") @@ -146,13 +182,13 @@ func main() { os.Exit(1) } - populateProviders() + generator := NewConfigGenerator() numcores := runtime.NumCPU() log.Debugf("Using all %d cores on machine", numcores) runtime.GOMAXPROCS(numcores) for _, pid := range enabledProviders { - p, ok := providers[pid] + p, ok := generator.Providers[pid] if !ok { log.Fatalf("Invalid/Unknown fronting provider: %s", pid) } @@ -162,25 +198,16 @@ func main() { loadMasquerades() loadProxiedSitesList() loadBlacklist() - loadFtVersion() yamlTmpl := string(embeddedconfig.GlobalTemplate) - - go feedMasquerades() - cas, masqs := coalesceMasquerades() - vetAndAssignMasquerades(cas, masqs) - - tempConfigDir, err := os.MkdirTemp("", "genconfig") + template, err := generator.GenerateConfig(context.Background(), yamlTmpl, masquerades, proxiedSites, blacklist, *numberOfWorkers, *minFreq, *minMasquerades, *maxMasquerades) if err != nil { - log.Fatalf("Unable to create temp configdir: %v", tempConfigDir) + log.Fatalf("Error generating configuration: %s", err) } - defer os.RemoveAll(tempConfigDir) - model, err := buildModel(tempConfigDir, "cloud.yaml", cas) - if err != nil { - log.Fatalf("Invalid configuration: %s", err) + if err := os.WriteFile("cloud.yaml", template, 0644); err != nil { + log.Fatalf("Error writing configuration: %s", err) } - generateTemplate(model, yamlTmpl, "cloud.yaml") } func loadMasquerades() { @@ -239,22 +266,21 @@ func loadProxiedSitesList() { } } -func loadFtVersion() { +func (c *ConfigGenerator) loadFtVersion() error { res, err := http.Get(ftVersionFile) if err != nil { - log.Fatalf("Error fetching FireTweet version file: %s", err) + return fmt.Errorf("error fetching FireTweet version file: %w", err) } defer func() { - if err := res.Body.Close(); err != nil { - log.Debugf("Error closing response body: %v", err) - } + _ = res.Body.Close() }() body, err := io.ReadAll(res.Body) if err != nil { - log.Fatalf("Could not read FT version file: %s", err) + return fmt.Errorf("could not read FT version file: %w", err) } - ftVersion = strings.TrimSpace(string(body)) + c.ftVersion = strings.TrimSpace(string(body)) + return nil } func loadBlacklist() { @@ -280,10 +306,10 @@ func loadTemplate(name string) string { return string(bytes) } -func feedMasquerades() { - wg.Add(*numberOfWorkers) +func (c *ConfigGenerator) feedMasquerades() { + c.wg.Add(*numberOfWorkers) for i := 0; i < *numberOfWorkers; i++ { - go grabCerts() + go c.grabCerts() } // feed masquerades in random order to get different order each time we run @@ -291,20 +317,20 @@ func feedMasquerades() { for _, i := range randomOrder { masq := masquerades[i] if masq != "" { - inputCh <- masq + c.inputCh <- masq } } - close(inputCh) - wg.Wait() - close(masqueradesCh) + close(c.inputCh) + c.wg.Wait() + close(c.masqueradesCh) } // grabCerts grabs certificates for the masquerades received on masqueradesCh and sends // *masquerades to masqueradesCh. -func grabCerts() { - defer wg.Done() +func (c *ConfigGenerator) grabCerts() { + defer c.wg.Done() - for masq := range inputCh { + for masq := range c.inputCh { parts := strings.Split(masq, " ") var providerID string if len(parts) == 2 { @@ -316,7 +342,7 @@ func grabCerts() { continue } - provider, ok := providers[providerID] + provider, ok := c.Providers[providerID] if !ok { log.Debugf("Skipping masquerade for unknown provider %s", providerID) continue @@ -356,7 +382,7 @@ func grabCerts() { } log.Debugf("Successfully grabbed certs for: %v", domain) - masqueradesCh <- &masquerade{ + c.masqueradesCh <- &masquerade{ Domain: domain, IpAddress: ip, ProviderID: providerID, @@ -365,11 +391,11 @@ func grabCerts() { } } -func coalesceMasquerades() (map[string]*castat, []*masquerade) { +func (c *ConfigGenerator) coalesceMasquerades() (map[string]*castat, []*masquerade) { count := make(map[string]int) // by provider allCAs := make(map[string]*castat) allMasquerades := make([]*masquerade, 0) - for m := range masqueradesCh { + for m := range c.masqueradesCh { ca := allCAs[m.RootCA.Cert] if ca == nil { ca = m.RootCA @@ -408,13 +434,13 @@ func coalesceMasquerades() (map[string]*castat, []*masquerade) { return trustedCAs, trustedMasquerades } -func vetAndAssignMasquerades(cas map[string]*castat, masquerades []*masquerade) { +func (c *ConfigGenerator) vetAndAssignMasquerades(cas map[string]*castat, masquerades []*masquerade, minMasquerades, maxMasquerades, numOfWorkers int) error { byProvider := make(map[string][]*masquerade, 0) for _, m := range masquerades { byProvider[m.ProviderID] = append(byProvider[m.ProviderID], m) } for pid, candidates := range byProvider { - provider, ok := providers[pid] + provider, ok := c.Providers[pid] if !ok { log.Debugf("Not vetting masquerades for unknown provider %s", pid) continue @@ -422,15 +448,17 @@ func vetAndAssignMasquerades(cas map[string]*castat, masquerades []*masquerade) if !provider.Enabled { log.Debugf("Not vetting masquerades for disabled provider %s", pid) } - vetted := vetMasquerades(cas, candidates) - if len(vetted) < *minMasquerades { - log.Fatalf("%s: %d masquerades was fewer than minimum of %d", pid, len(vetted), *minMasquerades) + vetted := c.vetMasquerades(cas, candidates, numOfWorkers, maxMasquerades) + if len(vetted) < minMasquerades { + log.Fatalf("%s: %d masquerades was fewer than minimum of %d", pid, len(vetted), minMasquerades) + return fmt.Errorf("%s: %d masquerades was fewer than minimum of %d", pid, len(vetted), minMasquerades) } provider.Masquerades = vetted } + return nil } -func vetMasquerades(cas map[string]*castat, masquerades []*masquerade) []*masquerade { +func (c *ConfigGenerator) vetMasquerades(cas map[string]*castat, masquerades []*masquerade, numberOfWorkers int, maxMasquerades int) []*masquerade { certPool := x509.NewCertPool() for _, ca := range cas { cert, err := keyman.LoadCertificateFromPEMBytes([]byte(strings.Replace(ca.Cert, `\n`, "\n", -1))) @@ -442,7 +470,7 @@ func vetMasquerades(cas map[string]*castat, masquerades []*masquerade) []*masque log.Debug("Added cert to pool") } - wg.Add(*numberOfWorkers) + c.wg.Add(numberOfWorkers) inCh := make(chan *masquerade, len(masquerades)) outCh := make(chan *masquerade, len(masquerades)) for _, masquerade := range masquerades { @@ -450,26 +478,26 @@ func vetMasquerades(cas map[string]*castat, masquerades []*masquerade) []*masque } close(inCh) - for i := 0; i < *numberOfWorkers; i++ { - go doVetMasquerades(certPool, inCh, outCh) + for i := 0; i < numberOfWorkers; i++ { + go c.doVetMasquerades(certPool, inCh, outCh) } - wg.Wait() + c.wg.Wait() close(outCh) - result := make([]*masquerade, 0, *maxMasquerades) + result := make([]*masquerade, 0, maxMasquerades) count := 0 for masquerade := range outCh { result = append(result, masquerade) count++ - if count == *maxMasquerades { + if count == maxMasquerades { break } } return result } -func doVetMasquerades(certPool *x509.CertPool, inCh chan *masquerade, outCh chan *masquerade) { +func (c *ConfigGenerator) doVetMasquerades(certPool *x509.CertPool, inCh chan *masquerade, outCh chan *masquerade) { log.Debug("Starting to vet masquerades") for _m := range inCh { m := &fronted.Masquerade{ @@ -477,7 +505,7 @@ func doVetMasquerades(certPool *x509.CertPool, inCh chan *masquerade, outCh chan IpAddress: _m.IpAddress, } - provider, ok := providers[_m.ProviderID] + provider, ok := c.Providers[_m.ProviderID] if !ok { log.Debugf("%v (%v) failed to vet: unknown provider %v", m.Domain, m.IpAddress, _m.ProviderID) continue @@ -491,17 +519,17 @@ func doVetMasquerades(certPool *x509.CertPool, inCh chan *masquerade, outCh chan } } log.Debug("Done vetting masquerades") - wg.Done() + c.wg.Done() } -func buildModel(configDir string, configName string, cas map[string]*castat) (map[string]interface{}, error) { +func (c *ConfigGenerator) buildModel(configName string, cas map[string]*castat) (map[string]interface{}, error) { casList := make([]*castat, 0, len(cas)) for _, ca := range cas { casList = append(casList, ca) } sort.Sort(ByTotal(casList)) - cfMasquerades := providers[defaultProviderID].Masquerades + cfMasquerades := c.Providers[defaultProviderID].Masquerades if len(cfMasquerades) == 0 { return nil, fmt.Errorf("%s: configuration contains no cloudfront masquerades for older clients.", configName) } @@ -509,7 +537,7 @@ func buildModel(configDir string, configName string, cas map[string]*castat) (ma aliased := make(map[string]bool) enabledProviders := make(map[string]*provider) - for k, v := range providers { + for k, v := range c.Providers { if v.Enabled { if len(v.Masquerades) > 0 { sort.Sort(ByDomain(v.Masquerades)) @@ -542,30 +570,22 @@ func buildModel(configDir string, configName string, cas map[string]*castat) (ma "cloudfrontMasquerades": cfMasquerades, "providers": enabledProviders, "proxiedsites": ps, - "ftVersion": ftVersion, + "ftVersion": c.ftVersion, }, nil } -func generateTemplate(model map[string]interface{}, tmplString string, filename string) { - tmpl, err := template.New(filename).Funcs(funcMap).Parse(tmplString) +func generateTemplate(model map[string]interface{}, tmplString string) ([]byte, error) { + tmpl, err := template.New("").Funcs(funcMap).Parse(tmplString) if err != nil { log.Errorf("Unable to parse template: %s", err) - return - } - out, err := os.Create(filename) - if err != nil { - log.Errorf("Unable to create %s: %s", filename, err) - return + return []byte{}, err } - defer func() { - if err := out.Close(); err != nil { - log.Debugf("Error closing file: %v", err) - } - }() - err = tmpl.Execute(out, model) + var out bytes.Buffer + err = tmpl.Execute(&out, model) if err != nil { - log.Errorf("Unable to generate %s: %s", filename, err) + log.Errorf("Unable to generate: %s", err) } + return out.Bytes(), nil } func run(prg string, args ...string) (string, error) { diff --git a/genconfig/genconfig_test.go b/genconfig/genconfig_test.go new file mode 100644 index 000000000..ca0ad3798 --- /dev/null +++ b/genconfig/genconfig_test.go @@ -0,0 +1,167 @@ +package main + +import ( + "context" + _ "embed" + "fmt" + "strings" + "testing" + + "github.com/getlantern/flashlight/v7/config" + "github.com/getlantern/flashlight/v7/domainrouting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +// This variable is only being used for test purposes! +// The original global.yaml.tmpl is located at flashlight repository: embbededconfig/global.yaml.tmpl +// +//go:embed testdata/global_test.yaml.tmpl +var globalTemplateTest string + +//go:embed testdata/blacklist.txt +var blacklistTest []byte + +func TestGenerateConfig(t *testing.T) { + require.NotEmpty(t, globalTemplateTest) + + ctx := context.Background() + + masquerades = []string{"96.16.55.171 a248.e.akamai.net akamai", "104.123.154.46 a248.e.akamai.net akamai", "3.164.129.16 Smentertainment.com cloudfront", "204.246.164.205 Smentertainment.com cloudfront"} + proxiedSites := filter{"googlevideo.com": true, "googleapis.com": true, "google.com": true} + blacklist := make(filter) + + loadFilterList(blacklistTest, &blacklist) + + var tests = []struct { + name string + givenContext context.Context + givenTemplate string + givenMasquerades []string + givenProxiedSites filter + givenBlacklist filter + givenNumberOfWorkers int + givenMinFrequency float64 + givenMinMasquerades int + givenMaxMasquerades int + assert func(*testing.T, string, error) + setup func() *ConfigGenerator + }{ + { + name: "should generate config with success", + givenContext: ctx, + givenTemplate: globalTemplateTest, + givenMasquerades: masquerades, + givenProxiedSites: proxiedSites, + givenBlacklist: blacklist, + givenNumberOfWorkers: 10, + givenMinFrequency: 10, + givenMinMasquerades: 1, + givenMaxMasquerades: 10, + assert: func(t *testing.T, cfg string, err error) { + require.NoError(t, err) + require.NotEmpty(t, cfg) + globalConfig, err := parseGlobal(ctx, []byte(cfg)) + require.NoError(t, err) + assert.NotNil(t, globalConfig) + assert.NotNil(t, globalConfig.Client) + assert.NotNil(t, globalConfig.Client.Fronted) + assert.NotNil(t, globalConfig.Client.Fronted.Providers) + assert.Contains(t, globalConfig.Client.Fronted.Providers, "cloudfront") + assert.Contains(t, globalConfig.Client.Fronted.Providers, "akamai") + assert.Contains(t, globalConfig.Client.MasqueradeSets, "cloudfront") + assert.NotNil(t, globalConfig.Client.Fronted.Providers["akamai"].VerifyHostname) + assert.Equal(t, *globalConfig.Client.Fronted.Providers["akamai"].VerifyHostname, "akamai.com") + }, + setup: func() *ConfigGenerator { + generator := NewConfigGenerator() + for _, provider := range generator.Providers { + provider.Enabled = true + } + require.NotNil(t, generator.Providers[defaultProviderID]) + return generator + }, + }, + { + name: "should generate config with success even with cloudfront disabled", + setup: func() *ConfigGenerator { + configGenerator := NewConfigGenerator() + configGenerator.Providers["cloudfront"].Enabled = false + configGenerator.Providers["akamai"].Enabled = true + return configGenerator + }, + givenContext: ctx, + givenTemplate: globalTemplateTest, + givenMasquerades: masquerades, + givenProxiedSites: proxiedSites, + givenBlacklist: blacklist, + givenNumberOfWorkers: 10, + givenMinFrequency: 10, + givenMinMasquerades: 1, + givenMaxMasquerades: 10, + assert: func(t *testing.T, cfg string, err error) { + require.NoError(t, err) + require.NotEmpty(t, cfg) + require.NoError(t, err) + + globalConfig, err := parseGlobal(ctx, []byte(cfg)) + require.NoError(t, err) + assert.NotNil(t, globalConfig) + assert.NotNil(t, globalConfig.Client) + assert.NotNil(t, globalConfig.Client.Fronted) + assert.NotNil(t, globalConfig.Client.Fronted.Providers) + assert.Contains(t, globalConfig.Client.MasqueradeSets, "cloudfront") + assert.Len(t, globalConfig.Client.MasqueradeSets["cloudfront"], 2) + assert.NotContains(t, globalConfig.Client.Fronted.Providers, "cloudfront") + assert.Contains(t, globalConfig.Client.Fronted.Providers, "akamai") + assert.NotNil(t, globalConfig.Client.Fronted.Providers["akamai"].VerifyHostname) + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + generator := tt.setup() + cfg, err := generator.GenerateConfig( + tt.givenContext, + tt.givenTemplate, + tt.givenMasquerades, + tt.givenProxiedSites, + tt.givenBlacklist, + tt.givenNumberOfWorkers, + tt.givenMinFrequency, + tt.givenMinMasquerades, + tt.givenMaxMasquerades) + tt.assert(t, string(cfg), err) + }) + } +} + +func parseGlobal(ctx context.Context, bytes []byte) (*config.Global, error) { + cfg := &config.Global{} + err := yaml.Unmarshal(bytes, cfg) + if err != nil { + return nil, fmt.Errorf("Unable to parse global config: %v", err) + } + + var direct, proxied int + for _, rule := range cfg.DomainRoutingRules { + switch rule { + case domainrouting.Direct: + direct++ + case domainrouting.Proxy: + proxied++ + } + } + + return cfg, nil +} + +func loadFilterList(data []byte, res *filter) { + for _, domain := range strings.Split(string(data), "\n") { + if domain != "" { + (*res)[domain] = true + } + } +} diff --git a/genconfig/provider_map.yaml b/genconfig/provider_map.yaml index a64d48a0a..76a9a86db 100644 --- a/genconfig/provider_map.yaml +++ b/genconfig/provider_map.yaml @@ -26,6 +26,7 @@ cloudfront: akamai: ping: 'https://fronted-ping.dsa.akamai.getiantem.org/ping' rejectStatus: 403 + verifyHostname: "akamai.com" mapping: iantem.io: nonexistent.iantem.io api.getiantem.org: api.dsa.akamai.getiantem.org diff --git a/genconfig/testdata/blacklist.txt b/genconfig/testdata/blacklist.txt new file mode 100644 index 000000000..522d25bbc --- /dev/null +++ b/genconfig/testdata/blacklist.txt @@ -0,0 +1,2 @@ +example1.com +example2.com diff --git a/genconfig/testdata/global_test.yaml.tmpl b/genconfig/testdata/global_test.yaml.tmpl new file mode 100644 index 000000000..adeef1e1f --- /dev/null +++ b/genconfig/testdata/global_test.yaml.tmpl @@ -0,0 +1,421 @@ +# cloud.yaml contains the default configuration that's made available on the +# internet. +uiaddr: 127.0.0.1:16823 +bordareportinterval: 15m0s +bordasamplepercentage: 0.0001 +pingsamplepercentage: 0 # back compatiblity for Lantern 5.7.2 and below +globalconfigpollinterval: 1h0m0s +proxyconfigpollinterval: 1m0s +logglysamplepercentage: 0.0001 # back compatiblity for Lantern before 3.6.0 +reportissueemail: support@lantern.jitbit.com + +# featuresenabled selectively enables certain features to some of the clients. +# +# The available features are: +# - proxybench +# - trafficlog +# - noborda +# - probeproxies +# - shortcut +# - detour +# - nohttpseverywhere +# +# Note - if noshortcut and nodetour are enabled, then all traffic will be proxied (no split tunneling) +# +# Each feature can be enabled on a *list* of client groups each defined by a +# combination of the following criteria. The zero value of each criterion means +# wildcard match. See flashlight/config/features.go for reference. +# - label: Meaningful string useful for collecting metrics +# - userfloor: 0-1. Together with userceil, defines a range of userIDs the group includes. +# - userceil: 0-1. +# - versionconstraints: A semantic version range of Lantern client, parsed by https://github.com/blang/semver. +# - platforms: Comma separated list of Lantern client platforms. Case-insensitive. +# - freeonly: Only if the current Lantern user is Free. +# - proonly: Only if the current Lantern user is Pro. Feature is disabled if both freeonly and proonly are set. +# - geocountries: Comma separated list of geolocated country of Lantern clients. Case-insensitive. +# - fraction: The fraction of clients to included when all other criteria are met. +# +# For example +# ------------------------------ +# +# featuresenabled: +# proxybench: +# - fraction: 0.01 # it used to be governed by bordasamplepercentage +# detour: +# - label: detour-ir +# platforms: windows,darwin,linux +# geocountries: ir +# - label: detour-cn +# geocountries: cn +# shortcut: +# - label: shortcut-cn +# geocountries: cn,ir +# probeproxies: +# - label: probeproxies +# geocountries: cn,ir +# + +featuresenabled: + interstitialads: + - label: show-interstitial-ads + geocountries: af,ax,al,dz,as,ad,ao,ai,aq,ag,ar,am,aw,au,at,az,bs,bh,bd,bb,by,be,bz,bj,bm,bt,bo,bq,ba,bw,bv,br,io,bn,bg,bf,bi,cv,kh,cm,ca,ky,cf,td,cl,cx,cc,co,km,cg,cd,ck,cr,hr,cu,cw,cy,cz,dk,dj,dm,do,ec,eg,sv,gq,er,ee,sz,et,fk,fo,fj,fi,fr,gf,pf,tf,ga,gm,ge,de,gh,gi,gr,gl,gd,gp,gu,gt,gg,gn,gw,gy,ht,hm,va,hn,hu,is,in,id,iq,ie,im,il,it,jm,jp,je,jo,kz,ke,ki,kp,kr,kw,kg,la,lv,lb,ls,lr,ly,li,lt,lu,mo,mg,mw,my,mv,ml,mt,mh,mq,mr,mu,yt,mx,fm,md,mc,mn,me,ms,ma,mz,mm,na,nr,np,nl,nc,nz,ni,ne,ng,nu,nf,mk,mp,no,om,pk,pw,ps,pa,pg,py,pe,ph,pn,pl,pt,pr,qa,re,ro,rw,bl,sh,kn,lc,mf,pm,vc,ws,sm,st,sa,sn,rs,sc,sl,sg,sx,sk,si,sb,so,za,gs,ss,es,lk,sd,sr,sj,se,ch,sy,tw,tj,tz,th,tl,tg,tk,to,tt,tn,tr,tm,tc,tv,ug,ua,ae,gb,us,um,uy,uz,vu,ve,vn,vg,vi,wf,eh,ye,zm,zw,ru,ir + platforms: android + otel: + - label: opentelemetry + noborda: + - label: disablebordaglobally + yinbi: + - label: yinbi + application: lantern + versionconstraints: "<1.0.0" + # googlesearchads: + # - label: lantern-ads + # application: lantern + # versionconstraints: ">6.7.7" + # platforms: darwin,windows + # geocountries: cn,ir + replica: + - label: replica-desktop + geocountries: cn,au,ir,de,ru,by,ca,ee,lt,ua,ae,lv,md,ge,uz,kz,tr,tm,am,az,kg,tj,tn,us,uk,at,be,br,cr,dk,do,ec,eg,sv,fr,fi,gr,gt,hk,in,ie,it,jp,kp,li,lu,mx,mm,nl,nz,ni,ps,pl,es,se,ch,sy,tw,th,tn,ye,cz,ge + platforms: darwin,windows,linux + # this filter works only for Lantern 6.1+ + application: lantern + - label: replica-android + geocountries: tn,de,ca,au,ae,at,be,br,ch,cn,cr,cz,dk,do,ec,eg,es,fi,fr,gr,gt,hk,ie,in,it,jp,kp,li,lu,mm,mx,ni,nl,nz,pl,ps,se,sv,sy,th,tr,tw,uk,us,ye + platforms: android + versionconstraints: ">=7.0.0" + - label: replica-android-early + geocountries: ir,ru,am,az,by,ee,ge,kz,kg,lv,lt,md,tj,tm,ua,uz + platforms: android + versionconstraints: ">=6.9.11" + - label: replica-android-qa + platforms: android + versionconstraints: ">=99.0.0" + - label: all-beam + application: beam + versionconstraints: "<4.0.0" + chat: + - label: chat-android + geocountries: ir + platforms: android + versionconstraints: ">=7.8.6" + fraction: 0.05 + # - label: chat-android-qa + # platforms: android + # versionconstraints: ">=99.0.0" + ######## New-Style configuration for detour, shortcut and probeproxies. These are disabled by default and have to be enabled in the config. ######## + #detour: + # - label: cn-detour + # geocountries: cn + shortcut: + - label: cn-shortcut + geocountries: cn + versionconstraints: "!7.4.0" + # - label: ir-desktop-shortcut + # geocountries: ir + # platforms: windows,darwin,linux + probeproxies: + - label: probeproxies + geocountries: ir,cn,us,nl,gb + + ######## End of New-Style configuration for detour, shortcut and probeproxies ######## + + ######## Old-Style configuration for detour, shortcut and probeproxies. These are enabled by default and have to be disabled in the config. ######## + nodetour: + - label: hk-privacy + geocountries: hk + - label: stealth + geocountries: us,cn + - label: detour-broken + geocountries: uz,ru + noshortcut: + - label: hk-privacy + geocountries: hk + - label: stealth + platforms: android + geocountries: ir + - label: shortcut-broken + geocountries: uz,ru + # noprobeproxies: + ######## End of Old-Style configuration for detour, shortcut and probeproxies ######## + + proxybench: + - label: proxybench + userfloor: 0 + userceil: 0 + geocountries: us + trackyoutube: + - label: cn-trackyoutube + userfloor: 0 + userceil: 0 + geocountries: us +featureoptions: + googlesearchads: + ad_format: "\n