mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-22 16:24:53 +09:00
@@ -95,6 +95,15 @@ func (m *mapSession) resetKeepAlive() {
|
|||||||
func (m *mapSession) stopFromBatcher() {
|
func (m *mapSession) stopFromBatcher() {
|
||||||
if m.cancelChClosed.CompareAndSwap(false, true) {
|
if m.cancelChClosed.CompareAndSwap(false, true) {
|
||||||
close(m.cancelCh)
|
close(m.cancelCh)
|
||||||
|
|
||||||
|
// A channel signal cannot interrupt a response write that is blocked on
|
||||||
|
// HTTP/2 flow control. Expire the stream's write deadline as well so a
|
||||||
|
// client that stopped reading cannot keep the map session, and therefore
|
||||||
|
// server shutdown, alive indefinitely.
|
||||||
|
err := http.NewResponseController(m.w).SetWriteDeadline(time.Now())
|
||||||
|
if err != nil && !errors.Is(err, http.ErrNotSupported) {
|
||||||
|
m.log.Debug().Caller().Err(err).Msg("failed to interrupt map response write")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -33,6 +34,69 @@ type delayedSuccessResponseWriter struct {
|
|||||||
writeCount int
|
writeCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deadlineResponseWriter simulates an HTTP/2 response blocked on flow control.
|
||||||
|
// The write only returns when the handler expires its per-stream write deadline.
|
||||||
|
type deadlineResponseWriter struct {
|
||||||
|
header http.Header
|
||||||
|
|
||||||
|
blockWrites atomic.Bool
|
||||||
|
writeCount atomic.Int64
|
||||||
|
|
||||||
|
writeStarted chan struct{}
|
||||||
|
writeStartedOnce sync.Once
|
||||||
|
deadlineSet chan struct{}
|
||||||
|
deadlineSetOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeadlineResponseWriter() *deadlineResponseWriter {
|
||||||
|
return &deadlineResponseWriter{
|
||||||
|
header: make(http.Header),
|
||||||
|
writeStarted: make(chan struct{}),
|
||||||
|
deadlineSet: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) Header() http.Header {
|
||||||
|
return w.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) WriteHeader(int) {}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
w.writeCount.Add(1)
|
||||||
|
|
||||||
|
if !w.blockWrites.Load() {
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.writeStartedOnce.Do(func() { close(w.writeStarted) })
|
||||||
|
<-w.deadlineSet
|
||||||
|
|
||||||
|
return 0, context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) Flush() {}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) SetWriteDeadline(deadline time.Time) error {
|
||||||
|
if !deadline.IsZero() {
|
||||||
|
w.deadlineSetOnce.Do(func() { close(w.deadlineSet) })
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) WriteStarted() <-chan struct{} {
|
||||||
|
return w.writeStarted
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) BlockWrites() {
|
||||||
|
w.blockWrites.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *deadlineResponseWriter) WriteCount() int64 {
|
||||||
|
return w.writeCount.Load()
|
||||||
|
}
|
||||||
|
|
||||||
func newDelayedSuccessResponseWriter(firstWriteDelay time.Duration) *delayedSuccessResponseWriter {
|
func newDelayedSuccessResponseWriter(firstWriteDelay time.Duration) *delayedSuccessResponseWriter {
|
||||||
return &delayedSuccessResponseWriter{
|
return &delayedSuccessResponseWriter{
|
||||||
header: make(http.Header),
|
header: make(http.Header),
|
||||||
@@ -475,3 +539,69 @@ func TestDeletedNodeEndsLongPoll(t *testing.T) {
|
|||||||
t.Fatal("an orphaned session would block server shutdown on clientStreamsOpen")
|
t.Fatal("an orphaned session would block server shutdown on clientStreamsOpen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeletedNodeInterruptsBlockedWrite(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
app := createTestApp(t)
|
||||||
|
user := app.state.CreateUserForTest("poll-delete-blocked-user")
|
||||||
|
createdNode := app.state.CreateRegisteredNodeForTest(user, "poll-delete-blocked-node")
|
||||||
|
require.NoError(t, app.state.UpdatePolicyManagerUsersForTest())
|
||||||
|
app.cfg.Tuning.BatchChangeDelay = 20 * time.Millisecond
|
||||||
|
app.cfg.Tuning.NodeMapSessionBufferedChanSize = 1
|
||||||
|
|
||||||
|
app.mapBatcher.Close()
|
||||||
|
require.NoError(t, app.state.Close())
|
||||||
|
|
||||||
|
reloadedState, err := state.NewState(app.cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
app.state = reloadedState
|
||||||
|
app.mapBatcher = mapper.NewBatcherAndMapper(app.cfg, app.state)
|
||||||
|
app.mapBatcher.Start()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
app.mapBatcher.Close()
|
||||||
|
require.NoError(t, app.state.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
nodeView, ok := app.state.GetNodeByID(createdNode.ID)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
writer := newDeadlineResponseWriter()
|
||||||
|
session := app.newMapSession(t.Context(), tailcfg.MapRequest{
|
||||||
|
Stream: true,
|
||||||
|
Version: tailcfg.CapabilityVersion(100),
|
||||||
|
}, writer, nodeView.AsStruct())
|
||||||
|
|
||||||
|
serveDone := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
session.serveLongPoll()
|
||||||
|
close(serveDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return writer.WriteCount() >= 2 && app.mapBatcher.IsConnected(nodeView.ID())
|
||||||
|
}, 2*time.Second, 10*time.Millisecond,
|
||||||
|
"expected the initial map and connect update to be delivered")
|
||||||
|
|
||||||
|
writer.BlockWrites()
|
||||||
|
app.Change(change.SelfUpdate(nodeView.ID()))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-writer.WriteStarted():
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("expected the initial map write to block")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := app.state.DeleteNode(nodeView)
|
||||||
|
require.NoError(t, err)
|
||||||
|
app.Change(c)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-serveDone:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("deleting a node must interrupt its blocked map response write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user