Skip to content

Commit 2ae8d8d

Browse files
authored
Merge pull request #11 from getlantern/fisk/preserve-baked-sni
config: apply default frontingsnis without a country code; preserve baked-in SNI
2 parents f9f3917 + 14c9267 commit 2ae8d8d

2 files changed

Lines changed: 118 additions & 13 deletions

File tree

config.go

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ type CA struct {
2727

2828
// Provider is a domain fronting provider (e.g. Akamai, CloudFront).
2929
type Provider struct {
30-
HostAliases map[string]string `yaml:"hostaliases"`
31-
PassthroughPatterns []string `yaml:"passthrupatterns"`
32-
TestURL string `yaml:"testurl"`
33-
Masquerades []*Masquerade `yaml:"masquerades"`
34-
VerifyHostname *string `yaml:"verifyhostname"`
30+
HostAliases map[string]string `yaml:"hostaliases"`
31+
PassthroughPatterns []string `yaml:"passthrupatterns"`
32+
TestURL string `yaml:"testurl"`
33+
Masquerades []*Masquerade `yaml:"masquerades"`
34+
VerifyHostname *string `yaml:"verifyhostname"`
3535
// Pipeline-emitted YAML keys are lowercase-concatenated, not
3636
// snake_case (the upstream generator uses lowercased Go field
3737
// names with no yaml tag); the tag here must match the wire
@@ -133,9 +133,11 @@ func (cfg *Config) CertPool() (*x509.CertPool, error) {
133133
return pool, nil
134134
}
135135

136-
// ExpandedProvider returns a copy of the provider with masquerades expanded
137-
// with SNI based on the country code. Host aliases are lowercased.
138-
// Passthrough patterns are also lowercased for efficient lookup.
136+
// ExpandedProvider returns a copy of the provider with each masquerade's SNI
137+
// resolved: a country-specific or "default" arbitrary SNI if the provider
138+
// configures one (the "default" strategy applies even with no country code),
139+
// otherwise the masquerade's baked-in SNI, otherwise empty (SNI omitted). Host
140+
// aliases and passthrough patterns are lowercased for efficient lookup.
139141
func ExpandedProvider(p *Provider, countryCode string) *Provider {
140142
ep := &Provider{
141143
HostAliases: make(map[string]string, len(p.HostAliases)),
@@ -154,8 +156,13 @@ func ExpandedProvider(p *Provider, countryCode string) *Provider {
154156
ep.PassthroughPatterns[i] = strings.ToLower(pt)
155157
}
156158

159+
// Select the SNI strategy: a country-specific entry if one matches, else the
160+
// "default" entry. The default applies even when no country code is set, so a
161+
// provider's default arbitrary-SNI strategy is active for every client — the
162+
// production client passes no country code, and gating "default" behind one
163+
// would leave the strategy permanently inert.
157164
var sniCfg *SNIConfig
158-
if countryCode != "" && p.FrontingSNIs != nil {
165+
if p.FrontingSNIs != nil {
159166
var ok bool
160167
sniCfg, ok = p.FrontingSNIs[countryCode]
161168
if !ok {
@@ -164,13 +171,41 @@ func ExpandedProvider(p *Provider, countryCode string) *Provider {
164171
}
165172

166173
for _, m := range p.Masquerades {
167-
sni := GenerateSNI(sniCfg, m.IpAddress)
168-
ep.Masquerades = append(ep.Masquerades, &Masquerade{
174+
// A generated SNI (country-specific or "default" arbitrary-SNI strategy)
175+
// takes precedence. Otherwise keep any SNI baked into the masquerade by
176+
// the config — this lets a provider whose edges require a specific front
177+
// SNI pin one per masquerade without depending on a country code being
178+
// set (the production client sets none). Empty stays empty (SNI omitted).
179+
sni := m.SNI
180+
if g := GenerateSNI(sniCfg, m.IpAddress); g != "" {
181+
sni = g
182+
}
183+
nm := &Masquerade{
169184
Domain: m.Domain,
170185
IpAddress: m.IpAddress,
171186
SNI: sni,
172-
VerifyHostname: p.VerifyHostname,
173-
})
187+
VerifyHostname: m.VerifyHostname,
188+
}
189+
// Resolve the hostname the edge cert is verified against on the SNI path
190+
// (dialFront): a per-masquerade value wins, then the provider default,
191+
// and finally the front Domain. Defaulting to Domain matters because the
192+
// SNI path otherwise falls back to chain-only verification when no
193+
// hostname is set — accepting any cert that chains to a trusted root
194+
// (for a single-CA pool like aliyun's GlobalSign R3, any R3-issued cert,
195+
// which a network MITM could present). We verify against Domain, NOT the
196+
// SNI: the SNI is often a decoy the served cert doesn't cover (akamai
197+
// edges send SNI=crunchbase.com but serve their a248.e.akamai.net cert),
198+
// whereas the cert IS valid for the front Domain — the same check the
199+
// no-SNI path already does.
200+
if nm.VerifyHostname == nil {
201+
nm.VerifyHostname = p.VerifyHostname
202+
}
203+
if nm.VerifyHostname == nil && sni != "" && nm.Domain != "" {
204+
// Point at the new masquerade's own Domain field rather than a
205+
// loop-local copy, avoiding a per-iteration heap allocation.
206+
nm.VerifyHostname = &nm.Domain
207+
}
208+
ep.Masquerades = append(ep.Masquerades, nm)
174209
}
175210
return ep
176211
}

config_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,76 @@ func TestExpandedProvider(t *testing.T) {
7373
ep := ExpandedProvider(p, "")
7474
assert.Equal(t, "api.cdn.com", ep.HostAliases["api.example.com"])
7575
})
76+
77+
t.Run("default arbitrary SNI applies without a country code", func(t *testing.T) {
78+
// The production client passes no country code; a provider's "default"
79+
// arbitrary-SNI strategy must still apply (e.g. akamai sending SNI
80+
// globally), not be gated behind a country code.
81+
dp := &Provider{
82+
Masquerades: []*Masquerade{{Domain: "cdn.example.com", IpAddress: "1.2.3.4"}},
83+
FrontingSNIs: map[string]*SNIConfig{
84+
"default": {UseArbitrarySNIs: true, ArbitrarySNIs: []string{"crunchbase.com"}},
85+
},
86+
}
87+
ep := ExpandedProvider(dp, "")
88+
require.Len(t, ep.Masquerades, 1)
89+
assert.Equal(t, "crunchbase.com", ep.Masquerades[0].SNI)
90+
})
91+
92+
t.Run("baked-in masquerade SNI is preserved when no SNI is generated", func(t *testing.T) {
93+
// A provider whose edges require a specific front SNI can pin one per
94+
// masquerade; with no arbitrary-SNI strategy it must survive expansion.
95+
bp := &Provider{
96+
Masquerades: []*Masquerade{{Domain: "img.alicdn.com", IpAddress: "1.2.3.4", SNI: "www.mobgslb.tbcache.com"}},
97+
}
98+
ep := ExpandedProvider(bp, "")
99+
require.Len(t, ep.Masquerades, 1)
100+
assert.Equal(t, "www.mobgslb.tbcache.com", ep.Masquerades[0].SNI)
101+
})
102+
103+
t.Run("VerifyHostname defaults to the front Domain when none is configured", func(t *testing.T) {
104+
// Avoids chain-only verification on the SNI path. It defaults to Domain
105+
// (the front domain the edge cert is actually for), NOT the SNI, which is
106+
// often a decoy the served cert doesn't cover.
107+
vp := &Provider{
108+
Masquerades: []*Masquerade{{Domain: "img.alicdn.com", IpAddress: "1.2.3.4", SNI: "www.mobgslb.tbcache.com"}},
109+
}
110+
ep := ExpandedProvider(vp, "")
111+
require.Len(t, ep.Masquerades, 1)
112+
require.NotNil(t, ep.Masquerades[0].VerifyHostname)
113+
assert.Equal(t, "img.alicdn.com", *ep.Masquerades[0].VerifyHostname)
114+
})
115+
116+
t.Run("no SNI leaves VerifyHostname unset (no-SNI path verifies Domain)", func(t *testing.T) {
117+
np := &Provider{Masquerades: []*Masquerade{{Domain: "cdn.example.com", IpAddress: "1.2.3.4"}}}
118+
ep := ExpandedProvider(np, "")
119+
require.Len(t, ep.Masquerades, 1)
120+
assert.Empty(t, ep.Masquerades[0].SNI)
121+
assert.Nil(t, ep.Masquerades[0].VerifyHostname)
122+
})
123+
124+
t.Run("per-masquerade VerifyHostname wins over the SNI default", func(t *testing.T) {
125+
pinned := "pinned.example"
126+
pp := &Provider{
127+
Masquerades: []*Masquerade{{Domain: "img.alicdn.com", IpAddress: "1.2.3.4", SNI: "www.mobgslb.tbcache.com", VerifyHostname: &pinned}},
128+
}
129+
ep := ExpandedProvider(pp, "")
130+
require.Len(t, ep.Masquerades, 1)
131+
require.NotNil(t, ep.Masquerades[0].VerifyHostname)
132+
assert.Equal(t, "pinned.example", *ep.Masquerades[0].VerifyHostname)
133+
})
134+
135+
t.Run("generated SNI overrides a baked-in SNI", func(t *testing.T) {
136+
op := &Provider{
137+
Masquerades: []*Masquerade{{Domain: "cdn.example.com", IpAddress: "1.2.3.4", SNI: "baked.example"}},
138+
FrontingSNIs: map[string]*SNIConfig{
139+
"default": {UseArbitrarySNIs: true, ArbitrarySNIs: []string{"generated.example"}},
140+
},
141+
}
142+
ep := ExpandedProvider(op, "")
143+
require.Len(t, ep.Masquerades, 1)
144+
assert.Equal(t, "generated.example", ep.Masquerades[0].SNI)
145+
})
76146
}
77147

78148
func TestParseConfigYAML(t *testing.T) {

0 commit comments

Comments
 (0)