From a0568bb29079930072abb712de1fdc1d8ca8c26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Thu, 7 Nov 2024 14:10:23 +0100 Subject: [PATCH 1/4] Switch to using cloudflare-ech.com as the target for the ech test --- internal/experiment/echcheck/measure.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/experiment/echcheck/measure.go b/internal/experiment/echcheck/measure.go index 807a43d969..1bfe0e2b61 100644 --- a/internal/experiment/echcheck/measure.go +++ b/internal/experiment/echcheck/measure.go @@ -17,7 +17,7 @@ import ( const ( testName = "echcheck" testVersion = "0.1.2" - defaultURL = "https://crypto.cloudflare.com/cdn-cgi/trace" + defaultURL = "https://cloudflare-ech.com/cdn-cgi/trace" ) var ( From 3fa848b7d5252dc8050c43ebea7429959de25be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Thu, 7 Nov 2024 14:27:07 +0100 Subject: [PATCH 2/4] Update echcheck test to 0.2.0 placing tls_handshakes into list * Update TLSHandshake model to include ECHConfig field * Update tests --- internal/experiment/echcheck/handshake.go | 4 +++- internal/experiment/echcheck/measure.go | 7 +++---- internal/experiment/echcheck/measure_test.go | 13 ++++++++----- internal/model/archival.go | 1 + 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/internal/experiment/echcheck/handshake.go b/internal/experiment/echcheck/handshake.go index 05a4e25267..080444cf16 100644 --- a/internal/experiment/echcheck/handshake.go +++ b/internal/experiment/echcheck/handshake.go @@ -34,7 +34,9 @@ func handshakeWithEch(ctx context.Context, conn net.Conn, zeroTime time.Time, utlsEchExtension.Id = echExtensionType utlsEchExtension.Data = payload - return handshakeWithExtension(ctx, conn, zeroTime, address, sni, []utls.TLSExtension{&utlsEchExtension}, logger) + hs := handshakeWithExtension(ctx, conn, zeroTime, address, sni, []utls.TLSExtension{&utlsEchExtension}, logger) + hs.ECHConfig = "GREASE" + return hs } func handshakeMaybePrintWithECH(doprint bool) string { diff --git a/internal/experiment/echcheck/measure.go b/internal/experiment/echcheck/measure.go index 1bfe0e2b61..bd1d12ffb1 100644 --- a/internal/experiment/echcheck/measure.go +++ b/internal/experiment/echcheck/measure.go @@ -16,7 +16,7 @@ import ( const ( testName = "echcheck" - testVersion = "0.1.2" + testVersion = "0.2.0" defaultURL = "https://cloudflare-ech.com/cdn-cgi/trace" ) @@ -30,8 +30,7 @@ var ( // TestKeys contains echcheck test keys. type TestKeys struct { - Control model.ArchivalTLSOrQUICHandshakeResult `json:"control"` - Target model.ArchivalTLSOrQUICHandshakeResult `json:"target"` + TLSHandshakes []*model.ArchivalTLSOrQUICHandshakeResult `json:"tls_handshakes"` } // Measurer performs the measurement. @@ -124,7 +123,7 @@ func (m *Measurer) Run( control := <-controlChannel target := <-targetChannel - args.Measurement.TestKeys = TestKeys{Control: control, Target: target} + args.Measurement.TestKeys = TestKeys{TLSHandshakes: []*model.ArchivalTLSOrQUICHandshakeResult{&control, &target}} return nil } diff --git a/internal/experiment/echcheck/measure_test.go b/internal/experiment/echcheck/measure_test.go index 2a53fbd938..14ae2b2929 100644 --- a/internal/experiment/echcheck/measure_test.go +++ b/internal/experiment/echcheck/measure_test.go @@ -114,10 +114,13 @@ func TestMeasurementSuccessRealWorld(t *testing.T) { // check results tk := msrmnt.TestKeys.(TestKeys) - if tk.Control.Failure != nil { - t.Fatal("unexpected control failure:", *tk.Control.Failure) - } - if tk.Target.Failure != nil { - t.Fatal("unexpected target failure:", *tk.Target.Failure) + for _, hs := range tk.TLSHandshakes { + if hs.Failure != nil { + if hs.ECHConfig == "GREASE" { + t.Fatal("unexpected exp failure:", hs.Failure) + } else { + t.Fatal("unexpected ctrl failure:", hs.Failure) + } + } } } diff --git a/internal/model/archival.go b/internal/model/archival.go index 8a17e745ee..affa15c332 100644 --- a/internal/model/archival.go +++ b/internal/model/archival.go @@ -248,6 +248,7 @@ type ArchivalTLSOrQUICHandshakeResult struct { NoTLSVerify bool `json:"no_tls_verify"` PeerCertificates []ArchivalBinaryData `json:"peer_certificates"` ServerName string `json:"server_name"` + ECHConfig string `json:"echconfig,omitempty"` T0 float64 `json:"t0,omitempty"` T float64 `json:"t"` Tags []string `json:"tags"` From 186df09a8fdbb459d47d45d4630e3170247bb25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Fri, 8 Nov 2024 14:18:54 +0100 Subject: [PATCH 3/4] Add support for OuterSni field * Add extra handshake that uses a different outer SNI field --- internal/experiment/echcheck/measure.go | 41 ++++++++++++++++++++----- internal/model/archival.go | 1 + 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/internal/experiment/echcheck/measure.go b/internal/experiment/echcheck/measure.go index bd1d12ffb1..3a19bde9c9 100644 --- a/internal/experiment/echcheck/measure.go +++ b/internal/experiment/echcheck/measure.go @@ -92,14 +92,22 @@ func (m *Measurer) Run( return netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ConnectOperation, err) } + ol = logx.NewOperationLogger(args.Session.Logger(), "echcheck: TCPConnect#3 %s", address) + conn3, err := dialer.DialContext(ctx, "tcp", address) + ol.Stop(err) + if err != nil { + return netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ConnectOperation, err) + } + // 3. Conduct and measure control and target TLS handshakes in parallel - controlChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) - targetChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) + noEchChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) + echWithMatchingOuterSniChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) + echWithExampleOuterSniChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() go func() { - controlChannel <- *handshake( + noEchChannel <- *handshake( ctx, conn, args.Measurement.MeasurementStartTimeSaved, @@ -110,7 +118,7 @@ func (m *Measurer) Run( }() go func() { - targetChannel <- *handshakeWithEch( + echWithMatchingOuterSniChannel <- *handshakeWithEch( ctx, conn2, args.Measurement.MeasurementStartTimeSaved, @@ -120,10 +128,29 @@ func (m *Measurer) Run( ) }() - control := <-controlChannel - target := <-targetChannel + exampleSni := "cloudflare.com" + go func() { + echWithExampleOuterSniChannel <- *handshakeWithEch( + ctx, + conn3, + args.Measurement.MeasurementStartTimeSaved, + address, + exampleSni, + args.Session.Logger(), + ) + }() - args.Measurement.TestKeys = TestKeys{TLSHandshakes: []*model.ArchivalTLSOrQUICHandshakeResult{&control, &target}} + noEch := <-noEchChannel + echWithMatchingOuterSni := <-echWithMatchingOuterSniChannel + echWithMatchingOuterSni.ServerName = parsed.Host + echWithMatchingOuterSni.OuterServerName = parsed.Host + echWithExampleOuterSni := <-echWithExampleOuterSniChannel + echWithExampleOuterSni.ServerName = parsed.Host + echWithExampleOuterSni.OuterServerName = exampleSni + + args.Measurement.TestKeys = TestKeys{TLSHandshakes: []*model.ArchivalTLSOrQUICHandshakeResult{ + &noEch, &echWithMatchingOuterSni, &echWithExampleOuterSni, + }} return nil } diff --git a/internal/model/archival.go b/internal/model/archival.go index affa15c332..1930dc53c5 100644 --- a/internal/model/archival.go +++ b/internal/model/archival.go @@ -248,6 +248,7 @@ type ArchivalTLSOrQUICHandshakeResult struct { NoTLSVerify bool `json:"no_tls_verify"` PeerCertificates []ArchivalBinaryData `json:"peer_certificates"` ServerName string `json:"server_name"` + OuterServerName string `json:"outer_server_name,omitempty"` ECHConfig string `json:"echconfig,omitempty"` T0 float64 `json:"t0,omitempty"` T float64 `json:"t"` From 2ef3d7b5c66f2c018b9245e37f1a21032126fb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Mon, 11 Nov 2024 11:28:14 +0100 Subject: [PATCH 4/4] Add support for randomizing handshake order --- internal/experiment/echcheck/handshake.go | 47 ++++++++++ internal/experiment/echcheck/measure.go | 108 ++++++++-------------- 2 files changed, 86 insertions(+), 69 deletions(-) diff --git a/internal/experiment/echcheck/handshake.go b/internal/experiment/echcheck/handshake.go index 080444cf16..3ecc648262 100644 --- a/internal/experiment/echcheck/handshake.go +++ b/internal/experiment/echcheck/handshake.go @@ -17,6 +17,52 @@ import ( const echExtensionType uint16 = 0xfe0d +func connectAndHandshake( + ctx context.Context, + startTime time.Time, + address string, sni string, outerSni string, + logger model.Logger) (chan model.ArchivalTLSOrQUICHandshakeResult, error) { + + channel := make(chan model.ArchivalTLSOrQUICHandshakeResult) + + ol := logx.NewOperationLogger(logger, "echcheck: TCPConnect %s", address) + var dialer net.Dialer + conn, err := dialer.DialContext(ctx, "tcp", address) + ol.Stop(err) + if err != nil { + return nil, netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ConnectOperation, err) + } + + go func() { + var res *model.ArchivalTLSOrQUICHandshakeResult + if outerSni == "" { + res = handshake( + ctx, + conn, + startTime, + address, + sni, + logger, + ) + } else { + res = handshakeWithEch( + ctx, + conn, + startTime, + address, + outerSni, + logger, + ) + // We need to set this explicitly because otherwise it will get + // overridden with the outerSni in the case of ECH + res.ServerName = sni + } + channel <- *res + }() + + return channel, nil +} + func handshake(ctx context.Context, conn net.Conn, zeroTime time.Time, address string, sni string, logger model.Logger) *model.ArchivalTLSOrQUICHandshakeResult { return handshakeWithExtension(ctx, conn, zeroTime, address, sni, []utls.TLSExtension{}, logger) @@ -36,6 +82,7 @@ func handshakeWithEch(ctx context.Context, conn net.Conn, zeroTime time.Time, hs := handshakeWithExtension(ctx, conn, zeroTime, address, sni, []utls.TLSExtension{&utlsEchExtension}, logger) hs.ECHConfig = "GREASE" + hs.OuterServerName = sni return hs } diff --git a/internal/experiment/echcheck/measure.go b/internal/experiment/echcheck/measure.go index 3a19bde9c9..243c44c88f 100644 --- a/internal/experiment/echcheck/measure.go +++ b/internal/experiment/echcheck/measure.go @@ -3,14 +3,13 @@ package echcheck import ( "context" "errors" + "math/rand" "net" "net/url" - "time" "github.com/ooni/probe-cli/v3/internal/logx" "github.com/ooni/probe-cli/v3/internal/measurexlite" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/runtimex" ) @@ -76,80 +75,51 @@ func (m *Measurer) Run( runtimex.Assert(len(addrs) > 0, "expected at least one entry in addrs") address := net.JoinHostPort(addrs[0], "443") - // 2. Set up TCP connections - ol = logx.NewOperationLogger(args.Session.Logger(), "echcheck: TCPConnect#1 %s", address) - var dialer net.Dialer - conn, err := dialer.DialContext(ctx, "tcp", address) - ol.Stop(err) - if err != nil { - return netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ConnectOperation, err) + handshakes := []func() (chan model.ArchivalTLSOrQUICHandshakeResult, error){ + // handshake with ECH disabled and SNI coming from the URL + func() (chan model.ArchivalTLSOrQUICHandshakeResult, error) { + return connectAndHandshake(ctx, args.Measurement.MeasurementStartTimeSaved, + address, parsed.Host, "", args.Session.Logger()) + }, + // handshake with ECH enabled and ClientHelloOuter SNI coming from the URL + func() (chan model.ArchivalTLSOrQUICHandshakeResult, error) { + return connectAndHandshake(ctx, args.Measurement.MeasurementStartTimeSaved, + address, parsed.Host, parsed.Host, args.Session.Logger()) + }, + // handshake with ECH enabled and hardcoded different ClientHelloOuter SNI + func() (chan model.ArchivalTLSOrQUICHandshakeResult, error) { + return connectAndHandshake(ctx, args.Measurement.MeasurementStartTimeSaved, + address, parsed.Host, "cloudflare.com", args.Session.Logger()) + }, } - ol = logx.NewOperationLogger(args.Session.Logger(), "echcheck: TCPConnect#2 %s", address) - conn2, err := dialer.DialContext(ctx, "tcp", address) - ol.Stop(err) - if err != nil { - return netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ConnectOperation, err) + // We shuffle the order in which the operations are done to avoid residual + // censorship issues. + rand.Shuffle(len(handshakes), func(i, j int) { + handshakes[i], handshakes[j] = handshakes[j], handshakes[i] + }) + + var channels [3](chan model.ArchivalTLSOrQUICHandshakeResult) + var results [3](model.ArchivalTLSOrQUICHandshakeResult) + + // Fire the handshakes in parallel + // TODO: currently if one of the connects fails we fail the whole result + // set. This is probably OK given that we only ever use the same address, + // but this may be something we want to change in the future. + for idx, hs := range handshakes { + channels[idx], err = hs() + if err != nil { + return err + } } - ol = logx.NewOperationLogger(args.Session.Logger(), "echcheck: TCPConnect#3 %s", address) - conn3, err := dialer.DialContext(ctx, "tcp", address) - ol.Stop(err) - if err != nil { - return netxlite.NewErrWrapper(netxlite.ClassifyGenericError, netxlite.ConnectOperation, err) + // Wait on each channel for the results to come in + for idx, ch := range channels { + results[idx] = <-ch } - // 3. Conduct and measure control and target TLS handshakes in parallel - noEchChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) - echWithMatchingOuterSniChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) - echWithExampleOuterSniChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult) - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - - go func() { - noEchChannel <- *handshake( - ctx, - conn, - args.Measurement.MeasurementStartTimeSaved, - address, - parsed.Host, - args.Session.Logger(), - ) - }() - - go func() { - echWithMatchingOuterSniChannel <- *handshakeWithEch( - ctx, - conn2, - args.Measurement.MeasurementStartTimeSaved, - address, - parsed.Host, - args.Session.Logger(), - ) - }() - - exampleSni := "cloudflare.com" - go func() { - echWithExampleOuterSniChannel <- *handshakeWithEch( - ctx, - conn3, - args.Measurement.MeasurementStartTimeSaved, - address, - exampleSni, - args.Session.Logger(), - ) - }() - - noEch := <-noEchChannel - echWithMatchingOuterSni := <-echWithMatchingOuterSniChannel - echWithMatchingOuterSni.ServerName = parsed.Host - echWithMatchingOuterSni.OuterServerName = parsed.Host - echWithExampleOuterSni := <-echWithExampleOuterSniChannel - echWithExampleOuterSni.ServerName = parsed.Host - echWithExampleOuterSni.OuterServerName = exampleSni - args.Measurement.TestKeys = TestKeys{TLSHandshakes: []*model.ArchivalTLSOrQUICHandshakeResult{ - &noEch, &echWithMatchingOuterSni, &echWithExampleOuterSni, + &results[0], &results[1], &results[2], }} return nil