TNT's Not Tunnel
Find a file
2026-05-28 11:07:44 +08:00
.github Build public release readiness foundation 2026-05-26 09:42:14 +08:00
demos Deepen TUI lifecycle and runtime readiness 2026-05-26 11:15:55 +08:00
docs Refresh development module map 2026-05-28 10:38:27 +08:00
include Generate tntctl command list from exec catalog 2026-05-28 10:36:22 +08:00
packaging Add Debian source package assembly 2026-05-28 09:23:43 +08:00
scripts Smoke-test installed log maintenance modes 2026-05-28 09:25:12 +08:00
src Generate tntctl command list from exec catalog 2026-05-28 10:36:22 +08:00
tests Refresh development module map 2026-05-28 10:38:27 +08:00
.gitignore Split tntctl local text catalog 2026-05-28 09:40:55 +08:00
install.sh Build public release readiness foundation 2026-05-26 09:42:14 +08:00
LICENSE Initial commit 2025-07-01 09:00:00 +08:00
Makefile Fix tntctl ASAN link flags 2026-05-28 11:07:44 +08:00
README.md Split tntctl local text catalog 2026-05-28 09:40:55 +08:00
SECURITY.md Build public release readiness foundation 2026-05-26 09:42:14 +08:00
tnt.1 Guard active help surfaces 2026-05-28 10:28:02 +08:00
tnt.service refactor: stabilize SSH runtime and add exec interface 2026-03-10 18:52:20 +08:00
tntctl.1 Add persisted message dump command 2026-05-27 09:37:51 +08:00

TNT - Terminal Network Talk

A minimalist terminal chat server with Vim-style interface over SSH.

Features

  • Zero config - Download and run, auto-generates SSH keys
  • SSH-based - Leverage mature SSH protocol for encryption and auth
  • Vim-style UI - Modal editing (INSERT/NORMAL/COMMAND)
  • UTF-8 native - Full Unicode support
  • High performance - Pure C, multi-threaded, sub-100ms startup
  • Secure - Rate limiting, auth failure protection, input validation
  • Persistent - Auto-saves chat history
  • Elegant - Flicker-free TUI rendering

Quick Start

Installation

One-liner:

curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh

The installer verifies downloaded release binaries against checksums.txt before installing them. Older releases may provide only tnt; newer releases also install tntctl.

From source:

git clone https://github.com/m1ngsama/TNT.git
cd TNT
make
sudo make install

Binary releases: https://github.com/m1ngsama/TNT/releases

Running

tnt              # default port 2222
tnt -p 3333      # custom port
tnt -d /var/lib/tnt
PORT=3333 tnt    # via env var

Connecting

ssh -p 2222 localhost

For a deployed server, replace localhost with your public host, for example chat.example.com.

Anonymous access by default: Users can connect with ANY username/password (or empty password). No SSH keys required. Perfect for public chat servers.

Usage

Keybindings

INSERT mode (default)

ESC        - Enter NORMAL mode
Enter      - Send message
Backspace  - Delete character
Ctrl+W     - Delete last word
Ctrl+U     - Delete line
Ctrl+C     - Enter NORMAL mode
Paste      - Multi-line paste stays in the input buffer

The input line shows remaining bytes near the message limit. Extra input past the limit is ignored with a terminal bell.

NORMAL mode

Opens at latest messages
Stays pinned to latest until you scroll up
i          - Return to INSERT mode
:          - Enter COMMAND mode
j/k        - Scroll down/up one line
Ctrl+D/U   - Scroll half page down/up
Ctrl+F/B   - Scroll full page down/up
PgDn/PgUp  - Scroll full page down/up
End/Home   - Jump to bottom/top
g/G        - Jump to top/bottom
?          - Show full key reference
Ctrl+C     - Exit chat

COMMAND mode

