An SSH and Mosh inspired tool written in Rust.
moshpit is a suite of tools for establishing encrypted, resilient remote terminal sessions:
| Binary | Crate | Role |
|---|---|---|
mps |
moshpits | Server β listens for incoming connections and spawns PTYs |
mp |
moshpit | Client β connects to a running mps server |
mp-keygen |
moshpit-keygen | Key management β generates and inspects ed25519 key pairs |
Sessions are authenticated with ed25519 key pairs. TCP is used only for the initial key exchange; once the exchange completes the connection switches to UDP exclusively (ports 50000β59999) for all terminal I/O. The server tracks full terminal screen state with a server-side vt100 emulator; on reconnect the client receives a single clean screen snapshot and repaints instantly rather than replaying raw scrollback history.
moshpit draws its core motivation from Mosh (Mobile Shell), the excellent remote terminal tool created by Keith Winstein and colleagues at MIT. Mosh demonstrated that a UDP-based transport with graceful handling of packet loss, reordering, and IP roaming could make remote terminal sessions feel dramatically more responsive and reliable than traditional SSH β particularly over high-latency or intermittent connections. moshpit was created as an exercise in rebuilding that idea from scratch in Rust, exploring different design trade-offs along the way.
- UDP terminal data channel β terminal I/O is carried over UDP rather than a reliable stream, allowing the session to survive network interruptions without blocking on TCP retransmit timeouts.
- Resilience to connectivity loss β both tools keep the session alive across short network outages and IP address changes; the client reconnects automatically without user intervention.
- Authenticated encryption β all data on the wire is encrypted and authenticated; neither tool relies on a plain-text transport at any layer.
- Client / server split β a lightweight server component (
mps/mosh-server) runs on the remote host and manages the PTY; a client (mp/mosh) runs locally and drives the terminal. - Server-side screen state β the server maintains a vt100 model of the current PTY screen; on reconnect the client receives a single screen snapshot for an instant, noise-free repaint.
- Client-side prediction β keystrokes are echoed locally and cursor movement is predicted to hide round-trip latency; predicted characters are underlined until the server confirms them.
| Concern | Mosh | moshpit |
|---|---|---|
| Language | C++ | Rust |
| Authentication | Delegated to SSH for the initial handshake; a one-time secret is passed back over SSH | Standalone ed25519 key-pair authentication β no SSH dependency |
| Transport model | Pure UDP after setup; Mosh's State Synchronization Protocol (SSP) keeps a diff of the full terminal screen state and sends only the latest snapshot | TCP is used solely for the ed25519 key exchange; all terminal I/O runs over UDP after the exchange completes. NAK-based selective retransmission with an adaptive RTT estimator ensures reliable, ordered delivery; a split data/control channel prevents control frames from being delayed by PTY data backlogs |
| Reconnect display sync | SSP sends the latest screen snapshot; client repaints from the diff immediately | Server maintains a vt100::Parser tracking the live PTY screen; on reconnect a single ScreenState frame delivers contents_formatted() bytes for an instant clean repaint. A 50 ms periodic task also sends ScreenState diffs during normal use so the client stays in sync even across network hiccups. |
| Client-side prediction | Mosh echoes keystrokes locally and predicts cursor movement to hide latency, underlining characters that have not yet been confirmed by the server | Same β keystrokes are echoed locally, cursor movement is predicted, and unconfirmed characters are underlined until the server output arrives |
| Encryption | AES-128-OCB authenticated encryption using a symmetric session key | Key exchange via an ed25519-based handshake; symmetric AES-256-GCM-SIV on the UDP channel with per-packet HMAC-SHA-512 authentication |
| Session multiplexing | One Mosh session per mosh-server process |
Same β one PTY per mps connection |
| Configuration | Minimal; primarily driven by command-line options | TOML config files with environment-variable overrides |
| UDP port range | 60001β61000 (by default) | 50000β59999 |
| License | GPL v3 | Apache 2.0 / MIT (your choice) |
Attribution: the name moshpit is a deliberate nod to Mosh, whose design and published research were a direct inspiration for this project. If you need production-grade, battle-tested remote terminal software, use Mosh. moshpit is an independent reimagining with different goals and trade-offs.
The client opens a TCP connection to the server's configured port (default 40404). The two sides run a mutual ed25519 key-pair authentication and key-exchange protocol over this connection. Once the handshake completes both halves of the TCP socket are released and the TCP connection is closed immediately β it is not kept alive, and is not used for anything after the key exchange.
All subsequent communication happens exclusively over UDP (server-side port range 50000β59999). Every frame is encrypted with AES-256-GCM-SIV and authenticated with a per-packet HMAC-SHA-512.
Reliable, ordered delivery is provided at the application layer using NAK-based selective retransmission:
- The receiver (
UdpReader) tracks the highest sequence number seen and maintains a reorder buffer. Any gap that persists beyond a 20β500 ms adaptive NAK timeout triggers aNakframe β a compact list of missing sequence numbers β sent back to the sender over the same UDP channel. When a gap opens mid-burst, aRepaintRequestis also sent immediately (rather than waiting for the first NAK retry cycle) so the server can deliver a fresh screen snapshot within one RTT. - The sender (
UdpSender) keeps a sliding retransmit buffer of the 512 most-recently transmitted wire-encoded packets. When aNakarrives the missing packets are looked up and resent immediately. The sender uses two separate outbound channels β a high-priority control channel (capacity 16) forKeepaliveandShutdownframes, and a data channel (capacity 256) for PTY diffs and screen states β so control frames are always polled first and bypass any data backlog. - Each gap is retried up to 4 times (with exponential backoff capped at 800 ms); after the limit is exceeded the gap is abandoned and the session proceeds.
- The NAK timeout adapts to the measured round-trip time using a Jacobson-Karels estimator. Outlier RTT spikes are clamped to 8Γ the current estimate rather than discarded, preventing a self-reinforcing loop where aggressive 20 ms NAKs worsen congestion on slow NAT paths.
- Large PTY bursts (more than 10 MTU-sized chunks from a single PTY read, e.g. a full-screen
htopredraw) are sent with 3Γ the configured inter-packet pacing delay to reduce burst loss on stateful NAT devices.
Because retransmission is handled entirely within the UDP layer there is no head-of-line blocking from TCP: a lost packet delays only the frames that depend on it, not the rest of the stream. Control frames are additionally isolated from data backpressure via the split channel design.
If the UDP path is interrupted the client automatically reconnects β performing a new TCP key exchange for the same logical session β and the server delivers a single ScreenState frame containing the current terminal contents so the display repaints instantly without replaying scrollback history.
This project has not yet completed a formal security hardening phase, external security review, or independent penetration testing. It may contain security flaws that could lead to data loss, session compromise, privilege misuse, or other unintended behavior.
Use this software at your own risk, especially in internet-facing, production, or high-trust environments.
All three binaries are available as separate AUR packages. Install them with any AUR helper (e.g. yay, paru) or manually with makepkg.
| AUR package | Installs | Notes |
|---|---|---|
moshpit-keygen |
mp-keygen |
No dependencies; install this first if building manually |
moshpit |
mp (client) |
Depends on moshpit-keygen |
moshpits |
mps (server) |
Depends on moshpit-keygen |
# Install the server (pulls in moshpit-keygen automatically)
yay -S moshpits
# Install the client (pulls in moshpit-keygen automatically)
yay -S moshpit
# Or install both in one go
yay -S moshpits moshpit# 1. Clone and build moshpit-keygen first (shared dependency)
git clone https://aur.archlinux.org/moshpit-keygen.git
cd moshpit-keygen
makepkg -si
cd ..
# 2. Clone and build the server
git clone https://aur.archlinux.org/moshpits.git
cd moshpits
makepkg -si
cd ..
# 3. Clone and build the client
git clone https://aur.archlinux.org/moshpit.git
cd moshpit
makepkg -si
cd ..# Remove server and client (keep keygen)
sudo pacman -R moshpits moshpit
# Remove everything including keygen
sudo pacman -Rs moshpits moshpit moshpit-keygenRequires a Rust toolchain (stable, 1.91.1 or later). Install all three binaries directly from crates.io:
# Key management tool (install first β the others depend on it)
cargo install keygen
# Client
cargo install moshpit
# Server
cargo install moshpitsTo install a specific version, append --version <x.y.z> to any of the commands above.
mp-keygen creates and inspects the ed25519 key pairs used by both the server and client.
Interactively generates a new ed25519 public/private key pair. The tool prompts for an output path and an optional passphrase.
mp-keygen generateDefault key locations (when the default path is accepted at the prompt):
| Key | Default path |
|---|---|
| Private key | ~/.mp/id_ed25519 |
| Public key | ~/.mp/id_ed25519.pub |
Displays the SHA-256 fingerprint of a public key file.
mp-keygen fingerprint ~/.mp/id_ed25519.pubVerifies a public key fingerprint string. Pass --randomart to verify a randomart image instead.
# Verify a fingerprint string
mp-keygen verify "SHA256:..."
# Verify a randomart image
mp-keygen verify --randomart "+--[ED25519 256]--+ ..."| Flag | Short | Description |
|---|---|---|
--verbose |
-v |
Increase log verbosity (repeatable) |
--quiet |
-q |
Decrease log verbosity (repeatable, conflicts with --verbose) |
-
Generate a server host key pair (run once):
# The server uses a separate key stored at ~/.mp/mps_host_ed25519_key[.pub] # by default. You can generate it with mp-keygen using a custom path: mp-keygen generate # Enter a path such as: /home/user/.mp/mps_host_ed25519_key
-
Create the config file at
~/.config/moshpits/moshpits.toml(see Configuration below). -
Start the server:
mps
mps [OPTIONS]
Options:
-v, --verbose Turn up logging verbosity (repeatable)
-q, --quiet Turn down logging verbosity (repeatable)
-e, --enable-std-output Enable logging to stdout/stderr
(not recommended when running as a daemon)
-c, --config-absolute-path <PATH> Absolute path to an alternate config file
-t, --tracing-absolute-path <PATH> Absolute path to an alternate tracing output file
-p, --private-key-path <PATH> Absolute path to the server private key
-k, --public-key-path <PATH> Absolute path to the server public key
--warmup-delay-ms <MILLIS> Extra delay (ms) after peer discovery before
sending terminal data
--pacing-delay-us <MICROS> Min inter-packet delay (Β΅s) between diff chunks
[default: 1000]
--term-type <TERM> TERM environment variable for spawned shells
[default: xterm-256color]
-h, --help Print help
-V, --version Print version
# Start with defaults (reads ~/.config/moshpits/moshpits.toml)
mps
# Start with verbose logging to stderr
mps -vv --enable-std-output
# Use a custom config file
mps --config-absolute-path /etc/moshpits/moshpits.toml
# Use non-default key files
mps --private-key-path /etc/moshpits/host_key \
--public-key-path /etc/moshpits/host_key.pub
# Use a different TERM type for specialized environments
mps --term-type screen-256color
# Tune for NAT devices with high warmup delay and packet pacing
mps --warmup-delay-ms 200 --pacing-delay-us 2000Default config file: ~/.config/moshpits/moshpits.toml
Environment variable prefix: MOSHPITS_ (nested keys separated by _, e.g. MOSHPITS_MPS_PORT=40404)
# ~/.config/moshpits/moshpits.toml
# ββ Logging ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Base verbosity offset applied to the tracing output file.
# 0 = INFO, positive values increase verbosity, negative decrease it.
verbose = 0
quiet = 0
# ββ Server listen address βββββββββββββββββββββββββββββββββββββββββββββββββββββ
[mps]
ip = "0.0.0.0" # IP address to listen on
port = 40404 # TCP port to listen on for client connections
# ββ Key files βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Defaults to ~/.mp/mps_host_ed25519_key and ~/.mp/mps_host_ed25519_key.pub
# when not set.
# private_key_path = "/path/to/mps_host_ed25519_key"
# public_key_path = "/path/to/mps_host_ed25519_key.pub"
# ββ PTY environment βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# TERM environment variable to set for spawned shells. Default: xterm-256color.
# Matches the VT100/VT220 emulation used by libmoshpit. Required when running
# as a systemd service to prevent ncurses application failures.
term_type = "xterm-256color"
# ββ NAT device tuning (optional) ββββββββββββββββββββββββββββββββββββββββββββββ
# Extra delay (ms) after peer discovery before sending bulk terminal data.
# Provides margin for NAT bindings on slow NAT devices when clients use --nat-warmup.
# warmup_delay_ms = 100
# Minimum inter-packet delay (Β΅s) between consecutive diff chunks from the same
# PTY read batch. Spreads back-to-back packets to prevent burst loss on stateful
# NAT devices. Default: 1000 (1 ms). Set to 0 to disable pacing.
# pacing_delay_us = 1000
# ββ Tracing (log output) ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# stdout layer β controls the format of log lines written to stderr when
# --enable-std-output is active.
[tracing.stdout]
with_target = false # include the Rust module path in each log line
with_thread_ids = false # include the thread ID
with_thread_names = false # include the thread name
with_line_number = false # include the source file line number
with_level = true # include the log level (ERROR, WARN, INFO, β¦)
# directives = "moshpits=debug,libmoshpit=info" # optional tracing filter
# file layer β controls the format and level of the persistent tracing file.
# Default log file: ~/.config/moshpits/logs/moshpits.log
[tracing.file]
quiet = 0
verbose = 0
[tracing.file.layer]
with_target = false
with_thread_ids = false
with_thread_names = false
with_line_number = false
with_level = true
# directives = "moshpits=debug"- Environment variables (
MOSHPITS_*) - Command-line flags
- Config file values
-
Generate a client key pair (run once):
mp-keygen generate # Accept the default path: ~/.mp/id_ed25519 -
Add the client's public key to the server's
authorized_keysfile.On the server, append the contents of the client's
~/.mp/id_ed25519.pubto~$TARGET_USER/.mp/authorized_keys(one key per line):# On the client β display the public key to copy cat ~/.mp/id_ed25519.pub # On the server β create the directory and file with the correct permissions mkdir -p ~/.mp && chmod 700 ~/.mp echo 'moshpit <base64-key> user@host' >> ~/.mp/authorized_keys chmod 600 ~/.mp/authorized_keys
The public key line format written by
mp-keygen generateis:moshpit <base64-encoded-public-key> user@hostPermission requirements:
~/.mpmust be mode0700andauthorized_keysmust be mode0600, otherwise the server will reject the connection. -
Connect to the server:
mp 192.168.1.10 # or with an explicit user mp alice@192.168.1.10
mp [OPTIONS] <SERVER_DESTINATION>
Arguments:
<SERVER_DESTINATION> IP address (or user@address) of the server to connect to
Options:
-v, --verbose Turn up logging verbosity (repeatable)
-q, --quiet Turn down logging verbosity (repeatable)
-c, --config-absolute-path <PATH> Absolute path to an alternate config file
-t, --tracing-absolute-path <PATH> Absolute path to an alternate tracing output file
-p, --private-key-path <PATH> Absolute path to the client private key
-k, --public-key-path <PATH> Absolute path to the client public key
-s, --server-port <PORT> Server TCP port (default: 40404)
--predict <MODE> Local-echo prediction: adaptive (default),
always, or never
--nat-warmup Send NAT warmup keepalives at UDP session start
--nat-warmup-count <N> Number of NAT warmup keepalives to send
[default: 3]
-h, --help Print help
-V, --version Print version
# Connect to a server on the default port (40404)
mp 192.168.1.10
# Connect as a specific user
mp alice@192.168.1.10
# Connect to a non-default port
mp --server-port 50505 192.168.1.10
# Verbose logging, custom key files
mp -vv \
--private-key-path ~/.mp/work_id_ed25519 \
--public-key-path ~/.mp/work_id_ed25519.pub \
alice@10.0.0.5
# Disable prediction for low-latency LANs
mp --predict never 192.168.1.10
# Enable NAT warmup for problematic NAT devices
mp --nat-warmup --nat-warmup-count 5 192.168.1.10
# Force prediction always on
mp --predict always user@remote-server.com
### moshpit configuration
**Default config file**: `~/.config/moshpit/moshpit.toml`
**Environment variable prefix**: `MOSHPIT_` (e.g. `MOSHPIT_SERVER_PORT=40404`)
```toml
# ~/.config/moshpit/moshpit.toml
# ββ Logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
verbose = 0
quiet = 0
# ββ Server connection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
server_port = 40404 # TCP port of the moshpits server
server_destination = "192.168.1.10" # "ip" or "user@ip"; overridden by the
# positional argument on the command line
# ββ Reconnection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Maximum back-off interval between automatic reconnect attempts (seconds).
# Clamped to the range [2, 86400]. Default: 3600 (1 hour).
max_reconnect_backoff_secs = 3600
# ββ Key files βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Defaults to ~/.mp/id_ed25519 and ~/.mp/id_ed25519.pub when not set.
# private_key_path = "/home/alice/.mp/id_ed25519"
# public_key_path = "/home/alice/.mp/id_ed25519.pub"
# ββ Local echo prediction βββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Control client-side keystroke prediction: adaptive (default), always, or never.
# Adaptive enables prediction on high-latency connections; 'never' disables it.
predict = "adaptive"
# ββ NAT traversal (optional) ββββββββββββββββββββββββββββββββββββββββββββββββββ
# Send warmup keepalives before UDP session starts to establish NAT bindings.
# Only useful on NAT paths; adds one round-trip of startup latency.
nat_warmup = false
nat_warmup_count = 3 # Number of keepalive frames to send (default: 3)
# ββ Tracing (log output) ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[tracing.stdout]
with_target = false
with_thread_ids = false
with_thread_names = false
with_line_number = false
with_level = true
# directives = "moshpit=debug,libmoshpit=info"
# Default log file: ~/.config/moshpit/logs/moshpit.log
[tracing.file]
quiet = 0
verbose = 0
[tracing.file.layer]
with_target = false
with_thread_ids = false
with_thread_names = false
with_line_number = false
with_level = true
- Environment variables (
MOSHPIT_*) - Command-line flags
- Config file values
Symptom: When running moshpits as a systemd service, commands like htop, vim, or less fail with:
Error opening terminal: unknown.
Cause: Systemd services run without a controlling terminal, so the TERM environment variable is not set. When moshpits spawns shells, it sets HOME, USER, LOGNAME, and SHELL but was missing TERM, causing ncurses applications to fail.
Solution: Use the term_type configuration option (default: xterm-256color):
# In ~/.config/moshpits/moshpits.toml
term_type = "xterm-256color"Or via CLI:
mps --term-type xterm-256colorOr via environment variable:
MOSHPITS_TERM_TYPE=xterm-256color mpsThe default xterm-256color matches the VT100/VT220 terminal emulation used by libmoshpit and works with most ncurses applications. For specialized environments, you can override it with other values like screen-256color, tmux-256color, or linux.
Symptom: Connection hangs after TCP key exchange, no UDP terminal data flows.
Solution: Use the --nat-warmup option on the client to send keepalive frames before the UDP session starts:
mp --nat-warmup --nat-warmup-count 5 192.168.1.10On the server, add a warmup delay to give the NAT device time to establish bindings:
mps --warmup-delay-ms 100For stateful NAT devices that drop bursty packets, enable packet pacing:
mps --pacing-delay-us 2000Symptom: Terminal updates are sluggish or incomplete, especially during full-screen redraws (e.g. htop, vim).
Solution: Increase the pacing delay on the server to spread packets over time:
mps --pacing-delay-us 5000Or in the config file:
pacing_delay_us = 5000Note: the server automatically applies 3Γ the configured pacing delay for large PTY bursts (more than 10 MTU-sized chunks from a single PTY read), so htop-style full-screen redraws are paced more aggressively than small incremental updates without any extra configuration.
Symptom: Occasional multi-second freezes in terminal output, particularly when running high-output programs over a congested or high-latency NAT connection.
Cause: If the adaptive NAK timeout converges to its minimum (20 ms) and the connection then experiences a real congestion spike, the aggressive NAK rate can worsen congestion, which in turn appears as more packet loss β a self-reinforcing loop. The adaptive estimator now clamps outlier RTT spikes rather than discarding them, so the timeout self-heals within a few keepalive intervals, but the symptom may still appear briefly.
Solution: If stalls persist, enable packet pacing to reduce burst pressure on the NAT device:
mps --pacing-delay-us 2000| Port range | Protocol | Direction | Purpose |
|---|---|---|---|
mps.port (e.g. 40404) |
TCP | Inbound to server | Key exchange only β connection switches to UDP after handshake |
| 50000β59999 | UDP | Inbound to server | Encrypted terminal data |
To the fullest extent permitted by applicable law, this software is provided "AS IS" and "AS AVAILABLE", without warranties or conditions of any kind, express or implied. By installing, running, or distributing this software, you assume all risks associated with its use.
The project author and contributors are not liable for any direct, indirect, incidental, special, exemplary, or consequential damages, including but not limited to system damage, data loss, security incidents, service interruption, or loss of profits, arising from use of this project.
If you do not agree with these terms, do not use this software.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.