Files
minio/docs/investigations/issue-154/probe.go
T
Feng Ruohang 48e1846525 fix(tls): honor Go key exchange defaults across transports
Remove the eight explicit curve overrides so Go 1.27 honors tlsmlkem=0
across Server listeners, node links and outbound transports. Remove the
unused shared curve option and add wire-level regression coverage.

Document CA trust and TLS upgrade behavior, retain the investigation
artifacts, and exclude their synthetic routes from the rebrand guard.
The product compatibility baseline remains unchanged.

Validation: focused race tests, HTTP tests, lint, compatibility guard
positive/negative controls, and a fresh Linux build with three isolated
OIDC integration scenarios all pass.

Adversarial review: Claude Code Fable 5.1, max effort.
Final verdict: APPROVE FOR COMMIT.

Signed-off-by: Feng Ruohang <rh@vonng.com>
2026-09-09 18:50:12 +08:00

166 lines
5.8 KiB
Go

//go:build ignore
// Diagnostic GET using the Server's actual transport constructor.
// Build explicitly from the SILO module root; see ../issue-154.md.
package main
import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/minio/minio/cmd"
xhttp "github.com/minio/minio/internal/http"
"github.com/pgsty/silo-pkg/v3/certs"
)
func main() {
endpoint := flag.String("url", os.Getenv("OIDC_URL"), "discovery URL; no credentials or query string")
ca := flag.String("ca", "", "same CA file or certs/CAs directory as Server")
h2 := flag.Bool("h2", false, "diagnostic: opt in to HTTP/2")
classical := flag.Bool("classical", false, "diagnostic: omit hybrid key exchange only")
defaultCurves := flag.Bool("default-curves", false, "diagnostic: let Go choose curves and honor its GODEBUG defaults")
tls12 := flag.Bool("tls12", false, "diagnostic: TLS 1.2 only; keeps certificate verification")
direct := flag.Bool("direct", false, "diagnostic: bypass environment proxy")
ip := flag.String("ip", "", "diagnostic: pin destination IP, preserving Host/SNI; requires -direct")
fresh := flag.Bool("fresh", false, "diagnostic: close idle connections between requests")
ua := flag.String("ua", "issue-154-probe", "HTTP User-Agent; supply actual Server UA to investigate a WAF")
n := flag.Int("n", 1, "number of GETs (1 to 3)")
flag.Parse()
u, err := url.Parse(*endpoint)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || *n < 1 || *n > 3 {
fmt.Fprintln(os.Stderr, "require an HTTPS URL without credentials/query/fragment and -n between 1 and 3")
os.Exit(2)
}
if *ip != "" && (!*direct || net.ParseIP(*ip) == nil) {
fmt.Fprintln(os.Stderr, "-ip requires a literal IP and -direct")
os.Exit(2)
}
if *classical && *defaultCurves {
fmt.Fprintln(os.Stderr, "choose at most one of -classical and -default-curves")
os.Exit(2)
}
tr := cmd.NewHTTPTransport()
tr.TLSClientConfig.RootCAs, err = certs.GetRootCAs(*ca)
if err != nil {
fmt.Fprintln(os.Stderr, "CA loading failed; check the local CA path")
os.Exit(2)
}
if *h2 {
tr.ForceAttemptHTTP2 = true
}
if *classical {
tr.TLSClientConfig.CurvePreferences = []tls.CurveID{tls.CurveP256, tls.X25519, tls.CurveP384, tls.CurveP521}
}
if *defaultCurves {
tr.TLSClientConfig.CurvePreferences = nil
}
if *tls12 {
tr.TLSClientConfig.MinVersion = tls.VersionTLS12
tr.TLSClientConfig.MaxVersion = tls.VersionTLS12
}
if *direct {
tr.Proxy = nil
}
if *ip != "" {
base := tr.DialContext
tr.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
host, port, e := net.SplitHostPort(address)
if e != nil {
return nil, e
}
if host == u.Hostname() {
address = net.JoinHostPort(*ip, port)
}
return base(ctx, network, address)
}
}
defer tr.CloseIdleConnections()
var mu sync.Mutex
log := func(format string, args ...any) { mu.Lock(); defer mu.Unlock(); fmt.Printf(format+"\n", args...) }
log("go=%s os=%s arch=%s h2=%v classical=%v default_curves=%v tls12=%v", runtime.Version(), runtime.GOOS, runtime.GOARCH, *h2, *classical, *defaultCurves, *tls12)
// Deliberately print no URL, headers, body, client ID, secret, or token.
req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
proxy := "direct"
if tr.Proxy != nil {
p, e := tr.Proxy(req)
if e != nil {
log("proxy_selection_error=%T", e)
os.Exit(2)
}
if p != nil {
proxy = p.Scheme + " proxy (address omitted)"
}
}
log("route=%s curves=%v", proxy, tr.TLSClientConfig.CurvePreferences)
client := &http.Client{Transport: xhttp.WithUserAgent(tr, func() string { return *ua }), Timeout: 20 * time.Second,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
failed := false
for i := 0; i < *n; i++ {
if *fresh {
tr.CloseIdleConnections()
}
log("request=%d", i+1)
trace := &httptrace.ClientTrace{
DNSDone: func(d httptrace.DNSDoneInfo) { log("dns_addresses=%v err=%s", d.Addrs, errorClass(d.Err)) },
ConnectStart: func(network, addr string) { log("connect=%s %s", network, addr) },
ConnectDone: func(_, addr string, e error) { log("connected=%s err=%s", addr, errorClass(e)) },
TLSHandshakeStart: func() { log("tls_start") },
TLSHandshakeDone: func(s tls.ConnectionState, e error) {
log("tls_done=0x%x cipher=%s alpn=%q resumed=%v verified_chains=%d err=%s", s.Version, tls.CipherSuiteName(s.CipherSuite), s.NegotiatedProtocol, s.DidResume, len(s.VerifiedChains), errorClass(e))
},
GotConn: func(c httptrace.GotConnInfo) { log("got_conn=%s reused=%v", c.Conn.RemoteAddr(), c.Reused) },
WroteRequest: func(w httptrace.WroteRequestInfo) { log("wrote_request err=%s", errorClass(w.Err)) },
GotFirstResponseByte: func() { log("first_response_byte") },
}
r := req.Clone(httptrace.WithClientTrace(context.Background(), trace))
resp, e := client.Do(r)
if e != nil {
log("get_error=%s", errorClass(e))
failed = true
continue
}
log("status=%d protocol=%s", resp.StatusCode, resp.Proto)
_, e = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if e != nil || resp.StatusCode != http.StatusOK {
failed = true
log("body_error=%s", errorClass(e))
}
}
if failed {
os.Exit(1)
}
}
func errorClass(err error) string {
if err == nil {
return "none"
}
if errors.Is(err, context.DeadlineExceeded) {
return "deadline"
}
// Error text may contain a private URL. Emit only category and concrete type.
category := "other"
s := err.Error()
for _, k := range []string{"connection reset by peer", "x509:", "TLS handshake timeout", "connection refused", "EOF"} {
if strings.Contains(s, k) {
category = k
break
}
}
return fmt.Sprintf("%s (%T)", category, err)
}