:list, :users        - Show online users
:nick <name>         - Change nickname
:msg <user> <message> - Send private message
:w <user> <text>     - Short alias for :msg
:inbox               - Show private messages
:last [N]            - Show last N messages from history (max 50, default 10)
:search <keyword>    - Search full message history (case-insensitive)
:mute-joins          - Toggle join/leave system notifications
:lang <en|zh>        - Switch UI language for this session
:help                - Show concise manual
:clear               - Clear command output
:q, :quit, :exit     - Disconnect
Up/Down              - Browse command history
ESC                  - Return to NORMAL mode

Command output pages use j/k, Ctrl+D/U, and g/G for paging. :inbox is live: press r to refresh it manually, and it refreshes when a new private message arrives while the inbox is open.

Special messages (INSERT mode)

/me <action>         - Send action (e.g. /me waves)
@username            - Mention user (bell + highlight)

Security Configuration

Access control:

# Require password
TNT_ACCESS_TOKEN="secret" tnt

# Bind to localhost only
TNT_BIND_ADDR=127.0.0.1 tnt

# Bind to specific IP
TNT_BIND_ADDR=192.168.1.100 tnt

# Store host key and logs in an explicit state directory
TNT_STATE_DIR=/var/lib/tnt tnt

# Show the public SSH endpoint in startup logs
TNT_PUBLIC_HOST=chat.example.com tnt

# Choose interactive UI language (en or zh; defaults from locale)
TNT_LANG=zh tnt

The same operational settings can be passed explicitly, which is often clearer in package scripts and one-off test deployments:

tnt \
  --bind 127.0.0.1 \
  --public-host chat.example.com \
  --max-connections 100 \
  --max-conn-per-ip 10 \
  --max-conn-rate-per-ip 30 \
  --idle-timeout 3600 \
  -p 2222 \
  -d /var/lib/tnt

Rate limiting:

# Max total connections (default 64)
TNT_MAX_CONNECTIONS=100 tnt

# Max concurrent sessions per IP (default 5)
TNT_MAX_CONN_PER_IP=10 tnt

# Max new connection attempts per IP in 60 seconds (default 10)
TNT_MAX_CONN_RATE_PER_IP=30 tnt

# Disable connection-rate and auth-failure blocking (testing only)
TNT_RATE_LIMIT=0 tnt

# Idle timeout in seconds (default 1800 = 30min, 0 to disable)
TNT_IDLE_TIMEOUT=3600 tnt

SSH logging:

# 0=none, 1=warning, 2=protocol, 3=packet, 4=functions (default 1)
TNT_SSH_LOG_LEVEL=3 tnt

Production example:

TNT_ACCESS_TOKEN="strong-password-123" \
TNT_BIND_ADDR=0.0.0.0 \
TNT_MAX_CONNECTIONS=200 \
TNT_MAX_CONN_PER_IP=30 \
TNT_MAX_CONN_RATE_PER_IP=60 \
TNT_SSH_LOG_LEVEL=1 \
tnt -p 2222

SSH Exec Interface

TNT also exposes a small non-interactive SSH surface for scripts:

ssh -p 2222 chat.example.com health
ssh -p 2222 chat.example.com stats --json
ssh -p 2222 chat.example.com users
ssh -p 2222 chat.example.com "tail -n 20"
ssh -p 2222 chat.example.com "dump -n 100"
ssh -p 2222 operator@chat.example.com post "service notice"
ssh -p 2222 chat.example.com post "/me deploys v2.0"

post identity: the message is attributed to the SSH login name (the user@ part of the URL, falling back to anonymous). In the default anonymous-access configuration there is no identity check, so any client can post as any name. Set TNT_ACCESS_TOKEN if you need authenticated posting.

See docs/INTERFACE.md for the stable exec command contract, exit statuses, and JSON field definitions.

Source and package-manager installs also include tntctl, a thin wrapper around the same SSH exec interface:

tntctl chat.example.com health
tntctl -p 2222 chat.example.com stats --json
tntctl -p 2222 chat.example.com dump -n 100
tntctl -l operator chat.example.com post "service notice"

Log Maintenance

