SNMP 的报文用 BER 编码:一种「标签 + 长度 + 内容」的嵌套写法。
v1 和 v2c 的团体名是明文的,public 在链路上原样可见。
这一节手搓一个 SNMP GetRequest,把每一层字节都拆开给你看。 这是全书唯一一次「不借助任何库,纯手工构造一个协议报文」。
一、BER:标签、长度、内容
SNMP 的报文格式叫 BER,
它的规则只有一条:每个东西都写成「一个标签字节、一段长度、然后是内容」。
内容本身可能又是一个这样的结构,所以整条报文能一层层拆开,像俄罗斯套娃。
长度字段还有一个约定:小于 128 就一个字节搞定;
大于等于 128 时,首字节的最高位置 1,低七位表示「后面还有几个字节是长度」。
所以 0x81 0x90 表示「后面跟 1 个字节的长度,值是 0x90」。
这套编码的好处是「不需要预先知道总长度」:解析器可以边读边算。
坏处是「变长」——长度不是固定位宽,所以没法「跳到第几个字段」。
现代的二进制协议(比如 Protobuf)也用了类似的思路,只是把「变长整数」做得更省。
二、一个 GetRequest 的六层
整条报文是「SEQUENCE 里装三样」:version、community、PDU。
version 是整数 1(表示 v2c),community 是明文的团体名,
PDU 里再装 request-id、error-status、error-index、varbind。
request-id 的作用是「配对响应」:因为 SNMP 跑在 UDP 上,响应可能乱序或者丢失,
所以靠这个号认领。
error-status 和 error-index 是「哪一条出错」的两级结构——
一个说「出错了」,一个说「是第几条」。
varbind 是「变量绑定」,它的结构是「一个 OID 加一个值」。
在 GetRequest 里,值是「空」(05 00 表示 null),
因为我们的意思是「请把这个 OID 的值给我」。
在 Response 里,值就被填上了。
三、161 和 162,都是 UDP
SNMP 只用两个端口:161 是「管理器来问」,162 是「设备主动报」。 两个都是 UDP。 为什么用 UDP 不用 TCP:因为「问一句答一句」用 UDP 更省,丢了再问一遍就行。 而且管理一台设备时,管理器和设备之间是「短交互」,为一次查询建一条 TCP 连接很不划算。 但 UDP 带来的问题也很实在:响应可能丢,所以客户端必须自己设超时和重试。 这也是为什么 SNMP 的默认超时通常是 1 秒、重试 3 次——这三个数成了几乎所有网管软件的默认值。
四、动手 Demo:手搓一个 SNMP 报文,看它到底长什么样
只用 bytes 和一个自己写的 BER 编码器,构造一条读 sysDescr.0 的 GetRequest。
然后把每一层拆开对照。
def ber_len(n):
if n < 128:
return bytes([n])
out = b''
while n:
out = bytes([n & 0xFF]) + out
n >>= 8
return bytes([0x80 | len(out)]) + out
def ber_tlv(tag, payload):
return bytes([tag]) + ber_len(len(payload)) + payload
def ber_int(v):
out = b'' if v == 0 else b''
while v:
out = bytes([v & 0xFF]) + out
v >>= 8
return ber_tlv(0x02, out or b'\x00')
def ber_oid(dotted):
parts = [int(x) for x in dotted.split('.')]
out = bytes([parts[0] * 40 + parts[1]])
for p in parts[2:]:
out += bytes([p]) if p < 128 else bytes([0x80 | (p >> 7), p & 0x7F])
return ber_tlv(0x06, out)
oid = '1.3.6.1.2.1.1.1.0' # sysDescr.0
varbind = ber_tlv(0x30, ber_oid(oid) + ber_tlv(0x05, b''))
vbl = ber_tlv(0x30, varbind)
pdu = ber_tlv(0xA0, ber_int(0x1234) + ber_int(0) + ber_int(0) + vbl)
msg = ber_tlv(0x30, ber_int(1) + ber_tlv(0x04, b'public') + pdu)
print(len(msg), msg.hex())
实跑输出:
--------------------------------------------------------------------
手搓一个 SNMP 报文,看它到底长什么样
--------------------------------------------------------------------
**SNMP 的报文用 BER 编码**——**一种「标签 + 长度 + 内容」的嵌套写法,每一层都一样。**
**所以整条报文能一层层拆开,像俄罗斯套娃。**
**目标是读 `sysDescr.0`**,OID 是 `1.3.6.1.2.1.1.1.0`。
**整条 GetRequest 一共 41 字节**:
302702010104067075626c6963a01a02
021234020100020100300e300c06082b
060102010101000500
**逐层拆开**:
层 字节 含义
-------------------------------------------------------------------------
外层 SEQUENCE `30 27` **整个报文**
version `02 01 01` **1 表示 SNMPv2c**
community `04 06 70 75 62 6C 69 63` **明文口令 public**
PDU 类型 `A0` **A0 是 GetRequest**
request-id `02 02 12 34` **用来配对响应**
error-status `02 01 00` **0 表示没出错**
error-index `02 01 00` **0 表示不是某一条出错**
varbind `30 0C 06 08 …` **要读哪个 OID**
**注意第二行**:**`community` 是明文的**,**`public` 这六个字母在链路上原样可见。**
**SNMPv1 和 v2c 都是这样**——**所以「用 SNMP 就别用默认团体名」是运维里最老的一条规矩。**
**v3 才加了认证和加密。**
**再看 OID 是怎么编码的**:
OID 编码 字节
-----------------------------------------------------------------
`1.3.6.1.2.1.1.1.0` `06 08 2b 06 01 02 01 01 01 00` 10
`1.3.6.1.2.1.1.5.0` `06 08 2b 06 01 02 01 01 05 00` 10
`1.3.6.1.4.1.9.1.1` `06 08 2b 06 01 04 01 09 01 01` 10
**第一个数被乘法合并了**:**`1.3` 变成 `1 乘 40 加 3`,也就是 43(0x2B)。**
**因为前两位的组合只有 6 种可能,一个字节足够。**
**后面每个数字如果小于 128 就占一字节,大了才用「多位拼接」。**
**所以 OID 的字节数不固定,只能现算。**
**SNMP 的端口只有两个**:**161 是「管理器来问」,162 是「设备主动报」。**
**两个都是 UDP**——**因为「问一句答一句」用 UDP 更省,丢了再问一遍就行。**
整条报文 41 字节,十六进制是 3027 020101 0406 7075626c6963 a01a ...。
开头的 30 27 就是「SEQUENCE,长度 39」,加上这两个字节正好 41。
注意第二层 04 06 7075626c6963:
04 是 OCTET STRING,06 是长度,后面六个字节就是 public 的 ASCII。
70 是 p、75 是 u、62 是 b……
所以团体名在链路上就是明文,抓包一眼就能看出来。
OID 的编码有个小机关:前两位被乘法合并了——1.3 变成 1 乘 40 加 3,也就是 43(0x2B)。
因为前两位的组合只有 6 种可能,一个字节足够。
后面每个数字如果小于 128 就占一字节,大了才用「多位拼接」。
所以 OID 的字节数不固定,只能现算。
30 秒实验
- 算一下:
1.3合并成一个字节,值是多少? - 想一下:为什么 SNMP 用 UDP 不用 TCP?
- 想一下:request-id 是干什么用的?
常见误区
- 以为「SNMP 的团体名是密码」。 它是明文口令,v3 才有认证和加密。
- 以为「BER 的长度是固定一字节」。 大于 128 就变长,这正是它难解析的地方。
- 以为「SNMP 用 TCP 更可靠所以更好」。 短交互用 UDP 更省,可靠性靠应用层重试。
自测三问(点开对答案)
1. SNMP 报文的三层结构是什么? —— **version、community、PDU**,整体装在 SEQUENCE 里。 2. `1.3.6.1` 的 `1.3` 怎么编码? —— **1 乘 40 加 3 等于 43**,也就是 `0x2B`。 3. 161 和 162 分别是什么? —— **161 是查询,162 是设备主动发的 Trap。**下一节看 MIB:SNMP 查的那棵树。
- ☐ (本节完,下一节 8.6.2 MIB)