From 30b98ac1d0f783a341689a39a1b2bee69e39ecba Mon Sep 17 00:00:00 2001 From: TassioSales Date: Wed, 14 Oct 2026 12:55:07 -0300 Subject: [PATCH 1/2] feat(tls): add ALPN NextProtos propagation and accept http2/http3 protocols - Allow http2 and http3 in ValidateOptions (-pr/options.Protocol) - Propagate ALPN NextProtos (h2, http/1.1) in TLS config and pass it to fastdialer DialTLSWithConfig - Configure transport Protocols and ALPN consistently for HTTP/1.1 and HTTP/2 - Add tests for ALPN configuration and protocol validation Fixes #2669 --- common/httpx/httpx.go | 73 ++++++++++++++++++++++++++++---------- common/httpx/httpx_test.go | 22 ++++++++++++ runner/options.go | 2 +- runner/runner_test.go | 12 +++++++ 4 files changed, 89 insertions(+), 20 deletions(-) diff --git a/common/httpx/httpx.go b/common/httpx/httpx.go index f07c9c64e..ff3abe037 100644 --- a/common/httpx/httpx.go +++ b/common/httpx/httpx.go @@ -140,25 +140,42 @@ func New(options *Options) (*HTTPX, error) { return nil } } + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS10, + } + if httpx.Options.SniName != "" { + tlsConfig.ServerName = httpx.Options.SniName + } + switch httpx.Options.Protocol { + case HTTP11: + tlsConfig.NextProtos = []string{"http/1.1"} + case HTTP2: + tlsConfig.NextProtos = []string{"h2"} + default: + tlsConfig.NextProtos = []string{"h2", "http/1.1"} + } + transport := &http.Transport{ DialContext: httpx.Dialer.Dial, DialTLSContext: httpx.buildTLSDialer(options), MaxIdleConnsPerHost: -1, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS10, - }, - DisableKeepAlives: true, + TLSClientConfig: tlsConfig, + DisableKeepAlives: true, } if httpx.Options.Protocol == HTTP11 { // disable http2 _ = os.Setenv("GODEBUG", "http2client=0") + var p http.Protocols + p.SetHTTP1(true) + p.SetHTTP2(false) + transport.Protocols = &p transport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{} - } - - if httpx.Options.SniName != "" { - transport.TLSClientConfig.ServerName = httpx.Options.SniName + } else if httpx.Options.Protocol == HTTP2 { + var p http.Protocols + p.SetHTTP2(true) + transport.Protocols = &p } if httpx.Options.HTTPProxy != "" { @@ -190,15 +207,19 @@ func New(options *Options) (*HTTPX, error) { var protocols http.Protocols protocols.SetHTTP2(true) protocols.SetUnencryptedHTTP2(true) - transport2 := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS10, - }, - Protocols: &protocols, + tlsConfig2 := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS10, + NextProtos: []string{"h2"}, } if httpx.Options.SniName != "" { - transport2.TLSClientConfig.ServerName = httpx.Options.SniName + tlsConfig2.ServerName = httpx.Options.SniName + } + transport2 := &http.Transport{ + DialContext: httpx.Dialer.Dial, + DialTLSContext: httpx.buildTLSDialer(options), + TLSClientConfig: tlsConfig2, + Protocols: &protocols, } httpx.client2 = &http.Client{ Transport: transport2, @@ -220,14 +241,28 @@ func New(options *Options) (*HTTPX, error) { } func (h *HTTPX) buildTLSDialer(options *Options) func(ctx context.Context, network, addr string) (net.Conn, error) { + tlsCfg := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS10, + } + if options.SniName != "" { + tlsCfg.ServerName = options.SniName + } + switch options.Protocol { + case HTTP11: + tlsCfg.NextProtos = []string{"http/1.1"} + case HTTP2: + tlsCfg.NextProtos = []string{"h2"} + default: + tlsCfg.NextProtos = []string{"h2", "http/1.1"} + } + if options.TlsImpersonate == "" { return func(ctx context.Context, network, addr string) (net.Conn, error) { - return h.Dialer.DialTLS(ctx, network, addr) + return h.Dialer.DialTLSWithConfig(ctx, network, addr, tlsCfg) } } - tlsCfg := &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS10} - strategy, identity := resolveImpersonateStrategy(options.TlsImpersonate) return func(ctx context.Context, network, addr string) (net.Conn, error) { diff --git a/common/httpx/httpx_test.go b/common/httpx/httpx_test.go index 9687d7d06..8a6fc5db6 100644 --- a/common/httpx/httpx_test.go +++ b/common/httpx/httpx_test.go @@ -199,3 +199,25 @@ func TestDoSwitchingProtocolsDoesNotHang(t *testing.T) { t.Fatal("Do hung on 101 Switching Protocols response") } } + +func TestProtocolALPNConfiguration(t *testing.T) { + for _, tc := range []struct { + proto Proto + expectedProtos []string + }{ + {HTTP11, []string{"http/1.1"}}, + {HTTP2, []string{"h2"}}, + {UNKNOWN, []string{"h2", "http/1.1"}}, + } { + opts := DefaultOptions + opts.Protocol = tc.proto + ht, err := New(&opts) + require.NoError(t, err) + require.NotNil(t, ht) + tr, ok := ht.client.HTTPClient.Transport.(*http.Transport) + require.True(t, ok) + require.NotNil(t, tr.TLSClientConfig) + require.Equal(t, tc.expectedProtos, tr.TLSClientConfig.NextProtos) + } +} + diff --git a/runner/options.go b/runner/options.go index f636b9d16..31813e4d2 100644 --- a/runner/options.go +++ b/runner/options.go @@ -871,7 +871,7 @@ func (options *Options) ValidateOptions() error { options.OutputCDN = "true" } - if !stringsutil.EqualFoldAny(options.Protocol, string(httpxcommon.UNKNOWN), string(httpxcommon.HTTP11)) { + if !stringsutil.EqualFoldAny(options.Protocol, string(httpxcommon.UNKNOWN), string(httpxcommon.HTTP11), string(httpxcommon.HTTP2), string(httpxcommon.HTTP3)) { return fmt.Errorf("invalid protocol: %s", options.Protocol) } diff --git a/runner/runner_test.go b/runner/runner_test.go index 78b6726be..dc06ab7dc 100644 --- a/runner/runner_test.go +++ b/runner/runner_test.go @@ -550,6 +550,18 @@ func TestStoreResponse_withMatcherSetsFlag(t *testing.T) { require.True(t, opts.HasMatcherOrFilter()) } +func TestProtocolValidation(t *testing.T) { + for _, proto := range []string{"", "http11", "http2", "http3", "HTTP11", "HTTP2", "HTTP3"} { + opts := &Options{Protocol: proto} + err := opts.ValidateOptions() + require.NoError(t, err, "protocol %s should be valid", proto) + } + invalidOpts := &Options{Protocol: "ftp"} + err := invalidOpts.ValidateOptions() + require.Error(t, err) +} + + func TestRunner_duplicate(t *testing.T) { const ( pageA = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nWelcomeHello world default page content here" From 35600b8ea2d167583b79abfe21c2d55785080329 Mon Sep 17 00:00:00 2001 From: TassioSales Date: Thu, 15 Oct 2026 09:06:53 -0300 Subject: [PATCH 2/2] fix(httpx): normalize protocol casing, use HTTP/2 dialer for client2, and warn on HTTP/3 - Normalize options.Protocol to lowercase in runner and New() to handle mixed casing (e.g. HTTP11, HTTP2) - Configure secondary transport (client2) with HTTP/2 dialer options so HTTP/2 probes always advertise h2 ALPN - Log warning when HTTP/3 is selected as it falls back to default TLS negotiation --- common/httpx/httpx.go | 8 +++++++- runner/runner.go | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/common/httpx/httpx.go b/common/httpx/httpx.go index ff3abe037..155c28f77 100644 --- a/common/httpx/httpx.go +++ b/common/httpx/httpx.go @@ -147,6 +147,10 @@ func New(options *Options) (*HTTPX, error) { if httpx.Options.SniName != "" { tlsConfig.ServerName = httpx.Options.SniName } + httpx.Options.Protocol = Proto(strings.ToLower(string(httpx.Options.Protocol))) + if httpx.Options.Protocol == HTTP3 { + gologger.Warning().Msg("HTTP/3 is experimental and currently falls back to default TLS negotiation") + } switch httpx.Options.Protocol { case HTTP11: tlsConfig.NextProtos = []string{"http/1.1"} @@ -215,9 +219,11 @@ func New(options *Options) (*HTTPX, error) { if httpx.Options.SniName != "" { tlsConfig2.ServerName = httpx.Options.SniName } + client2Opts := *options + client2Opts.Protocol = HTTP2 transport2 := &http.Transport{ DialContext: httpx.Dialer.Dial, - DialTLSContext: httpx.buildTLSDialer(options), + DialTLSContext: httpx.buildTLSDialer(&client2Opts), TLSClientConfig: tlsConfig2, Protocols: &protocols, } diff --git a/runner/runner.go b/runner/runner.go index c903054dc..2a5262605 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -235,7 +235,7 @@ func New(options *Options) (*Runner, error) { } httpxOptions.Resolvers = options.Resolvers httpxOptions.TlsImpersonate = options.TlsImpersonate - httpxOptions.Protocol = httpx.Proto(options.Protocol) + httpxOptions.Protocol = httpx.Proto(strings.ToLower(options.Protocol)) var key, value string httpxOptions.CustomHeaders = make(map[string][]string)