@ -28,6 +28,8 @@ import (
"tailscale.com/tstime"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
"tailscale.com/util/set"
)
)
// Client provides a http.Client to connect to tailcontrol over
// Client provides a http.Client to connect to tailcontrol over
@ -46,6 +48,7 @@ type Client struct {
// mu protects the following
// mu protects the following
mu sync . Mutex
mu sync . Mutex
closed bool
closed bool
connPool set . HandleSet [ * Conn ] // all live connections
}
}
// ClientOpts contains options for the [NewClient] function. All fields are
// ClientOpts contains options for the [NewClient] function. All fields are
@ -175,9 +178,15 @@ func NewClient(opts ClientOpts) (*Client, error) {
// It is a no-op and returns nil if the connection is already closed.
// It is a no-op and returns nil if the connection is already closed.
func ( nc * Client ) Close ( ) error {
func ( nc * Client ) Close ( ) error {
nc . mu . Lock ( )
nc . mu . Lock ( )
defer nc . mu . Unlock ( )
live := nc . connPool
nc . closed = true
nc . closed = true
nc . mu . Unlock ( )
for _ , c := range live {
c . Close ( )
}
nc . Client . CloseIdleConnections ( )
nc . Client . CloseIdleConnections ( )
return nil
return nil
}
}
@ -249,18 +258,31 @@ func (nc *Client) dial(ctx context.Context) (*Conn, error) {
return nil , err
return nil , err
}
}
ncc := NewConn ( clientConn . Conn )
nc . mu . Lock ( )
nc . mu . Lock ( )
handle := set . NewHandle ( )
ncc := NewConn ( clientConn . Conn , func ( ) { nc . noteConnClosed ( handle ) } )
mak . Set ( & nc . connPool , handle , ncc )
if nc . closed {
if nc . closed {
nc . mu . Unlock ( )
nc . mu . Unlock ( )
ncc . Close ( ) // Needs to be called without holding the lock.
ncc . Close ( ) // Needs to be called without holding the lock.
return nil , errors . New ( "noise client closed" )
return nil , errors . New ( "noise client closed" )
}
}
defer nc . mu . Unlock ( )
defer nc . mu . Unlock ( )
return ncc , nil
return ncc , nil
}
}
// noteConnClosed notes that the *Conn with the given handle has closed and
// should be removed from the live connPool (which is usually of size 0 or 1,
// except perhaps briefly 2 during a network failure and reconnect).
func ( nc * Client ) noteConnClosed ( handle set . Handle ) {
nc . mu . Lock ( )
defer nc . mu . Unlock ( )
nc . connPool . Delete ( handle )
}
// post does a POST to the control server at the given path, JSON-encoding body.
// post does a POST to the control server at the given path, JSON-encoding body.
// The provided nodeKey is an optional load balancing hint.
// The provided nodeKey is an optional load balancing hint.
func ( nc * Client ) Post ( ctx context . Context , path string , nodeKey key . NodePublic , body any ) ( * http . Response , error ) {
func ( nc * Client ) Post ( ctx context . Context , path string , nodeKey key . NodePublic , body any ) ( * http . Response , error ) {