Skip to content

Commit

Permalink
Switch to using cloudflare-ech.com as the target for the ech test (#1658
Browse files Browse the repository at this point in the history
)

## Checklist

- [x] I have read the [contribution
guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md)
- [x] reference issue for this pull request:
ooni/probe#1453
- [x] if you changed anything related to how experiments work and you
need to reflect these changes in the ooni/spec repository, please link
to the related ooni/spec pull request:
ooni/spec#297
- [x] if you changed code inside an experiment, make sure you bump its
version number

## Description

Changes to the ECHCheck experiment.

* Replace default URL with cloudflare-ech.com
* Add support for performing an additional ECH handshake with a
different ClientHelloOuter SNI
* Randomize the order of the handshakes
  • Loading branch information
hellais authored Nov 20, 2024
1 parent a66b589 commit e64f736
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 56 deletions.
51 changes: 50 additions & 1 deletion internal/experiment/echcheck/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -34,7 +80,10 @@ 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"
hs.OuterServerName = sni
return hs
}

func handshakeMaybePrintWithECH(doprint bool) string {
Expand Down
96 changes: 46 additions & 50 deletions internal/experiment/echcheck/measure.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ 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"
)

const (
testName = "echcheck"
testVersion = "0.1.2"
defaultURL = "https://crypto.cloudflare.com/cdn-cgi/trace"
testVersion = "0.2.0"
defaultURL = "https://cloudflare-ech.com/cdn-cgi/trace"
)

var (
Expand All @@ -30,8 +29,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.
Expand Down Expand Up @@ -77,54 +75,52 @@ 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
}
}

// 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
controlChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult)
targetChannel := make(chan model.ArchivalTLSOrQUICHandshakeResult)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

go func() {
controlChannel <- *handshake(
ctx,
conn,
args.Measurement.MeasurementStartTimeSaved,
address,
parsed.Host,
args.Session.Logger(),
)
}()

go func() {
targetChannel <- *handshakeWithEch(
ctx,
conn2,
args.Measurement.MeasurementStartTimeSaved,
address,
parsed.Host,
args.Session.Logger(),
)
}()

control := <-controlChannel
target := <-targetChannel

args.Measurement.TestKeys = TestKeys{Control: control, Target: target}
args.Measurement.TestKeys = TestKeys{TLSHandshakes: []*model.ArchivalTLSOrQUICHandshakeResult{
&results[0], &results[1], &results[2],
}}

return nil
}
Expand Down
13 changes: 8 additions & 5 deletions internal/experiment/echcheck/measure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
2 changes: 2 additions & 0 deletions internal/model/archival.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ 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"`
Tags []string `json:"tags"`
Expand Down

0 comments on commit e64f736

Please sign in to comment.