Skip to content

fix(mev): stop the backrunme sender silently forcing HTTP/1.1 - #222

Merged
datluongductuan merged 1 commit into
mainfrom
fix/bloxroute-backrunme-transport
Aug 27, 2026
Merged

fix(mev): stop the backrunme sender silently forcing HTTP/1.1#222
datluongductuan merged 1 commit into
mainfrom
fix/bloxroute-backrunme-transport

Conversation

@datluongductuan

Copy link
Copy Markdown
Member

Vấn đề

NewBloxrouteBackrunmeSender tự dựng Transport từ đầu và gán cho nó một TLSClientConfig để bật InsecureSkipVerify, chép theo curl --insecure trong docs bloxroute:

httpClient := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    },
}

Đúng một field đó âm thầm ép mọi submit về HTTP/1.1. Luật nằm ở Transport.protocols() trong net/http:

case !t.ForceAttemptHTTP2 && (t.TLSClientConfig != nil || t.Dial != nil || t.DialContext != nil || ...):
    // KHONG bat HTTP/2

Lý do: để negotiate h2, Go phải sửa tls.Config của bạn — chèn "h2" vào NextProtos (xem http2configureTransports). Nó từ chối làm vậy với config do caller cung cấp, trừ khi ForceAttemptHTTP2 được set. Đây là quyết định có chủ đích của stdlib (Go issue 14275), không phải bug — nhưng hệ quả thì hoàn toàn im lặng.

Vì sao đáng sửa: trên h2 một connection multiplex hàng trăm stream nên MaxIdleConnsPerHost gần như vô nghĩa. Trên h1 thì mặc định của Go là 2 (field hiện 0, map sang hằng http.DefaultMaxIdleConnsPerHost), nên mỗi request đồng thời từ thứ 3 trở đi phải bắt tay TCP+TLS lại. Peak đo được ở endpoint này là 54 req/s.

InsecureSkipVerifythừabackrunme.blxrbdn.com phục vụ h2 bình thường với xác thực cert đầy đủ:

&http.Transport{TLSClientConfig:{skip}}   -> HTTP/1.1     <- hien tai
&http.Transport{} (TLSClientConfig=nil)   -> HTTP/2.0
DefaultTransport.Clone()                  -> HTTP/2.0     IdleConnTimeout=1m30s

Nên nó không mua được gì, mà mất cả giao thức lẫn chống MITM — đáng lưu ý vì payload ở đây là bundle đã ký.

Sửa

Clone default của stdlib thay vì dựng Transport từ đầu. Một http.Transport zero-value còn mất thêm:

field zero-value DefaultTransport
ForceAttemptHTTP2 false true
IdleConnTimeout 0 = không bao giờ hết hạn 90s
MaxIdleConns 0 = không giới hạn 100
Proxy nil ProxyFromEnvironment
dial timeout / keepalive không có 30s / 30s

IdleConnTimeout = 0 đáng ngại riêng: conn rảnh nằm mãi chờ server đóng, và Go chỉ phát hiện lúc dùng lại — thường biểu hiện thành lỗi EOF lác đác.

Test

TestBackrunmeSenderNegotiatesHTTP2 dựng httptest server có EnableHTTP2, cho chính transport của sender gửi request, rồi assert r.Proto == "HTTP/2.0" ở phía server.

Chi tiết quan trọng: test chỉ thêm RootCAs để tin cert tự ký, không thay sender.httpClient bằng srv.Client(). Bản nháp đầu tôi làm thế và test thành vacuous — nó kiểm transport của httptest chứ không phải của constructor, nên pass bất kể constructor làm gì.

Mutation-check: hoàn nguyên về transport cũ thì test fail đúng ở hành vi quan sát được, không phải ở proxy cấu trúc —

Error:    Not equal:
          expected: "HTTP/2.0"
          actual  : "HTTP/1.1"
Messages: backrunme submits must negotiate HTTP/2; a caller-supplied TLSClientConfig silently disables it

go build ./... sạch, go test ./pkg/mev/... pass. golangci-lint run ./pkg/mev/... trở về đúng baseline của main (7 issue có sẵn: gosec 1, modernize 6) — thay đổi này không thêm issue nào.

Phạm vi

Chỉ đụng NewBloxrouteBackrunmeSender. Signature giữ nguyên vì tradinglib được nhiều repo dùng chung.

NewMevShareSender có vấn đề họ hàng — nó đi qua mev-share-go/rpc.NewClient, cái này hardcode &http.Client{} và không có điểm inject. Nhưng nó không hỏng: &http.Client{}Transport nil nên rơi về http.DefaultTransport, vốn đã ForceAttemptHTTP2: true → vẫn h2. Sửa nó cần đụng dependency ngoài mà không giải quyết bug nào đang tồn tại, nên để riêng.

🤖 Generated with Claude Code

NewBloxrouteBackrunmeSender built its Transport from scratch and gave it
a TLSClientConfig for InsecureSkipVerify, mirroring the `curl --insecure`
in bloxroute's docs.

net/http only negotiates HTTP/2 when it may append "h2" to
TLSClientConfig.NextProtos, and Transport.protocols refuses to touch a
caller-supplied config unless ForceAttemptHTTP2 is set (Go issue 14275).
That single field pinned every submit to HTTP/1.1, where the default
MaxIdleConnsPerHost of 2 costs a fresh TCP+TLS handshake per concurrent
submit past the second. Measured peak on this endpoint is 54 req/s.

backrunme.blxrbdn.com serves h2 under normal certificate verification, so
the skip bought nothing and cost both the protocol and MITM protection --
which matters here because the payload is a signed bundle.

Cloning the stdlib default also restores IdleConnTimeout (a zero-value
Transport never expires idle conns), Proxy, and the dial timeout and
keepalive, none of which the hand-built Transport had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZ2nnbqg3wa9rTMFaPEZzm
@datluongductuan
datluongductuan merged commit 7e2962c into main Aug 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants