📚 第八章 应用协议
📄 一句话概括

TELNET 做的事情只有一件:把键盘上敲的字符原样塞进 TCP 连接。 它加了一层 3 字节的协商序列(IAC),而且全程明文。

📝 本节和前面几节的关系

TELNET 在 2026 年已经基本绝迹,但它是理解「远程终端」的原型。 这一节会亲手往 socket 里逐字符发数据,看「裸 TCP」到底长什么样。

一、它只做「搬运」这一件事

TELNET 的设计目标极其朴素:让你在远端机器上看到和本地一样的终端。 所以它要解决两件事:一是把你敲的键送过去,二是把对面的输出送回来。 除此之外它什么都不管。 早期的 TELNET 是字符模式:敲一个键就发一个字节,不攒成一行再发。 因为要「远端回显」——你敲的每个字符都由服务器回送一份,本地只负责显示,不负责保存。 这套模型就是今天 SSH 交互式会话的原型。 这个设计有一个隐藏的好处:所有状态都在服务器上。 你断线重连,服务器那边的 shell 还在跑,接着往下看就行。 这正是「远程登录」和「远程执行」的区别。

二、IAC:把自己也塞进同一条管子

TELNET 有一个第二层协议,叫选项协商。 为了让两端能对齐终端类型、窗口大小、行尾格式,它定义了以 IAC(0xFF)开头的三字节序列。 注意这套协商是「带内」的:它和普通文字挤在同一个字节流里。 所以想在 TELNET 里传一个真正的 0xFF,必须写两遍。 这个「转义」的坑,后来的 SSH 直接绕开了——它用二进制协议,一切按长度字段切分。 带内信令是 1970 年代的主流做法,因为它省一个连接、省一层复用。 但代价是「数据通道要留出保留字」——这和 SMTP 里「正文中单独一行的点要写两遍」是同一个毛病。

三、致命问题:密码在链路上

TELNET 没有任何加密,所以「抓包就能看见密码」。 而且不只是密码——你敲了几个退格、先删了哪个字,全都能还原。 这一点在局域网里也许还能忍,但只要路径上经过任何一个可被监听的节点,就等于把密码交出去。 所以在 1990 年代末,TELNET 被 SSH 迅速取代—— 不是因为功能不够,是因为「不安全的默认值」不可接受。 有意思的是「加密」并没有让协议变复杂多少:SSH 的交互模型和 TELNET 几乎一样, 只是在外面套了一层加密和认证。 所以「安全」常常不是重新设计,是「照着原设计加一层」。

四、动手 Demo:TELNET 就是「明文裸 TCP」,我亲自发一遍

完全不用 HTTP 客户端,自己往 socket 里逐字符写字符。 再对比一下「同一段密码,明文和加密后在链路上长什么样」。

import socket, hashlib

# 逐字符发送,不用任何应用层库
s = socket.create_connection(('127.0.0.1', port), timeout=5)
text = 'GET /a HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n'
for ch in text:
    s.sendall(ch.encode('latin-1'))
buf = b''
while True:
    c = s.recv(65536)
    if not c:
        break
    buf += c

# 演示用的流密码:密钥流 = SHA-256(密钥 || 计数器)
pw = 'P@ssw0rd-2026'
key = b'demo-session-key'
ks = b''
for ctr in range(4):
    ks += hashlib.sha256(key + bytes([ctr])).digest()
ct = bytes(b ^ k for b, k in zip(pw.encode(), ks))
print('明文', pw)
print('密文', ct.hex())

实跑输出:

--------------------------------------------------------------------
TELNET 就是「明文裸 TCP」,我亲自发一遍
--------------------------------------------------------------------
**TELNET 做的事情只有一件**:**把你在键盘上敲的字符,原样塞进一个 TCP 连接。**
**早期的 TELNET 是字符模式:敲一个键就发一个字节,不攒成一行再发。**
**这套「远端回显」的模型,就是今天 SSH 交互式会话的原型。**

**逐字符发送 47 次,收到 193 字节。**
**这就是 TELNET 的全部:一个字节一个字节地搬运。**

**这就是它的致命问题**:**登录时敲的密码,在链路上是这样走的。**
**下面把同一段密码的「TELNET 版」和「加密后」并排给你看。**

  写法            链路上看到的内容                长度
  --------------------------------------------------------
  TELNET 明文     P@ssw0rd-2026                   13 字节
  加密之后        b9cfa3ae03b08786b4f4fcd05f…     13 字节

**第一行是「抓包就能看见密码」**——**不只密码,连你敲了几个退格都能看见。**
**第二行是用一个演示用的流密码算的**(密钥流取 SHA-256,**不是 SSH 真用的算法**),
**只为说明「链路上是一堆看不出意义的字节」。**
**密码长度没变,内容完全不可读。**
**这就是 TELNET 在 2026 年基本绝迹的唯一原因。**

**它还有第二层协议**,叫选项协商:**为了让两端能跑在不同的终端类型上**,
**TELNET 定义了以 IAC(0xFF)开头的三字节序列**:

  序列              字节          含义
  ---------------------------------------------
  IAC WILL 选项     255 251 n     我打算启用 n
  IAC DO 选项       255 253 n     请你启用 n
  IAC WONT 选项     255 252 n     我不启用 n
  IAC DONT 选项     255 254 n     你不许启用 n

**注意每个都是 3 字节**:**一个命令、一个动作、一个选项号。**
**这套协商是「带内」的**——**它和普通文字挤在同一个字节流里。**
**所以想在 TELNET 里传一个真正的 0xFF 字节,得写两遍。**
**这个「转义」的坑,后来的 SSH 直接绕开了:它用二进制协议,一切按长度字段切分。**

逐字符发送 47 次,收到 193 字节。 没有行缓冲、没有帧头,就是「一个字节一个字节地搬」。 这就是 TELNET 的全部。 第二行密文是用一个演示用的流密码算的——密钥流取 SHA-256,不是 SSH 真用的算法。 它只想说明一件事:加密之后,密码长度没变,但内容完全不可读。 真实的 SSH 用 chacha20 或 AES,机制同类。 注意「长度没变」这一点:流密码是逐字节异或,所以密文和明文一样长。 分组密码(AES)要填充到 16 的倍数,长度会变。 这两个流派在「报文开销」上的差别,就是从这里来的。

30 秒实验

  1. 量一下:逐字符发和一次性 sendall 差多少?
  2. 算一下:IAC 三字节协商,一次完整的选项交换要多少字节?
  3. 想一下:为什么带内信令必须留转义字符?

常见误区

  1. 以为「TELNET 只是个老工具」。 它定义了远程终端的交互模型,SSH 沿用至今。
  2. 以为「加密会让报文变长很多」。 流密码不改变长度,分组密码才要填充。
  3. 以为「明文只是不安全」。 它连「敲了几个退格」都泄露,信息量比密码本身还大。
自测三问(点开对答案) 1. TELNET 为什么是字符模式? —— **因为要远端回显**:每个字符由服务器回送一份,本地只显示。 2. IAC 序列为什么是 3 字节? —— **一个命令、一个动作、一个选项号。** 3. TELNET 为什么被 SSH 取代? —— **全程明文,抓包就能看见密码。**

下一节看 SSH 具体补了哪几个洞。

相关笔记