Anyone know how to get the telnet crate to work?

I can telnet from my MacBook to. an Ubuntu machine like so:

āžœ  telnet 192.168.0.107
Trying 192.168.0.107...
Connected to 192.168.0.107.
Escape character is '^]'.
nx login: user
Password: 
Last login: Mon Jan 10 08:26:21 EET 2022 from 192.168.0.104 on pts/4
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.9.201-tegra aarch64)
...
user@konalantie-nx:~$ 

So I try and use the telnet crate to make a telnet connection starting with the example codes here: https://crates.io/crates/telnet

Well, it connects without error and I get events coming back but I never see any data event come back. I'm expecting a login prompt or some such.

So I start pulling apart the received events and end up with this:

fn main() {
    let address: String = "192.168.0.107".to_string();
    let port: u16 = 23;

    let address = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::from_str(&address).expect("Invalid address")),
        port,
    );

    println!("Openning telnet connection to: {}", address);

    let mut telnet = Telnet::connect(address, 256).expect("Couldn't connect to the server...");

    loop {
        println!("Writing to telnet connection");
        let buffer = "ls\n".as_bytes();
        telnet.write(&buffer).expect("Write Error");

        println!("Waiting for data on telnet connection");
        let event = telnet
            .read_timeout(Duration::new(5, 0))
            .expect("Read Error");

        println!("Got telnet event: {:?}", event);
        match event {
            Event::Data(data) => {
                println!("1 ***: {:?}", data);
            }
            Event::UnknownIAC(_iac) => {
                println!("2: ");
            }
            Event::TimedOut => {
                println!("5: ");
            }
            Event::NoData => {
                println!("6: ");
            }
            Event::Error(_telnet_error) => {
                println!("7: ");
            }
            Event::Negotiation(Action::Will, TelnetOption::Compress2) => {
                telnet
                    .negotiate(&Action::Do, TelnetOption::Compress2)
                    .expect("Compress2 option failed");
            }
            Event::Subnegotiation(TelnetOption::Compress2, _) => {
                telnet.begin_zlib();
            }
            Event::Negotiation(_action, _telnet_option) => {
                println!("8: ");
            }
            Event::Subnegotiation(_telnet_option, _buffer) => {
                println!("9: ");
            }
        }
    }
}

Which outputs:

Openning telnet connection to: 192.168.0.107:23
Writing to telnet connection
Waiting for data on telnet connection
Negotiation(Do, TTYPE)
Got telnet event: Negotiation(Do, TTYPE)
8: 
Writing to telnet connection
Waiting for data on telnet connection
Negotiation(Do, TSPEED)
Got telnet event: Negotiation(Do, TSPEED)
8: 
Writing to telnet connection
Waiting for data on telnet connection
Negotiation(Do, XDISPLOC)
Got telnet event: Negotiation(Do, XDISPLOC)
8: 
Writing to telnet connection
Waiting for data on telnet connection
Negotiation(Do, NewEnvironment)
Got telnet event: Negotiation(Do, NewEnvironment)
8: 
Writing to telnet connection
Waiting for data on telnet connection
TimedOut
Got telnet event: TimedOut
5: 
Writing to telnet connection
...
...

No sign of any data events, even though I am writing a "ls" command successfully.

Am I missing some telnet negotiation?

Anyone have any ideas?

Never used telnet but it seems like there is a negotiation of options that happens when connecting to a server. See this stack overflow for a way to possibly view what those negotiations look like for your connection. Stack Overflow. Without reading up on the full protocol I would try to mimic the negotiation strategy of the built in client.

I would strongly recommend not using telnet.

For remote access, Telnet has several well-known problems - the most serious being that your credentials (username & password) are sent in plain-text over the network, and are trivial for someone to capture. On a closed network this can technically be safe enough to get away with, but there's very little benefit to using telnet over other protocols in that context, and in any other context, this is a significant disadvantage.

