Skip to content

Commit 587b47d

Browse files
authored
feat: add ECH GREASE Report (#11)
1 parent 562fd01 commit 587b47d

5 files changed

Lines changed: 20085 additions & 191 deletions

File tree

curl/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This is a custom build of `curl` with ECH support from the [DEfO project](https:
1414

1515
A helper script, `build-curl.sh`, is provided to automate the build process for `curl` and its dependency, `openssl`.
1616

17-
To build the ECH-enabled `curl`, run the script from the `greasereport` directory and provide an output path:
17+
To build the ECH-enabled `curl`, run the script from the project root and provide an output path:
1818

1919
```sh
2020
./curl/build-curl.sh <output_directory>
@@ -23,7 +23,7 @@ To build the ECH-enabled `curl`, run the script from the `greasereport` director
2323
For example, to build `curl` and place the output in the `workspace` directory:
2424

2525
```sh
26-
./curl/build-curl.sh ./workspace
26+
./curl/build-curl.sh workspace
2727
```
2828

2929
The script will download the source code for `openssl` and `curl`, build them, and install the final binaries in the specified output directory.

greasereport/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ You need to build the ECH-enabled `curl` and place it in the workspace directory
88

99
## Running
1010

11-
To run the tool, use the `go run` command from the `ech-test` directory:
11+
To run the tool, use the `go run` command from the project root directory. On Linux, you may need to set the `LD_LIBRARY_PATH` to point to the `lib` directory in your workspace so the custom `curl` binary can find its OpenSSL dependencies:
1212

1313
```sh
14-
go run ./greasereport --topN 100
14+
LD_LIBRARY_PATH="workspace/lib" go run ./greasereport --topN 100
1515
```
1616

1717
This will:

greasereport/main.go

Lines changed: 18 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -15,204 +15,26 @@
1515
package main
1616

1717
import (
18-
"bytes"
1918
"context"
2019
"encoding/csv"
2120
"flag"
2221
"fmt"
2322
"log/slog"
2423
"os"
25-
"os/exec"
2624
"path/filepath"
2725
"strconv"
28-
"strings"
2926
"sync"
3027
"time"
3128

29+
"github.com/Jigsaw-Code/ech-research/internal/echtest"
3230
"github.com/Jigsaw-Code/ech-research/internal/tranco"
3331
"github.com/Jigsaw-Code/ech-research/internal/workspace"
3432
"golang.org/x/sync/semaphore"
3533
)
3634

37-
type TestResult struct {
38-
Domain string
39-
Rank int
40-
ECHGrease bool
41-
Error string
42-
CurlExitCode int
43-
CurlErrorName string
44-
DNSLookup time.Duration
45-
TCPConnection time.Duration
46-
TLSHandshake time.Duration
47-
ServerTime time.Duration
48-
TotalTime time.Duration
49-
HTTPStatus int
50-
}
51-
52-
// TODO: Deduplicate this with the unified ECH testing package (internal/echtest).
53-
var curlExitCodeNames = map[int]string{
54-
1: "CURLE_UNSUPPORTED_PROTOCOL",
55-
2: "CURLE_FAILED_INIT",
56-
3: "CURLE_URL_MALFORMAT",
57-
4: "CURLE_NOT_BUILT_IN",
58-
5: "CURLE_COULDNT_RESOLVE_PROXY",
59-
6: "CURLE_COULDNT_RESOLVE_HOST",
60-
7: "CURLE_COULDNT_CONNECT",
61-
8: "CURLE_WEIRD_SERVER_REPLY",
62-
9: "CURLE_REMOTE_ACCESS_DENIED",
63-
11: "CURLE_FTP_WEIRD_PASV_REPLY",
64-
13: "CURLE_FTP_WEIRD_227_FORMAT",
65-
14: "CURLE_FTP_CANT_GET_HOST",
66-
15: "CURLE_FTP_CANT_RECONNECT",
67-
17: "CURLE_FTP_COULDNT_SET_TYPE",
68-
18: "CURLE_PARTIAL_FILE",
69-
19: "CURLE_FTP_COULDNT_RETR_FILE",
70-
21: "CURLE_QUOTE_ERROR",
71-
22: "CURLE_HTTP_RETURNED_ERROR",
72-
23: "CURLE_WRITE_ERROR",
73-
25: "CURLE_UPLOAD_FAILED",
74-
26: "CURLE_READ_ERROR",
75-
27: "CURLE_OUT_OF_MEMORY",
76-
28: "CURLE_OPERATION_TIMEDOUT",
77-
30: "CURLE_FTP_PORT_FAILED",
78-
31: "CURLE_FTP_COULDNT_USE_REST",
79-
33: "CURLE_RANGE_ERROR",
80-
34: "CURLE_HTTP_POST_ERROR",
81-
35: "CURLE_SSL_CONNECT_ERROR",
82-
36: "CURLE_BAD_DOWNLOAD_RESUME",
83-
37: "CURLE_FILE_COULDNT_READ_FILE",
84-
38: "CURLE_LDAP_CANNOT_BIND",
85-
39: "CURLE_LDAP_SEARCH_FAILED",
86-
41: "CURLE_FUNCTION_NOT_FOUND",
87-
42: "CURLE_ABORTED_BY_CALLBACK",
88-
43: "CURLE_BAD_FUNCTION_ARGUMENT",
89-
45: "CURLE_INTERFACE_FAILED",
90-
47: "CURLE_TOO_MANY_REDIRECTS",
91-
48: "CURLE_UNKNOWN_OPTION",
92-
49: "CURLE_TELNET_OPTION_SYNTAX",
93-
51: "CURLE_PEER_FAILED_VERIFICATION",
94-
52: "CURLE_GOT_NOTHING",
95-
53: "CURLE_SSL_ENGINE_NOTFOUND",
96-
54: "CURLE_SSL_ENGINE_SETFAILED",
97-
55: "CURLE_SEND_ERROR",
98-
56: "CURLE_RECV_ERROR",
99-
58: "CURLE_SSL_CERTPROBLEM",
100-
59: "CURLE_SSL_CIPHER",
101-
60: "CURLE_SSL_CACERT",
102-
61: "CURLE_BAD_CONTENT_ENCODING",
103-
62: "CURLE_LDAP_INVALID_URL",
104-
63: "CURLE_FILESIZE_EXCEEDED",
105-
64: "CURLE_USE_SSL_FAILED",
106-
65: "CURLE_SEND_FAIL_REWIND",
107-
66: "CURLE_SSL_ENGINE_INITFAILED",
108-
67: "CURLE_LOGIN_DENIED",
109-
68: "CURLE_TFTP_NOTFOUND",
110-
69: "CURLE_TFTP_PERM",
111-
70: "CURLE_REMOTE_DISK_FULL",
112-
71: "CURLE_TFTP_ILLEGAL",
113-
72: "CURLE_TFTP_UNKNOWNID",
114-
73: "CURLE_REMOTE_FILE_EXISTS",
115-
74: "CURLE_TFTP_NOSUCHUSER",
116-
75: "CURLE_CONV_FAILED",
117-
76: "CURLE_CONV_REQD",
118-
77: "CURLE_SSL_CACERT_BADFILE",
119-
78: "CURLE_REMOTE_FILE_NOT_FOUND",
120-
79: "CURLE_SSH",
121-
80: "CURLE_SSL_SHUTDOWN_FAILED",
122-
81: "CURLE_AGAIN",
123-
82: "CURLE_SSL_CRL_BADFILE",
124-
83: "CURLE_SSL_ISSUER_ERROR",
125-
84: "CURLE_FTP_PRET_FAILED",
126-
85: "CURLE_RTSP_CSEQ_ERROR",
127-
86: "CURLE_RTSP_SESSION_ERROR",
128-
87: "CURLE_FTP_BAD_FILE_LIST",
129-
88: "CURLE_CHUNK_FAILED",
130-
89: "CURLE_NO_CONNECTION_AVAILABLE",
131-
90: "CURLE_SSL_PINNEDPUBKEYNOTMATCH",
132-
91: "CURLE_SSL_INVALIDCERTSTATUS",
133-
92: "CURLE_HTTP2_STREAM",
134-
93: "CURLE_RECURSIVE_API_CALL",
135-
94: "CURLE_AUTH_ERROR",
136-
95: "CURLE_HTTP3",
137-
96: "CURLE_QUIC_CONNECT_ERROR",
138-
}
139-
140-
func runTest(curlPath string, domain tranco.Domain, echGrease bool, maxTime time.Duration) TestResult {
141-
result := TestResult{
142-
Domain: domain.Name,
143-
Rank: domain.Rank,
144-
ECHGrease: echGrease,
145-
}
146-
147-
url := "https://" + domain.Name
148-
149-
// curl -w "dnslookup:%{time_namelookup},tcpconnect:%{time_connect},tlsconnect:%{time_appconnect},servertime:%{time_starttransfer},total:%{time_total},httpstatus:%{http_code}" --head -s --ech grease https://example.com
150-
args := []string{
151-
"-w",
152-
"dnslookup:%{time_namelookup},tcpconnect:%{time_connect},tlsconnect:%{time_appconnect},servertime:%{time_starttransfer},total:%{time_total},httpstatus:%{http_code}",
153-
"--head",
154-
"-s", // silent
155-
"--max-time",
156-
strconv.FormatFloat(maxTime.Seconds(), 'f', -1, 64),
157-
}
158-
if echGrease {
159-
args = append(args, "--ech", "grease")
160-
} else {
161-
args = append(args, "--ech", "false")
162-
}
163-
args = append(args, url)
164-
165-
slog.Debug("running curl", "path", curlPath, "args", args)
166-
cmd := exec.Command(curlPath, args...)
167-
var out bytes.Buffer
168-
var stderr bytes.Buffer
169-
cmd.Stdout = &out
170-
cmd.Stderr = &stderr
171-
172-
err := cmd.Run()
173-
if err != nil {
174-
result.Error = fmt.Sprintf("failed to run curl: %v, stderr: %s", err, stderr.String())
175-
if exitError, ok := err.(*exec.ExitError); ok {
176-
result.CurlExitCode = exitError.ExitCode()
177-
result.CurlErrorName = curlExitCodeNames[result.CurlExitCode]
178-
}
179-
return result
180-
}
181-
182-
// parse the output
183-
// dnslookup:0.001,tcpconnect:0.002,tlsconnect:0.003,servertime:0.004,total:0.005,httpstatus:200
184-
parts := strings.Split(out.String(), ",")
185-
for _, part := range parts {
186-
kv := strings.Split(part, ":")
187-
if len(kv) != 2 {
188-
continue
189-
}
190-
key := kv[0]
191-
value := kv[1]
192-
193-
switch key {
194-
case "dnslookup":
195-
f, _ := strconv.ParseFloat(value, 64)
196-
result.DNSLookup = time.Duration(f * float64(time.Second))
197-
case "tcpconnect":
198-
f, _ := strconv.ParseFloat(value, 64)
199-
result.TCPConnection = time.Duration(f * float64(time.Second))
200-
case "tlsconnect":
201-
f, _ := strconv.ParseFloat(value, 64)
202-
result.TLSHandshake = time.Duration(f * float64(time.Second))
203-
case "servertime":
204-
f, _ := strconv.ParseFloat(value, 64)
205-
result.ServerTime = time.Duration(f * float64(time.Second))
206-
case "total":
207-
f, _ := strconv.ParseFloat(value, 64)
208-
result.TotalTime = time.Duration(f * float64(time.Second))
209-
case "httpstatus":
210-
i, _ := strconv.Atoi(value)
211-
result.HTTPStatus = i
212-
}
213-
}
214-
215-
return result
35+
type DomainTestResult struct {
36+
Rank int
37+
echtest.TestResult
21638
}
21739

21840
func main() {
@@ -239,7 +61,7 @@ func main() {
23961
// Determine curl binary path.
24062
curlPath := *curlPathFlag
24163
if curlPath == "" {
242-
curlPath = filepath.Join(workspaceDir, "output", "bin", "curl")
64+
curlPath = filepath.Join(workspaceDir, "bin", "curl")
24365
}
24466

24567
// Ensure Tranco list is present.
@@ -274,18 +96,25 @@ func main() {
27496
os.Exit(1)
27597
}
27698

277-
resultsCh := make(chan TestResult, 2*(*topNFlag))
99+
resultsCh := make(chan DomainTestResult, 2*(*topNFlag))
278100

279101
var csvWg sync.WaitGroup
280102
csvWg.Add(1)
281103
go func() {
282104
defer csvWg.Done()
283105
for result := range resultsCh {
106+
errorStr := result.GoError
107+
if errorStr == "" && result.CurlErrorMessage != "" {
108+
errorStr = result.CurlErrorMessage
109+
} else if errorStr == "" && result.Stderr != "" && result.CurlExitCode != 0 {
110+
errorStr = result.Stderr
111+
}
112+
284113
record := []string{
285114
result.Domain,
286115
strconv.Itoa(result.Rank),
287116
strconv.FormatBool(result.ECHGrease),
288-
result.Error,
117+
errorStr,
289118
strconv.Itoa(result.CurlExitCode),
290119
result.CurlErrorName,
291120
strconv.FormatInt(result.DNSLookup.Milliseconds(), 10),
@@ -314,7 +143,8 @@ func main() {
314143
defer sem.Release(1)
315144
defer wg.Done()
316145
slog.Info("Testing domain", "rank", d.Rank, "domain", d.Name, "ech_grease", false)
317-
resultsCh <- runTest(curlPath, d, false, *maxTimeFlag)
146+
res := echtest.Run(curlPath, d.Name, false, *maxTimeFlag, "", nil)
147+
resultsCh <- DomainTestResult{Rank: d.Rank, TestResult: res}
318148
}(domain)
319149

320150
if err := sem.Acquire(context.Background(), 1); err != nil {
@@ -325,7 +155,8 @@ func main() {
325155
defer sem.Release(1)
326156
defer wg.Done()
327157
slog.Info("Testing domain", "rank", d.Rank, "domain", d.Name, "ech_grease", true)
328-
resultsCh <- runTest(curlPath, d, true, *maxTimeFlag)
158+
res := echtest.Run(curlPath, d.Name, true, *maxTimeFlag, "", nil)
159+
resultsCh <- DomainTestResult{Rank: d.Rank, TestResult: res}
329160
}(domain)
330161
}
331162

0 commit comments

Comments
 (0)