Persisted public history is stored as messages.log in the TNT state directory. For manual maintenance, archive and compact it with:

scripts/logrotate.sh /var/lib/tnt/messages.log 100 10000

The script archives the full log, keeps the last KEEP_LINES records in the active file, compresses the archive when gzip is available, and can be previewed with --dry-run.

Installed binaries also include offline checks for the v1 log format:

tnt --log-check /var/lib/tnt/messages.log
tnt --log-recover /var/lib/tnt/messages.log > messages.recovered.log

--log-check prints record counts and exits non-zero when invalid records are found. --log-recover writes valid records to stdout and reports skipped records to stderr; it never edits the source log in place.

Development

Building

make              # standard build
make debug        # debug build (with symbols)
make asan         # AddressSanitizer build
make release-check # local release/package preflight
make check        # static analysis (cppcheck)
make clean        # clean build artifacts

Testing

make test          # run comprehensive test suite and fail on regressions
make test-advisory # run integration tests as advisory checks
make anonymous-access-test # verify default anonymous login behavior
make connection-limit-test # verify per-IP concurrency and rate limits
make security-test # run security feature checks
make stress-test   # run configurable concurrent-client stress test
make soak-test     # run idle/reconnect/control-plane soak test
make slow-client-test # run slow interactive-client backpressure test
make user-lifecycle-test # run a two-user TUI lifecycle test
make ci-test       # run the same checks as GitHub Actions

# Individual tests
cd tests
./test_basic.sh              # basic functionality
./test_security_features.sh  # security features
./test_anonymous_access.sh   # anonymous access
./test_connection_limits.sh  # per-IP concurrency and rate limits
./test_stress.sh             # stress test
./test_soak.sh               # soak test
./test_slow_client.sh        # slow-client backpressure
./test_user_lifecycle.sh     # two-user TUI lifecycle

Test coverage:

  • Basic functionality: 3 tests
  • Anonymous access: 2 tests
  • Security features: 12 tests
  • Stress test: configurable concurrent clients (CLIENTS=20 DURATION=60 make stress-test)
  • Slow-client test: an unread interactive SSH client cannot block health, stats, post, tail, or server survival checks

Dependencies

  • libssh (>= 0.9.0) - SSH protocol library
  • pthread - POSIX threads
  • gcc/clang - C11 compiler

Ubuntu/Debian:

sudo apt-get install libssh-dev

macOS:

brew install libssh

Fedora/RHEL:

sudo dnf install libssh-devel

Project Structure

TNT/
├── src/              # source code
│   ├── main.c        # entry point
│   ├── cli_text.c    # startup CLI help and option text
│   ├── command_catalog.c # command metadata, usage, and argument shape
│   ├── commands.c    # COMMAND-mode command dispatch
│   ├── exec_catalog.c # SSH exec command matching, usage, and argument shape
│   ├── exec.c        # SSH exec command dispatch
│   ├── tntctl.c      # local wrapper around the SSH exec interface
│   ├── tntctl_text.c # tntctl help and option text
│   ├── ssh_server.c  # SSH server implementation
│   ├── bootstrap.c   # SSH authentication and session bootstrap
│   ├── chat_room.c   # chat room logic
│   ├── message.c     # message persistence
│   ├── history_view.c # message viewport and scroll state
│   ├── help_text.c   # full-screen key reference content
│   ├── manual.c      # concise manual panel rendering
│   ├── manual_text.c # concise manual content
│   ├── i18n.c        # UI language and locale selection
│   ├── i18n_text.c   # shared UI text catalog
│   ├── ratelimit.c   # connection limits and rate limiting
│   ├── tui.c         # terminal UI rendering
│   ├── tui_status.c  # status/input line rendering
│   └── utf8.c        # UTF-8 character handling
├── include/          # header files
├── tests/            # test scripts
├── docs/             # documentation
├── packaging/        # package-manager drafts and release checklist
├── scripts/          # operational scripts
├── Makefile          # build configuration
└── README.md         # this file

Deployment

