Merge v2.11 into v3.6

This commit is contained in:
mmatur 2026-02-11 14:23:17 +01:00
commit a28da8a226
No known key found for this signature in database
GPG key ID: 2FFE42FC256CFF8E
3 changed files with 138 additions and 1 deletions

View file

@ -1,3 +1,10 @@
## [v2.11.37](https://github.com/traefik/traefik/tree/v2.11.37) (2026-02-11)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.36...v2.11.37)
**Bug fixes:**
- **[healthcheck]** Validate healthcheck path configuration (#12642 by @rtribotte)
- **[tls, server]** Cap TLS record length to RFC 8446 limit in ClientHello peeking (#12638 by @mmatur)
## [v2.11.36](https://github.com/traefik/traefik/tree/v2.11.36) (2026-02-02)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.35...v2.11.36)

View file

@ -18,7 +18,15 @@ import (
"github.com/traefik/traefik/v3/pkg/tcp"
)
const defaultBufSize = 4096
const (
defaultBufSize = 4096
// Per RFC 8446 Section 5.1, the maximum TLS record payload length is 2^14 (16384) bytes.
// A ClientHello is always a plaintext record, so any value exceeding this limit is invalid
// and likely indicates an attack attempting to force oversized per-connection buffer allocations.
// However, in practice the go server handshake can read up to 16384 + 2048 bytes,
// so we need to allow for some extra bytes to avoid rejecting valid handshakes.
maxTLSRecordLen = 16384 + 2048
)
// Router is a TCP router.
type Router struct {
@ -406,6 +414,14 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) {
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
if recLen > maxTLSRecordLen {
log.Debug().Msgf("Error while peeking client hello bytes, oversized record: %d", recLen)
return &clientHello{
isTLS: true,
peeked: getPeeked(br),
}, nil
}
if recordHeaderLen+recLen > defaultBufSize {
br = bufio.NewReaderSize(br, recordHeaderLen+recLen)
}

View file

@ -1,6 +1,7 @@
package tcp
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
@ -648,6 +649,7 @@ func Test_Routing(t *testing.T) {
_ = serverHTTPS.Serve(httpsForwarder)
}()
// The HTTPS forwarder will be added as tcp.TLSHandler (to handle TLS).
router.SetHTTPSForwarder(httpsForwarder)
stoppedTCP := make(chan struct{})
@ -1082,6 +1084,118 @@ func checkHTTPSTLS12(addr string, timeout time.Duration) error {
return checkHTTPS(addr, timeout, tls.VersionTLS12)
}
// Test_clientHelloInfo_oversizedRecordLength verifies that clientHelloInfo
// does not block or allocate excessive memory when a client sends a TLS
// record header with a maliciously large record length (up to 0xFFFF).
//
// Without the fix, clientHelloInfo allocates a ~65KB bufio.Reader and blocks
// on Peek(65540), waiting for bytes that never arrive (until readTimeout).
// With the fix, records exceeding the TLS maximum plaintext size (16384)
// are rejected immediately.
func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) {
testCases := []struct {
desc string
recLen uint16
}{
{
desc: "max uint16 record length (0xFFFF)",
recLen: 0xFFFF,
},
{
desc: "just above TLS maximum (18433)",
recLen: 18433,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
type result struct {
hello *clientHello
err error
}
resultCh := make(chan result, 1)
go func() {
br := bufio.NewReader(serverConn)
hello, err := clientHelloInfo(br)
resultCh <- result{hello, err}
}()
// Send a TLS record header with an oversized record length.
// Only the 5-byte header is sent; the client then stalls.
hdr := []byte{
0x16, // Content Type: Handshake
0x03, 0x03, // Version: TLS 1.2
byte(test.recLen >> 8), // Length high byte
byte(test.recLen & 0xFF), // Length low byte
}
_, err := clientConn.Write(hdr)
require.NoError(t, err)
// Without the fix, clientHelloInfo blocks on Peek(recLen+5)
// since only 5 bytes are available. The test would time out.
// With the fix, it returns immediately.
select {
case r := <-resultCh:
require.NoError(t, r.err)
require.NotNil(t, r.hello)
assert.True(t, r.hello.isTLS)
case <-time.After(5 * time.Second):
t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped")
}
})
}
}
// Test_clientHelloInfo_validRecordLength verifies that clientHelloInfo
// still works correctly with legitimate TLS record sizes.
func Test_clientHelloInfo_validRecordLength(t *testing.T) {
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
type result struct {
hello *clientHello
err error
}
resultCh := make(chan result, 1)
go func() {
br := bufio.NewReader(serverConn)
hello, err := clientHelloInfo(br)
resultCh <- result{hello, err}
}()
// Build a TLS record header with a small (valid) record length.
recLen := 100
hdr := []byte{
0x16, // Content Type: Handshake
0x03, 0x03, // Version: TLS 1.2
byte(recLen >> 8), // Length high byte
byte(recLen & 0xFF), // Length low byte
}
payload := make([]byte, recLen)
_, err := clientConn.Write(append(hdr, payload...))
require.NoError(t, err)
clientConn.Close()
select {
case r := <-resultCh:
require.NoError(t, r.err)
require.NotNil(t, r.hello)
assert.True(t, r.hello.isTLS)
case <-time.After(5 * time.Second):
t.Fatal("clientHelloInfo blocked on valid TLS record")
}
}
func TestPostgres(t *testing.T) {
router, err := NewRouter()
require.NoError(t, err)