Skip to content

test(security): SshTunnelManager suite with an in-memory SSH fake - #1097

Merged
ZhuchkaTriplesix merged 7 commits into
devfrom
issue/1041-ssh-tunnel-manager-tests
Oct 8, 2026
Merged

ZhuchkaTriplesix merged 7 commits into
devfrom
issue/1041-ssh-tunnel-manager-tests

Conversation

@ZhuchkaTriplesix

Copy link
Copy Markdown
Member

Summary

A test suite for SshTunnelManager (#1031) that needs no SSH server, plus four fixes that writing it exposed.

Test seam. SshTunnelManager now takes its TCP connector and SSH client builder as injectable functions (SshTunnelManager.forTesting, default behavior unchanged). test/support/fake_ssh.dart provides an in-memory server and client: it presents a host key, checks host-key trust, then password or public-key authentication, counts keep-alive pings, and echoes forwarded connections. Only the tunnel's own loopback listener is a real socket.

ssh_tunnel_manager_test.dart

  • Loopback and ports: the tunnel is reported and reachable on 127.0.0.1, is not reachable through any non-loopback local IPv4 address, each target gets its own free port, bytes written to the local port reach the remote target and come back.
  • Authentication: password, wrong password (SshAuthenticationException, client closed), unreachable bastion, jump host (dialed first, bastion reached through it, jump password with fallback to the main password), Ed25519 and RSA keys, passphrase-protected keys, wrong passphrase, garbage key, key read from privateKeyPath, server rejecting the key. Keys are generated with ssh-keygen at test time (no private keys in the repo); those tests skip when it is unavailable.
  • Secret scrubbing: credentials are cleared after a successful handshake, including those passed for a reused tunnel.
  • Host keys: formatFingerprint is the SHA-256 hex digest, a pinned fingerprint matches (also colon-separated and upper-case), a changed key raises SshHostKeyMismatchException before any password is sent and closes the client (and the jump host client), no pin means first-use trust.
  • testSshConnection: disabled, empty host / user, success with fingerprint and closed client, bad credentials.
  • Lifecycle: ref-counting, idempotent release, separate sessions per target, a dropped SSH connection is replaced, closeAll, release after closeAll, app shutdown, keep-alive pings start and stop (and a zero interval disables them).

Fixes in the manager

  • Credentials passed for a reused tunnel were never cleared; they are now.
  • A session whose SSH client had dropped was overwritten in the pool without closing it, leaving its loopback listener open; the stale session is now closed.
  • A handle released its tunnel by pool key, so a handle of a replaced session could release the session that took its place; handles now hold their own session.
  • disconnectAllExternalServices (app shutdown / lifecycle cleanup) never closed SSH tunnels; it now calls SshTunnelManager.instance.closeAll() after disconnecting the services.

Notes

  • "Zeroing" is clearing the references (Dart strings are immutable); the tests assert exactly that.
  • Written without running locally; CI is the first run. The non-loopback check skips on machines without such an interface.

Closes #1041

)

Make the tunnel's TCP connector and SSH client builder injectable and cover
loopback binding, ephemeral ports, byte forwarding, password / private-key /
passphrase / jump-host authentication, secret scrubbing, host-key pinning,
ref-counting, dropped-connection replacement, keep-alive and shutdown.

Fixes found on the way: credentials passed for a reused tunnel were not
cleared, a replaced session left its loopback listener open, handles of a
replaced session could release the session that took its place, and app
shutdown did not close open tunnels.
@github-actions github-actions Bot added tests Theme parser epic network Remote install, HTTP download core Core library logic and services connections Database connections, URI parsing, pools P3 Low priority / Polish & Enhancements labels Oct 7, 2026
@ZhuchkaTriplesix
ZhuchkaTriplesix merged commit 4e45478 into dev Oct 8, 2026
23 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

connections Database connections, URI parsing, pools core Core library logic and services network Remote install, HTTP download P3 Low priority / Polish & Enhancements tests Theme parser epic

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant