Making a telnet server with Bash and tcpserver

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.

Leave a Reply

Your email address will not be published. Required fields are marked *