Skip to content

feat(forward): ForwardTracker Protocol for local-forward lifecycle hooks#807

Open
AlexMKX wants to merge 1 commit into
ronf:developfrom
AlexMKX:feat/forward-tracker
Open

feat(forward): ForwardTracker Protocol for local-forward lifecycle hooks#807
AlexMKX wants to merge 1 commit into
ronf:developfrom
AlexMKX:feat/forward-tracker

Conversation

@AlexMKX
Copy link
Copy Markdown

@AlexMKX AlexMKX commented May 21, 2026

Adds optional tracker kwarg to SSHClientConnection.forward_local_port that accepts a ForwardTracker (typing.Protocol) with two hooks:

connection_made(orig_host, orig_port) -> None
connection_lost(orig_host, orig_port, exc) -> None

The hooks fire on every client connect/disconnect on the local listener. Hook exceptions are swallowed so a buggy tracker cannot break the forwarder.

Backward compatible: tracker defaults to None, preserving existing behavior with no overhead.

Use case: passive observation of local-forward activity (idle-based auto-shutdown, byte counters, etc.) without modifying transport code.

Adds optional tracker kwarg to SSHClientConnection.forward_local_port
that accepts a ForwardTracker (typing.Protocol) with two hooks:

  connection_made(orig_host, orig_port) -> None
  connection_lost(orig_host, orig_port, exc) -> None

The hooks fire on every client connect/disconnect on the local
listener. Hook exceptions are swallowed so a buggy tracker cannot
break the forwarder.

Backward compatible: tracker defaults to None, preserving existing
behavior with no overhead.

Use case: passive observation of local-forward activity (idle-based
auto-shutdown, byte counters, etc.) without modifying transport code.
@ronf
Copy link
Copy Markdown
Owner

ronf commented May 23, 2026

Thanks for the PR. Could you say a little more about the use case you have in mind for this? I'm not really seeing how this would be able to do things like byte counts or idle tracking if you are only sending connection_made and connection_lost messages through the tracker.

I'm wondering if some of this could be handled by using the existing accept_handler. It only runs right now on a new client connection coming in, giving you the client IP address and port and even lets you decide based on that whether to allow the forwarded connection or not. It doesn't currently run when a connection closes (either cleanly or with an error), but that could be added as something like an error_handler. In the future, support for a progress_handler could also be added here to report on bytes transferred, much like what exists in AsyncSSH right now for the SFTP/SCP file copy functions.

@AlexMKX
Copy link
Copy Markdown
Author

AlexMKX commented May 26, 2026

Hello @ronf, thank you for the questions.

The main use case I have in mind is handling idle forwarded connections. This came from a practical need in this tool: https://github.com/AlexMKX/garuda-tunnel

It is basically a “poor man's Teleport” for automating Kubernetes access through Terraform or an Ansible Kubernetes inventory source. It runs as a daemon, exposes local ports through a bastion, and uses AsyncSSH under the hood.

For safety and operational visibility, I would like to track the number of live forwarded connections and apply an idle timeout to traffic going through the mapped ports. A per-byte counter would be better, of course, but for this use case an active connection count is a reasonable first approximation without inviting a whole committee of extra machinery into the room.

I agree that accept_handler is close to this area. The main gap is that it currently only covers the beginning of the connection. Having something symmetrical for connection close, perhaps an error_handler or close_handler, would probably solve a good part of this. A future progress_handler for byte counts would also be useful and would fit nicely with what AsyncSSH already does for SFTP/SCP transfers.

So I am not strongly attached to the exact tracker API shape from the PR. The important part for me is having a lightweight way to observe connection lifecycle events for forwarded connections, and later possibly traffic progress, without each user of port forwarding having to rebuild that logic around AsyncSSH internals.

@ronf
Copy link
Copy Markdown
Owner

ronf commented May 27, 2026

Thanks for the added context... I'm in the middle of some other feature work at the moment, but once that's wrapped up I'll take a closer look at this, probably in the next couple of weeks.

I'm thinking that you could still have a ForwardTracker class at the application level if you wanted here, and instantiate that with details of the connection it is associated with. The arguments to accept_handler and close_handler in forward_local_port() could point at the bound methods of that specific ForwardTracker object. The same could apply in the future for tracking byte counts.

In fact, you could probably have a factory method on ForwardTracker that actually called forward_local_port() with all the necessary bound methods as arguments. This will need to be an async method so you can't use the constructor directly, but this should work as an async class method if you wanted to avoid needing to call the constructor first yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants