mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-29 14:39:16 -04:00
Let fetchSSHAction own the per-fetch deadline and pass the connection cancellation context directly from clientAuth. Remove the duplicate timeout and its deferred cancellation inside the action loop. Derive the longer transport watchdog from the fetch timeout, keeping both durations unchanged. Use virtual time to test the fetch deadline across retries, earlier parent deadlines, and parent cancellation. Verify that a successful fetch releases its request context. Updates tailscale/corp#50380 Change-Id: I185958ca19566f04cbd68f33e4b1dad3d18aebb7 Signed-off-by: James Tucker <james@tailscale.com>
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
)
|
|
|
|
type delegateBackend struct {
|
|
ipnLocalBackend
|
|
request func(*http.Request) (*http.Response, error)
|
|
}
|
|
|
|
func (b *delegateBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
|
return b.request(req)
|
|
}
|
|
|
|
func TestFetchSSHActionTimeout(t *testing.T) {
|
|
for _, mode := range []string{"timeout", "retry-timeout", "parent-deadline", "parent-cancel"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
start := time.Now()
|
|
wantDeadline := start.Add(delegateFetchTimeout)
|
|
wantElapsed := delegateFetchTimeout
|
|
wantErr := context.DeadlineExceeded
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
switch mode {
|
|
case "parent-deadline":
|
|
var stop context.CancelFunc
|
|
ctx, stop = context.WithTimeout(ctx, time.Minute)
|
|
defer stop()
|
|
wantElapsed = time.Minute
|
|
wantDeadline = start.Add(wantElapsed)
|
|
case "parent-cancel":
|
|
time.AfterFunc(time.Minute, cancel)
|
|
wantElapsed = time.Minute
|
|
wantErr = context.Canceled
|
|
}
|
|
calls := 0
|
|
b := &delegateBackend{request: func(req *http.Request) (*http.Response, error) {
|
|
calls++
|
|
if got, ok := req.Context().Deadline(); !ok || !got.Equal(wantDeadline) {
|
|
t.Errorf("request deadline = %v, %v; want %v", got, ok, wantDeadline)
|
|
}
|
|
if mode == "retry-timeout" {
|
|
return nil, errors.New("temporarily unavailable")
|
|
}
|
|
<-req.Context().Done()
|
|
return nil, req.Context().Err()
|
|
}}
|
|
c := &conn{srv: &server{lb: b, logf: t.Logf}}
|
|
if _, err := c.fetchSSHAction(ctx, "https://unused/action"); !errors.Is(err, wantErr) {
|
|
t.Errorf("fetchSSHAction error = %v, want %v", err, wantErr)
|
|
}
|
|
if got := time.Since(start); got != wantElapsed {
|
|
t.Errorf("fetch duration = %v, want %v", got, wantElapsed)
|
|
}
|
|
if calls == 0 || mode == "retry-timeout" && calls < 2 {
|
|
t.Errorf("request calls = %d", calls)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFetchSSHActionCancelsRequestOnSuccess(t *testing.T) {
|
|
var requestContext context.Context
|
|
b := &delegateBackend{request: func(req *http.Request) (*http.Response, error) {
|
|
requestContext = req.Context()
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(`{"Accept":true}`)),
|
|
}, nil
|
|
}}
|
|
c := &conn{srv: &server{lb: b, logf: t.Logf}}
|
|
action, err := c.fetchSSHAction(context.Background(), "https://unused/action")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !action.Accept {
|
|
t.Fatal("delegated action did not accept")
|
|
}
|
|
if err := requestContext.Err(); err != context.Canceled {
|
|
t.Errorf("request context error = %v, want context.Canceled", err)
|
|
}
|
|
}
|