refactor(hub): make SSHTransport the single owner of each system's SSH connection

The updater and on-demand requests kept separate copies of the SSH client
(sys.client and the transport's client) and synced them after each request.
That allowed a closed client to be reinstalled over a newer one and leaked
connections that were replaced without being closed.

The updater now dials, opens sessions and tears down timed-out connections
through the transport. The dial keeps the TCP keepalive and handshake
deadline, and an OnConnect callback handles the per-connection resets.
The transport is created under a lock and agentVersion is now atomic.
This commit is contained in:
henrygd
2026-09-29 11:51:13 -04:00
parent 97db8bd199
commit 1997984325
11 changed files with 376 additions and 394 deletions

View File

@@ -42,12 +42,14 @@ func TestSSHNetworkMonitorReconnectSync(t *testing.T) {
t.Cleanup(sys.closeSSHConnection)
requests := make(chan monitor.SyncRequest, 10)
var failSync atomic.Bool
var connections atomic.Int32
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
connections.Add(1)
go func() {
server, channels, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
@@ -138,15 +140,17 @@ func TestSSHNetworkMonitorReconnectSync(t *testing.T) {
require.False(t, sys.monitorsNeedSync.Load())
fetch()
require.Empty(t, requests, "steady-state fetch must not resync")
require.Equal(t, int32(1), connections.Load(), "stats and monitor sync must share one connection")
// Simulate loss of the agent process/connection and its in-memory monitors.
require.NoError(t, sys.client.Load().Close())
require.NoError(t, sys.sshTransport.GetClient().Close())
fetch()
require.ElementsMatch(t, configs, receive().Configs)
require.False(t, sys.monitorsNeedSync.Load())
require.Equal(t, int32(2), connections.Load(), "reconnect must open exactly one new connection")
// Failed replacements are retried on the next successful stats fetch.
require.NoError(t, sys.client.Load().Close())
require.NoError(t, sys.sshTransport.GetClient().Close())
failSync.Store(true)
fetch()
require.ElementsMatch(t, configs, receive().Configs)
@@ -160,7 +164,7 @@ func TestSSHNetworkMonitorReconnectSync(t *testing.T) {
probe.Set("enabled", false)
require.NoError(t, app.SaveNoValidate(probe))
}
require.NoError(t, sys.client.Load().Close())
require.NoError(t, sys.sshTransport.GetClient().Close())
fetch()
require.Empty(t, receive().Configs, "empty replacement must clear stale monitors")
}