For interacting with systems that don't use credentials, Telnet is probably the wrong protocol. As you've discovered, it includes a substantial protocol state machine for negotiating things like terminal options and X displays, which make sense in the context of remote access to a UNIX-like system, but not in other contexts.

For remote access, in general I'd recommend using ssh. Pretty much every UNIX-like system comes with sshd these days - Ubuntu's is on by default, if I remember right. There's at least one good ssh crate, and there are Rust bindings for libssh2, if you prefer. SSH encrypts traffic, including credential exchanges, and offers authentication modes that don't require sending credentials over the network at all.

For interacting with services that expose a text-based socket, you're generally better off connecting directly to that socket using Rust's socket primitives than trying to use telnet. Telnet is not a trivial protocol, and while you can get away with using telnet for non-telnet services sometimes, you'll run into issues sooner or later.

The direct answer to your question is that you need to reply to the DO option negotiations with an appropriate WILL or WONT reply, and if you reply with a WILL response, also with whatever subsequent connection setup is appropriate. RFC 854 explains further.

1 Like

Ah yes. I figured that out some hours after posting, a lot of googling around and experimenting with responding the various tenet events. Eventually I hit one event from the server that asks about "NAWS". Which turns out to be a request to negotiate terminal window dimensions. Also turns out I have to reply to that, either accepting or reacting the request will do (Will or Wont in Telnet speak) before the server will communicate.

I meant to come back and say all this but got distracted by conference calls. Anyway here is how my experimental code loos now, which does get me a log in prompt, accept user name and password, then execute "ls":

use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::str::FromStr;
use std::time::Duration;
use telnet::{Action, Event, Telnet, TelnetOption};
//use telnet::{Event, Telnet};

enum State {
    Idle,
    User,
    Pass,
    Ls,
}

