The telnet protocol is an interesting one full of a lot of history I am totally ignorant to. While playing around with implementing a telnet server that spoke some of the more telnet specific protocol language, I went through the list of telnet command options and one stood out particularly to me. Option 19, byte macros, allows for customizable macro definitions where a single byte will be translated into a replacement string after being received. Unfortunately, no telnet client I found was willing to support this option and would always just respond with IAC WONT BM
, telnet protocol for refusing to support an option, and would proceed to ignore any subnegotiations specifying byte macro translations.
Telnet's protocol for setting options is fairly simple and involves some negotiations between clients and servers. Either party can send a DO
or DONT
byte to the other, followed by an option code. The other party will then respond with a WILL
or WONT
byte followed by the option code telling the requester if it will or will not support that option. All of these communications begin with an IAC
byte, short for Interpret As Command. A request for byte macro option support, for example, would look something like IAC DO BM
. A response to it could look like IAC WILL BM
. These constants translate to a fixed byte value. For example, IAC translates to 0xFF, or as the telnet rfc calls it, "Data Byte 255." The series of constants IAC DO BM
translated to hex would look like 0xFF 0xFD 0x13
.
Byte Macro is an obscure and likely long forgotten and unsupported telnet option that upon googling, nobody really seems to talk or think about. It allows for one side of the telnet connection to tell the other that instead of sending a certain series of bytes, to only send a single byte instead that it will replace with that series of bytes before processing it. In common terms, the server could tell the client, "Hey, instead of sending me the string 'nslookup', just send me a single 0x96 instead and I'll know what you mean." The rfc from 1977 states this option is intended to serve as a simple data compression technique.
A subnegotiation for an actual byte macro definition is in the form of IAC SB BM [DEFINE] [macro byte] [count] [replacement string] IAC SE
where SB and SE define the start and end of a subnegotation respectfully, [DEFINE] is a constant 1, [macro byte] is the byte that the sender wants to receive instead of [replacement string] and will take to mean it instead, and [count] is the number of bytes of [replacement string]. If the recipient of this subnegotiation supports the byte macro option, it will send back a subnegotation detailing that it is accepting the definition of the macro byte. The recipient can also refuse definitions for various reasons. The below image shows an example of a definition being made of macro byte 0x89 being used to represent the replacement string of "rc".
When playing with implementing my own telnet server in nodejs I tried to see if the default telnet client on my debian box, Linux Netkit 0.17 telnet, would respond positively to a request to support the byte macro option and was disappointed when it wouldn't. Upon looking at a few more clients, none of them were willing to support it out of the box. I don't know if there is a client out there that supports the option or not and instead of researching that further I decided to jump the gun and implement my own wrapper for the client I already had on my machine to support a subset of the byte macro option.
The wrapper I wrote for the telnet client involves a few logical parts:
The custom telnet server implementation I wrote is very naive and functions with a few rules:
IAC DO BM
followed by a list of BM definition subnegotiations based on a hardcoded list of desired byte macros to createThe actual source code of these components can be found here. A segment of the resulting communication between these components setting a byte macro and then using it can be seen as follows.
The first message is from the server to the client, telling it to support byte macros and then listing a subnegotiation for a byte macro where instead of sending "ls" the client will send 0x89.
The second message is from the client to the server, telling it that it will support byte macros and then accepting the byte macro for 0x89.
The third message is from the client to the server, telling it "\x89 -a\r\n", or, the byte macro translated version of "ls -a\r\n".
The fourth message is from the server to the client and includes the response to the server running ls -a
on its /bin/sh subprocess.
It is clear that the client and server both recognize the byte macro to translate 0x89 as a macro for ls and the command is run successfully.
Byte Macros, while they may not be useful in any realistic situation or even supported, are an example and reminder that even protocols as old and simple as telnet can support strange things, and that verifying a communication for a protocol is "valid" doesn't mean it isn't potentially using some options nobody has ever heard of under the hood that may allow for unforeseen consequences. It is very interesting that writing a wrapper for a telnet client to do byte macro expansion is valid telnet protocol and that there's even a reserved option for it. Learning and understanding the full feature sets of applications and protocols is a surprising and sometimes scary experience that can reveal just how many strange oddities there are covered in dust that could very well break some system later down the road because they are so rarely thought of.