systemd Service

sudo cp tnt.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable tnt
sudo systemctl start tnt

# Optional: override defaults without editing the unit
sudo tee /etc/default/tnt >/dev/null <<'EOF'
PORT=2222
TNT_BIND_ADDR=0.0.0.0
TNT_STATE_DIR=/var/lib/tnt
TNT_MAX_CONNECTIONS=200
TNT_MAX_CONN_PER_IP=30
TNT_MAX_CONN_RATE_PER_IP=60
TNT_RATE_LIMIT=1
TNT_SSH_LOG_LEVEL=0
TNT_PUBLIC_HOST=chat.example.com
EOF

Docker

FROM alpine:latest
RUN apk add --no-cache libssh
COPY tnt /usr/local/bin/
EXPOSE 2222
CMD ["tnt"]

See docs/DEPLOYMENT.md for details.

Packaging

Package-manager drafts live in packaging/. Current targets are Arch/AUR (tnt-chat), Homebrew tap formula, and Ubuntu PPA notes.

Before preparing a release locally:

make release-check

Longer local preflight can opt into runtime soak and slow-client coverage:

RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check

Before publishing package recipes, replace placeholder checksums and run:

make release-check-strict

Files

messages.log    - Chat history (RFC3339 format)
host_key        - SSH host key (auto-generated, 4096-bit RSA)
motd.txt        - Message of the Day (optional, shown to users on connect)
tnt.service     - systemd service unit

The persisted chat-history format is documented in docs/MESSAGE_LOG.md.

MOTD (Message of the Day)

Place a motd.txt file in the state directory to show a welcome message to every user on connect. Users see the MOTD before entering the chat and press any key to continue.

# Example (assuming default state dir)
cat > motd.txt <<'EOF'
Welcome to the chat server!
Be respectful. No spam.
EOF

Delete motd.txt to disable the MOTD.

Documentation

Performance

  • Startup: < 100ms (even with 100k+ message history)
  • Memory: ~2MB (idle)
  • Concurrency: Supports 100+ concurrent connections
  • Throughput: 1000+ messages/second

Troubleshooting

"Connection closed by remote host" right after ssh -p 2222 host

TNT has very little it can say to the SSH client before disconnecting, so any pre-auth rejection just looks like a generic close. Common causes, fastest to slowest fix:

Likely cause Why Fix
Per-IP concurrent limit TNT_MAX_CONN_PER_IP (default 5) Close other sessions, or raise the env var
Per-IP connection rate More than TNT_MAX_CONN_RATE_PER_IP attempts in 60 s Wait 5 min (block window), or raise the limit
Auth-failure ban 5 wrong passwords / failed kex in a row Wait 5 min
Global cap TNT_MAX_CONNECTIONS (default 64) is full Wait for someone to leave
Firewall The host's ufw / iptables doesn't open 2222 Open the port

The server admin can confirm which by checking the systemd journal (sudo journalctl -u tnt -n 50 --no-pager) — the rejection reason is logged to stderr with the offending IP.

Idle disconnect

After TNT_IDLE_TIMEOUT seconds (default 1800 = 30 min) of no keystrokes, TNT prints a localized idle-timeout notice and closes the channel. Set the env var to 0 to disable.

Known Limitations

  • Single chat room (no multi-room support yet)
  • TUI displays at most 100 messages at once; use :last N or :search to access older history from disk
  • Ctrl+W only recognizes ASCII space as word boundary

Contributing

Contributions welcome! See CONTRIBUTING.md

Process:

  1. Fork the repository
  2. Create feature branch (git checkout -b feature/AmazingFeature)
  3. Commit changes (git commit -m 'Add some AmazingFeature')
  4. Push to branch (git push origin feature/AmazingFeature)
  5. Open Pull Request

License

MIT License - see LICENSE

Acknowledgments

  • libssh - SSH protocol implementation
  • Linux kernel community - Code style and engineering practices

Contact


"Talk is cheap. Show me the code." - Linus Torvalds