本文正在参与 “网络协议必知必会”征文活动
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