Skip to content

m1ngsama/TNT

Repository files navigation

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/a/o      - 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
:reply <text>        - Reply to latest private message
:r <text>            - Short alias for :reply
:inbox               - Show private messages
:inbox clear         - Clear private messages for this session
:last [N]            - Show last N messages from history (max 50, default 10)
:search <keyword>    - Search message history (shows last 15 matches)
: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 shows incoming and sent private messages newest-first; press r to refresh it manually, and it refreshes when a new private message arrives while the inbox is open. :reply text and :r text send to the latest private-message peer. Unread incoming private messages are marked with * until :inbox renders. The inbox title shows a transient unread count when new private messages are present. :inbox clear removes private messages and the reply target for this session. Private messages are per-session only and are not written to messages.log.

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. Private messages and local inbox state are intentionally excluded. 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
│   ├── module_protocol.c # external module JSONL protocol helpers
│   ├── module_runtime.c # optional external module supervisor
│   ├── json_text.c   # small JSON string helpers
│   ├── input_buffer.c # validated terminal input buffer helpers
│   ├── 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_INTEGRATION=1 RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check

Before publishing package recipes, download the explicit release source archive, replace placeholder checksums, and run:

SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z-source.tar.gz make package-publish-check

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. Experimental community modules should follow the external-process protocol in docs/MODULE_PROTOCOL.md. Module-generated content must always include a plain-text fallback so TNT can keep working on basic terminal clients and preserve the stable messages.log v1 history contract.

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

About

TNT's Not Tunnel

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors