Telnet servers are everywhere. They are simple, insecure by nature, outdated, and often badly implemented. Let’s do that, shall we? 🙂
A simple way to make any command line interface listen to incoming TCP connections is tcpserver, provided as part of the ucspi-tcp or ucspi-tcp-ipv6 package on Debian. Using this, we can make any Bash script available on the network.
Take a look at the following command:
# /usr/bin/tcpserver -c 10 -HR -u 65534 -g 65534 0.0.0.0 2048 /usr/local/bin/script
Here, we’re running tcpserver as root. This is to allow it to later change to a lower privilege level by switching to UID 65534 and GID 65534, nobody:nogroup. We’re also limiting the number of simultaneous connections to 10 and skipping a few reverse lookups. We’re listening on all available interfaces on port 2048.
Each incoming connection will start one instance of the script /usr/local/bin/script, which will be running as the user and group provided. If you do not specify -u and -g, the script will be running as the user that started tcpserver. Be careful with this.
Everything printed by the script to stdout will go to the client, and attempting to read from stdin will read from the client. We can test this with a short script:
#!/usr/bin/env bash echo -n "Please enter your name: " read -r name echo "Hello, $name" echo
Running this script through tcpserver, I can now connect to it with telnet:
$ telnet localhost 2048 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Please enter your name: Testy McTest Hello, Testy McTest Connection closed by foreign host.
That was easy. Now for the annoying stuff. RFC 854 specifies all sorts of things we should pay attention to, such as option negotiation and escape sequences. Most of this isn’t important, but we do need to pay attention to some of it if we want our telnet server to be smarter than a bag of hammers. Handling ^C (ctrl+c) would be good, as would proper telnet line endings, as if you try to connect to the above script with telnet on Windows, it won’t be pretty.
Responding properly to escape sequences requires reading incoming characters one by one – we can’t wait for a line ending. Properly formatting our line endings requires either printing every line like printf “Hello!\r\n”. Alternatively, all output could be run though another command that replaces Linux style newlines (\n) with Telnet style line endings (\r\n).
I wrote a set of functions to handle all of this for me. I included them in the Cathedral ShellFunc library, but they can run on their own if a few supporting functions are exported as well.
You can get shellfunc_telnet here if you’re interested.
With it, our script will work with Windows clients, line editing functions correctly, ctrl+c will disconnect properly, line endings are corrected, and we have login support with characters displayed as asterisks.
Here’s an example script that uses shellfunc_telnet:
#!/usr/bin/env bash source shellfunc_telnet echo "$TCPREMOTEIP:$TCPREMOTEPORT ++ ($$)" >&2 trap 'sf_killchildren || :; echo "$TCPREMOTEIP:$TCPREMOTEPORT -- ($$)" >&2' EXIT # the password is "test" testhash="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" sf_tn_init echo if ! sf_tn_read -t 120 -P "Please enter your name: " name || [[ -z "$name" ]]; then echo -e "\nNo name, no entry" exit 0 fi echo "Hello, $name" if ! sf_tn_read -t 120 -P "Password: " -p pass; then echo exit 0 fi echo "Your pass hash is $pass" if [[ "$pass" != "$testhash" ]]; then echo "Password incorrect. Go away." exit 0 fi echo "Password correct. Welcome." echo "I will now loop and print your lines back to you." echo "Press ctrl+c or give me an empty line to quit" while true; do sf_tn_read -t 120 line if (( $? == 69 )); then # interrupt echo "Interrupted!" elif (( $? > 128 )); then # timeout echo "Too slow!" fi [[ -n "$line" ]] || break echo "Your line: $line" done echo "Bye." echo sleep 1
Here’s a sample session:
$ telnet localhost 2048 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Please enter your name: Testy McTest Hello, Testy McTest Password: **** Your pass hash is 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 Password correct. Welcome. I will now loop and print your lines back to you. Press ctrl+c or give me an empty line to quit line 1 Your line: line 1 foo Your line: foo Q Your line: Q Interrupted! Bye. Connection closed by foreign host.
It even works on Windows!
We can make the script run at boot time by adding it to root’s crontab as such:
@reboot /usr/bin/tcpserver -c 10 -HR -u 65534 -g 65534 0.0.0.0 2048 /usr/local/bin/script 2>&1 | logger -t tcp-2048 -p user.notice
Here, we’re running our tcpserver at boot time, we’re taking anything our script prints to standard error, which is currently a log of new connections and disconnects, and puts it in the system log.
If you’re curious about sf_killchildren, you can read more about it here.
Have fun. Drop me a line if you do something cool with this.