tsshd: UDP-based SSH Server with Roaming Support
tsshd is a UDP-based SSH server built for unreliable networks. It supports seamless roaming across networks and IP changes, and works well on high-latency links such as cellular connections and unstable Wi-Fi.
tsshd aims to be fully compatible with OpenSSH while providing additional capabilities:
- Survives sleep, wake, and temporary network loss.
- Roams seamlessly across networks and IP changes.
- Supports UDP port forwarding (Local and Remote).
Comparison
tsshd was inspired by mosh, and the tsshd works like mosh-server, while the tssh --udp works like mosh.
| Feature | mosh ( mosh-server ) | tssh ( tsshd ) |
|---|---|---|
| Low Latency | ?? | ✅ KCP |
| Keep Alive | ✅ | ✅ |
| Client Roaming | ✅ | ✅ |
| Local Echo & Line Editing | ✅ | Not Planned |
| Multi Platform / Windows | mosh#293 | ✅ |
| SSH X11 Forwarding | mosh#41 | ✅ |
| SSH Agent Forwarding | mosh#120 | ✅ |
| SSH Port Forwarding | mosh#337 | ✅ |
| Output Scrollback | mosh#122 | ✅ |
| OSC52 Sequence | mosh#637 | ✅ |
| ProxyJump | mosh#970 | ✅ |
| tmux -CC Integration | mosh#1078 | ✅ |
tssh and tsshd works exactly like ssh, there are no plans to support local echo and line editing, and will not have the mosh issues: mosh#1041, mosh#1281, mosh#1295, etc.
How to use
-
Install tssh on the client ( your local machine ).
-
Install tsshd on the server ( the remote host ).
-
Use
tssh --udp xxxto log in. The usage is the same as standard SSH.- Latency-sensitive users can specify the
--kcpoption. - Alternatively, configure the following in
~/.ssh/configto omit the--udpor--kcpoption:Host xxx #!! UdpMode ( Yes | QUIC | KCP )
- Latency-sensitive users can specify the
How it works
-
The
tsshplays the role ofsshon the client side, while thetsshdacts assshdon the server side. -
The
tsshfirst logs in to the server normally as an ssh client, and then starts a newtsshdprocess on the server, where each session has its owntsshdprocess. -
The
tsshdprocess listens on a random UDP port in the range 61001–61999 (configurable viaTsshdPort), and sends the port number and session secret keys back to thetsshprocess through the SSH channel. The SSH connection is then closed, andtsshcommunicates withtsshdover UDP.
Installation
-
Install with apt on Ubuntu
sudo apt install tsshdsudo apt update && sudo apt install software-properties-common sudo add-apt-repository ppa:trzsz/ppa && sudo apt update sudo apt install tsshd -
Install with apt on Debian
sudo apt install tsshdsudo apt install curl gpg curl -s 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x7074ce75da7cc691c1ae1a7c7e51d1ad956055ca' \ | gpg --dearmor -o /usr/share/keyrings/trzsz.gpg echo 'deb [signed-by=/usr/share/keyrings/trzsz.gpg] https://ppa.launchpadcontent.net/trzsz/ppa/ubuntu jammy main' \ | sudo tee /etc/apt/sources.list.d/trzsz.list sudo apt update sudo apt install tsshd -
Install with dnf on Fedora / CentOS / RHEL
sudo dnf install tsshdsudo dnf copr enable @trzsz/tsshd sudo dnf install tsshd -
Install with yum on Legacy CentOS / RHEL
sudo yum install tsshd-
Install with gemfury repository.
echo '[trzsz] name=Trzsz Repo baseurl=https://yum.fury.io/trzsz/ enabled=1 gpgcheck=0' | sudo tee /etc/yum.repos.d/trzsz.repo sudo yum install tsshd -
Install with wlnmp repository. It’s not necessary to configure the epel repository for tsshd.
curl -fsSL "https://sh.wlnmp.com/wlnmp.sh" | bash sudo yum install tsshd
-
-
Install with yay on ArchLinux
yay -S tsshdyay -Syu yay -S tsshd -
Install with Homebrew on MacOS
brew install tsshdbrew install tsshd -
Install with scoop on Windows
scoop install tsshdscoop bucket add extras scoop install tsshdNeed to allow
C:\Users\<user>\scoop\apps\tsshd\<version>\tsshd.exethrough the firewall for it to work properly. -
Install with Go ( Requires go 1.25 or later )
go install github.com/trzsz/tsshd/cmd/tsshd@latest# latest release go install github.com/trzsz/tsshd/cmd/tsshd@latest # latest development version (main branch) go install github.com/trzsz/tsshd/cmd/tsshd@mainThe binaries are usually located in ~/go/bin/ ( C:\Users\your_name\go\bin\ on Windows ).
-
Build from source ( Requires go 1.25 or later )
sudo make installgit clone --depth 1 https://github.com/trzsz/tsshd.git cd tsshd make sudo make install -
Download from the GitHub Releases and install locally
download and install locallysudo apt install /tmp/tsshd_*.deb sudo dpkg -i /tmp/tsshd_*.deb sudo dnf install /tmp/tsshd_*.rpm sudo yum install /tmp/tsshd_*.rpm sudo rpm -i /tmp/tsshd_*.rpm tar zxvf tsshd_*.tar.gz && sudo cp tsshd_*/tsshd /usr/bin/
Supported Terminals
The following clients or terminals support the tsshd server:
-
trzsz-ssh ( tssh ) – An SSH client designed as a drop-in replacement for the OpenSSH client.
-
rootshell - A free, Metal-accelerated terminal emulator for iPhone, iPad, Vision Pro, and Mac.
Reconnection
┌───────────────────────┐ ┌───────────────────────┐
│ │ │ │
│ tssh (process) │ │ tsshd (process) │
│ │ │ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ │ │ │ │ │ │
│ │ KCP/QUIC Client │ │ │ │ KCP/QUIC Server │ │
│ │ │ │ │ │ │ │
│ └───────┬───▲───────┘ │ │ └───────┬───▲───────┘ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ ┌───────▼───┴───────┐ │ │ ┌───────▼───┴───────┐ │
│ │ ├─┼────────────────┼─► │ │
│ │ Client Proxy │ │ │ │ Server Proxy │ │
│ │ ◄─┼────────────────┼─┤ │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
└───────────────────────┘ └───────────────────────┘
-
The client
KCP/QUIC ClientandClient Proxyare on the same machine and in the same process, and the connection between them will not be interrupted. -
The server
KCP/QUIC ServerandServer Proxyare on the same machine and in the same process, and the connection between them will not be interrupted. -
If the client doesn’t receive a heartbeat from the server for a period of time, it might be due to network changes causing the original connection to be interrupted. In this case, the
Client Proxywill re-establish a connection to theServer Proxy, and communicate through the new connection after successful authentication. From the perspective of theKCP/QUIC Clientand theKCP/QUIC Server, the connection is never interrupted.
Security Model
-
Client ProxyandKCP/QUIC Clientrun in the same process, andServer ProxyandKCP/QUIC Serverrun in the same process on the server. The proxy implements thenet.PacketConninterface, so packets are exchanged directly in memory rather than through the local network stack. This prevents other local processes from intercepting or injecting packets. -
Server Proxyaccepts packets from only one authenticated client address at a time. If the client reconnects from a new IP or port (for example after a network change), the newClient Proxymust authenticate again. Once authenticated, the new address replaces the previous one and packets from the old address are ignored. -
When a
Client Proxyconnects or reconnects, it sends an authentication packet encrypted with AES-256-GCM. The encryption key is a session-specific key generated by the server and delivered to the client through the SSH channel during login. -
The
Server Proxyverifies the client ID and ensures that the authentication sequence number is strictly monotonically increasing across all previously observed authentication packets to prevent replay attacks. Upon successful verification, the server marks the client address as authenticated and responds with an encrypted authentication acknowledgment. -
Communication between the client and server uses encrypted transports provided by kcp-go or quic-go. QUIC uses TLS 1.3 as the underlying security protocol to ensure confidentiality and integrity of transmitted data, and supports key updates throughout the connection lifecycle. For KCP, a custom rekey mechanism is implemented to periodically rotate encryption keys with forward secrecy, ensuring that all traffic remains encrypted end-to-end.
Configurations
Server Configuration (tsshd)
-
By default, tsshd reuses OpenSSH configuration (default
/etc/ssh/sshd_configon Unix-like systems), ensuring behavior is consistent with OpenSSH. -
If
$XDG_CONFIG_HOME/tsshd/sshd_configexists (default~/.config/tsshd/sshd_config), tsshd prefers it over the OpenSSH config, even if it is empty.
Client Configuration (tssh)
Host xxx
#!! UdpMode Yes
#!! TsshdPort 61001-61999
#!! TsshdPath ~/go/bin/tsshd
#!! UdpAliveTimeout 86400
#!! UdpHeartbeatTimeout 3
#!! UdpReconnectTimeout 15
#!! ShowNotificationOnTop yes
#!! ShowFullNotifications yes
#!! UdpProxyMode UDP
#!! UdpMTU 1400
-
UdpMode:No(the default: tssh works in TCP mode),Yes(default protocol:QUIC),QUIC(QUIC protocol: faster speed),KCP(KCP protocol: lower latency). -
TsshdPort: Specifies the port range that tsshd listens on, default is [61001, 61999]. You can specify multiple discrete ports (e.g.,6022,7022) or multiple discrete ranges (e.g.,8010-8020,9020-9030,10080); tsshd will randomly choose an available port. You can also specify the port on the command line using--tsshd-port. -
TsshdPath: Specifies the path to the tsshd binary on the server, lookup in $PATH if not configured. You can also specify the path on the command line using--tsshd-path. -
UdpAliveTimeout: If the disconnection lasts longer thanUdpAliveTimeoutin seconds, tssh and tsshd will both exit, and no longer support reconnection. The default is 86400 seconds. -
UdpHeartbeatTimeout: If the disconnection lasts longer thanUdpHeartbeatTimeoutin seconds, tssh will try to reconnect to the server by a new path. The default is 3 seconds. -
UdpReconnectTimeout: If the disconnection lasts longer thanUdpReconnectTimeoutin seconds, tssh will display a notification indicating that the connection has been lost. The default is 15 seconds. -
ShowNotificationOnTop: Whether the connection loss notification is displayed on the top. The default is yes, which may overwrite some of the previous output. Set it toNoto display notifications on the current line of the cursor. -
ShowFullNotifications: Whether to display the full notifications or a brief notification. The default is yes, which may output several lines to the screen. Set it toNowill output only one line. -
UdpProxyMode: The default transport protocol isUDP. IfUDPtraffic is blocked by firewalls in your network environment, you can set it toTCPto work around the restriction, though this may introduce additional latency. -
UdpMTU: Sets the maximum transmission unit (MTU) for UDP packets. Default is 1400.
UDP Port Forwarding
When using tssh as the client, UDP port forwarding is supported.
-
Command-line
-L/-Roptions are extended with audp/prefix (the/can also be replaced with:,_, or-):-L udp/[bind_address:]port:host:hostport -L udp:[bind_address:]port:/remote_socket -L udp_/local_socket:host:hostport -L udp-/local_socket:/remote_socket -R udp/[bind_address:]port:host:hostport -R udp:[bind_address:]port:/local_socket -R udp_/remote_socket:host:hostport -R udp-/remote_socket:/local_socket -
Configuration is similar to
LocalForwardandRemoteForward, with an addedUDPprefix (case-insensitive):UdpLocalForward [bind_address:]port host:hostport UdpLocalForward [bind_address:]port /remote_socket UdpLocalForward /local_socket host:hostport UdpLocalForward /local_socket /remote_socket UdpRemoteForward [bind_address:]port host:hostport UdpRemoteForward [bind_address:]port /local_socket UdpRemoteForward /remote_socket host:hostport UdpRemoteForward /remote_socket /local_socket -
ForwardUdpTimeout: Sets the idle timeout for UDP forwarding sessions; the corresponding forwarding session will be cleared automatically if no data is sent or received within this period to free resources. Default is 5 minutes.
Developer Guide: Building Custom SSH Services
tsshd is more than just a binary program; it is a powerful framework that allows you to build custom SSH applications with seamless roaming and low-latency capabilities.
A. Quick Start: Custom Business Logic
You can easily inject your own interaction logic into an SSH session using the middleware mechanism provided by tsshd.
func main() {
// Use tsshd.RunMain as the entry point and inject custom middleware
code, err := tsshd.RunMain(
tsshd.WithMiddleware(func(next tsshd.Handler) tsshd.Handler {
return func(sess tsshd.Session) {
term := term.NewTerminal(sess, "Enter your name: ")
name, _ := term.ReadLine()
fmt.Fprintf(sess, "Hello, %s! This is a custom SSH service with roaming support.\r\n", name)
}
}),
)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
os.Exit(code)
}
How it Works:
- OpenSSH Bootstrapping: When a client connects using
tssh --udp, it first logs in via the standard SSH protocol. - Process Spawning: OpenSSH spawns your custom binary on the server side.
- Protocol Switch: The program starts and listens on a random UDP port, sends the session keys back to the client, and then the client switches to the QUIC/KCP protocol to communicate directly with the program.
Note: If your binary is not in the system
PATH, you must specify the path on the client side using theTsshdPathconfig or the--tsshd-pathCLI option.
B. Advanced: Building Integrated Servers with Wish
If you don’t want to rely on the system’s OpenSSH to spawn processes, or if you want to build a pure, single-binary custom SSH server, you can combine Wish (based on gliderlabs/ssh) with tsshd.
In this mode, your program can play two roles simultaneously:
- Regular SSH Server (TCP): Listens on port 22 or a custom port to handle the initial login.
- tsshd Server (UDP): Handles roaming, reconnection, and low-latency transmission.
Core Design Concept: Adapter Pattern
To reuse business logic (such as terminal interaction) across both Wish and tsshd, you can define a unified Session interface to abstract away the underlying differences:
// Unified Session interface to make business logic shared between Wish (TCP) and tsshd (UDP)
type Session = tsshd.Session
func handleBusiness(sess Session) {
fmt.Fprintf(sess, "Current session type: %T\r\n", sess)
// Write your business logic here...
}
Session Handoff (Process Handoff)
When Wish receives a request from the client to start tsshd, you can use exec.Command to re-execute the current binary (with tsshd arguments) to achieve a seamless switch from a “standard SSH handshake” to “UDP low-latency transmission”:
- Instruction Detection: Capture the client’s request to execute
tsshdwithin the Wish middleware. - Secondary Launch: Start a sub-process locally on the server (running the current program in
tsshdmode). - Environment Inheritance: Pass current connection info (like
SSH_CONNECTION) to the sub-process.
C. Why Choose This Architecture?
- Development Efficiency: Leverage the Go ecosystem to write SSH services as easily as writing Web middleware.
- Hybrid Deployment: Package both client (
tssh) and server (tsshd) logic into a single binary, simplifying distribution. - Superior Experience: Users enjoy the security of traditional SSH alongside a smooth experience in high-latency or unstable network environments, including automatic reconnection and seamless roaming.
D. Examples
To help you get started quickly, we provide fully working sample code in the examples/ directory. You can use these as templates for your own custom SSH services.
-
examples/hello The most basic implementation. It demonstrates how to use
tsshd.WithMiddlewareto intercept an SSH session, print a greeting, and read user input using thetermpackage. Perfect for building interactive CLI tools. -
examples/sshd A more comprehensive example showing how to handle actual command execution. It demonstrates how to properly route PTY (interactive) and Direct (batch) execution flows, handle terminal window resizing, and stream standard I/O directly to local sub-processes.
-
examples/wish A complete showcase of the Hybrid Architecture mentioned above. It implements the adapter pattern to unify the
Sessioninterface, allowing the same business logic to run seamlessly across a traditional TCP SSH server (Wish) and the low-latency UDPtsshdserver, complete with the process handoff mechanism.
Tip: You can run these examples locally and test them using the
tsshclient to experience the low-latency and roaming features firsthand!
Contact
Feel free to email the author lonnywong@qq.com, or create an issue. Welcome to join the QQ group: 318578930.
Sponsor
❤️ Sponsor trzsz ❤️, buy the author a drink 🍺 ? Thank you for your support!