Merge branch v2.11 into v3.6
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Build and Publish Documentation / Doc Process (push) Waiting to run
Build experimental image on branch / build-webui (push) Waiting to run
Build experimental image on branch / Build experimental image on branch (push) Waiting to run

This commit is contained in:
romain 2026-03-18 15:43:34 +01:00
commit b1b520b186
15 changed files with 227 additions and 113 deletions

View file

@ -1,3 +1,14 @@
## [v2.11.41](https://github.com/traefik/traefik/tree/v2.11.41) (2026-03-18)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.40...v2.11.41)
**Bug fixes:**
- **[http]** Add maxResponseBodySize configuration on HTTP provider ([#12788](https://github.com/traefik/traefik/pull/12788) @gndz07)
- **[tls]** Support fragmented TLS client hello ([#12787](https://github.com/traefik/traefik/pull/12787) @rtribotte)
- **[middleware, authentication]** Make basic auth check timing constant ([#12803](https://github.com/traefik/traefik/pull/12803) @rtribotte)
**Documentation:**
- Bump mkdocs-traefiklabs to use consent mode ([#12804](https://github.com/traefik/traefik/pull/12804) @darkweaver87)
## [v3.6.10](https://github.com/traefik/traefik/tree/v3.6.10) (2026-03-06)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.9...v3.6.10)

View file

@ -10,6 +10,12 @@ Adding Basic Authentication
The BasicAuth middleware grants access to services to authorized users only.
!!! warning "Timing attacks"
The BasicAuth middleware is vulnerable to timing attacks when the configured users' password hashes do not use the same algorithm and cost.
However, when the configured user's password hashes are of the same algorithm and cost, the middleware guarantees the same comparison time between existing and non-existing users.
This prevents an attacker from leveraging the time difference to determine whether a user exists.
## Configuration Examples
```yaml tab="Docker & Swarm"

View file

@ -200,3 +200,25 @@ providers:
```bash tab="CLI"
--providers.http.tls.insecureSkipVerify=true
```
### `maxResponseBodySize`
_Optional, Default=-1_
Defines the maximum size of the response body in bytes.
If left unset (or set to -1), the response body size is unrestricted which can have performance implications.
```yaml tab="File (YAML)"
providers:
http:
maxResponseBodySize: -1
```
```toml tab="File (TOML)"
[providers.http]
maxResponseBodySize = -1
```
```bash tab="CLI"
--providers.http.maxResponseBodySize=-1
```

View file

@ -321,6 +321,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-http" href="#opt-providers-http" title="#opt-providers-http">providers.http</a> | Enables HTTP provider. | false |
| <a id="opt-providers-http-endpoint" href="#opt-providers-http-endpoint" title="#opt-providers-http-endpoint">providers.http.endpoint</a> | Load configuration from this endpoint. | |
| <a id="opt-providers-http-headers-name" href="#opt-providers-http-headers-name" title="#opt-providers-http-headers-name">providers.http.headers._name_</a> | Define custom headers to be sent to the endpoint. | |
| <a id="opt-providers-http-maxresponsebodysize" href="#opt-providers-http-maxresponsebodysize" title="#opt-providers-http-maxresponsebodysize">providers.http.maxresponsebodysize</a> | Defines the maximum size of the response body in bytes. | -1 |
| <a id="opt-providers-http-pollinterval" href="#opt-providers-http-pollinterval" title="#opt-providers-http-pollinterval">providers.http.pollinterval</a> | Polling interval for endpoint. | 5 |
| <a id="opt-providers-http-polltimeout" href="#opt-providers-http-polltimeout" title="#opt-providers-http-polltimeout">providers.http.polltimeout</a> | Polling timeout for endpoint. | 5 |
| <a id="opt-providers-http-tls-ca" href="#opt-providers-http-tls-ca" title="#opt-providers-http-tls-ca">providers.http.tls.ca</a> | TLS CA | |

View file

@ -933,6 +933,9 @@ Load configuration from this endpoint.
`--providers.http.headers.<name>`:
Define custom headers to be sent to the endpoint.
`--providers.http.maxresponsebodysize`:
Defines the maximum size of the response body in bytes. (Default: ```-1```)
`--providers.http.pollinterval`:
Polling interval for endpoint. (Default: ```5```)

View file

@ -933,6 +933,9 @@ Load configuration from this endpoint.
`TRAEFIK_PROVIDERS_HTTP_HEADERS_<NAME>`:
Define custom headers to be sent to the endpoint.
`TRAEFIK_PROVIDERS_HTTP_MAXRESPONSEBODYSIZE`:
Defines the maximum size of the response body in bytes. (Default: ```-1```)
`TRAEFIK_PROVIDERS_HTTP_POLLINTERVAL`:
Polling interval for endpoint. (Default: ```5```)

View file

@ -310,6 +310,7 @@
[providers.http.headers]
name0 = "foobar"
name1 = "foobar"
maxResponseBodySize = 42
[providers.http.tls]
ca = "foobar"
cert = "foobar"

View file

@ -354,6 +354,7 @@ providers:
cert: foobar
key: foobar
insecureSkipVerify: true
maxResponseBodySize: 42
plugin:
PluginConf0:
name0: foobar

View file

@ -1,7 +1,7 @@
mkdocs==1.4.3
mkdocs-include-markdown-plugin==7.2.0
mkdocs-exclude==1.0.2
mkdocs-traefiklabs>=100.0.7
mkdocs-traefiklabs>=100.1.0
mkdocs-redirects==1.2.2
click==8.1.7

View file

@ -3,8 +3,10 @@ package auth
import (
"context"
"fmt"
"maps"
"net/http"
"net/url"
"slices"
"strings"
goauth "github.com/abbot/go-http-auth"
@ -27,6 +29,7 @@ type basicAuth struct {
removeHeader bool
name string
notFoundSecret string
checkSecret func(password, secret string) bool
singleflightGroup *singleflight.Group
}
@ -40,12 +43,18 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu
return nil, err
}
// To prevent timing attacks, we need to compute a hash even if the user is not found.
// We assume it to be safe only when the users hashes are all from the same algorithm,
// so we can pick the first one as a random hash to compute.
notFoundSecret := users[slices.Collect(maps.Values(users))[0]]
ba := &basicAuth{
next: next,
users: users,
headerField: authConfig.HeaderField,
removeHeader: authConfig.RemoveHeader,
name: name,
notFoundSecret: notFoundSecret,
checkSecret: goauth.CheckSecret,
singleflightGroup: new(singleflight.Group),
}
@ -68,8 +77,9 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger := middlewares.GetLogger(req.Context(), b.name, typeNameBasic)
user, password, ok := req.BasicAuth()
var authenticated bool
if ok {
ok = b.checkPassword(user, password)
authenticated = b.checkPassword(user, password)
}
logData := accesslog.GetLogData(req)
@ -77,7 +87,7 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logData.Core[accesslog.ClientUsername] = user
}
if !ok {
if !authenticated {
logger.Debug().Msg("Authentication failed")
observability.SetStatusErrorf(req.Context(), "Authentication failed")
@ -101,19 +111,21 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
func (b *basicAuth) checkPassword(user, password string) bool {
secret := b.auth.Secrets(user, b.auth.Realm)
if secret == "" {
return false
}
key := password + secret
match, _, _ := b.singleflightGroup.Do(key, func() (any, error) {
if secret == "" {
_ = b.checkSecret(password, b.notFoundSecret)
return false, nil
}
return b.checkSecret(password, secret), nil
})
return match.(bool)
}
func (b *basicAuth) secretBasic(user, realm string) string {
func (b *basicAuth) secretBasic(user, _ string) string {
if secret, ok := b.users[user]; ok {
return secret
}

View file

@ -25,6 +25,8 @@ import (
var _ provider.Provider = (*Provider)(nil)
const defaultMaxResponseBodySize = -1
// Provider is a provider.Provider implementation that queries an HTTP(s) endpoint for a configuration.
type Provider struct {
Endpoint string `description:"Load configuration from this endpoint." json:"endpoint" toml:"endpoint" yaml:"endpoint"`
@ -35,12 +37,14 @@ type Provider struct {
httpClient *http.Client
lastConfigurationHash uint64
MaxResponseBodySize int64 `description:"Defines the maximum size of the response body in bytes." json:"maxResponseBodySize,omitempty" toml:"maxResponseBodySize,omitempty" yaml:"maxResponseBodySize,omitempty" export:"true"`
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.PollInterval = ptypes.Duration(5 * time.Second)
p.PollTimeout = ptypes.Duration(5 * time.Second)
p.MaxResponseBodySize = defaultMaxResponseBodySize
}
// Init the provider.
@ -168,7 +172,19 @@ func (p *Provider) fetchConfigurationData() ([]byte, error) {
return nil, fmt.Errorf("received non-ok response code: %d", res.StatusCode)
}
return io.ReadAll(res.Body)
if p.MaxResponseBodySize < 0 {
return io.ReadAll(res.Body)
}
data, err := io.ReadAll(io.LimitReader(res.Body, p.MaxResponseBodySize+1))
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
if int64(len(data)) > p.MaxResponseBodySize {
return nil, errors.New("response body too large")
}
return data, nil
}
// decodeConfiguration decodes and returns the dynamic configuration from the given data.

View file

@ -14,6 +14,7 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/tls"
"k8s.io/utils/ptr"
)
func TestProvider_Init(t *testing.T) {
@ -65,15 +66,17 @@ func TestProvider_SetDefaults(t *testing.T) {
assert.Equal(t, provider.PollInterval, ptypes.Duration(5*time.Second))
assert.Equal(t, provider.PollTimeout, ptypes.Duration(5*time.Second))
assert.Equal(t, int64(-1), provider.MaxResponseBodySize)
}
func TestProvider_fetchConfigurationData(t *testing.T) {
tests := []struct {
desc string
statusCode int
headers map[string]string
expData []byte
expErr require.ErrorAssertionFunc
desc string
statusCode int
headers map[string]string
expData []byte
expErr require.ErrorAssertionFunc
maxResponseBodySize *int64
}{
{
desc: "should return the fetched configuration data",
@ -97,6 +100,25 @@ func TestProvider_fetchConfigurationData(t *testing.T) {
statusCode: http.StatusInternalServerError,
expErr: require.Error,
},
{
desc: "should return an error response body is too long when maxResponseBodySize is 0",
statusCode: http.StatusOK,
maxResponseBodySize: ptr.To(int64(0)),
expErr: require.Error,
},
{
desc: "should return an error response body is too long when response is longer than maxResponseBodySize",
statusCode: http.StatusOK,
maxResponseBodySize: ptr.To(int64(1)),
expErr: require.Error,
},
{
desc: "should return the fetched configuration data when response is the same length with maxResponseBodySize",
statusCode: http.StatusOK,
maxResponseBodySize: ptr.To(int64(2)),
expData: []byte("{}"),
expErr: require.NoError,
},
}
for _, test := range tests {
@ -118,11 +140,15 @@ func TestProvider_fetchConfigurationData(t *testing.T) {
}))
defer srv.Close()
provider := Provider{
Endpoint: srv.URL,
Headers: test.headers,
PollInterval: ptypes.Duration(1 * time.Second),
PollTimeout: ptypes.Duration(1 * time.Second),
var provider Provider
provider.SetDefaults()
provider.Headers = test.headers
provider.Endpoint = srv.URL
provider.PollTimeout = ptypes.Duration(1 * time.Second)
provider.PollInterval = ptypes.Duration(1 * time.Second)
if test.maxResponseBodySize != nil {
provider.MaxResponseBodySize = *test.maxResponseBodySize
}
err := provider.Init()
@ -201,11 +227,12 @@ func TestProvider_Provide(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
provider := Provider{
Endpoint: server.URL,
PollTimeout: ptypes.Duration(1 * time.Second),
PollInterval: ptypes.Duration(100 * time.Millisecond),
}
var provider Provider
provider.SetDefaults()
provider.Endpoint = server.URL
provider.PollTimeout = ptypes.Duration(1 * time.Second)
provider.PollInterval = ptypes.Duration(100 * time.Millisecond)
err := provider.Init()
require.NoError(t, err)
@ -257,11 +284,12 @@ func TestProvider_ProvideConfigurationOnlyOnceIfUnchanged(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
provider := Provider{
Endpoint: server.URL + "/endpoint",
PollTimeout: ptypes.Duration(1 * time.Second),
PollInterval: ptypes.Duration(100 * time.Millisecond),
}
var provider Provider
provider.SetDefaults()
provider.Endpoint = server.URL + "/endpoint"
provider.PollTimeout = ptypes.Duration(1 * time.Second)
provider.PollInterval = ptypes.Duration(100 * time.Millisecond)
err := provider.Init()
require.NoError(t, err)

View file

@ -51,7 +51,8 @@ func (r *Router) servePostgres(conn tcp.WriteCloser) {
return
}
br := bufio.NewReader(conn)
var peeked bytes.Buffer
br := bufio.NewReader(io.TeeReader(conn, &peeked))
b := make([]byte, len(PostgresStartTLSMsg))
_, err = br.Read(b)
@ -93,7 +94,7 @@ func (r *Router) servePostgres(conn tcp.WriteCloser) {
}
// We are in TLS mode and if the handler is not TLSHandler, we are in passthrough.
proxiedConn := r.GetConn(conn, hello.peeked)
proxiedConn := r.GetConn(conn, peeked.String())
if _, ok := handlerTCPTLS.(*tcp.TLSHandler); !ok {
proxiedConn = &postgresConn{WriteCloser: proxiedConn}
}

View file

@ -19,15 +19,8 @@ import (
"github.com/traefik/traefik/v3/pkg/tcp"
)
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
)
// errClientHelloRead is used as a sentinel error to break the TLS handshake once we have read the ClientHello.
var errClientHelloRead = errors.New("client hello successfully read")
// Router is a TCP router.
type Router struct {
@ -127,7 +120,9 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
}
// TODO -- Check if ProxyProtocol changes the first bytes of the request
br := bufio.NewReader(conn)
var peeked bytes.Buffer
br := bufio.NewReader(io.TeeReader(conn, &peeked))
postgres, err := isPostgres(br)
if err != nil {
conn.Close()
@ -135,7 +130,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
}
if postgres {
r.servePostgres(r.GetConn(conn, getPeeked(br)))
r.servePostgres(r.GetConn(conn, peeked.String()))
return
}
@ -168,9 +163,9 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
handler, _ := r.muxerTCP.Match(connData)
switch {
case handler != nil:
handler.ServeTCP(r.GetConn(conn, hello.peeked))
handler.ServeTCP(r.GetConn(conn, peeked.String()))
case r.httpForwarder != nil:
r.httpForwarder.ServeTCP(r.GetConn(conn, hello.peeked))
r.httpForwarder.ServeTCP(r.GetConn(conn, peeked.String()))
default:
conn.Close()
}
@ -179,7 +174,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
// Handling ACME-TLS/1 challenges.
if !r.acmeTLSPassthrough && slices.Contains(hello.protos, tlsalpn01.ACMETLS1Protocol) {
r.acmeTLSALPNHandler().ServeTCP(r.GetConn(conn, hello.peeked))
r.acmeTLSALPNHandler().ServeTCP(r.GetConn(conn, peeked.String()))
return
}
@ -193,14 +188,14 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
// In order not to depart from the behavior in 2.6,
// we only allow an HTTPS router to take precedence over a TCP-TLS router if it is _not_ an HostSNI(*) router
// (so basically any router that has a specific HostSNI based rule).
handlerHTTPS.ServeTCP(r.GetConn(conn, hello.peeked))
handlerHTTPS.ServeTCP(r.GetConn(conn, peeked.String()))
return
}
// Contains also TCP TLS passthrough routes.
handlerTCPTLS, catchAllTCPTLS := r.muxerTCPTLS.Match(connData)
if handlerTCPTLS != nil && !catchAllTCPTLS {
handlerTCPTLS.ServeTCP(r.GetConn(conn, hello.peeked))
handlerTCPTLS.ServeTCP(r.GetConn(conn, peeked.String()))
return
}
@ -208,19 +203,19 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
// We end up here for e.g. an HTTPS router that only has a PathPrefix rule,
// which under the scenes is counted as an HostSNI(*) rule.
if handlerHTTPS != nil {
handlerHTTPS.ServeTCP(r.GetConn(conn, hello.peeked))
handlerHTTPS.ServeTCP(r.GetConn(conn, peeked.String()))
return
}
// Fallback on TCP TLS catchAll.
if handlerTCPTLS != nil {
handlerTCPTLS.ServeTCP(r.GetConn(conn, hello.peeked))
handlerTCPTLS.ServeTCP(r.GetConn(conn, peeked.String()))
return
}
// To handle 404s for HTTPS.
if r.httpsForwarder != nil {
r.httpsForwarder.ServeTCP(r.GetConn(conn, hello.peeked))
r.httpsForwarder.ServeTCP(r.GetConn(conn, peeked.String()))
return
}
@ -375,7 +370,6 @@ type clientHello struct {
serverName string // SNI server name
protos []string // ALPN protocols list
isTLS bool // whether we are a TLS handshake
peeked string // the bytes peeked from the hello while getting the info
}
// clientHelloInfo returns various data from the clientHello handshake,
@ -396,74 +390,46 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) {
if hdr[0] == recordTypeSSLv2 {
// we consider SSLv2 as TLS, and it will be refused by real TLS handshake.
return &clientHello{
isTLS: true,
peeked: getPeeked(br),
isTLS: true,
}, nil
}
return &clientHello{
peeked: getPeeked(br),
}, nil // Not TLS.
return &clientHello{}, nil // Not TLS.
}
const recordHeaderLen = 5
hdr, err = br.Peek(recordHeaderLen)
if err != nil {
return nil, fmt.Errorf("peeking client hello headers: %w", err)
}
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
if recLen > maxTLSRecordLen {
return nil, fmt.Errorf("peeking client hello bytes, oversized record: %d", recLen)
}
if recordHeaderLen+recLen > defaultBufSize {
br = bufio.NewReaderSize(br, recordHeaderLen+recLen)
}
helloBytes, err := br.Peek(recordHeaderLen + recLen)
if err != nil {
return nil, fmt.Errorf("peeking client hello bytes: %w", err)
}
sni := ""
var protos []string
server := tls.Server(helloSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
var (
sni string
protos []string
)
server := tls.Server(readOnlyConn{r: br}, &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
sni = hello.ServerName
protos = hello.SupportedProtos
return nil, nil
// This error prevents unnecessary additional steps in the TLS ClientHello message processing.
return nil, errClientHelloRead
},
})
_ = server.Handshake()
if handshakeErr := server.Handshake(); !errors.Is(handshakeErr, errClientHelloRead) {
return nil, fmt.Errorf("reading client hello: %w", handshakeErr)
}
return &clientHello{
serverName: sni,
isTLS: true,
peeked: getPeeked(br),
protos: protos,
}, nil
}
func getPeeked(br *bufio.Reader) string {
peeked, err := br.Peek(br.Buffered())
if err != nil {
log.Error().Err(err).Msg("Error while peeking bytes")
return ""
}
return string(peeked)
}
// helloSniffConn is a net.Conn that reads from r, fails on Writes,
// readOnlyConn is a net.Conn that reads from r, fails on Writes,
// and crashes otherwise.
type helloSniffConn struct {
type readOnlyConn struct {
net.Conn // nil; crash on any unexpected use
r io.Reader
}
// Read reads from the underlying reader.
func (c helloSniffConn) Read(p []byte) (int, error) { return c.r.Read(p) }
func (c readOnlyConn) Read(p []byte) (int, error) { return c.r.Read(p) }
// Write crashes all the time.
func (helloSniffConn) Write(p []byte) (int, error) { return 0, io.EOF }
func (readOnlyConn) Write(_ []byte) (int, error) { return 0, io.EOF }

View file

@ -1151,12 +1151,34 @@ func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) {
}
}
// Test_clientHelloInfo_validRecordLength verifies that clientHelloInfo
// still works correctly with legitimate TLS record sizes.
func Test_clientHelloInfo_validRecordLength(t *testing.T) {
// Test_clientHelloInfo_tlsRecordFragmentation documents a known limitation:
// clientHelloInfo only reads a single TLS record. When a ClientHello handshake
// message is split across multiple TLS records (RFC 5246 §6.2.1), the SNI cannot
// be extracted, leaving serverName empty and allowing SNI-based routing to be bypassed.
func Test_clientHelloInfo_tlsRecordFragmentation(t *testing.T) {
serverName := "foo.example.com"
record := buildClientHelloRecord(t, serverName)
const hdrLen = 5
payload := record[hdrLen:]
ver1, ver2 := record[1], record[2]
var recordsData bytes.Buffer
for _, part := range [][]byte{payload[:len(serverName)/2], payload[len(serverName)/2:]} {
recordsData.WriteByte(0x16)
recordsData.WriteByte(ver1)
recordsData.WriteByte(ver2)
recordsData.WriteByte(byte(len(part) >> 8))
recordsData.WriteByte(byte(len(part)))
recordsData.Write(part)
}
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
t.Cleanup(func() {
_ = serverConn.Close()
_ = clientConn.Close()
})
type result struct {
hello *clientHello
@ -1170,30 +1192,51 @@ func Test_clientHelloInfo_validRecordLength(t *testing.T) {
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...))
_, err := clientConn.Write(recordsData.Bytes())
require.NoError(t, err)
clientConn.Close()
_ = clientConn.Close()
select {
case r := <-resultCh:
require.NoError(t, r.err)
require.NotNil(t, r.hello)
assert.True(t, r.hello.isTLS)
assert.Equal(t, serverName, r.hello.serverName)
case <-time.After(5 * time.Second):
t.Fatal("clientHelloInfo blocked on valid TLS record")
t.Fatal("clientHelloInfo blocked")
}
}
// buildClientHelloRecord captures a real TLS ClientHello record from Go's TLS stack
// for the given serverName.
// It returns the raw record bytes and the byte offset of the SNI value within those bytes.
func buildClientHelloRecord(t *testing.T, serverName string) []byte {
t.Helper()
serverConn, clientConn := net.Pipe()
recordCh := make(chan []byte, 1)
go func() {
buf := make([]byte, 65536)
n, _ := serverConn.Read(buf)
_ = serverConn.Close()
recordCh <- buf[:n]
}()
go func() {
tlsConn := tls.Client(clientConn, &tls.Config{
ServerName: serverName,
InsecureSkipVerify: true, //nolint:gosec
})
_ = tlsConn.Handshake()
_ = clientConn.Close()
}()
record := <-recordCh
return record
}
func TestPostgres(t *testing.T) {
router, err := NewRouter()
require.NoError(t, err)