TELNET 协议

1,960 阅读3分钟

本文正在参与 “网络协议必知必会”征文活动

TELNET协议一种简单的基于文本的协议,它可以用来实现远程终端,让用户可以远程对服务器进行操作。尽管现在的远程终端基本上是基于 ssh 的了,但它的思想还是值得我们进行学习。

TELNET协议的架构一种 C/S 架构,一般 TELNET 服务器会运行在 23 端口,不过有个小问题,由于它基本上是明文传输(这也是TELNET淘汰的原因),所以在如今的情况下,直接安装的TELNET服务一般是在本地运行的,不对外开放,这里也不建议直接对外开放。由于我们是出于学习的目的,所以可以用一个稍微折中的办法,通过 ssh 将它的端口转发到本地,这样中间的连接也是加密过的,即便稍后的我们将实现的TELNET客户端,真的是连接远程机器也可以比较的安全进行实验。

回到TELNET协议,它为我们定义了与远程机器交互的方式,我们先这样考虑,假设我们从最朴素的想法出发,怎么做一个远程终端呢?一般会想到的就是,我给一条指令你就给我执行。其实实际上也差不多,稍微可能有些不太一样的是请求什么时候发送。

还有一个问题是主机和主机之间是可能是有差异的,比如文件尾部的按键,在windows上是 Ctrl+Z 而在 linux 上是 Ctrl+D ,还有一些其它的小细节差异,这让它们可能有不一致的行为。TELNET 定义了一个名为网络虚拟终端的东西,在这个终端中定义了一些具体的指令,从而让行为不会有歧义。

我们尝试将实现一个简易的 TELNET 客户端,首先需要建立一个TCP连接,这里使用先前提到的端口转发到本地的端口。

auto address = net::makeAddress("127.0.0.1", 23);

类似前文,我们可以把TCP也封装一个工具函数,用来建立连接。

SOCKET dialTCP(sockaddr_in *address) {
    init(); // windows 平台的初始化函数
    auto client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (client < 0) {
        printf("failure create socket %d", client);
        exit(-3);
    }
    auto r = connect(client, (sockaddr *) address, sizeof(sockaddr_in));
    if (r != 0) {
        printf("Failure on connecting %d", r);
        exit(-3);
    }
    return client;
}

这样我们可以像如下方式,简单的建立一个TCP连接。

auto client = net::dialTCP(&address);

接下来考虑对连接的处理,TELNET还有一个很重要的机制,就是它的协商机制,但是这其实会涉及到比较多的与终端相关的内容,倒是和协议本身反而远了些,所以这里我们就简单的处理下,具体内容可以参考 RFC的相关文档,不太好展开介绍。只简单介绍下,TELNET中有两类数据,一类是普通数据,一类是控制数据。控制数据是比特位以1起始的数据。其中有一个特别的符号IAC,用来表示控制,随后会有特别的控制字符,其中又有一些是用来协商的。

为了简单些考虑,这里也不过多深入。 所以对于具体的接受到的数据,我们这样处理。实际上就是对于服务器进行明确的拒绝,不同意任何协商。 这样的问题是,终端的功能会被限制,但是足够用于了解TELNET协议了。

void handle_data(SOCKET client, const array<char, 1024> &data, int data_length) {
    array<char, 1024> output{};
    int outLength = 0;

    for (int i = 0; i < data_length; i++) {
        if (data[i] == IAC) {
            output[outLength++] = IAC;
            i++;
            switch (data[i]) {
                case WILL: //251
                    output[outLength++] = DONT;
                    i++;
                    output[outLength++] = data[i];
                    break;
                case WONT: //252
                    break;
                case DO://253
                    output[outLength++] = WONT;
                    i++;
                    output[outLength++] = data[i];
                    break;
                case DONT://254
                    break;
                default:
                    break;
            }
        } else {
            printf("%c", data[i]);
        }
    }
    auto r = send(client, output.data(), outLength, 0);
    if (r == -1) {
        printf("failure %d \n", r);
        exit(-3);
    }

}

再回到具体的 socket 处理部分,我们使用线程来同时进行发送和接受数据。所以两个部分的处理逻辑如下:

[[noreturn]] void handle_receive(SOCKET client) {
    array<char, 1024> buf{};
    while (true) {
        auto length = recv(client, buf.data(), buf.max_size(), 0);
        handle_data(client, buf, length);
    }
}

[[noreturn]] void handle_input(SOCKET client) {
    string s;
    while (true) {
        getline(cin, s);
        s.append("\r\n");
        send(client, s.data(), (int) s.length(), 0);
        s.clear();
    }
}

然后启动两个线程,分别做这两个处理。

thread t1{handle_receive, client};
thread t2{handle_input, client};
t2.detach();
t1.join();

这样一个基本的telnet客户端已经可以使用了,不过可以发现的是其实我们并没有进行网络虚拟终端到本机的转换,它被我们刻意的忽略了。但是TELNET的基本处理流程已经展示出来了。剩下要实现更完善的部分,可能就需要些小修小改了。

参考:RFC 854,RFC 855