Java实现的Telnet协议 - 计算机网络课设

本文最后更新于:2020年7月7日 晚上

概览:Java实现的Telnet协议,Telnet相关知识,操作协商细节,代码实现。

课设题目

设计题目:利用JAVA实现TELNET协议

设计要求:TELNET协议允许用户用一台终端来访问远程的主机 ,它允许终端于主机之间以半双工的方式交换信息,可参阅RFC864[6-13]。本次设计要求利用JAVA实现TELNET协议的基本功能 。

Telnet相关知识

Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议。Telnet协议的目的是提供一个相对通用的,双向的通信方法,允许界面终端设备和面向终端的过程能通过一个标准过程进行互相交互。应用Telnet协议能够把本地用户所使用的计算机变成远程主机系统的一个终端。

Telnet协议具有如下的特点:Telnet协议可以适应于多个不同的操作系统,在Windows电脑上连接的Linux系统的机器可以传输命令,Telnet协议可以传输特定的控制命令,例如中止进程的命令等,并且由于Telnei两端的机器与操作系统之间的异构型,Telnet不能严格规定每一个Telnet连接的详细配置,故Telnet协议支持双方进行协商。

Telnet协议主要由三部分组成:

(1)网络虚拟终端 NVT;

(2)操作协商定义;

(3)协商有限自动机。

网络虚拟终端 NVT

网络虚拟终端(NVT)是一种虚拟的终端设备,它被客户和服务器所采用,用来建立数据表示和解释的一致性。Telne使用网络虚拟终端字符集来处理异构系统的远程登录问题。网络虚拟终端字符集是一个通用接口, 在远程登陆连接上,客户软件将终端用户输入转化为标准的NVT数据和命令序列,经TCP连接传到远地机上的服务器,服务器再将NVT序列转换为原地系统的内部格式。这样终端键盘输入的异质性就被NVT所屏蔽,NVT这里实现了统一,只需要与NVT打交道即可。

Telnet的操作协商

TELNET协议允许通信机器协商会话过程中所使用的各种选项,通过一组标准过程来建立这些选项。协商选项的使用考虑了主计算机可提供超出虚拟终端服务范围的服务的可能性。例如窗口的大小、终端类型、字符回显等信息都要进行协商。

协商主要是通过Telnet指令来进行的,Telnet指令格式是:

IAC 命令码 选项码
255

常见的命令码:

名称 码字(byte) 描述
EOF 236 文件结束符
SUSP 237 挂起当前进程(作业控制)
ABORT 238 异常终止进程
EOR 239 记录结束符i
NOP 241 无操作
WILL 251 开始执行指示选项或证实设备现已经开始执行指示选项
WONT 252 拒绝执行指示选项或证实设备现已开始执行指示选项。
DO 253 另一方执行的请求,或者证实期望对方执行的请求
DONT 254 另一方停止执行的命令,或者证实一方不再期待另一方执行的命令。

常见选项码:

选项标识(byte) 名称
1 回显
3 抑制继续进行
5 状态
6 定时标记
24 终端类型
31 窗口大小

选项协商的6种情况

进行协商有4中类型的请求:

(1)WILL:发送方本身想激活某个选项。

(2)DO:发送方想让接受端激活某个选项。

(3)WONT:发送方本身想禁止某个选项。

(4)DONT:发送方想让接受端去禁止某个选项。

发送者 接收者 说明
WILL DO 发送者想激活某选项,接收者接受该选项请求
WILL DONT 发送者想激活某选项,接收者拒接该选项请求
DO WILL 发送者希望接收者激活某选项,接收者接受该请求
DO WONT 发送者希望接收者激活某选项,接收者拒绝该请求
WONT DONT 发送者希望使某选项无效,接收者必须接受该请求
WONT WONT 发送者希望对方使某选项无效,接收者必须接受该请求

Linux开启Telnet

因为本次实验是拿一台虚拟机上的Linux作为服务器来做测试,所以需要开启Linux的Telnet服务。

参考我的博文: Linux-Telnet配置

