traefik/pkg/server/server_entrypoint_tcp_test.go

385 lines
11 KiB
Go
Raw Permalink Normal View History

package server
import (
"bufio"
"context"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
2023-02-03 14:24:05 +00:00
"github.com/traefik/traefik/v3/pkg/config/static"
tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp"
"github.com/traefik/traefik/v3/pkg/tcp"
"golang.org/x/net/http2"
)
func TestShutdownHijacked(t *testing.T) {
router, err := tcprouter.NewRouter()
require.NoError(t, err)
2022-03-17 17:02:08 +00:00
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
conn, _, err := rw.(http.Hijacker).Hijack()
require.NoError(t, err)
resp := http.Response{StatusCode: http.StatusOK}
err = resp.Write(conn)
require.NoError(t, err)
}))
2020-12-29 09:54:03 +00:00
testShutdown(t, router)
}
func TestShutdownHTTP(t *testing.T) {
router, err := tcprouter.NewRouter()
require.NoError(t, err)
2022-03-17 17:02:08 +00:00
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
time.Sleep(time.Second)
}))
2020-12-29 09:54:03 +00:00
testShutdown(t, router)
}
func TestShutdownTCP(t *testing.T) {
2022-03-17 17:02:08 +00:00
router, err := tcprouter.NewRouter()
require.NoError(t, err)
2024-01-23 10:34:05 +00:00
err = router.AddTCPRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
2022-11-24 16:06:07 +00:00
_, err := http.ReadRequest(bufio.NewReader(conn))
if err != nil {
return
}
2022-11-24 16:06:07 +00:00
resp := http.Response{StatusCode: http.StatusOK}
_ = resp.Write(conn)
}))
2022-03-17 17:02:08 +00:00
require.NoError(t, err)
testShutdown(t, router)
}
2022-03-17 17:02:08 +00:00
func testShutdown(t *testing.T, router *tcprouter.Router) {
2020-12-29 09:54:03 +00:00
t.Helper()
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.LifeCycle.RequestAcceptGraceTimeout = 0
epConfig.LifeCycle.GraceTimeOut = ptypes.Duration(5 * time.Second)
epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(5 * time.Second)
epConfig.RespondingTimeouts.WriteTimeout = ptypes.Duration(5 * time.Second)
entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
// We explicitly use an IPV4 address because on Alpine, with an IPV6 address
// there seems to be shenanigans related to properly cleaning up file descriptors
Address: "127.0.0.1:0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
HTTP2: &static.HTTP2Config{},
}, nil, nil)
require.NoError(t, err)
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
2022-11-24 16:06:07 +00:00
t.Cleanup(func() { _ = conn.Close() })
epAddr := entryPoint.listener.Addr().String()
request, err := http.NewRequest(http.MethodHead, "http://127.0.0.1:8082", nil)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
2022-11-24 16:06:07 +00:00
// We need to do a write on conn before the shutdown to make it "exist".
// Because the connection indeed exists as far as TCP is concerned,
// but since we only pass it along to the HTTP server after at least one byte is peeked,
// the HTTP server (and hence its shutdown) does not know about the connection until that first byte peeked.
err = request.Write(conn)
require.NoError(t, err)
2022-11-24 16:06:07 +00:00
reader := bufio.NewReaderSize(conn, 1)
// Wait for first byte in response.
_, err = reader.Peek(1)
require.NoError(t, err)
go entryPoint.Shutdown(context.Background())
// Make sure that new connections are not permitted anymore.
// Note that this should be true not only after Shutdown has returned,
// but technically also as early as the Shutdown has closed the listener,
// i.e. during the shutdown and before the gracetime is over.
var testOk bool
2024-02-19 14:44:03 +00:00
for range 10 {
loopConn, err := net.Dial("tcp", epAddr)
if err == nil {
loopConn.Close()
time.Sleep(100 * time.Millisecond)
continue
}
if !strings.HasSuffix(err.Error(), "connection refused") && !strings.HasSuffix(err.Error(), "reset by peer") {
t.Fatalf(`unexpected error: got %v, wanted "connection refused" or "reset by peer"`, err)
}
testOk = true
break
}
if !testOk {
t.Fatal("entry point never closed")
}
// And make sure that the connection we had opened before shutting things down is still operational
resp, err := http.ReadResponse(reader, request)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
2022-03-17 17:02:08 +00:00
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcprouter.Router) (net.Conn, error) {
go entryPoint.Start(context.Background())
entryPoint.SwitchRouter(router)
2024-02-19 14:44:03 +00:00
for range 10 {
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
return conn, err
}
return nil, errors.New("entry point never started")
}
func TestReadTimeoutWithoutFirstByte(t *testing.T) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second)
entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
Address: ":0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
HTTP2: &static.HTTP2Config{},
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
require.NoError(t, err)
2022-03-17 17:02:08 +00:00
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
2019-09-26 09:00:06 +00:00
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
errChan := make(chan error)
go func() {
b := make([]byte, 2048)
_, err := conn.Read(b)
errChan <- err
}()
select {
case err := <-errChan:
require.Equal(t, io.EOF, err)
case <-time.Tick(5 * time.Second):
t.Error("Timeout while read")
}
}
func TestReadTimeoutWithFirstByte(t *testing.T) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second)
entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
Address: ":0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
HTTP2: &static.HTTP2Config{},
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
require.NoError(t, err)
2022-03-17 17:02:08 +00:00
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
_, err = conn.Write([]byte("GET /some HTTP/1.1\r\n"))
require.NoError(t, err)
errChan := make(chan error)
go func() {
b := make([]byte, 2048)
_, err := conn.Read(b)
errChan <- err
}()
select {
case err := <-errChan:
require.Equal(t, io.EOF, err)
case <-time.Tick(5 * time.Second):
t.Error("Timeout while read")
}
}
func TestKeepAliveMaxRequests(t *testing.T) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.KeepAliveMaxRequests = 3
entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
Address: ":0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
HTTP2: &static.HTTP2Config{},
2024-01-03 15:45:47 +00:00
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
http.DefaultClient.Transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
},
}
resp, err := http.Get("http://" + entryPoint.listener.Addr().String())
require.NoError(t, err)
require.False(t, resp.Close)
err = resp.Body.Close()
require.NoError(t, err)
resp, err = http.Get("http://" + entryPoint.listener.Addr().String())
require.NoError(t, err)
require.False(t, resp.Close)
err = resp.Body.Close()
require.NoError(t, err)
resp, err = http.Get("http://" + entryPoint.listener.Addr().String())
require.NoError(t, err)
require.True(t, resp.Close)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestKeepAliveMaxTime(t *testing.T) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.KeepAliveMaxTime = ptypes.Duration(time.Millisecond)
entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
Address: ":0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
HTTP2: &static.HTTP2Config{},
2024-01-03 15:45:47 +00:00
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
http.DefaultClient.Transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
},
}
resp, err := http.Get("http://" + entryPoint.listener.Addr().String())
require.NoError(t, err)
require.False(t, resp.Close)
err = resp.Body.Close()
require.NoError(t, err)
time.Sleep(time.Millisecond)
resp, err = http.Get("http://" + entryPoint.listener.Addr().String())
require.NoError(t, err)
require.True(t, resp.Close)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestKeepAliveH2c(t *testing.T) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.KeepAliveMaxRequests = 1
2024-11-20 13:08:24 +00:00
entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
Address: ":0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
HTTP2: &static.HTTP2Config{},
2024-11-20 13:08:24 +00:00
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
http2Transport := &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
return conn, nil
},
}
client := &http.Client{Transport: http2Transport}
resp, err := client.Get("http://" + entryPoint.listener.Addr().String())
require.NoError(t, err)
require.False(t, resp.Close)
err = resp.Body.Close()
require.NoError(t, err)
_, err = client.Get("http://" + entryPoint.listener.Addr().String())
require.Error(t, err)
// Unlike HTTP/1, where we can directly check `resp.Close`, HTTP/2 uses a different
// mechanism: it sends a GOAWAY frame when the connection is closing.
// We can only check the error type. The error received should be poll.ErrClosed from
// the `internal/poll` package, but we cannot directly reference the error type due to
// package restrictions. Since this error message ("use of closed network connection")
// is distinct and specific, we rely on its consistency, assuming it is stable and unlikely
// to change.
require.Contains(t, err.Error(), "use of closed network connection")
}