tsnet: block in Server.Dial until backend is Running
Some checks are pending
checklocks / checklocks (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
Dockerfile build / deploy (push) Waiting to run
CI / race-root-integration (1/4) (push) Waiting to run
CI / race-root-integration (2/4) (push) Waiting to run
CI / race-root-integration (3/4) (push) Waiting to run
CI / race-root-integration (4/4) (push) Waiting to run
CI / test (-coverprofile=/tmp/coverage.out, amd64) (push) Waiting to run
CI / test (-race, amd64, 1/3) (push) Waiting to run
CI / test (-race, amd64, 2/3) (push) Waiting to run
CI / test (-race, amd64, 3/3) (push) Waiting to run
CI / test (386) (push) Waiting to run
CI / windows (push) Waiting to run
CI / privileged (push) Waiting to run
CI / vm (push) Waiting to run
CI / race-build (push) Waiting to run
CI / cross (386, linux) (push) Waiting to run
CI / cross (amd64, darwin) (push) Waiting to run
CI / cross (amd64, freebsd) (push) Waiting to run
CI / cross (amd64, openbsd) (push) Waiting to run
CI / cross (amd64, windows) (push) Waiting to run
CI / cross (arm, 5, linux) (push) Waiting to run
CI / cross (arm, 7, linux) (push) Waiting to run
CI / cross (arm64, darwin) (push) Waiting to run
CI / cross (arm64, linux) (push) Waiting to run
CI / cross (arm64, windows) (push) Waiting to run
CI / cross (loong64, linux) (push) Waiting to run
CI / ios (push) Waiting to run
CI / crossmin (amd64, illumos) (push) Waiting to run
CI / crossmin (amd64, plan9) (push) Waiting to run
CI / crossmin (amd64, solaris) (push) Waiting to run
CI / crossmin (ppc64, aix) (push) Waiting to run
CI / android (push) Waiting to run
CI / wasm (push) Waiting to run
CI / tailscale_go (push) Waiting to run
CI / fuzz (push) Waiting to run
CI / depaware (push) Waiting to run
CI / go_generate (push) Waiting to run
CI / go_mod_tidy (push) Waiting to run
CI / licenses (push) Waiting to run
CI / staticcheck (386, windows) (push) Waiting to run
CI / staticcheck (amd64, darwin) (push) Waiting to run
CI / staticcheck (amd64, linux) (push) Waiting to run
CI / staticcheck (amd64, windows) (push) Waiting to run
CI / notify_slack (push) Blocked by required conditions
CI / check_mergeability (push) Blocked by required conditions

Updates #14715

Change-Id: I8c91e94fd1c6278c7f94a6b890274ed8a01e6f25
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-01-21 09:50:45 -08:00
committed by Brad Fitzpatrick
parent 2729942638
commit b50d32059f
2 changed files with 72 additions and 0 deletions

View File

@ -169,9 +169,41 @@ func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, e
if err := s.Start(); err != nil {
return nil, err
}
if err := s.awaitRunning(ctx); err != nil {
return nil, err
}
return s.dialer.UserDial(ctx, network, address)
}
// awaitRunning waits until the backend is in state Running.
// If the backend is in state Starting, it blocks until it reaches
// a terminal state (such as Stopped, NeedsMachineAuth)
// or the context expires.
func (s *Server) awaitRunning(ctx context.Context) error {
st := s.lb.State()
for {
if err := ctx.Err(); err != nil {
return err
}
switch st {
case ipn.Running:
return nil
case ipn.NeedsLogin, ipn.Starting:
// Even after LocalBackend.Start, the state machine is still briefly
// in the "NeedsLogin" state. So treat that as also "Starting" and
// wait for us to get out of that state.
s.lb.WatchNotifications(ctx, ipn.NotifyInitialState, nil, func(n *ipn.Notify) (keepGoing bool) {
if n.State != nil {
st = *n.State
}
return st == ipn.NeedsLogin || st == ipn.Starting
})
default:
return fmt.Errorf("tsnet: backend in state %v", st)
}
}
}
// HTTPClient returns an HTTP client that is configured to connect over Tailscale.
//
// This is useful if you need to have your tsnet services connect to other devices on

View File

@ -232,6 +232,46 @@ func startServer(t *testing.T, ctx context.Context, controlURL, hostname string)
return s, status.TailscaleIPs[0], status.Self.PublicKey
}
func TestDialBlocks(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
// Make one tsnet that blocks until it's up.
s1, _, _ := startServer(t, ctx, controlURL, "s1")
ln, err := s1.Listen("tcp", ":8080")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
// Then make another tsnet node that will only be woken up
// upon the first dial.
tmp := filepath.Join(t.TempDir(), "s2")
os.MkdirAll(tmp, 0755)
s2 := &Server{
Dir: tmp,
ControlURL: controlURL,
Hostname: "s2",
Store: new(mem.Store),
Ephemeral: true,
getCertForTesting: testCertRoot.getCert,
}
if *verboseNodes {
s2.Logf = log.Printf
}
t.Cleanup(func() { s2.Close() })
c, err := s2.Dial(ctx, "tcp", "s1:8080")
if err != nil {
t.Fatal(err)
}
defer c.Close()
}
func TestConn(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)