From 923e22c2b40750f8103b4cbcbb860ad393d256a0 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Wed, 24 Jun 2026 16:07:34 -0600 Subject: [PATCH 1/3] config: apply default frontingsnis without a country code; preserve baked-in SNI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExpandedProvider previously generated an SNI only when a country code was set (countryCode != ''). The production client (radiance NewFronted) sets no country code, so every provider's arbitrary-SNI strategy was inert and all fronting used no SNI — conspicuous, since a browser-fingerprinted ClientHello normally carries SNI. Two changes: - The 'default' frontingsnis entry now applies even with no country code (country-specific entries still override). This activates a provider's default arbitrary-SNI strategy for every client — e.g. akamai's default list (verified: 20/20 edges front 202 with arbitrary SNI, same as no SNI, so no regression). akamai's explicit cn entry (usearbitrarysnis false) still wins for China, preserving no-SNI there. - A masquerade's baked-in SNI is now preserved through expansion unless an arbitrary SNI is generated for it. This lets the config pin a per-edge front SNI (e.g. aliyun edges that reject most SNIs but accept the service domain they actually host) without relying on a country code. Established behavior is unchanged for providers with no frontingsnis or a false default and no baked-in SNI (cloudfront): SNI stays empty. Co-Authored-By: Claude Opus 4.8 --- config.go | 35 +++++++++++++++++++++++++---------- config_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/config.go b/config.go index 3365f62..bb42e2a 100644 --- a/config.go +++ b/config.go @@ -27,11 +27,11 @@ type CA struct { // Provider is a domain fronting provider (e.g. Akamai, CloudFront). type Provider struct { - HostAliases map[string]string `yaml:"hostaliases"` - PassthroughPatterns []string `yaml:"passthrupatterns"` - TestURL string `yaml:"testurl"` - Masquerades []*Masquerade `yaml:"masquerades"` - VerifyHostname *string `yaml:"verifyhostname"` + HostAliases map[string]string `yaml:"hostaliases"` + PassthroughPatterns []string `yaml:"passthrupatterns"` + TestURL string `yaml:"testurl"` + Masquerades []*Masquerade `yaml:"masquerades"` + VerifyHostname *string `yaml:"verifyhostname"` // Pipeline-emitted YAML keys are lowercase-concatenated, not // snake_case (the upstream generator uses lowercased Go field // names with no yaml tag); the tag here must match the wire @@ -133,9 +133,11 @@ func (cfg *Config) CertPool() (*x509.CertPool, error) { return pool, nil } -// ExpandedProvider returns a copy of the provider with masquerades expanded -// with SNI based on the country code. Host aliases are lowercased. -// Passthrough patterns are also lowercased for efficient lookup. +// ExpandedProvider returns a copy of the provider with each masquerade's SNI +// resolved: a country-specific or "default" arbitrary SNI if the provider +// configures one (the "default" strategy applies even with no country code), +// otherwise the masquerade's baked-in SNI, otherwise empty (SNI omitted). Host +// aliases and passthrough patterns are lowercased for efficient lookup. func ExpandedProvider(p *Provider, countryCode string) *Provider { ep := &Provider{ HostAliases: make(map[string]string, len(p.HostAliases)), @@ -154,8 +156,13 @@ func ExpandedProvider(p *Provider, countryCode string) *Provider { ep.PassthroughPatterns[i] = strings.ToLower(pt) } + // Select the SNI strategy: a country-specific entry if one matches, else the + // "default" entry. The default applies even when no country code is set, so a + // provider's default arbitrary-SNI strategy is active for every client — the + // production client passes no country code, and gating "default" behind one + // would leave the strategy permanently inert. var sniCfg *SNIConfig - if countryCode != "" && p.FrontingSNIs != nil { + if p.FrontingSNIs != nil { var ok bool sniCfg, ok = p.FrontingSNIs[countryCode] if !ok { @@ -164,7 +171,15 @@ func ExpandedProvider(p *Provider, countryCode string) *Provider { } for _, m := range p.Masquerades { - sni := GenerateSNI(sniCfg, m.IpAddress) + // A generated SNI (country-specific or "default" arbitrary-SNI strategy) + // takes precedence. Otherwise keep any SNI baked into the masquerade by + // the config — this lets a provider whose edges require a specific front + // SNI pin one per masquerade without depending on a country code being + // set (the production client sets none). Empty stays empty (SNI omitted). + sni := m.SNI + if g := GenerateSNI(sniCfg, m.IpAddress); g != "" { + sni = g + } ep.Masquerades = append(ep.Masquerades, &Masquerade{ Domain: m.Domain, IpAddress: m.IpAddress, diff --git a/config_test.go b/config_test.go index b5dfcb1..61a4d13 100644 --- a/config_test.go +++ b/config_test.go @@ -73,6 +73,44 @@ func TestExpandedProvider(t *testing.T) { ep := ExpandedProvider(p, "") assert.Equal(t, "api.cdn.com", ep.HostAliases["api.example.com"]) }) + + t.Run("default arbitrary SNI applies without a country code", func(t *testing.T) { + // The production client passes no country code; a provider's "default" + // arbitrary-SNI strategy must still apply (e.g. akamai sending SNI + // globally), not be gated behind a country code. + dp := &Provider{ + Masquerades: []*Masquerade{{Domain: "cdn.example.com", IpAddress: "1.2.3.4"}}, + FrontingSNIs: map[string]*SNIConfig{ + "default": {UseArbitrarySNIs: true, ArbitrarySNIs: []string{"crunchbase.com"}}, + }, + } + ep := ExpandedProvider(dp, "") + require.Len(t, ep.Masquerades, 1) + assert.Equal(t, "crunchbase.com", ep.Masquerades[0].SNI) + }) + + t.Run("baked-in masquerade SNI is preserved when no SNI is generated", func(t *testing.T) { + // A provider whose edges require a specific front SNI can pin one per + // masquerade; with no arbitrary-SNI strategy it must survive expansion. + bp := &Provider{ + Masquerades: []*Masquerade{{Domain: "img.alicdn.com", IpAddress: "1.2.3.4", SNI: "www.mobgslb.tbcache.com"}}, + } + ep := ExpandedProvider(bp, "") + require.Len(t, ep.Masquerades, 1) + assert.Equal(t, "www.mobgslb.tbcache.com", ep.Masquerades[0].SNI) + }) + + t.Run("generated SNI overrides a baked-in SNI", func(t *testing.T) { + op := &Provider{ + Masquerades: []*Masquerade{{Domain: "cdn.example.com", IpAddress: "1.2.3.4", SNI: "baked.example"}}, + FrontingSNIs: map[string]*SNIConfig{ + "default": {UseArbitrarySNIs: true, ArbitrarySNIs: []string{"generated.example"}}, + }, + } + ep := ExpandedProvider(op, "") + require.Len(t, ep.Masquerades, 1) + assert.Equal(t, "generated.example", ep.Masquerades[0].SNI) + }) } func TestParseConfigYAML(t *testing.T) { From 19f6ed1c3de437a6b1644028451b209d8a2ac432 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Wed, 24 Jun 2026 17:05:32 -0600 Subject: [PATCH 2/3] config: verify SNI-path edge cert against Domain (not chain-only) (Copilot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExpandedProvider set VerifyHostname only from the provider default; when nil, dialFront's SNI path verified the edge cert chain-only (any cert chaining to a trusted root — for aliyun's single GlobalSign-R3 pool, any R3-issued cert a MITM could present). This PR populates SNI more often, so the weak path applied more widely. Fix: default VerifyHostname to the front Domain (and preserve a per-masquerade value if set). Verify against Domain, NOT the SNI: the SNI is often a decoy the served cert doesn't cover — akamai edges send SNI=crunchbase.com but serve their CN=a248.e.akamai.net cert (confirmed: valid for the Domain, not the SNI). Verifying against the SNI broke akamai (0/20 front); against Domain it's 20/20, and aliyun is unchanged (~55%) since the *.alicdn.com cert covers img.alicdn.com. This matches the hostname check the no-SNI path already performs against Domain. Co-Authored-By: Claude Opus 4.8 --- config.go | 21 ++++++++++++++++++++- config_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/config.go b/config.go index bb42e2a..4d01786 100644 --- a/config.go +++ b/config.go @@ -180,11 +180,30 @@ func ExpandedProvider(p *Provider, countryCode string) *Provider { if g := GenerateSNI(sniCfg, m.IpAddress); g != "" { sni = g } + // Resolve the hostname the edge cert is verified against on the SNI path + // (dialFront): a per-masquerade value wins, then the provider default, + // and finally the front Domain. Defaulting to Domain matters because the + // SNI path otherwise falls back to chain-only verification when no + // hostname is set — accepting any cert that chains to a trusted root + // (for a single-CA pool like aliyun's GlobalSign R3, any R3-issued cert, + // which a network MITM could present). We verify against Domain, NOT the + // SNI: the SNI is often a decoy the served cert doesn't cover (akamai + // edges send SNI=crunchbase.com but serve their a248.e.akamai.net cert), + // whereas the cert IS valid for the front Domain — the same check the + // no-SNI path already does. + verifyHostname := m.VerifyHostname + if verifyHostname == nil { + verifyHostname = p.VerifyHostname + } + if verifyHostname == nil && sni != "" && m.Domain != "" { + d := m.Domain + verifyHostname = &d + } ep.Masquerades = append(ep.Masquerades, &Masquerade{ Domain: m.Domain, IpAddress: m.IpAddress, SNI: sni, - VerifyHostname: p.VerifyHostname, + VerifyHostname: verifyHostname, }) } return ep diff --git a/config_test.go b/config_test.go index 61a4d13..9ff306b 100644 --- a/config_test.go +++ b/config_test.go @@ -100,6 +100,38 @@ func TestExpandedProvider(t *testing.T) { assert.Equal(t, "www.mobgslb.tbcache.com", ep.Masquerades[0].SNI) }) + t.Run("VerifyHostname defaults to the front Domain when none is configured", func(t *testing.T) { + // Avoids chain-only verification on the SNI path. It defaults to Domain + // (the front domain the edge cert is actually for), NOT the SNI, which is + // often a decoy the served cert doesn't cover. + vp := &Provider{ + Masquerades: []*Masquerade{{Domain: "img.alicdn.com", IpAddress: "1.2.3.4", SNI: "www.mobgslb.tbcache.com"}}, + } + ep := ExpandedProvider(vp, "") + require.Len(t, ep.Masquerades, 1) + require.NotNil(t, ep.Masquerades[0].VerifyHostname) + assert.Equal(t, "img.alicdn.com", *ep.Masquerades[0].VerifyHostname) + }) + + t.Run("no SNI leaves VerifyHostname unset (no-SNI path verifies Domain)", func(t *testing.T) { + np := &Provider{Masquerades: []*Masquerade{{Domain: "cdn.example.com", IpAddress: "1.2.3.4"}}} + ep := ExpandedProvider(np, "") + require.Len(t, ep.Masquerades, 1) + assert.Empty(t, ep.Masquerades[0].SNI) + assert.Nil(t, ep.Masquerades[0].VerifyHostname) + }) + + t.Run("per-masquerade VerifyHostname wins over the SNI default", func(t *testing.T) { + pinned := "pinned.example" + pp := &Provider{ + Masquerades: []*Masquerade{{Domain: "img.alicdn.com", IpAddress: "1.2.3.4", SNI: "www.mobgslb.tbcache.com", VerifyHostname: &pinned}}, + } + ep := ExpandedProvider(pp, "") + require.Len(t, ep.Masquerades, 1) + require.NotNil(t, ep.Masquerades[0].VerifyHostname) + assert.Equal(t, "pinned.example", *ep.Masquerades[0].VerifyHostname) + }) + t.Run("generated SNI overrides a baked-in SNI", func(t *testing.T) { op := &Provider{ Masquerades: []*Masquerade{{Domain: "cdn.example.com", IpAddress: "1.2.3.4", SNI: "baked.example"}}, From 14c92678c62f7482745e7c85a67e86ec2b8139a0 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Wed, 24 Jun 2026 17:14:54 -0600 Subject: [PATCH 3/3] config: avoid per-masquerade alloc in VerifyHostname fallback (Copilot) Build the expanded masquerade first and point VerifyHostname at its own Domain field when defaulting, instead of taking the address of a loop-local copy (which escaped to the heap each iteration the fallback applied). Behavior unchanged; no extra allocation. Co-Authored-By: Claude Opus 4.8 --- config.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/config.go b/config.go index 4d01786..9d4bbf7 100644 --- a/config.go +++ b/config.go @@ -180,6 +180,12 @@ func ExpandedProvider(p *Provider, countryCode string) *Provider { if g := GenerateSNI(sniCfg, m.IpAddress); g != "" { sni = g } + nm := &Masquerade{ + Domain: m.Domain, + IpAddress: m.IpAddress, + SNI: sni, + VerifyHostname: m.VerifyHostname, + } // Resolve the hostname the edge cert is verified against on the SNI path // (dialFront): a per-masquerade value wins, then the provider default, // and finally the front Domain. Defaulting to Domain matters because the @@ -191,20 +197,15 @@ func ExpandedProvider(p *Provider, countryCode string) *Provider { // edges send SNI=crunchbase.com but serve their a248.e.akamai.net cert), // whereas the cert IS valid for the front Domain — the same check the // no-SNI path already does. - verifyHostname := m.VerifyHostname - if verifyHostname == nil { - verifyHostname = p.VerifyHostname + if nm.VerifyHostname == nil { + nm.VerifyHostname = p.VerifyHostname } - if verifyHostname == nil && sni != "" && m.Domain != "" { - d := m.Domain - verifyHostname = &d + if nm.VerifyHostname == nil && sni != "" && nm.Domain != "" { + // Point at the new masquerade's own Domain field rather than a + // loop-local copy, avoiding a per-iteration heap allocation. + nm.VerifyHostname = &nm.Domain } - ep.Masquerades = append(ep.Masquerades, &Masquerade{ - Domain: m.Domain, - IpAddress: m.IpAddress, - SNI: sni, - VerifyHostname: verifyHostname, - }) + ep.Masquerades = append(ep.Masquerades, nm) } return ep }