# TFTP 协议速览

> 依据：RFC 1350（基础协议）、RFC 2347（option extension）、
> RFC 2348（blksize）、RFC 2349（timeout / tsize）。
> 本文只做实现参考，以 RFC 原文为准。

## 定位

TFTP 是 FTP 的极简版，用于无盘工作站网络引导、嵌入式设备固件上传、
网络设备配置备份等场景。

| 特性 | TFTP | FTP |
| --- | --- | --- |
| 传输层 | UDP，69 端口 | TCP，21 + 数据连接 |
| 认证 | 无 | 有 |
| 目录浏览 | 不支持 | 支持 |
| 默认块大小 | 512 字节 | 更大 |
| 可靠性 | 应用层停等 + 重传 | TCP 保证 |

安全提醒：TFTP 无认证、无加密，只应部署在受信任的内网。

## 报文格式

所有报文以 2 字节 opcode 开头。

### RRQ / WRQ（opcode 1 / 2）

```
 2 bytes     string    1 byte     string   1 byte
+--------+-----------+------+-----------+------+
| Opcode |  Filename |  0   |    Mode   |  0   |
+--------+-----------+------+-----------+------+
```

后续为可选的 `Option\0Value\0` 序列（RFC 2347）。

### DATA（opcode 3）

```
 2 bytes     2 bytes      n bytes
+--------+---------------+---------+
| Opcode |   Block #     |  Data   |
+--------+---------------+---------+
```

`n` 取值范围 0..blksize（默认 512）。**小于 blksize 的数据包表示文件结束**。

### ACK（opcode 4）

```
 2 bytes     2 bytes
+--------+---------------+
| Opcode |   Block #     |
+--------+---------------+
```

### ERROR（opcode 5）

```
 2 bytes     2 bytes      string    1 byte
+--------+---------------+---------+------+
| Opcode |  ErrorCode    |  ErrMsg |  0   |
+--------+---------------+---------+------+
```

错误码：0 未定义、1 文件不存在、2 访问违规、3 磁盘满、
4 非法操作、5 未知 TID、6 文件已存在、7 无此用户、8 选项协商失败。

## 交互流程

```
Client                          Server
  |-- RRQ/WRQ (到 69 端口) ------>|
  |<-- 从随机高端口回 DATA/ACK ---|   (此后在该 TID 上通信)
  |-- ACK ----------------------->|
  ... 每 blksize 字节一个 DATA，停等协议
```

要点：

1. **TID**：服务端在收到首个请求后，从新的临时端口（TID）应答；客户端后续
   只接受该 TID 的报文。这既是会话标识，也是基本的安全边界。
2. **停等**：每发一个 DATA 等一个对应 block 号的 ACK，收到再发下一个。
3. **结束**：DATA 载荷 < blksize 即文件结束，仍需对该包回复 ACK。
4. **重传与去重**：超时重传必须避免「Sorcerer's Apprentice Syndrome」——
   对重复的 DATA 只重发 ACK，不能重复应答导致报文爆炸。
5. **block 回绕**：block 号为 u16，大文件会回绕，比较时需按模运算处理。
6. **netascii 模式**：文本模式，`\n` ↔ `\r\n`、`\r` ↔ `\r\0` 双向转换；
   octet 模式为二进制直传。

## 选项协商（RFC 2347/2348/2349）

客户端在 RRQ/WRQ 中附加选项，服务端在 OACK（opcode 6）中确认：

| 选项 | 含义 | 取值 |
| --- | --- | --- |
| `blksize` | 数据块大小 | 8..65464 |
| `timeout` | 重传超时（秒） | 1..255 |
| `tsize` | 文件总大小（字节） | 读请求时 client 填 0，server 回真实值 |

服务端若拒绝某选项，应回 ERROR code 8；客户端必须能退回默认值工作。

### 协商规则（实现约定）

- **未知选项名**：服务端忽略，不报错（RFC 2347 §2）。
- **取值非法**（非十进制、越界）：忽略该选项，其余照常协商。
- **`tsize` 的形状**：读请求填占位 0（服务端回真实文件大小），
  写请求填将要发送的真实字节数。形状不符才是失败。
- **`blksize` 上限 65464**：block 号 u16 加 4 字节头必须装进同一个报文，
  所以不是 65535。
- **重复选项**：RFC 未定义，取第一次出现的值。
- **OACK 顺序**：与请求中的选项顺序一致；`OACK` 取代「block 0 的 ACK」，
  之后的 DATA 从 block 1 起，按协商后的 blksize 发送。
- **无任何选项被接受**：不发空 OACK，直接回 DATA / ACK。

## 客户端实现约定（M3）

- **传输抽象**：客户端只依赖 `Transport::send` / `Transport::recv`，
  `recv` 按 `peer`（TID）过滤，报文来源不对就当作没收到。
- **超时重传**：每个 DATA / ACK 都等对方的应答，超时按 `max_retries` 重发；
  读传输里客户端无事可发时重复 RRQ（首块前）或重发上一个 ACK（首块后），
  避免「一声不吭等到预算耗尽」。
- **Sorcerer's Apprentice**：重复的 DATA 只重发同一个 ACK，不再交付数据；
  重复的 ACK 直接忽略。两条路径都不改变文件内容。
- **block 回绕**：比较用 `block_before` / `block_after` 的「半圆」规则
  （RFC 1982 序列号比较），65535 与 0 相邻的语义靠它成立；朴素的 `+1`
  比较会在回绕处把合法块判成过期。
- **netascii**：编码时把行尾的 `\r` 暂存到下一块再决定是 `\r\n` 还是
  `\r\0`，所以行尾正好落在块边界上也能还原成一个换行。
