Integrating trzsz with a Custom Go SSH Client
If you are building a custom SSH client, terminal emulator, or internal tooling utilizing golang.org/x/crypto/ssh, integrating trzsz-go directly into your binary is a massive workflow improvement over requiring users to install the external trzsz CLI wrapper.
This guide breaks down the architectural requirements and provides a robust implementation example.
The Conceptual Architecture
In a standard Go SSH implementation, you pipe local Stdin/Stdout directly to the SSH session's pipes.
To enable trzsz, you must intercept this stream by injecting trzsz.NewTrzszFilter between the user and the server.
os.Stdin ┌────────┐ os.Stdin ┌─────────────┐ ServerIn ┌────────┐
───────────►│ ├─────────────►│ ├─────────────►│ │
│ │ │ TrzszFilter │ │ │
◄───────────│ Client │◄─────────────┤ │◄─────────────┤ Server │
os.Stdout │ │ os.Stdout └─────────────┘ ServerOut │ │
◄───────────│ │◄──────────────────────────────────────────┤ │
os.Stderr └────────┘ stderr └────────┘
The TrzszFilter runs background goroutines scanning ServerOut for trzsz trigger strings. When detected, it pauses standard data forwarding, handles the file transfer protocol locally, and then resumes normal shell operation.
Implementation Steps
The following code demonstrates a complete SSH shell initialization sequence with trzsz properly attached. (Note: Error handling is truncated for brevity, see examples/ssh_client.go in the repository for the fully compilable source).
1. Terminal Preparation
Before attaching the filter, you must place the terminal into raw mode and request a Pseudo-Terminal (PTY) from the SSH server.
import (
"os"
"golang.org/x/term"
"golang.org/x/crypto/ssh"
)
func startSession(client *ssh.Client, session *ssh.Session) error {
fd := int(os.Stdin.Fd())
// Put the terminal into raw mode
state, err := term.MakeRaw(fd)
if err != nil { return err }
defer term.Restore(fd, state)
// Get terminal dimensions
width, height, _ := term.GetSize(fd)
// Request the PTY
session.RequestPty("xterm-256color", height, width, ssh.TerminalModes{})
2. Injecting the Filter
Obtain the IO pipes from the SSH session and instantiate the filter.
serverIn, _ := session.StdinPipe()
serverOut, _ := session.StdoutPipe()
session.Stderr = os.Stderr // Stderr is typically passed through untouched
// Instantiate the Filter
trzszFilter := trzsz.NewTrzszFilter(os.Stdin, os.Stdout, serverIn, serverOut,
trzsz.TrzszOptions{
TerminalColumns: int32(width), // Required for accurate progress bars
DetectDragFile: true, // Enables drag-and-drop
EnableZmodem: true, // Supports legacy lrzsz
EnableOSC52: true, // Supports clipboard integration
})
// CRITICAL: Always clean up the filter routines on exit
defer trzszFilter.Close()
defer trzszFilter.ResetTerminal()
3. Handling Window Resizes (SIGWINCH)
If the user resizes their terminal window, both the SSH server and the TrzszFilter need to know about the new dimensions, otherwise, progress bars will render incorrectly.
ch := make(chan os.Signal, 1)
// Note: Use signal.Notify(ch, syscall.SIGWINCH) on Unix systems
go func() {
for range ch {
width, height, err := term.GetSize(fd)
if err == nil {
session.WindowChange(height, width)
trzszFilter.SetTerminalColumns(int32(width))
}
}
}()
defer func() { signal.Stop(ch); close(ch) }()
4. Advanced: High-Speed TCP Tunneling
By default, trzsz transfers data over the standard SSH PTY stdout stream. PTYs are incredibly inefficient for large binary transfers because they parse and interpret characters (like translating \n to \r\n).
To achieve maximum throughput, trzsz supports opening a direct TCP port tunnel over the SSH connection. You must provide a connector callback to the filter so it knows how to dial back to the server when requested.
// Provide a Dial hook to bypass the PTY for binary transfers
trzszFilter.SetTunnelConnector(func(port int) net.Conn {
// client is the *ssh.Client
conn, _ := client.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
return conn
})
5. Start the Shell
Finally, invoke the shell and wait for the session to conclude.
session.Shell()
session.Wait()
return nil
}
Important Consideration: Intercepting Local I/O
In the example above, os.Stdin and os.Stdout are mapped directly to the filter. If your custom application also needs to print messages to the screen (e.g., "Connection Established"), doing so will race with the filter's output rendering.
If you need strict control over local IO, use io.Pipe() to create custom client input/output buffers, pass the pipe ends into NewTrzszFilter, and carefully orchestrate your local console writes.