Telnet Daemon
May 23, 2026 · View on GitHub
Telnet is one access method for the shared BinktermPHP Terminal Server. The post-login feature set (netmail, echomail, file areas, doors, polls, shoutbox, editor behavior, and menu flow) is documented in:
This document covers Telnet-specific transport setup, daemon operation, and troubleshooting.
Features
- Telnet option negotiation — IAC-based negotiation for NAWS (terminal size), echo control, and suppress-go-ahead
- Dual listener (plain + TLS) — listens on a plain-text port (default 2323) and a TLS-encrypted port (default 8023) simultaneously; TLS cert is auto-generated on first run if not provided
- Optional ANSI login screen — place an ANSI art file at
telnet/screens/login.ansto display it instead of the default login banner (sent as raw ANSI with CRLF normalization) - Connection rate limiting — rejects repeated rapid connections from a single IP before forking a child process, preventing flood attacks from exhausting process table slots
Requirements
In addition to the base Terminal Server requirements, the Telnet daemon requires:
- PHP extension:
sockets— for the raw TCP listener
Usage
Starting the Daemon
Run the daemon with default settings (0.0.0.0:2323):
php telnet/telnet_daemon.php
Command Line Options
Specify custom host and port:
php telnet/telnet_daemon.php --host=0.0.0.0 --port=2323
Specify API base URL:
php telnet/telnet_daemon.php --api-base=http://127.0.0.1
For HTTPS with a self-signed certificate:
php telnet/telnet_daemon.php --api-base=https://your-host --insecure
Enable debug logging (shows API URLs, screen dimensions, login attempts, and misc debugging information to console and telnet sessions):
php telnet/telnet_daemon.php --debug
Available Options
| Option | Default | Description |
|---|---|---|
--host | 0.0.0.0 | IP address to bind to (use 127.0.0.1 for localhost only) |
--port | 2323 | Plain-text TCP port to listen on |
--no-tls | (off) | Disable the TLS listener (TLS is enabled by default) |
--tls-port | 8023 | TCP port for the TLS listener |
--tls-cert | auto-generated | Path to TLS certificate PEM file |
--tls-key | auto-generated | Path to TLS private key PEM file |
--api-base | SITE_URL | Base URL for API requests (e.g., http://localhost) |
--insecure | (off) | Accept self-signed SSL certificates for API calls |
--debug | (off) | Enable verbose debug logging to console |
--daemon | (off) | Run as a background daemon process |
--pid-file | data/run/telnetd.pid | Path to write the daemon PID file |
Environment Variable Equivalents
Command-line options take precedence over .env values.
.env variable | Default | Description |
|---|---|---|
TELNET_BIND_HOST | 0.0.0.0 | Bind address (equivalent to --host) |
TELNET_PORT | 2323 | Plain-text port (equivalent to --port) |
TELNET_TLS | true | Set to false to disable TLS entirely |
TELNET_TLS_PORT | 8023 | TLS listener port (equivalent to --tls-port) |
TELNET_TLS_CERT | (empty) | Path to TLS cert PEM (equivalent to --tls-cert) |
TELNET_TLS_KEY | (empty) | Path to TLS key PEM (equivalent to --tls-key) |
TLS Support
The Telnet daemon runs two listeners simultaneously by default: a plain-text listener on port 2323 and a TLS-encrypted listener on port 8023. Both listeners expose the same BBS session.
On first start the daemon checks for a certificate and key at data/telnetd.crt and data/telnetd.key. If they do not exist, a self-signed certificate is generated automatically (using ext-openssl or the openssl CLI as a fallback) and stored there.
To disable TLS:
php telnet/telnet_daemon.php --no-tls
Or in .env:
TELNET_TLS=false
To use your own certificate instead of the auto-generated one:
php telnet/telnet_daemon.php --tls-cert=/etc/ssl/mycert.pem --tls-key=/etc/ssl/mykey.pem
Or in .env:
TELNET_TLS_CERT=/etc/ssl/mycert.pem
TELNET_TLS_KEY=/etc/ssl/mykey.pem
TLS connections are logged with the cipher suite and key size (e.g., TLS connection from 1.2.3.4 [TLSv1.2 AES128-GCM-SHA256 128-bit]).
Running as a Service
Systemd
Create a systemd service file for automatic startup:
sudo nano /etc/systemd/system/binkterm-telnet.service
[Unit]
Description=BinktermPHP Telnet Daemon
After=network.target
[Service]
Type=simple
User=yourusername
Group=yourusername
WorkingDirectory=/path/to/binkterm
ExecStart=/usr/bin/php /path/to/binkterm/telnet/telnet_daemon.php
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable binkterm-telnet
sudo systemctl start binkterm-telnet
sudo systemctl status binkterm-telnet
Cron (Alternative)
@reboot /usr/bin/php /path/to/binkterm/telnet/telnet_daemon.php --daemon
Connecting
Users can connect using any telnet client:
Command Line Telnet
telnet your-bbs-hostname 2323
PuTTY (Windows)
putty -telnet your-bbs-hostname -P 2323
Or configure a saved session:
- Host Name:
your-bbs-hostname - Port:
2323 - Connection type: Telnet
UTF-8 Capable Terminal Clients
If you want a terminal with strong UTF-8 support, use a client such as ZOC or PuTTY.
ZOC
- Add a new Telnet session
- Address:
your-bbs-hostname - Port:
2323
PuTTY
- Host Name:
your-bbs-hostname - Port:
2323 - Connection type: Telnet
- Under
Window -> Translation, select a UTF-8 character set if needed
Client Compatibility
- UTF-8 behavior varies by terminal emulator and font configuration
- Echo handling varies by telnet client
- Some clients may not properly support NAWS negotiation
- ANSI color support depends on terminal emulator capabilities
Security Considerations
Authentication
- Users authenticate with their BinktermPHP web credentials
- Passwords are transmitted to the API over HTTP(S)
- Consider using HTTPS for API connections in production
Network Security
- Daemon listens on all interfaces (
0.0.0.0) by default - Use
--host=127.0.0.1to restrict to localhost only - Consider firewall rules to restrict access by IP
- Monitor logs for suspicious login activity
Connection Rate Limiting
The daemon tracks the number of TCP connections accepted from each remote IP
within a rolling fixed window. If an IP exceeds the limit, the connection is
accepted at the socket level (to avoid a half-open backlog), a short error
message is written to the client, and the socket is closed — no child process
is forked. Rejections are logged to data/logs/telnetd.log.
The window is fixed per IP: the first connection from an IP starts the
clock; the counter resets only after TELNET_RATE_LIMIT_WINDOW seconds have
elapsed since that first connection, not since the most recent one.
.env variable | Default | Description |
|---|---|---|
TELNET_RATE_LIMIT_MAX | 5 | Maximum connections allowed from one IP per window. Set to 0 to disable rate limiting entirely. |
TELNET_RATE_LIMIT_WINDOW | 60 | Window duration in seconds. |
The defaults allow 5 connections per minute per IP, which is sufficient for
any legitimate user. Adjust TELNET_RATE_LIMIT_MAX downward if you are seeing
active floods, or set it to 0 on private/LAN-only installs.
Fail2ban
The telnet daemon writes to data/logs/telnetd.log. Example fail2ban
configuration files are provided in docs/fail2ban/ for banning IPs that
trigger the connection rate limiter:
docs/fail2ban/filter.d/binkterm-telnet-ratelimit.confdocs/fail2ban/jail.d/binkterm-telnet-ratelimit.local.example
Example install steps on Linux:
sudo cp docs/fail2ban/filter.d/binkterm-telnet-ratelimit.conf /etc/fail2ban/filter.d/
sudo cp docs/fail2ban/jail.d/binkterm-telnet-ratelimit.local.example /etc/fail2ban/jail.d/binkterm-telnet-ratelimit.local
sudo sed -i 's#/path/to/binkterm#/var/www/binkterm-php#' /etc/fail2ban/jail.d/binkterm-telnet-ratelimit.local
sudo systemctl restart fail2ban
sudo fail2ban-client status binkterm-telnet-ratelimit
The filter matches log lines like:
[2026-03-19 02:12:09] [1441153] [INFO] Rate limit exceeded for 66.205.238.49 - connection rejected
The daemon suppresses duplicate log lines within a rate-limit window, so only
one line is written per offending IP per window. The provided jail uses
maxretry = 1 so fail2ban bans immediately on the first logged rejection.
Troubleshooting
Connection Issues
Problem: Cannot connect to telnet daemon
Solutions:
- Verify daemon is running:
ps aux | grep telnet_daemon - Check port is listening:
netstat -an | grep 2323 - Check firewall rules:
sudo ufw status - Try localhost connection:
telnet localhost 2323
API Connection Issues
Problem: "Failed to authenticate" or API errors
Solutions:
- Verify web interface is accessible
- Check API base URL setting
- Test API manually:
curl http://localhost/api/auth/login - Enable debug mode:
--debugflag - Check API logs in web server error log
Screen Display Issues
Problem: Message lists overflow or don't fit screen
Solutions:
- Verify terminal supports NAWS negotiation
- Check debug output for detected screen dimensions
- Try a different terminal emulator with solid UTF-8 support, such as ZOC or PuTTY
- Manually resize terminal window to trigger NAWS update
Editor Issues
Problem: Arrow keys not working or inserting strange characters
Solutions:
- Verify terminal type is set correctly (ANSI or VT100)
- Try a different terminal emulator
- Check telnet client configuration
- Try ZOC or PuTTY with UTF-8 enabled
Connection Rate Limiting
Problem: Legitimate users receive "Too many connections from your IP. Please try again later."
Solutions:
- Wait for the current window to expire (default: 60 seconds from the first connection in the window) then reconnect
- Check
data/logs/telnetd.logfor lines containing "Rate limit exceeded" to confirm which IP is being blocked - Raise
TELNET_RATE_LIMIT_MAXin.envif the default of 5 connections per minute is too restrictive for your users - Set
TELNET_RATE_LIMIT_MAX=0in.envand restart the daemon to disable rate limiting entirely on private/LAN installs
Login Rate Limiting
Problem: "Too many failed login attempts"
Solutions:
- Wait 60 seconds for rate limit to expire
- Check logs for the IP address being rate limited
- Verify correct username and password
- Contact administrator if a legitimate account is locked
Development Notes
Debug Mode
php telnet/telnet_daemon.php --debug
Debug output includes API URL, detected screen dimensions, messages-per-page calculation, connection events, login attempt tracking, and API request/response details.
Signal Handling
- SIGCHLD — Reaps zombie child processes (forked connections)
- SIGTERM — Graceful shutdown with cleanup
- SIGINT — Graceful shutdown on Ctrl+C
Connection Flow
- Client connects to the plain-text or TLS listener
- Parent checks per-IP connection rate limit — closes socket immediately if exceeded
- Daemon forks child process (Linux/macOS) or handles directly (Windows)
- If TLS: child performs TLS handshake; on failure, connection is dropped
- Child performs telnet option negotiation (NAWS, TTYPE, echo control)
- Child probes for ANSI color support via TTYPE; enables color automatically if supported
- Child displays ANSI/Sixel login screen or default banner
- User sees pre-login menu (Login / Register / Reset password / QWK / Quit)
- User authenticates via API (up to 3 attempts)
- Main menu displayed with message counts
- User navigates menus and performs actions
- Connection closed and child exits
- Parent reaps zombie process via SIGCHLD
Code Structure
telnet/telnet_daemon.php— Main daemon script- Telnet protocol implementation with IAC negotiation
- ANSI escape code support for colors and cursor control
Contributing
When contributing to the telnet daemon:
- Test with multiple terminal emulators (PuTTY, ZOC, standard telnet)
- Verify Windows compatibility (single connection mode)
- Test with different screen sizes (24 rows, 40 rows, etc.)
- Follow existing code conventions
- Add debug logging for new features
- Update this documentation
See Also
- Terminal Server — shared feature set used by all terminal access methods
- SSH Server — encrypted SSH-2 alternative to Telnet
License
Same as BinktermPHP — BSD License. See LICENSE.md for details.