diff --git a/aliyun-provider.yaml b/aliyun-provider.yaml new file mode 100644 index 0000000..1aa3855 --- /dev/null +++ b/aliyun-provider.yaml @@ -0,0 +1,113 @@ +# Alibaba Cloud (Aliyun) CDN domain-fronting provider. +# +# Built & verified 2026-06-23 (US vantage point). Aliyun CDN edges route by the +# HTTP Host header (h1) / :authority (h2) and ignore the TLS SNI for origin +# selection, so a censor doing SNI filtering sees only the innocent front domain +# (img.alicdn.com) while the request reaches the real target. Confirmed +# cross-organization: a TLS session bearing Alibaba's *.tbcdn.cn cert (SNI +# img.alicdn.com) reached Bilibili (s1.hdslb.com) and Momo (img.momocdn.com) +# origins purely via the Host header. +# +# Two operational constraints discovered: +# 1. TLS layer — the edge node must hold a cert valid for the SNI you present. +# img.alicdn.com is ideal: Alibaba's own *.tbcdn.cn cert (SAN *.alicdn.com, +# *.taobao.com, ...) is provisioned on ~every Aliyun node. +# 2. The target Host must be an onboarded Aliyun CDN customer. A non-customer +# Host (e.g. www.example.com) makes the edge silently drop the connection. +# => Lantern's own DCDN distributions (config/api/geo/update.dcdn.getiantem.org) +# are onboarded and the hostaliases below are validated end-to-end. +# +# This file is a complete, standalone Config (parseable via ParseConfigYAML) so +# it can be loaded directly for testing. The `aliyun:` block under `providers:` +# is what gets merged into the upstream getlantern/fronted pipeline output. + +trustedcas: + # Anchor for the Alibaba leaf cert chain: + # *.tbcdn.cn -> GlobalSign GCC R3 OV TLS CA 2024 -> GlobalSign Root CA - R3 + - commonname: GlobalSign Root CA - R3 + cert: | + -----BEGIN CERTIFICATE----- + MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G + A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp + Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 + MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG + A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI + hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 + RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT + gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm + KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd + QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ + XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw + DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o + LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU + RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp + jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK + 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX + mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs + Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH + WD9f + -----END CERTIFICATE----- + +providers: + aliyun: + # Origin -> Aliyun DCDN accelerated domain. The value is the *accelerated + # domain* (sent as the fronted Host), NOT the *.w.kunlungr.com edge CNAME: + # the edge vhosts on the accelerated domain (Host=CNAME returns 403). + # Each accelerated domain is an "overseas"-scope DCDN distribution whose + # origin is the raw iantem.io backend (api.iantem.io / update.iantem.io, + # bypassing Cloudflare) with back-to-origin SNI = that origin and + # back-to-origin Host = the service domain. Validated 2026-06-23: fronting + # each via img.alicdn.com returns byte-identical responses to the origin. + # NOTE: These hostaliases and testurl are real, validated mappings. + # aliyun_live_test.go reads this file directly; if you intend placeholders, + # update the PR description and replace these with clearly marked REPLACE-ME values. + hostaliases: + config.getiantem.org: config.dcdn.getiantem.org + api.getiantem.org: api.dcdn.getiantem.org + geo.getiantem.org: geo.dcdn.getiantem.org + update.getlantern.org: update.dcdn.getiantem.org + # testurl is POSTed through a vetted front (verifyWithPost expects 202). + # NOTE: the services above answer /ping with 404/301, not 202 — a + # dedicated 202 ping distribution (cf. Akamai's fronted-ping) should be + # added before relying on runtime vetting; this points at one for now. + testurl: https://config.dcdn.getiantem.org/ping + # Pin TLS verification to a real Alibaba cert. Because SNI is set (see + # frontingsnis), dialFront takes the hostname-verifying path: without a + # verifyhostname it would be chain-only (any GlobalSign-R3-chained cert + # would pass). Every masquerade edge serves Alibaba's *.tbcdn.cn cert, + # whose SANs include *.alicdn.com, so this single value validates the + # leaf for all of the rotating frontingsnis (img/gw/a.alicdn.com). + # NOTE: ExpandedProvider applies this provider-level value to every + # masquerade; per-masquerade verifyhostname is NOT propagated. + verifyhostname: img.alicdn.com + # frontingsnis drives the wire SNI. ExpandedProvider regenerates each + # masquerade's SNI from this list (deterministic by IP hash). A non-empty + # country code MUST be passed (WithCountryCode) for SNI to be emitted; + # otherwise the no-SNI fallback verifies the cert against the masquerade + # Domain (img.alicdn.com), which also works here. + frontingsnis: + default: + usearbitrarysnis: true + arbitrarysnis: + - img.alicdn.com + - gw.alicdn.com + - a.alicdn.com + # Masquerades: Aliyun edge IPs verified 2026-06-23 to present the Alibaba + # *.tbcdn.cn cert. Each entry is just domain + ipaddress — SNI comes from + # frontingsnis and the verify hostname from the provider-level + # verifyhostname above (per-masquerade sni/verifyhostname are ignored by + # ExpandedProvider). The upstream pipeline should harvest a much larger + # pool from in-region vantage points; this is a seed set across 3 /24s. + masquerades: + - domain: img.alicdn.com + ipaddress: 155.102.54.137 + - domain: img.alicdn.com + ipaddress: 155.102.54.138 + - domain: img.alicdn.com + ipaddress: 163.181.66.200 + - domain: img.alicdn.com + ipaddress: 163.181.66.201 + - domain: img.alicdn.com + ipaddress: 8.25.82.183 + - domain: img.alicdn.com + ipaddress: 8.25.82.184 diff --git a/aliyun_live_test.go b/aliyun_live_test.go new file mode 100644 index 0000000..b57cb3a --- /dev/null +++ b/aliyun_live_test.go @@ -0,0 +1,106 @@ +package domainfront + +import ( + "context" + "io" + "net/http" + "os" + "strings" + "testing" + "time" + + utls "github.com/refraction-networking/utls" + "github.com/stretchr/testify/require" +) + +// TestAliyunProviderLive exercises aliyun-provider.yaml against the real Aliyun +// CDN. It proves the security-critical path end to end: the config's GlobalSign +// root verifies the live Alibaba *.tbcdn.cn chain, the generated SNI is accepted +// by the edge, and the edge routes a *cross-organization* Host (Bilibili's +// s1.hdslb.com) over a TLS session that presents Alibaba's certificate — i.e. +// domain fronting works through this library's own dial + verify code. +// +// Skipped by default (it talks to the public internet). Run with: +// +// DOMAINFRONT_LIVE=1 go test -run TestAliyunProviderLive -v +func TestAliyunProviderLive(t *testing.T) { + if os.Getenv("DOMAINFRONT_LIVE") == "" { + t.Skip("live network test; set DOMAINFRONT_LIVE=1 to run") + } + + raw, err := os.ReadFile("aliyun-provider.yaml") + require.NoError(t, err) + + cfg, err := ParseConfigYAML(raw) + require.NoError(t, err) + + // The GlobalSign root parses into a usable pool. + pool, err := cfg.CertPool() + require.NoError(t, err) + + // Expand with a country code so FrontingSNIs drives the wire SNI + // (config.go only emits SNI when countryCode != ""). + require.Contains(t, cfg.Providers, "aliyun", "aliyun-provider.yaml must define the 'aliyun' provider") + p := ExpandedProvider(cfg.Providers["aliyun"], "cn") + require.NotEmpty(t, p.Masquerades) + + const crossOrgHost = "s1.hdslb.com" // Bilibili — unrelated to Alibaba + + // Phase 1: prove the security-critical path through the library's own + // dialFront — the config's GlobalSign root verifies the live Alibaba + // *.tbcdn.cn chain under the production Chrome_131 ClientHello, for every + // masquerade, using the SNI that FrontingSNIs generated. + // For each masquerade: dial with the production Chrome_131 ClientHello + // (which Aliyun answers with HTTP/2 over ALPN), then drive the library's + // real doRequest — proving the h2 transport path fronts a cross-org Host + // (Bilibili) over a TLS session bearing Alibaba's certificate. + var dialed int + var frontedOK, frontedOverH2 bool + for _, m := range p.Masquerades { + require.NotEmpty(t, m.SNI, "expanded masquerade should carry an SNI") + + f := newFront(m, "aliyun") + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + res := dialFront(ctx, f, pool, utls.HelloChrome_131, NetDialer{}) + if res.err != nil { + cancel() + t.Logf("dial %s (SNI %s) failed: %v", m.IpAddress, m.SNI, res.err) + continue + } + dialed++ + proto := negotiatedProtocol(res.conn) + t.Logf("TLS+verify OK: ip=%s sni=%s alpn=%s (cert chained to config's GlobalSign root)", m.IpAddress, m.SNI, proto) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+crossOrgHost+"/", nil) + require.NoError(t, err) + resp, rerr := (&roundTripper{}).doRequest(req, res.conn, crossOrgHost, nil) + if rerr != nil { + if cerr := res.conn.Close(); cerr != nil { + t.Logf("close conn for %s failed: %v", m.IpAddress, cerr) + } + cancel() + t.Logf("fronted GET via %s failed: %v", m.IpAddress, rerr) + continue + } + body, readErr := io.ReadAll(io.LimitReader(resp.Body, 256)) + if cerr := resp.Body.Close(); cerr != nil { // h2Body close tears down the conn + t.Logf("close response body for %s failed: %v", m.IpAddress, cerr) + } + cancel() + t.Logf("fronted %s via %s (SNI %s, %s): HTTP %d, proto=HTTP/%d, body=%q (readErr=%v)", + crossOrgHost, m.IpAddress, m.SNI, proto, resp.StatusCode, resp.ProtoMajor, strings.TrimSpace(string(body)), readErr) + // Require a clean body read so a truncated/failed response can't be + // miscounted as a successful front. + if resp.StatusCode == http.StatusOK && readErr == nil { + frontedOK = true + // Only count it as proving the h2 path when ALPN actually + // negotiated h2 and the response came back as HTTP/2. + if proto == "h2" && resp.ProtoMajor == 2 { + frontedOverH2 = true + } + } + } + require.NotZero(t, dialed, "no Aliyun edge IP completed TLS + GlobalSign verification") + require.True(t, frontedOK, "no edge served the cross-org Host (%s) — fronting did not route", crossOrgHost) + require.True(t, frontedOverH2, "no edge fronted %s over HTTP/2 — the h2 path was not exercised", crossOrgHost) +}