[vworld-tech] Telnet negotiation
Jon Leonard
jleonard at slimy.com
Sat Jan 24 21:59:35 PST 2004
On Sat, Jan 24, 2004 at 11:20:42PM -0500, Brian Hook wrote:
[snip: telnet RFC non-compliance, and how to cope]
> I'm trying to suppress local echo for password entry. If I transmit:
>
> IAC,WILL,ECHO
>
> This suppresses local echo with MS Telnet and Cygwin Telnet (I'm
> assuming the latter is effectively the same as Linux telnet?).
> Presumably this happens because I'm telling the client, "Hey, I'm
> going to echo...so stop".
>From what I've seen, telnet-like clients fall into three categories:
1) Real, compliant telnets.
2) Line-at-a-time clients that don't understand telnet protocols.
3) MS telnet. (There's at least one other with the same behavior, actually.)
For the first class, you bascially send the IAC,WILL,ECHO and IAC_WONT,ECHO
sequences to turn off and on local echo. There's a bit of a complication
involving acks that I'll get to later.
For the second class (most older MUD clients), it doesn't matter what telnet
options you send, the client will do local-echo anyway. So you either
detect early on that it's not a real telnet client and don't bother, or you
try once, and the (lack of) response tells you to not try again. In fact,
the RFCs pretty much dictate that you can't try again until you've gotten
a response.
For MS's telnet, my experience was that it always assumed character mode
and remote-echo. It's not that IAC WILL ECHO turns off echo, but rather
that the default is wrong, and there's no way to change it. However, you
can use that behavior to identify it: Only a client broken like MS telnet
will even send a 1-byte packet as its first packet.
The code I use for this detection:
/* Work around $#%^*! MS "telnet" (and similar) */
if (connections[data]->lecho == 1)
{
/* TODO: check result, etc */
write(fd,buffer,length);
} else if (connections[data]->lecho == 2) {
if (length == 1)
{
connections[data]->lecho = 1;
errmsg = "Not a telnet client";
/* Out of band notification */
listeners[connections[data]->listenindex]->messa
ge(connections[data]->mid,errmsg,strlen(errmsg),1);
}
else
connections[data]->lecho = 0;
}
I use lecho of 2 as a "don't know yet" flag, and set it to 1 when I detect
a client that needs server-side echo, and set it to 0 when I detect a client
that doesn't. And when it is set to 1, I write the bytes back as echo.
[more snippage]
> But once I echo is disabled in MS Telnet, I can't figure out if
> there's any way to re-enable it. That's a client I'd really like to
> be marginally compatible with.
>
> The suppress-local-echo option (45?) seems very poorly supported, so
> I've given up on that as an alternative.
>
> Anyone have any wisdom about telnet compatibility for MUD servers?
It's much as you have said: RFC compliance is not very good, and the
strategy you described is (with minor modification), about all you can do.
The other thing I'd like to add is the strategy I wound up using for
compliance with the RFC's rules on when to send requests. Essentially, if
you ask for something, you shouldn't ask again until something has changed,
and definitely shouldn't ask for something when the other side hasn't sent
a response. So in the case of a class 2 client above, the user will see
the echo-off code (once), and will never be sent another request.
Fortunately that's pretty much what the user would want.
I wound up using six states in the state machine, corresponding to the
two choices of {on,off} crossed with {normal,request_sent,changed_request}.
/* Telnet negotiation stuff
* We only care about echo, but have to handle all telnet options.
*
* turn on echo: { IAC, WONT, TELOPT_ECHO, 0 } We expect DONT ECHO response.
* turn off echo: { IAC, WILL, TELOPT_ECHO, 0 } We expect DO ECHO response.
* Only do it if it's real, and also we're not closing it.
* echo state machine:
* on = WONT off = WILL got_on = DONT got_off = DO
* OFF "on" ON_SNT - "on" ON -
* ON - "off" OFF_SNT - "on" ON
* OFF_SNT OFF_REV - ON OFF
* ON_SNT - ON_REV ON OFF
* OFF_REV - OFF_SNT ON "on" ON_SNT
* ON_REV ON_SNT - "off" OFF_SNT OFF
*
* DONT ECHO in ECHO_OFF: send echo_on -> ECHO_ON: they insisted ...
* for RFC compliance rather than a belief that it will happen or matter.
* DONT ECHO in ECHO_ON:
* we're already not doing that
* DONT ECHO in ECHO_OFF_SNT: -> ECHO_ON
* they refused to turn off local echoing
* DONT ECHO in ECHO_ON_SNT: -> ECHO_ON
* they're acknowleging our request. Yay!
* DONT ECHO in ECHO_OFF_REVERSED: -> ECHO_ON
* they refused to turn off local echoing, and
* we've changed our mind since then anyway
* DONT ECHO in ECHO_ON_REVERSED: send echo_off, -> ECHO_OFF_SNT
* thanks, but we've changed our mind
*
* DO ECHO in ECHO_OFF:
* we're already doing that
* DO ECHO in ECHO_ON: send echo_on
* they asked ... we'll decline.
* DO ECHO in ECHO_OFF_SNT: -> ECHO_OFF
* they're acknowleging our request. Yay!
* DO ECHO in ECHO_ON_SNT: -> ECHO_OFF
* they refused to turn on local echoing. Bad them.
* DO ECHO in ECHO_OFF_REVERSED: send echo_on -> ECHO_ON_SNT
* thanks, but we've changed our mind
* DO ECHO in ECHO_ON_REVERSED: -> ECHO_OFF
* they refused to turn on local echoing (bad),
* but we've changed our mind since then anyway
*
* DONT (x): RFC says Ignore it.
* They probably shouldn't send it anyway...
* DO (x): decline with IAC WONT (x)
* We don't know how to do that.
* WONT (x): do nothing.
* I don't know what you're refusing, so we'll ignore you.
* (you rfc ignorer!)
* WILL (x): ask them not to with IAC DONT (x)
* I don't know what you're talking about, so we'll ask you not to.
*/
The last bit covers negotiations for options other than ECHO. The options
are all nicely structured so that a conforming client has a simple, legal
response to anything that it doesn't know how to handle (and the defaults
all work, too). That way old and new clients can interoperate nicely.
Jon Leonard
More information about the vworld-tech
mailing list