把 DNS 服务变为一个聊天室

阮一峰周刊 369 期 中这样一篇文章 破解加拿大航空的飞机上网 当时看了之后觉得很有意思,但没有额外作什么思考。过了两周的某一天早上在上班路上突然想到了这个文章,因为是上班路上有点无聊,所以就思考了一下这个问题:如果在某一网络中,只有 DNS 协议的请求能成功,那么在这个网络中的人们能否自由彼此通信?

先说结论,肯定是可以的,只需要单建一个特殊的 DNS 服务器就可以了。

核心思想:把文本消息伪装成 DNS 查询,放到 QNAME 中发给服务器,服务器收到特殊的 DNS 查询,将结果按照 TXT 查询格式返回内容。

上行数据包 (消息)

<group>.<sid>.u.<seq>.<nonce>.<payload1>.<payload2>....[.<TLD>].
^------^-----^-^-----^-------^--------------------------^------^
| | | | | | |
| | | | | | |
| | | | | | └── 可选域 (权威模式)
| | | | | |
| | | | | └── 密文分片(Base64,≤30/段)
| | | | |
| | | | └── 随机数 (Base32)
| | | |
| | | └── 分片号 (从0递增)
| | |
| | └── 数据包方向 (u = 上行)
| |
| └── 会话 ID (Base32)
|
└── 组 ID (Base32)

上行数据包 (轮询)

<group>.<sid>.p.0.<nonce>[.<TLD>].
^------^-----^-^-^--------^------^
| | | | | |
| | | | | └── 可选域 (权威模式)
| | | | |
| | | | └── 随机数 (Base32)
| | | |
| | | └── 固定 0 (简单轮询)
| | |
| | └── 数据包方向 (p = 下行)
| |
| └── 会话 ID (Base32)
|
└── 组 ID (Base32)

服务端决策

服务端收到DNS查询

├─ 查询类型TXT? ──否──> TXT "ok"
│ │
│ |是
│ │
├─ 域名格式正确? ──否──> TXT "ok"
│ │
│ |是
│ │
├─ 解析域名标签
│ ├─ 提取: group, sid, dir, seq, nonce, payload
│ │
│ └─ Base32解码成功? ──否──> TXT "ok"
│ │
│ |是
│ │
├─ 更新会话(touch)
│ └─ 记录: sid, group, 最后活跃时间

├─ 判断方向标识 (dir)
│ │
│ ├─ dir = "u" (上行消息)
│ │ │
│ │ ├─ 解密payload
│ │ │ │
│ │ │ ├─ 成功? ──否──> TXT "bad"
│ │ │ │ │
│ │ │ │ |是
│ │ │ │ │
│ │ │ ├─ 广播到同组其他会话
│ │ │ │ └─ 放入各会话的下行队列
│ │ │ │
│ │ │ └─ 构造ACK响应
│ │ │ ├─ 明文: "ok" + 4字节序列号
│ │ │ ├─ 加密: AES-GCM
│ │ │ └─ 返回: TXT "<encrypted_ack>"
│ │ │
│ │ └──> [上行确认包]
│ │
│ ├─ dir = "p" (轮询请求)
│ │ │
│ │ ├─ 获取会话(sid)
│ │ │
│ │ ├─ 检查下行队列
│ │ │ │
│ │ │ ├─ 队列非空?
│ │ │ │ │
│ │ │ │ ├─ 是: 取出第一条消息
│ │ │ │ │ ├─ 加密消息
│ │ │ │ │ ├─ Base64URL编码
│ │ │ │ │ ├─ 按maxLength分片
│ │ │ │ │ └─ 返回: TXT "<片段1>" "<片段2>" ...
│ │ │ │ │ └──> 【下行消息包】
│ │ │ │ │
│ │ │ │ └─ 否: 返回空内容
│ │ │ │ ├─ 加密空字节数组
│ │ │ │ └─ 返回: TXT "<encrypted_empty>"
│ │ │ │ └──> 【空响应包】
│ │ │ │
│ │ │ └─ 加密失败? ──> TXT "bad"
│ │ │
│ │ └──> 根据队列状态返回
│ │
│ └─ dir = 其他
│ └──> TXT "noop"

└─ 发送DNS响应

最后就是实现