Telnet的协商流程测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Test {

public static void main(String[] args) throws IOException {
//尝试建立socket连接
Socket telnetSocket = new Socket("192.168.117.129",23);//TCP,注意修改为自己的IP

OutputStream outputStream = telnetSocket.getOutputStream();
InputStream inputStream = telnetSocket.getInputStream();

byte[] bytes = new byte[1024*8];

int len = inputStream.read(bytes);

System.out.println("第一次接收响应,接受到的回应长度为"+len);

for(int j=0;j<len;j++){
int x = byteToInt(bytes[j]);
System.out.println(x);
}

/*
第一次连接服务器,服务器会向客户端发送12个字节
Do-253表示期望对方执行的请求
1. 255 253 24:IAC Do 24表示终端类型
2. 255 253 32:IAC Do 32表示终端速度
3. 255 253 35:IAC Do 35表示X显示定位
4. 255 253 39:IAC Do 39不知道
*/

byte[] first_responce = {(byte)255,(byte)252,(byte)24,
(byte)255,(byte)252,(byte)32,
(byte)255,(byte)252,(byte)35,
(byte)255,(byte)252,(byte)39};

outputStream.write(first_responce);
outputStream.flush();

System.out.println("第一次发送过去了");

/*
第一次回应,拒绝掉服务器的全部请求
Won't - 252表示拒绝执行指示选项
1. 255 252 24 IAC WONT 24 拒绝
2. 255 252 32 IAC WONT 32
3. 255 252 35 IAC WONT 35
4. 255 252 39 IAC WONT 39
*/

len = inputStream.read(bytes);

System.out.println("第二次接收响应,接受到的回应长度为"+len);

for(int j=0;j<len;j++){
int x = byteToInt(bytes[j]);
System.out.println(x);
}

/*
服务器第二次响应,继续向客户端发送15个字节
Will 表示自己请求使用的服务
Do -253表示期望对方执行的请求
1. 255 251 3:IAC Will 3表示抑制向前,
2. 255 253 1:IAC Do 1表示回显,期望客户端进行回显
3. 255 253 31:IAC Do 31表示窗口尺寸
4. 255 251 5:IAC Will 5表示状态
5. 255 253 33:IAC Do 33表示远程流控制 期望客户端进行远程流控制
*/

byte[] second_responce = {(byte)255,(byte)253,(byte)3,
(byte)255,(byte)252,(byte)1,
(byte)255,(byte)252,(byte)31,
(byte)255,(byte)254,(byte)5,
(byte)255,(byte)252,(byte)33};

outputStream.write(second_responce);//发送给服务器
outputStream.flush();

System.out.println("第二次发送过去了");

/*
第二次回应,拒绝掉服务器的全部请求
Won't 表示拒绝执行指示选项
1. 255 253 3 Do 表示期望服务器开启抑制向前的功能
2. 255 252 1 Won't 拒绝客户端回显
3. 255 252 31 拒绝客户端窗口尺寸
4. 255 254 5 Don't 希望服务端禁止状态功能
5. 255 252 33 拒绝执行远程流控制
*/

len = inputStream.read(bytes);

System.out.println("第三次接收响应,接受到的回应长度为"+len);

for(int j=0;j<len;j++){
int x = byteToInt(bytes[j]);
System.out.println(x);
}

/*
服务器第三次响应,继续向客户端发送3个字节
Will 表示自己请求使用的服务
Do 表示期望对方执行的请求
1. 255 251 1:IAC Will 1表示服务器请求回显
*/

byte[] three_responce = {(byte)255,(byte)253,(byte)1};

outputStream.write(three_responce);
outputStream.flush();

System.out.println("第三次发送过去了");

/*
第三次回应,表示赞成服务器使用回显
1. 255 253 1
*/

System.out.println("-----------");

len = inputStream.read(bytes);

System.out.println("第四次接收响应");

String str = new String(bytes,0,len,"UTF-8");
System.out.println(str);

//接收到登陆信息
}

//byte转int
public static int byteToInt(byte b) {
int x = b & 0xff;
return x;
}
}

当程序与虚拟机中的linux建立连接之后,服务器会立刻发送协商信息给程序,接收信息后进行协商即可。具体的协商过程都在代码与注释中。

Telnet协商自动机

