Files
coder/tailnet/requests_test.go
dylanhuff-at-coder ae76be47d3 fix: prevent tailnet coordination panics (#29300)
Malformed coordinate requests can panic per-connection coordinator
goroutines and terminate coderd. Disconnect requests and nested cleanup
of peers with full response buffers can also attempt to send on or close
an already-closed channel.

Validate request structure before authorization or state mutation in
both tailnet coordinators, stop processing after disconnect, and avoid
removing a peer twice during nested cleanup. Unexpected per-connection
panics close the affected connection through the existing lost-peer
cleanup.


https://linear.app/codercom/issue/PLAT-595/security-nil-node-in-tailnet-updateself-crashes-entire-coderd-process
2026-09-22 09:31:49 -07:00

101 lines
3.3 KiB
Go

package tailnet_test
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/coder/v2/tailnet/test"
"github.com/coder/coder/v2/testutil"
)
func TestCoordinateeAuthNilNode(t *testing.T) {
t.Parallel()
for name, auth := range map[string]tailnet.CoordinateeAuth{
"Agent": tailnet.AgentCoordinateeAuth{ID: uuid.New()},
"Client": tailnet.ClientCoordinateeAuth{AgentID: uuid.New()},
"ClientUser": tailnet.ClientUserCoordinateeAuth{},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
require.NotPanics(t, func() {
require.EqualError(t, auth.Authorize(ctx, &proto.CoordinateRequest{
UpdateSelf: &proto.CoordinateRequest_UpdateSelf{},
}), "update_self node is required")
})
})
}
}
func TestCoordinator_InvalidRequests(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
coordinator := tailnet.NewCoordinator(slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}))
t.Cleanup(func() { require.NoError(t, coordinator.Close()) })
test.InvalidCoordinateRequestTest(ctx, t, coordinator)
}
func TestCoordinator_RequestPanic(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
coordinator := tailnet.NewCoordinator(slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}))
t.Cleanup(func() { require.NoError(t, coordinator.Close()) })
test.CoordinateRequestPanicTest(ctx, t, coordinator)
}
func TestValidateCoordinateRequest(t *testing.T) {
t.Parallel()
validRFH := &proto.CoordinateRequest_ReadyForHandshake{Id: tailnet.UUIDToByteSlice(uuid.New())}
for _, tc := range []struct {
name string
req *proto.CoordinateRequest
err string // empty means the request is accepted
}{
{name: "NilRequest", err: "coordinate request is required"},
{name: "EmptyRequest", req: &proto.CoordinateRequest{}},
{
name: "NilNode",
req: &proto.CoordinateRequest{UpdateSelf: &proto.CoordinateRequest_UpdateSelf{}},
err: "update_self node is required",
},
{
name: "EmptyNode",
req: &proto.CoordinateRequest{UpdateSelf: &proto.CoordinateRequest_UpdateSelf{Node: &proto.Node{}}},
},
{name: "NilReadyForHandshake", req: &proto.CoordinateRequest{ReadyForHandshake: nil}},
{
name: "EmptyReadyForHandshake",
req: &proto.CoordinateRequest{ReadyForHandshake: []*proto.CoordinateRequest_ReadyForHandshake{}},
},
{
name: "ValidReadyForHandshake",
req: &proto.CoordinateRequest{ReadyForHandshake: []*proto.CoordinateRequest_ReadyForHandshake{validRFH}},
},
{
name: "NilReadyForHandshakeEntry",
req: &proto.CoordinateRequest{ReadyForHandshake: []*proto.CoordinateRequest_ReadyForHandshake{nil}},
err: "ready_for_handshake entries must not be nil",
},
{
name: "NilReadyForHandshakeAfterValid",
req: &proto.CoordinateRequest{ReadyForHandshake: []*proto.CoordinateRequest_ReadyForHandshake{validRFH, nil}},
err: "ready_for_handshake entries must not be nil",
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := tailnet.ValidateCoordinateRequest(tc.req)
if tc.err == "" {
require.NoError(t, err)
return
}
require.EqualError(t, err, tc.err)
})
}
}