cloudflare / go

Go with Cloudflare experimental patches
BSD 3-Clause "New" or "Revised" License
329 stars 49 forks source link

Creating a ECH client connection #138

Open mbinns opened 1 year ago

mbinns commented 1 year ago

I am trying to create an ECH connection as a client and I was hoping to use this fork in a proof of concept example.

Looking through the source it looks like if I specify ECHEnabled: true in my TLS config without specifying an ECHConfig it will fail over to GREASE. I am missing on what value/how to create my own ECHConfig struct to pass that in so that I can get a legitimate ECH connection

Performing a dig -t TYPE65 crypto.cloudflare.com request provides me the following and I am assuming ech=<value> is the config I need?

crypto.cloudflare.com.  198     IN      HTTPS   1 . alpn="http/1.1,h2" ipv4hint=162.159.137.85,162.159.138.85 ech=AEb+DQBClAAgACCA88XUXJSY8yxlp6HWzE6uW3fyWQRcLW1e8o48eVAIfQAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA ipv6hint=2606:4700:7::a29f:8955,2606:4700:7::a29f:8a55

I see you guys have an ECHConfig struct in tls/ech_config.go and clearly the tls config options take an array for that is there a marshaling function that I should be using with the raw value that comes back from the DNS lookup?

Also how then would I go about modifying the ClientHelloInner to point to the remote resource I am trying to access.

Sorry for the potentially naive questions, I've been reading over the source and trying to piece things together but I figured I might have better luck asking.

As an example of what I have so far in a toy example. I am fairly certain this is a successful GREASE request?

func main() {
    req, _ := http.NewRequest("GET", "https://crypto.cloudflare.com/cdn-cgi/trace", nil)

    req.Host =  "crypto.cloudflare.com"
    req.Header.Set("User-Agent", "Mozilla/5.0")

    client := http.Client{
        Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            ServerName:         "crypto.cloudflare.com",
            ECHEnabled:         true,
            },
        },
    }

    o, err:= client.Do(req)

    /* Print response */
    if nil != err {
        log.Fatalf("Dump: %v", err)
    }
    bodyBytes, _ := io.ReadAll(o.Body)
    fmt.Printf("%s\n", string(bodyBytes))
}

Thanks again for your time and work on this!

cjpatton commented 1 year ago

Hi @mbinns, to enable ECH in our test client, you need to

  1. Set config.ECHEnabled = true (you've alredy got this part)
  2. Set config.ClientECHConfigs to the []ECHConfig advertised by the DNS server.

Looks like you've already done the hard part of fetching the ECH configs. To parse them:

    echConfigListBase64 := "AEb+DQBClAAgACCA88XUXJSY8yxlp6HWzE6uW3fyWQRcLW1e8o48eVAIfQAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"
    echConfigListBtyes, err := base64.StdEncoding.DecodeString(echConfigListBase64)
    if err != nil {
        panic(err)
    }

    echConfigList, err := tls.UnmarshalECHConfigs(echConfigListBtyes)
    if err != nil {
        panic(err)
    }

Also, just FYI that we rotate our ECH config fairly frequently ... you should only expect a given config to be accepted for a couple of hours.

cjpatton commented 1 year ago

As an example of what I have so far in a toy example. I am fairly certain this is a successful GREASE request?

Yes, seting ECHEnabled without providing ECH configs will result in a GREASE handshake.

mbinns commented 1 year ago

Yup I have supporting code to grab the ECH config on an hourly basis since you guys mentioned you rotate frequently in one of your blog posts. I think you mentioned you guys leave some wiggle room in there because DNS cache issues on client systems/networks?

looks like I was able to successfully hit crypto.cloudflare.com/cdn-cgi/trace I received this:

fl=541f47
h=crypto.cloudflare.com
ip=<removed>
ts=1673402819.281
visit_scheme=https
uag=Mozilla/5.0
colo=SEA
sliver=none
http=http/1.1
loc=US
tls=TLSv1.3
sni=encrypted
warp=off
gateway=off
kex=X25519

The SNI shows encrypted and the connection in wireshark looks good, so looks like things worked out great there!

Probably an obvious question, but how do I go about specifying the values like SNI inside the InnerHello. from reading the source it looks like the inner hello is derived from the outer hello. and there doesn't seem to be a way to modify the values separately?

My ignorant attempt would be to just change the tls.ServerName and the req.Host values to what I want the inner to represent.

cjpatton commented 1 year ago

When ECH is enabled, config.ServerName is used as the inner SNI. The outer SNI is specified by the ECHConfig.

cjpatton commented 1 year ago

I think you mentioned you guys leave some wiggle room in there because DNS cache issues on client systems/networks?

Yeah, there is wiggle room to allow for the DNS server and TLS server to get out of sync (very unlikely, at least in our deployment) or the TLS client and TLS server to get out of sync.

mbinns commented 1 year ago

It looks like I have things working on my end.

Some other questions, if you have time (if not feel free to close)

If the server at crypto.cloudflare.com supports a HelloRR I would be willing to put some time into trying to make the client side work if you guys are interested in an outside PR.

Also I wanted to say thanks for helping me out with all the questions!