协商自动机是我在看别人的博客时发现的名词,就拿来用了。协商自动机的主要任务是,对于服务器发送过来的数据,分辨清楚它是协商消息还就是普通的数据。

如果是协商消息的话,就根据预先设置好的策略,进行回复,如果是数据的话,就展示给用户。

这解析服务器数据的过程就像是读取文件获得固定内容或者编译器读取代码那样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//检测接收到的数据是协商还是数据,进行协商,处理并返回数据
public String negotiate(byte buf[],int lens) throws IOException{

byte front = (byte)0;//用来存储当前正在解析的buf[]中的数据

List<Byte> databuf = new ArrayList<Byte>(); //存储数据
List<Byte> negbuf = new ArrayList<Byte>(); //存储协商命令

int seg_offset = 0; //记录处理的位置

byte neg_state = STATE_DATA;//协商状态,表明解析数据类型

while(seg_offset < lens){

//获取到当前要处理的字节
front = buf[seg_offset];
seg_offset++;

switch (neg_state){
case STATE_DATA://是数据
if(front == IAC){//255
neg_state = STATE_IAC;
negbuf.add(IAC);//加入到待回复的数组中
}
else{
databuf.add(front);
}
break;
case STATE_IAC://是IAC
switch(front){
case WILL:
//IAC后接WILL
neg_state = STATE_IACWILL;
break;
case WONT:
neg_state = STATE_IACWONT;
break;
case DONT:
neg_state = STATE_IACDONT;
break;
case DO:
neg_state = STATE_IACDO;
break;
default://其它情况都是数据类型
//TODO:有可能读出了IAC但是,实际上只是一个单独的字符
neg_state = STATE_DATA;
databuf.add(IAC);
break;
}
break;
case STATE_IACWILL://如果是协商类型 IAC WILL,表示服务器自己想做的服务
switch (front){
//这里如果是功能1 ECHO或者功能3 E3是选择DO,其他都是DONT
case TELOPT_ECHO:
//negbuf[] = TELOPT_ECHO;
//TODO: 记得继续往下
negbuf.add(DO);
negbuf.add(TELOPT_ECHO);
neg_state = STATE_DATA;
break;
case TELOPT_E3:
negbuf.add(DO);
negbuf.add(TELOPT_E3);
neg_state = STATE_DATA;
break;
default:
//其他情况都先拒绝
negbuf.add(DONT);
negbuf.add(front);
neg_state = STATE_DATA;
break;
}
break;
case STATE_IACDO://如果协商类型是IAC DO,表示服务器期待客户端做某事
//对于服务器发送的DO请求,全部使用WONT拒绝
negbuf.add(WONT);
negbuf.add(front);
neg_state = STATE_DATA;
break;
case STATE_IACWONT://如果协商类型是IAC WONT,表示服务器拒绝执行我的请求
//实际上服务器并没有发过这种请求,不做处理
case STATE_IACDONT://如果协商类型是IAC DONT,表示服务器不希望我做某些请求
//服务器也没有发送过这种请求,不处理
neg_state =STATE_DATA;
break;
}

}

//回应服务器的协商
if(!negbuf.isEmpty() && (negbuf.size() % 3 == 0)){
Byte[] bytes = negbuf.toArray(new Byte[negbuf.size()]);//ArrayList转Byte[]数组
byte[] responce = new byte[negbuf.size()];
ByteTobyte(bytes,responce); //转为byte[]
send(responce); //发送数据
return null;
}

//如果没有协商而是数据的话,通过函数返回
if(databuf != null){
Byte[] bytes = databuf.toArray(new Byte[databuf.size()]);//ArrayList转Byte[]数组
byte[] message = new byte[databuf.size()];
ByteTobyte(bytes,message);//转为byte[]
String str = new String(message,0,databuf.size(),"UTF-8");
return str;
}

return null;
}

协商自动机执行的结果是,对于服务器发送的协商,程序解析完成后,根据预先设置的情况,向服务器发送协商结果。如果是服务器发送来的数据,就显示到控制台。

完整代码

TelnetClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class TelnetClient {

private Socket telnetSocket;
private OutputStream outputStream;
private InputStream inputStream;

/*-------------协商状态常数-------------*/
private final static byte STATE_DATA = 0;
private final static byte STATE_IAC = 1;
private final static byte STATE_IACWILL = 2;
private final static byte STATE_IACWONT = 3;
private final static byte STATE_IACDO = 4;
private final static byte STATE_IACDONT = 5;

/*-------------协商过程中用到的数据-------------*/
//IAC - init sequence for telnet negotiation.telnet协商的初始化序列
private final static byte IAC = (byte) 255;

//EF [IAC] WILL 开始执行指示选项或证实设备现已开始执行指示选项
private final static byte WILL = (byte) 251;

//FB [IAC] WON'T 拒绝执行指示选项或拒绝计息执行指示选项
private final static byte WONT = (byte) 252;

//FC [IAC] DO 另一方执行的请求,或者证实期望对方执行的请求,指示选项
private final static byte DO = (byte) 253;

//FD [IAC] DON'T 另一方停止执行的命令,或者证实乙方不再期待另一方执行的命令,指示选项
private final static byte DONT = (byte) 254;

//FO Telnet option: echo text 回显字符 RFC:857
private final static byte TELOPT_ECHO = (byte) 1; /* echo on/off */

//一次传送一个字符方式
private final static byte TELOPT_E3 = (byte) 3;

//与特定的主机建立telnet连接
public void link(String host,int port) throws IOException{
telnetSocket = new Socket(host,port);
outputStream = telnetSocket.getOutputStream();
inputStream = telnetSocket.getInputStream();

byte[] bytes = new byte[1024*8];
int len = 0;

while(true){
len = inputStream.read(bytes);
String str = negotiate(bytes,len);

if(str != null){
// len = inputStream.read(bytes);
// String str1 = negotiate(bytes,len);
// str += str1;
System.out.print(str);
break;
}
}
}

//发送数据给服务器
public void send(byte[] bytes) throws IOException {
outputStream.write(bytes);
outputStream.flush();
}

//从服务器接收数据
public String receive() throws IOException{
byte[] bytes = new byte[1024*8];
int len = inputStream.read(bytes);
if (len < 0) throw new IOException("Connection closed.");
String str = negotiate(bytes,len);
return str;
}

//检测接收到的数据是协商还是数据,进行协商,处理并返回数据
public String negotiate(byte buf[],int lens) throws IOException{

byte front = (byte)0;

List<Byte> databuf = new ArrayList<Byte>(); //存储数据
List<Byte> negbuf = new ArrayList<Byte>(); //存储协商命令

int seg_offset = 0; //记录处理的位置

byte neg_state = STATE_DATA;

while(seg_offset < lens){

//获取到当前要处理的字节
front = buf[seg_offset];
seg_offset++;

switch (neg_state){
case STATE_DATA://是数据
if(front == IAC){//255
neg_state = STATE_IAC;
negbuf.add(IAC);//加入到待回复的数组中
}
else{
databuf.add(front);
}
break;
case STATE_IAC://是IAC
switch(front){
case WILL:
//IAC后接WILL
neg_state = STATE_IACWILL;
break;
case WONT:
neg_state = STATE_IACWONT;
break;
case DONT:
neg_state = STATE_IACDONT;
break;
case DO:
neg_state = STATE_IACDO;
break;
default://其它情况都是数据类型
//TODO:有可能读出了IAC但是,实际上只是一个单独的字符
neg_state = STATE_DATA;
databuf.add(IAC);
break;
}
break;
case STATE_IACWILL://如果是协商类型 IAC WILL,表示服务器自己想做的服务
switch (front){
//这里如果是功能1 ECHO或者功能3 E3是选择DO,其他都是DONT
case TELOPT_ECHO:
//negbuf[] = TELOPT_ECHO;
//TODO: 记得继续往下
negbuf.add(DO);
negbuf.add(TELOPT_ECHO);
neg_state = STATE_DATA;
break;
case TELOPT_E3:
negbuf.add(DO);
negbuf.add(TELOPT_E3);
neg_state = STATE_DATA;
break;
default:
//其他情况都先拒绝
negbuf.add(DONT);
negbuf.add(front);
neg_state = STATE_DATA;
break;
}
break;
case STATE_IACDO://如果协商类型是IAC DO,表示服务器期待客户端做某事
//对于服务器发送的DO请求,全部使用WONT拒绝
negbuf.add(WONT);
negbuf.add(front);
neg_state = STATE_DATA;
break;
case STATE_IACWONT://如果协商类型是IAC WONT,表示服务器拒绝执行我的请求
//实际上服务器并没有发过这种请求,不做处理
case STATE_IACDONT://如果协商类型是IAC DONT,表示服务器不希望我做某些请求
//服务器也没有发送过这种请求,不处理
neg_state =STATE_DATA;
break;
}

}

//回应服务器的协商
if(!negbuf.isEmpty() && (negbuf.size() % 3 == 0)){
Byte[] bytes = negbuf.toArray(new Byte[negbuf.size()]);//ArrayList转Byte[]数组
byte[] responce = new byte[negbuf.size()];
ByteTobyte(bytes,responce); //转为byte[]
send(responce); //发送数据
return null;
}

//如果没有协商而是数据的话,通过函数返回
if(databuf != null){
Byte[] bytes = databuf.toArray(new Byte[databuf.size()]);//ArrayList转Byte[]数组
byte[] message = new byte[databuf.size()];
ByteTobyte(bytes,message);//转为byte[]
String str = new String(message,0,databuf.size(),"UTF-8");
return str;
}

return null;
}

//byte转int
public int byteToInt(byte b) {
int x = b & 0xff;
return x;
}

//Byte[] 转byte[]
public void ByteTobyte(Byte[] bytes,byte[] byts){
for(int i=0;i<bytes.length;i++){
byts[i] = bytes[i];
}
}

//延迟等待时间
public void sleep(int second){
try {
Thread.sleep(second*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//减去接收到的服务器的数据中与输入命令重复的部分
public String subStr(String orgstr,String objstr){
//orgstr: 子串 , objstr:目标处理串
if(orgstr == null){
return objstr;//如果原串为空,说明是第一次执行,直接返回数据
}

if(objstr.indexOf(orgstr.substring(0,orgstr.length()-1)) == 0){//如果子串位于目标串的开头位置再进行处理
return objstr.substring(orgstr.length()+1);
}
//发送的数据为 msfadmin\n
//接收的数据为 msfadmin\r\n
//所以检索时orgstr长度减1,return时orgstr长度加1
return objstr;//其他情况也返回原串
}
}

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.IOException;
import java.util.Scanner;

public class Main {

public static void main(String[] args) throws IOException {

Scanner sc = new Scanner(System.in);

//ip地址
String remotehost = "192.168.117.129";

String inputStr = null;
String showStr = null;

//建立连接,并进行协商
TelnetClient tc = new TelnetClient();
tc.link(remotehost,23);

//协商完成,开始和用户交互
while(true){
//获取用户输入
inputStr = sc.nextLine();
inputStr += "\n";

//发送用户的数据
tc.send(inputStr.getBytes());
tc.sleep(2);

//接收服务器的数据
showStr = tc.receive();

//字符串处理
showStr = tc.subStr(inputStr,showStr);
System.out.print(showStr);
}
}
}

结果展示

不足

在资料中看到了NVT的说明,但是至今没搞懂NVT在哪里实现,我在Windows端下发送的命令加上\n,就可以被执行,加上\r\n就不会被执行,但是NVT不是统一使用的\r\n作为换行符吗?

关于协商自动机,因为大多数Telnet协议的参数说明都很模糊,大部分网上查不到,并且RFC文档我看不太明白,所以那个协商函数是根据我在本地测试的情况编写的,好多问题根本没有考虑到。所以向这些基本操作可以使用,但是像是vi这种命令去编辑文本就不能用了。

答辩的时候,老师询问了如果服务器的telnet进程突然出现了问题,一直给你发送消息你该怎么办,等好多特殊情况,我确实不知道该如何解决.

参考链接与资料分享

https://tools.ietf.org/html/rfc854

中文RFC文档阅读 701-1000

Telnet协议详解

Telnet协议的java实现

《基于Windows的TCPIP编程》王罡 清华大学出版社:https://pan.baidu.com/s/1jvDF1zBPmD0e4YyBQWebkw 提取码:y8dg