mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-06-19 01:56:34 +00:00

Previously, during local flow migration the current connection context was not part of the migration and would cause the flow to still be listening on the connection context of the old connection (before the migration). This meant that if a flow was migrated from connection 0 to connection 1, and connection 0 goes away, the flow would be early terminated incorrectly with the context lifetime of connection 0. The new connection context is provided during migration of a flow and will trigger the observe loop for the flow lifetime to be rebound to this provided context. Closes TUN-8748
405 lines
11 KiB
Go
405 lines
11 KiB
Go
package v3_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
v3 "github.com/cloudflare/cloudflared/quic/v3"
|
|
)
|
|
|
|
var (
|
|
expectedContextCanceled = errors.New("expected context canceled")
|
|
|
|
testOriginAddr = net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:0"))
|
|
testLocalAddr = net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:0"))
|
|
)
|
|
|
|
func TestSessionNew(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
session := v3.NewSession(testRequestID, 5*time.Second, nil, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
|
if testRequestID != session.ID() {
|
|
t.Fatalf("session id doesn't match: %s != %s", testRequestID, session.ID())
|
|
}
|
|
}
|
|
|
|
func testSessionWrite(t *testing.T, payload []byte) {
|
|
log := zerolog.Nop()
|
|
origin := newTestOrigin(makePayload(1280))
|
|
session := v3.NewSession(testRequestID, 5*time.Second, &origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
|
n, err := session.Write(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != len(payload) {
|
|
t.Fatal("unable to write the whole payload")
|
|
}
|
|
if !slices.Equal(payload, origin.write[:len(payload)]) {
|
|
t.Fatal("payload provided from origin and read value are not the same")
|
|
}
|
|
}
|
|
|
|
func TestSessionWrite_Max(t *testing.T) {
|
|
payload := makePayload(1280)
|
|
testSessionWrite(t, payload)
|
|
}
|
|
|
|
func TestSessionWrite_Min(t *testing.T) {
|
|
payload := makePayload(0)
|
|
testSessionWrite(t, payload)
|
|
}
|
|
|
|
func TestSessionServe_OriginMax(t *testing.T) {
|
|
payload := makePayload(1280)
|
|
testSessionServe_Origin(t, payload)
|
|
}
|
|
|
|
func TestSessionServe_OriginMin(t *testing.T) {
|
|
payload := makePayload(0)
|
|
testSessionServe_Origin(t, payload)
|
|
}
|
|
|
|
func testSessionServe_Origin(t *testing.T, payload []byte) {
|
|
log := zerolog.Nop()
|
|
eyeball := newMockEyeball()
|
|
origin := newTestOrigin(payload)
|
|
session := v3.NewSession(testRequestID, 3*time.Second, &origin, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log)
|
|
defer session.Close()
|
|
|
|
ctx, cancel := context.WithCancelCause(context.Background())
|
|
defer cancel(context.Canceled)
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- session.Serve(ctx)
|
|
}()
|
|
|
|
select {
|
|
case data := <-eyeball.recvData:
|
|
// check received data matches provided from origin
|
|
expectedData := makePayload(1500)
|
|
v3.MarshalPayloadHeaderTo(testRequestID, expectedData[:])
|
|
copy(expectedData[17:], payload)
|
|
if !slices.Equal(expectedData[:17+len(payload)], data) {
|
|
t.Fatal("expected datagram did not equal expected")
|
|
}
|
|
cancel(expectedContextCanceled)
|
|
case err := <-ctx.Done():
|
|
// we expect the payload to return before the context to cancel on the session
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := <-done
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatal(err)
|
|
}
|
|
if !errors.Is(context.Cause(ctx), expectedContextCanceled) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSessionServe_OriginTooLarge(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
eyeball := newMockEyeball()
|
|
payload := makePayload(1281)
|
|
origin := newTestOrigin(payload)
|
|
session := v3.NewSession(testRequestID, 2*time.Second, &origin, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log)
|
|
defer session.Close()
|
|
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- session.Serve(context.Background())
|
|
}()
|
|
|
|
select {
|
|
case data := <-eyeball.recvData:
|
|
// we never expect a read to make it here because the origin provided a payload that is too large
|
|
// for cloudflared to proxy and it will drop it.
|
|
t.Fatalf("we should never proxy a payload of this size: %d", len(data))
|
|
case err := <-done:
|
|
if !errors.Is(err, v3.SessionIdleErr{}) {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionServe_Migrate(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
eyeball := newMockEyeball()
|
|
pipe1, pipe2 := net.Pipe()
|
|
session := v3.NewSession(testRequestID, 2*time.Second, pipe2, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log)
|
|
defer session.Close()
|
|
|
|
done := make(chan error)
|
|
eyeball1Ctx, cancel := context.WithCancelCause(context.Background())
|
|
go func() {
|
|
done <- session.Serve(eyeball1Ctx)
|
|
}()
|
|
|
|
// Migrate the session to a new connection before origin sends data
|
|
eyeball2 := newMockEyeball()
|
|
eyeball2.connID = 1
|
|
eyeball2Ctx := context.Background()
|
|
session.Migrate(&eyeball2, eyeball2Ctx, &log)
|
|
|
|
// Cancel the origin eyeball context; this should not cancel the session
|
|
contextCancelErr := errors.New("context canceled for first eyeball connection")
|
|
cancel(contextCancelErr)
|
|
select {
|
|
case <-done:
|
|
t.Fatalf("expected session to still be running")
|
|
default:
|
|
}
|
|
if context.Cause(eyeball1Ctx) != contextCancelErr {
|
|
t.Fatalf("first eyeball context should be cancelled manually: %+v", context.Cause(eyeball1Ctx))
|
|
}
|
|
|
|
// Origin sends data
|
|
payload2 := []byte{0xde}
|
|
pipe1.Write(payload2)
|
|
|
|
// Expect write to eyeball2
|
|
data := <-eyeball2.recvData
|
|
if len(data) <= 17 || !slices.Equal(payload2, data[17:]) {
|
|
t.Fatalf("expected data to write to eyeball2 after migration: %+v", data)
|
|
}
|
|
|
|
select {
|
|
case data := <-eyeball.recvData:
|
|
t.Fatalf("expected no data to write to eyeball1 after migration: %+v", data)
|
|
default:
|
|
}
|
|
|
|
err := <-done
|
|
if !errors.Is(err, v3.SessionIdleErr{}) {
|
|
t.Error(err)
|
|
}
|
|
if eyeball2Ctx.Err() != nil {
|
|
t.Fatalf("second eyeball context should be not be cancelled")
|
|
}
|
|
}
|
|
|
|
func TestSessionServe_Migrate_CloseContext2(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
eyeball := newMockEyeball()
|
|
pipe1, pipe2 := net.Pipe()
|
|
session := v3.NewSession(testRequestID, 2*time.Second, pipe2, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log)
|
|
defer session.Close()
|
|
|
|
done := make(chan error)
|
|
eyeball1Ctx, cancel := context.WithCancelCause(context.Background())
|
|
go func() {
|
|
done <- session.Serve(eyeball1Ctx)
|
|
}()
|
|
|
|
// Migrate the session to a new connection before origin sends data
|
|
eyeball2 := newMockEyeball()
|
|
eyeball2.connID = 1
|
|
eyeball2Ctx, cancel2 := context.WithCancelCause(context.Background())
|
|
session.Migrate(&eyeball2, eyeball2Ctx, &log)
|
|
|
|
// Cancel the origin eyeball context; this should not cancel the session
|
|
contextCancelErr := errors.New("context canceled for first eyeball connection")
|
|
cancel(contextCancelErr)
|
|
select {
|
|
case <-done:
|
|
t.Fatalf("expected session to still be running")
|
|
default:
|
|
}
|
|
if context.Cause(eyeball1Ctx) != contextCancelErr {
|
|
t.Fatalf("first eyeball context should be cancelled manually: %+v", context.Cause(eyeball1Ctx))
|
|
}
|
|
|
|
// Origin sends data
|
|
payload2 := []byte{0xde}
|
|
pipe1.Write(payload2)
|
|
|
|
// Expect write to eyeball2
|
|
data := <-eyeball2.recvData
|
|
if len(data) <= 17 || !slices.Equal(payload2, data[17:]) {
|
|
t.Fatalf("expected data to write to eyeball2 after migration: %+v", data)
|
|
}
|
|
|
|
select {
|
|
case data := <-eyeball.recvData:
|
|
t.Fatalf("expected no data to write to eyeball1 after migration: %+v", data)
|
|
default:
|
|
}
|
|
|
|
// Close the connection2 context manually
|
|
contextCancel2Err := errors.New("context canceled for second eyeball connection")
|
|
cancel2(contextCancel2Err)
|
|
err := <-done
|
|
if err != context.Canceled {
|
|
t.Fatalf("session Serve should be done: %+v", err)
|
|
}
|
|
if context.Cause(eyeball2Ctx) != contextCancel2Err {
|
|
t.Fatalf("second eyeball context should have been cancelled manually: %+v", context.Cause(eyeball2Ctx))
|
|
}
|
|
}
|
|
|
|
func TestSessionClose_Multiple(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
origin := newTestOrigin(makePayload(128))
|
|
session := v3.NewSession(testRequestID, 5*time.Second, &origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
|
err := session.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !origin.closed.Load() {
|
|
t.Fatal("origin wasn't closed")
|
|
}
|
|
// subsequent closes shouldn't call close again or cause any errors
|
|
err = session.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSessionServe_IdleTimeout(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
origin := newTestIdleOrigin(10 * time.Second) // Make idle time longer than closeAfterIdle
|
|
closeAfterIdle := 2 * time.Second
|
|
session := v3.NewSession(testRequestID, closeAfterIdle, &origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
|
err := session.Serve(context.Background())
|
|
if !errors.Is(err, v3.SessionIdleErr{}) {
|
|
t.Fatal(err)
|
|
}
|
|
// session should be closed
|
|
if !origin.closed {
|
|
t.Fatalf("session should be closed after Serve returns")
|
|
}
|
|
// closing a session again should not return an error
|
|
err = session.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSessionServe_ParentContextCanceled(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
// Make idle time and idle timeout longer than closeAfterIdle
|
|
origin := newTestIdleOrigin(10 * time.Second)
|
|
closeAfterIdle := 10 * time.Second
|
|
|
|
session := v3.NewSession(testRequestID, closeAfterIdle, &origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
err := session.Serve(ctx)
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatal(err)
|
|
}
|
|
// session should be closed
|
|
if !origin.closed {
|
|
t.Fatalf("session should be closed after Serve returns")
|
|
}
|
|
// closing a session again should not return an error
|
|
err = session.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSessionServe_ReadErrors(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
origin := newTestErrOrigin(net.ErrClosed, nil)
|
|
session := v3.NewSession(testRequestID, 30*time.Second, &origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
|
err := session.Serve(context.Background())
|
|
if !errors.Is(err, net.ErrClosed) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
type testOrigin struct {
|
|
// bytes from Write
|
|
write []byte
|
|
// bytes provided to Read
|
|
read []byte
|
|
readOnce atomic.Bool
|
|
closed atomic.Bool
|
|
}
|
|
|
|
func newTestOrigin(payload []byte) testOrigin {
|
|
return testOrigin{
|
|
read: payload,
|
|
}
|
|
}
|
|
|
|
func (o *testOrigin) Read(p []byte) (n int, err error) {
|
|
if o.closed.Load() {
|
|
return -1, net.ErrClosed
|
|
}
|
|
if o.readOnce.Load() {
|
|
// We only want to provide one read so all other reads will be blocked
|
|
time.Sleep(10 * time.Second)
|
|
}
|
|
o.readOnce.Store(true)
|
|
return copy(p, o.read), nil
|
|
}
|
|
|
|
func (o *testOrigin) Write(p []byte) (n int, err error) {
|
|
if o.closed.Load() {
|
|
return -1, net.ErrClosed
|
|
}
|
|
o.write = make([]byte, len(p))
|
|
copy(o.write, p)
|
|
return len(p), nil
|
|
}
|
|
|
|
func (o *testOrigin) Close() error {
|
|
o.closed.Store(true)
|
|
return nil
|
|
}
|
|
|
|
type testIdleOrigin struct {
|
|
duration time.Duration
|
|
closed bool
|
|
}
|
|
|
|
func newTestIdleOrigin(d time.Duration) testIdleOrigin {
|
|
return testIdleOrigin{
|
|
duration: d,
|
|
}
|
|
}
|
|
|
|
func (o *testIdleOrigin) Read(p []byte) (n int, err error) {
|
|
time.Sleep(o.duration)
|
|
return -1, nil
|
|
}
|
|
|
|
func (o *testIdleOrigin) Write(p []byte) (n int, err error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (o *testIdleOrigin) Close() error {
|
|
o.closed = true
|
|
return nil
|
|
}
|
|
|
|
type testErrOrigin struct {
|
|
readErr error
|
|
writeErr error
|
|
}
|
|
|
|
func newTestErrOrigin(readErr error, writeErr error) testErrOrigin {
|
|
return testErrOrigin{readErr, writeErr}
|
|
}
|
|
|
|
func (o *testErrOrigin) Read(p []byte) (n int, err error) {
|
|
return 0, o.readErr
|
|
}
|
|
|
|
func (o *testErrOrigin) Write(p []byte) (n int, err error) {
|
|
return len(p), o.writeErr
|
|
}
|
|
|
|
func (o *testErrOrigin) Close() error {
|
|
return nil
|
|
}
|