fn main() {
    let address: String = "192.168.0.107".to_string();
    let port: u16 = 23;
    let mut state = State::User;

    let address = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::from_str(&address).expect("Invalid address")),
        port,
    );

    println!("Openning telnet connection to: {}", address);

    let mut telnet = Telnet::connect(address, 256).expect("Couldn't connect to the server...");

    loop {
        // Write someting to try and stimulate a response
        /*
                println!("Writing to telnet connection");
                let buffer = "ls\n".as_bytes();
                telnet.write(&buffer).expect("Write Error");
        */
        println!("Waiting for data on telnet connection");
        //        let event = telnet.read().expect("Read error");

        let event = telnet
            .read_timeout(Duration::new(1, 0))
            .expect("Read Error");

        println!("Got telnet event: {:?}", event);
        match event {
            Event::Data(data) => {
                println!(
                    "1 ********************: {:?}",
                    String::from_utf8_lossy(&data)
                );
                println!("Writing to telnet connection");

                match state {
                    State::User => {
                        let buffer = "user_here\n".as_bytes();
                        telnet.write(&buffer).expect("Write Error");
                        state = State::Pass;
                    }
                    State::Pass => {
                        let buffer = "password_here\n".as_bytes();
                        telnet.write(&buffer).expect("Write Error");
                        state = State::Ls;
                    }
                    State::Ls => {
                        let buffer = "ls\n".as_bytes();
                        telnet.write(&buffer).expect("Write Error");
                        state = State::Idle;
                    }
                    State::Idle => {}
                }
            }
            Event::UnknownIAC(_iac) => {
                println!("2: ");
            }
            Event::TimedOut => {
                println!("5: ");
            }
            Event::NoData => {
                println!("6: ");
            }
            Event::Error(_telnet_error) => {
                println!("7: ");
            }

            // REPLY TO THE NEGOTIATION EVENTS WE ACTUALLY DO GET!
            Event::Negotiation(Action::Do, TelnetOption::TTYPE) => {
                println!("EVENT: DO TTYPE");
                telnet
                    .negotiate(&Action::Wont, TelnetOption::TTYPE)
                    .expect("TTYPE option failed");
            }
            Event::Negotiation(Action::Do, TelnetOption::TSPEED) => {
                println!("EVENT: DO TSPEED");
                telnet
                    .negotiate(&Action::Wont, TelnetOption::TSPEED)
                    .expect("TSPEED option failed");
            }
            Event::Negotiation(Action::Do, TelnetOption::XDISPLOC) => {
                println!("EVENT: DO XDISPLOC");
                telnet
                    .negotiate(&Action::Wont, TelnetOption::XDISPLOC)
                    .expect("XDISPLOC option failed");
            }
            Event::Negotiation(Action::Do, TelnetOption::NewEnvironment) => {
                println!("EVENT: DO NewEnvironment");
                telnet
                    .negotiate(&Action::Wont, TelnetOption::NewEnvironment)
                    .expect("NewEnvironment option failed");
            }
            Event::Negotiation(Action::Will, TelnetOption::SuppressGoAhead) => {
                println!("EVENT: WILL SuppressGoAhead");
                telnet
                    .negotiate(&Action::Dont, TelnetOption::SuppressGoAhead)
                    .expect("SuppressGoAhead option failed");
            }
            Event::Negotiation(Action::Do, TelnetOption::Echo) => {
                println!("EVENT: DO Echo");
                telnet
                    .negotiate(&Action::Wont, TelnetOption::Echo)
                    .expect("Echo option failed");
            }
            // It seems to be essential to reply to Negotiate About Windows Size when connecting to telnetd on Ubuntu.
            // "Will" and "wont" both work.
            Event::Negotiation(Action::Do, TelnetOption::NAWS) => {
                println!("EVENT: DO NAWS");
                telnet
                    .negotiate(&Action::Wont, TelnetOption::NAWS)
                    .expect("NAWS option failed");
            }
            Event::Negotiation(Action::Will, TelnetOption::Status) => {
                println!("EVENT: WILL Status");
                telnet
                    .negotiate(&Action::Dont, TelnetOption::Status)
                    .expect("Status option failed");
            }
            Event::Negotiation(Action::Do, TelnetOption::LFLOW) => {
                println!("EVENT: DO LFLOW");
                telnet
                    .negotiate(&Action::Will, TelnetOption::LFLOW)
                    .expect("LFLOW option failed");
            }
            // Compression events
            Event::Negotiation(Action::Will, TelnetOption::Compress2) => {
                telnet
                    .negotiate(&Action::Do, TelnetOption::Compress2)
                    .expect("Compress2 option failed");
            }
            Event::Subnegotiation(TelnetOption::Compress2, _) => {
                telnet.begin_zlib();
            }
            Event::Negotiation(_action, _telnet_option) => {
                println!("8: ");
            }
            Event::Subnegotiation(_telnet_option, _buffer) => {
                println!("9: ");
            }
        }
    }
}

It needs some massaging to get into a usable condition but at least I have characters going up and down the line.

Thanks for your concern. I agree. Normally I would never dream of using telnet.

However, the problem I face today is communicating with some no so new equipment that only has a telnet interface. Luckily said equipment is always locked in a metal cabinet and not connected to the internet. The machine I am connecting from is on the end of a foot of ethernet cable in the same cabinet. That machine does communicate over the net, it's down to me to be sure there is sufficient isolation in there.

Newer models of said equipment do indeed use ssh. But there is a lot of old stuff still in use.

I didn't read the RFC, and would rather not know anymore about telnet. Got it working by guess work, trial and error. As long as it works in this particular case we are happy.

2 Likes

Extremely valid, and I was afraid that might be the explanation. Nonetheless, I felt it needed to be said, and I'm glad you're of like mind.

The telnet RFC is relatively readable, and the state machine for clients isn't too complex. Responding with WONT to every single option will work with many servers - you simply won't get features like terminal type negotiation, falling back to lowest common denominators.

I will note that telnet uses \r\n line endings, even when dealing with UNIX systems which have \n line endings locally. Most implementations are permissive about this, for the obvious reason, but you might run into weirdness if you send bare NLs.

1 Like

Ah ha. Thanks. I was just turning my mind to those line endings.