1. SNMP 协议简介#
不同于 HTTP 协议在 Web 客户端和服务器之间传输超文本数据,SNMP(也称简单网络管理协议)主要用于网络设备的监控和管理。它允许网络管理员在中央控制台上,远程收集和查看各种网络设备(如路由器、交换机、服务器、打印机)的运行状态,例如 CPU 占用率、接口流量、温度,甚至可以远程修改设备的配置。
2. SNMP 工作原理与架构设计#
SNMP 采用”管理者-代理”模型,核心思想是将网络管理功能分离为两个角色:管理者负责决策与监控,代理负责数据采集与响应。该架构主要由三个核心组件构成:
- NMS:即网络管理系统,是安装在管理员电脑或监控服务器上的管理软件,负责向设备发起查询指令或下发配置修改命令。
- Agent:即代理进程,运行在被管理的网络设备上,持续监听来自 NMS 的请求,负责收集设备的本地硬件和运行数据,并将结果返回给 NMS。
- MIB:即管理信息库,类似于数据库的表结构定义(Schema),是一个树状的虚拟数据库,定义了设备上所有可被管理的参数。树上的每一个叶子节点都有一个全球唯一的编号,称为 OID(对象标识符)。例如
1.3.6.1.2.1.1.1代表设备的系统描述信息。
基于该架构,SNMP 支持两种交互模式:
- 拉取模式(Polling):NMS 主动向 Agent 发送 Get 请求,获取特定 OID 对应的数据。
- 推送模式(Trap):当设备发生紧急故障时(例如某个网口突然断开),Agent 会主动向 NMS 发送告警报文,无需等待 NMS 的轮询。
3. SNMP 报文格式#
3.1. 与 HTTP 的编码差异#
在底层通信上,HTTP 通常基于面向连接的 TCP,而 SNMP 基于无连接的轻量级 UDP,端口 161 用于常规轮询,162 用于 Trap 接收。
在数据描述与编码上,SNMP 使用 ASN.1(抽象语法标记)来定义复杂的数据结构,并通过 BER(基本编码规则)将这些结构转换为紧凑的二进制比特流进行网络传输。
在编码上,HTTP/1.1 报文是人类可读的 ASCII 纯文本,通过回车换行符分割。而 SNMP 为了追求极致的网络传输效率,采用了面向机器的二进制编码。
BER 编码的 TLV 结构
BER 编码的基本单位是 TLV 三元组(Type-Length-Value),每个字段都由三部分组成:
- Type:1 字节,标识数据类型。高 2 位表示类别(Universal=00、Application=01、Context=10、Private=11),低 5 位表示具体标签编号。
- Length:指示 Value 部分的字节数。当长度 ≤ 127 时用 1 字节表示;当长度 > 127 时,最高位置 1,其余位表示后续字节数(如
0x81表示后 1 字节是长度,0x82表示后 2 字节是长度)。 - Value:实际的数据内容,可以嵌套新的 TLV 结构。
例如共同体名 public 的 BER 编码为 04 06 70 75 62 6C 69 63:
| 字段 | 十六进制 | 含义 |
|---|---|---|
| Type | 04 | OCTET STRING(字符串类型) |
| Length | 06 | 6 字节 |
| Value | 70 75 62 6C 69 63 | ASCII 编码的 “public” |
BER 常用数据类型标识
| 类型 | 十六进制标识 | 说明 |
|---|---|---|
| INTEGER | 02 | 整数,用于版本号、Request ID、Error Status 等 |
| OCTET STRING | 04 | 字节串,用于共同体名、系统描述等文本数据 |
| NULL | 05 | 空值,用于请求报文中的变量值占位 |
| OBJECT IDENTIFIER | 06 | OID 标识符,用于表示 MIB 树中的节点 |
| SEQUENCE | 30 | 序列容器,用于封装嵌套的 TLV 结构 |
PDU 类型的 BER 标识
SNMP 的 PDU 类型使用 Context 类别的标签编码,具体标识如下:
| PDU 类型 | 十六进制标识 | 说明 |
|---|---|---|
| GetRequest-PDU | A0 | 查询请求 |
| GetNextRequest-PDU | A1 | 查询下一个节点请求 |
| Response-PDU | A2 | 响应报文(SNMPv2 中替代 GetResponse) |
| SetRequest-PDU | A3 | 设置请求 |
| Trap-PDU | A4 | 陷阱报文(SNMPv2 中已废弃) |
| GetBulkRequest-PDU | A5 | 批量查询请求(SNMPv2 新增) |
| InformRequest-PDU | A6 | 通知请求(SNMPv2 新增) |
| SNMPv2-Trap-PDU | A7 | SNMPv2 陷阱报文(SNMPv2 新增) |
3.2. 报文整体结构#
由于上述编码差异,SNMP 的报文格式与 HTTP 有较大不同。一个标准的 SNMPv2c 报文主要由三大部分组成:
1. 版本号
标识 SNMP 的版本,版本字段按序号从 0 开始递增:v1 编码为 0,v2c 编码为 1,v3 编码为 2。
2. 共同体
共同体(Community)字段是 SNMPv1/v2c 中管理进程与代理进程之间的共享口令,用于标识管理方对代理方的操作权限。共同体名以明文形式在网络中传输,接收方通过比对该字段判断请求是否合法。常见的默认值为 public(只读)和 private(读写),但在生产环境中应替换为自定义字符串,以降低未授权访问的风险。
| 共同体名 | 权限 | 对应场景 |
|---|---|---|
public | 只读(Read-Only) | 监控查询:NMS 获取设备的 CPU 使用率、接口流量、系统描述等信息,不允许修改设备配置 |
private | 读写(Read-Write) | 配置管理:NMS 不仅能查询信息,还能通过 SetRequest 修改设备参数(如关闭接口、修改 sysContact) |
| 自定义名称 | 按配置决定 | 生产环境中应将默认的 public/private 改为自定义字符串,防止未授权访问 |
该机制的安全缺陷在于:共同体名以明文在网络中传输,任何抓包工具均可直接读取,攻击者一旦截获即可伪造请求。这是 SNMPv1/v2c 的核心安全缺陷,SNMPv3 通过 USM(基于用户的安全模型)引入了加密和认证机制来解决此问题。
3. PDU(协议数据单元)
PDU 是 SNMP 报文的核心载荷,承载了具体的管理操作指令和数据。SNMP 规定了 5 种 PDU 类型,按用途可分为三类:
- 请求类:由管理进程发起
- get-request:从代理进程处提取一个或多个参数值
- get-next-request:从代理进程处提取紧跟当前参数值的下一个参数值
- set-request:设置代理进程的一个或多个参数值
- 响应类:由代理进程返回
- get-response:对上述请求的应答,返回一个或多个参数值
- 通知类:由代理进程主动发起
- trap:当设备发生紧急事件时,代理进程主动向管理进程发送的告警报文
3.3. 核心概念#
管理进程:请求发起方,类似 Web 浏览器,一般运行在网管中心的监控服务器(NMS)上,负责主动向各地的网络设备发出查询指令、下发配置修改命令,或者被动地接收设备发来的紧急报警。
代理进程:请求响应方,类似 Web 服务器,运行在被监控的网络设备上的后台程序,负责收集自己所在设备的硬件和运行状态,并把这些数据整理好存入数据库(MIB),当管理进程发出查询请求时,读取 MIB 里的数据告诉管理进程。
3.4. PDU 内部字段#
请求标识符(Request ID)(请求/响应类 PDU)
由于 UDP 是无连接协议,报文可能乱序到达,因此该 ID 用于让管理进程识别返回的响应报文对应哪一个请求报文。
差错状态(Error Status)(请求/响应类 PDU)
用于响应报文中,提示查询是否成功。具体数字含义如下:
| 值 | 名称 | 含义 |
|---|---|---|
| 0 | noError | 操作成功 |
| 1 | tooBig | 响应报文过大,超出限制 |
| 2 | noSuchName | 请求的 OID 不存在 |
| 3 | badValue | Set 操作的值或语法无效 |
| 4 | readOnly | 试图修改只读变量 |
| 5 | genErr | 其他一般性错误 |
差错索引(Error Index)(请求/响应类 PDU)
当出现 noSuchName、badValue 或 readOnly 的差错时,由代理进程在响应中设置的一个整数,指明有差错的变量在变量列表中的索引位置。
Trap 类型(generic-trap)(仅 Trap PDU)
此字段正式的名称是 generic-trap,共分为以下 7 种:
| 值 | 类型 | 含义 |
|---|---|---|
| 0 | coldStart | 代理进程冷启动 |
| 1 | warmStart | 代理进程热启动 |
| 2 | linkDown | 网络接口故障 |
| 3 | linkUp | 网络接口恢复 |
| 4 | authenticationFailure | 认证失败 |
| 5 | egpNeighborLoss | EGP 邻居丢失 |
| 6 | enterpriseSpecific | 企业自定义事件 |
当 trap 类型为 linkDown、linkUp 或 egpNeighborLoss 时,变量绑定中的第一个变量应标识对应的接口。
特定代码(Specific Trap)(仅 Trap PDU)
若 trap 类型为 enterpriseSpecific,则指明代理自定义的事件代码,否则为 0。
时间戳(Time Stamp)(仅 Trap PDU)
指明自代理进程初始化到 trap 所报告的事件发生所经历的时间,单位为 ms。例如时间戳为 1908 表明在代理初始化后 1908ms 发生了该事件。
变量绑定(Variable Bindings)(所有 PDU)
指明一个或多个变量的名和对应的值。在 get-request 或 get-next-request 报文中,变量的值应忽略。管理变量表示管理对象类型在某一时刻的值(称该类型的实例),SNMP 以管理变量作为操作对象。
管理变量的表示方法:形如 x.y,其中 x 是管理对象的 OID(对象标识符),y 是能唯一确定对象类型值的一组数字——在标量变量中为 0,在表型变量中是该表的索引(如接口号或路由表中的目的网络地址)。
例如:MIB 中定义了管理对象 ipAdEntNetMask,其 OID 为 1.3.6.1.1.5.6.1.3,它是路由表中的一项。若某行的索引和目的网络地址为 129.102.1.0,则该变量的完整名称为 1.3.6.1.1.5.6.1.3.129.102.1.0。其中 x.y 的 y 称为实例,用于唯一标识具体的管理变量。
在请求报文中,变量绑定包含待查询的 OID,Value 为空;在响应报文中,代理进程会在 Value 字段填入具体的系统数据。
3.5. OID 的 BER 编码规则#
OID 在 BER 编码中有特殊的压缩规则。前两个子标识符(通常为 1.3)会被合并为一个字节,计算公式为 40 × x + y。例如 1.3 编码为 0x2B(即 40 × 1 + 3 = 43)。后续每个子标识符按以下规则编码:
- 值 < 128:直接用 1 字节表示。
- 值 ≥ 128:使用多字节编码,每个字节的最高位作为延续标志(1 表示后续还有字节,0 表示结束),低 7 位存储数据。
以 OID 1.3.6.1.2.1.1.2.0 为例:
| 子标识符 | 原始值 | 编码字节 | 说明 |
|---|---|---|---|
| 1.3 | 1, 3 | 2B | 40×1+3=43=0x2B |
| 6 | 6 | 06 | < 128,直接编码 |
| 1 | 1 | 01 | < 128,直接编码 |
| 2 | 2 | 02 | < 128,直接编码 |
| 1 | 1 | 01 | < 128,直接编码 |
| 1 | 1 | 01 | < 128,直接编码 |
| 2 | 2 | 02 | < 128,直接编码 |
| 0 | 0 | 00 | < 128,直接编码 |
因此完整的 BER 编码为 06 08 2B 06 01 02 01 01 02 00,其中 06 是 OBJECT IDENTIFIER 的类型标签,08 是长度(8 字节)。
再以 OID 1.3.6.1.4.1.674 为例,其中子标识符 674 > 127,需要多字节编码:
674 = 0x2A2
二进制:0000 0101 0010 0010
分组(7位一组):0000101 0100010
设置延续标志:10000101 01000010
十六进制:0x85 0x22plaintext因此 674 编码为 85 22,完整的 OID BER 编码为 06 07 2B 06 01 04 01 85 22。