SNMP 协议简介。

这里先对 SNMP v2c 及之前版本的协议概念做简单介绍。

概念

SNMP(Simple Network Management Protocol,简单网络管理协议)是在 SGMP(Simple Gateway Monitoring Protocol,简单网关监控协议)的基础上实现的。在 SGMP 的基础上,SNMP 添加了新的管理信息结构和管理信息库。

SNMP 协议用于收集和整理 IP 网络上被管理设备的信息,它可以修改这些信息,从而改变设备的行为。

SNMP 是基于 UDP 协议的。

基本组件

SNMP 由一组网络管理标准组成,包括应用层协议、数据库模式和一组数据对象。

  • Manager 负责监视网络上的主机或设备;
  • 每个被管理的系统会运行 Agent,通过 SNMP 协议向 Manager 报告信息。

三个关键组件:

  • 被管理的设备
  • Agent,代理进程,是运行在被管理设备上的软件
  • 网络管理系统 Network management station (NMS) 是运行在 Manager 上的软件。

在 NMS 使用 SNMP 协议向 Agent 查询被管理设备上的信息时,Agent 根据 SNMP 的请求将所需的数据公开为变量。

SNMP 协议本身并不定义系统应该提供哪些变量,而是使用可扩展的设计,让应用程序定义自己的层次结构,这些层次结构被表述为管理信息库(Management information base, MIB)。

MIB 描述了设备子系统的管理数据结构,它们使用包含对象标识符(object identifiers, OID)的分层命名空间。每个 OID 标识一个可以通过 SNMP 读取或设置的变量。

MIB 使用管理信息结构 2.0 版(SMIv2、RFC 2578)(ASN.1 的子集)定义的符号。

工作方式

SNMP 端口是 SNMP 通信端点,SNMP 消息传输通过 UDP 进行,通常使用 UDP 端口号161/162。有时也使用传输层安全性(TLS)或数据报传输层安全性(DTLS)协议,端口使用情况如下表所示。

过程协议端口号
代理进程接收请求信息UDP161
NMS 与代理进程之间的通信UDP161
NMS 接收通知信息UDP162
代理进程生成通知信息-任何可用的端口
接收请求信息TLS/DTLS10161
接收通知信息TLS/DTLS10162

版本

SNMP 有三种版本:SNMPv1,SNMPv2c 和 SNMPv3。

  • SNMPv1:SNMP 的第一个版本,它提供了一种监控和管理计算机网络的系统方法,它基于团体名认证,安全性较差,且返回报文的错误码也较少。它在 RFC 1155 和 RFC 1157 中定义。
  • SNMPv2c:第二个版本 SNMPv2c 引入了 GetBulk 和 Inform 操作,支持更多的标准错误码信息,支持更多的数据类型。它在 RFC 1901,RFC 1905和 RFC 1906中定义。
  • SNMPv3:鉴于 SNMPv2c 在安全性方面没有得到改善,IETF 颁布了 SNMPv3 版本,提供了基于 USM(User Security Module)的认证加密和基于 VACM(View-based Access Control Model)的访问控制,是迄今为止最安全的版本。SNMPv3 在 RFC 1905,RFC 1906,RFC 2571,RFC 2572,RFC 2574 和 RFC 2575中定义。

用法

使用 net-snmp 工具管理 Agent。

Get 请求:

snmpget -v2c -c public 192.168.202.173 1.3.6.1.4.1.9.9.95.1.3.1.1.7

GetNext 请求:

snmpgetnext -v2c -c public 192.168.202.173 .1

Set 请求

snmpset -c public -v 1 192.168.202.152 1.3.6.1.2.1.1.5.0 s test

GetBulk 请求

snmpbulkget -v2c -c public 192.168.202.152 iso.0.8802.1.1.2.1.3.3.0

可以看到,我们可以进行一些请求(get、getnext、set、bulkget、etc.);设置 snmp 版本、设置 community、设置 OID 等操作,那么这些操作和设置都代表什么含义呢?

MIB 与 OID

MIB 是用来翻译 OID 的工具。在上文中,OID 是一串又一串的由数字、字母和点组成的字符,这对人类并不是很友好,不过我们可以通过 MIB 去阅读 OID 串。

例如,经过完全验证的 OID 一般都由 .iso.org.dod.internet.private 开始,表示成数字串为 .1.3.6.1.4。我们可以去 https://oidref.com/ 查询通用的 OID 及其含义。

private 的后面是 enterprise,我们可以到 https://www.iana.org/assignments/enterprise-numbers/ 查询所有已经注册的分配。

我对 OID 具体的实现并不感兴趣,而是对怎样使用它更感兴趣。如果有机会的话以后再整理吧。

我们可以使用 snmpwalk 获取指定 Agent 的所有 OID。

snmpwalk 也是 Net-SNMP 工具集合的一部分,它扫描 OID 的方法很简单:

snmpwalk -v <snmp version> -c <community> <ip> <oid>

想要通过 SNMP 2c 协议扫描 IP 为 192.168.x.y,community 为 public 的所有 OID,我们可以输入:

snmpwalk -v 2c -c public 192.168.x.y .1

它的格式形如 iso.<num1>.<num2>.<num n>,其中的 num 可以是任意数字。

PDU

SNMPv1 指定了五种核心协议数据单元(protocol data units, PDUs),SNMPv2 中添加了 GetBulkRequestInformRequest PDU,SNMPv3 添加了 Report PDU。

操作类型发送端描述备注
GetNMSGet 操作可以从 Agent 中提取一个或多个参数值。-
GetNextNMSGetNext 操作可以从 Agent 中按照字典序提取下一个参数值。-
SetNMSSet 操作可以设置 Agent 的一个或多个参数值。-
ResponseAgentResponse 操作可以返回一个或多个参数值。这个操作是由 Agent 发出的,它是 GetRequest、GetNextRequest、SetRequest 和 GetBulkRequest 四种操作的响应操作。Agent 接收到来自 NMS 的 Get/Set 指令后,通过 MIB 完成相应的查询/修改操作,然后利用 Response 操作将信息回应给 NMS。-
TrapAgentTrap 信息是 Agent 主动向 NMS 发出的信息,告知管理进程设备端出现的情况。-
GetBulkNMSGetBulk 操作实现了 NMS 对被管理设备的信息群查询。SNMPv1版本不支持 GetBulk 操作
InformAgentInformRequest 是 Agent 向 NMS 主动发送告警。与 Trap 告警不同的是,被管理设备发送 Inform 告警后,需要 NMS 回复 InformResponse 来进行确认。SNMPv1版本不支持 Inform 操作
Report

协议格式

一般来说,整个 SNMP 消息三个字段组成:

  • SNMP Version:SNMP 的版本;
  • SNMP Community:团体字;
  • SNMP PDU:上文介绍的 PDU。

PDU 是复杂的,它由多个数据构建而成:

其中:

  • Request ID 用来标注特定 SNMP 的请求,它会在 Agent 返回的消息中被设置;
  • Error 在发送时的默认值是 0,只有 Agent 处理出错时才会被设置。具体的错误类型见 附录 1
  • Error Index 保存了发生错误时导致出错的对象的指针,默认为 0。
  • Varbind 由两个字段组成,第一个字段是 OID,用于指定特定参数,第二个字段是包含特定参数的值。
    • 这里的问题是,怎样使用 net-snmp 一次发送多个 Varbind?

协议解析

SNMP 报文格式是经典的 TLV(Type-Length-Value)字段,对于一些简单的数据,它的格式是这样的:

而对于复杂的嵌套数据(比如 Sequence 和 PDUs),则是嵌套保存的:

对 OID 编码需要遵循两个原则:

  1. OID 的前两个数字 x.y 需要使用公式 (40*x)+y 编码为一个值。例如,一般的 OID 前两个数字总是 1.3,那么按照公式就可以编码为 43(0x2B);
  2. OID 剩余的数字编码为一个字节;
    • 但是对于大数字有特殊的规则,因为一个字节只能表示不超过 255 的数字,因此规定任何超过 127 的数字都必须用一个字节以上进行编码,并用字节的最高位作为标志,让接收者知道这个数字会用多个数字编码。
    • 例如,想要编码数字 2680,就需要分成两个字节 0x94 0x78,在计算的时候用 (0x94-0x80)*0x80+0x78=2680 来计算。

参考协议格式协议解析这两段,我们可以对任意 SNMP 报文进行解析,下面给出一个书意图:

SNMP v3

(以 Cisco 的设备为例)

新的元素

  • SNMP 视图(SNMP View):定义可以在 Agent 上看到的内容,避免未授权访问;
  • SNMP 用户(SNMP User):SNMP User 和 SNMP Group 相关联,在关联时定义了用户名、密码、加密和身份验证级别;
  • SNMP 群组(SNMP Group):SNMP View 和 SNMP Group 关联,定义了访问类型(如只读/读写)。与设备交互期间启动的安全方法由 SNMP Group 决定。

认证模型

  • 身份验证:用于确保只有预期的收件人才能读取 trap。创建消息时,会根据实体的 EngineID 为其赋予一个特殊密钥。该密钥与预期接收者共享并用于接收消息。
  • 加密 - 隐私对 SNMP 消息的有效负载进行加密,确保未经授权的用户无法读取。任何被截获的 trap 信息都将充满乱码,无法读取。在 SNMP 信息必须通过互联网传输的应用中,隐私功能尤其有用。

安全级别

SNMP Group 中有三种安全级别:

  • noAuthnoPriv - 无需验证和隐私的通信;
  • authNoPriv - 有身份验证但无隐私的通信。用于身份验证的协议是 MD5 和 SHA;
  • authPriv - 带有身份验证和隐私保护功能的通信。用于身份验证的协议有 MD5 和 SHA,用于隐私保护的协议有 DES 和 AES。

SNMPv3 的报文解析见附录 5. SNMPv3 报文解析

参考资料

附录 1. PDU 错误类型

  • 0x00 — No error occurred
  • 0x01 — Response message too large to transport
  • 0x02 — The name of the requested object was not found
  • 0x03 — A data type in the request did not match the data type in the SNMP agent
  • 0x04 — The SNMP manager attempted to set a read-only parameter
  • 0x05 — General Error (some error other than the ones listed above)

附录 2. 原始数据类型

Primitive Data TypesIdentifierComplex Data TypesIdentifier
Integer0x02Sequence0x30
Octet String0x04GetRequest PDU0xA0
Null0x05GetResponse PDU0xA2
Object Identifier0x06SetRequest PDU0xA3

附录 3. scapy 实现不完整问题

scapy 提供了一个 snmpwalk 函数,位于 scapy.layers.snmp

def snmpwalk(dst, oid="1", community="public"):
    try:
        while True:
            r = sr1(IP(dst=dst) / UDP(sport=RandShort()) / SNMP(community=community, PDU=SNMPnext(varbindlist=[SNMPvarbind(oid=oid)])), timeout=2, chainCC=1, verbose=0, retry=2)  # noqa: E501
            if r is None:
                print("No answers")
                break
            if ICMP in r:
                print(repr(r))
                break
            print("%-40s: %r" % (r[SNMPvarbind].oid.val, r[SNMPvarbind].value))
            oid = r[SNMPvarbind].oid
 
    except KeyboardInterrupt:
        pass
 

我们可以轻松调用这个函数:

snmpwalk(dst="192.168.202.152", oid=".1", community="public")

行为差异

理论上,Net-SNMP 和 scapy 提供的 snmpwalk 应当行为相同,但在用 scapy 的时候会出现这样的问题:

1.0.8802.1.1.2.1.3.3.0                  : <ASN1_STRING[b'ohmytest']>
1.0.8802.1.1.2.1.3.4.0                  : <ASN1_STRING[b'MikroTik RouterOS 7.1.1 (stable) x86']>
1.0.8802.1.1.2.1.3.5.0                  : <ASN1_STRING[b'(']>
... ...
1.3.6.1.2.1.31.1.1.1.3.1                : 0x0 <ASN1_COUNTER32[0]>
1.3.6.1.2.1.31.1.1.1.4.1                : 0x0 <ASN1_COUNTER32[0]>
1.3.6.1.2.1.31.1.1.1.5.1                : 0x0 <ASN1_COUNTER32[0]>
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[2], line 1
----> 1 snmpwalk(dst="192.168.202.152", oid=".1", community="public")

File ~/anaconda3/envs/fuzz/lib/python3.9/site-packages/scapy/layers/snmp.py:293, in snmpwalk(dst, oid, community)
    291             print(repr(r))
    292             break
--> 293         print("%-40s: %r" % (r[SNMPvarbind].oid.val, r[SNMPvarbind].value))
    294         oid = r[SNMPvarbind].oid
    296 except KeyboardInterrupt:

File ~/anaconda3/envs/fuzz/lib/python3.9/site-packages/scapy/packet.py:1327, in Packet.__getitem__(self, cls)
   1325     else:
   1326         name = cast(str, lname)
-> 1327     raise IndexError("Layer [%s] not found" % name)
   1328 return ret

IndexError: Layer [SNMPvarbind] not found

在对 OID 1.3.6.1.2.1.31.1.1.1.5.1 进行 next 时报错了,这是 Net-SNMP 中 snmpwalk 不会出现的问题。

复现包

我们可以通过 scapy 构造一个简单的报文复现:

def create_getnext_request(ver="v2c", community="public", ip="127.0.0.1", oid=".1"):
    get_payload = IP(dst=ip) / UDP(sport=161, dport=161) / SNMP(
        version=ver, community=community, PDU=SNMPnext(
            varbindlist=[SNMPvarbind(oid=ASN1_OID(oid))]))
    return get_payload
 
p = sr1(create_getnext_request(ip="192.168.202.152", oid="1.3.6.1.2.1.31.1.1.1.5.1"))

得到的 p 是:

Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets

p

<IP  version=4 ihl=5 tos=0x0 len=75 id=7276 flags=DF frag=0 ttl=63 proto=udp chksum=0x195f src=192.168.202.152 dst=172.31.206.118 |<UDP  sport=snmp dport=snmp len=55 chksum=0xdb74 |<Raw  load='0-\x02\x01\x01\x04\x06public\\xa2 \x02\x01\x00\x02\x01\x00\x02\x01\x000\x150\x13\x06\x0b+\x06\x01\x02\x01\x1f\x01\x01\x01\x06\x01F\x04\x01\x0c3\\xd3' |>>>

hexdump(p)

0000  45 00 00 4B 4D 05 40 00 3F 11 E8 C5 C0 A8 CA 98  E..KM.@.?.......
0010  AC 1F CE 76 00 A1 00 A1 00 37 88 66 30 2D 02 01  ...v.....7.f0-..
0020  01 04 06 70 75 62 6C 69 63 A2 20 02 01 00 02 01  ...public. .....
0030  00 02 01 00 30 15 30 13 06 0B 2B 06 01 02 01 1F  ....0.0...+.....
0040  01 01 01 06 01 46 04 01 0C 42 26                 .....F...B&

可以看到,收到的包被解析成了 Raw 格式,无法解析成 SNMP 报文,也难怪上面会报错。我们对比 snmpgetnext:

$ snmpgetnext -v2c -c public 192.168.202.152 1.3.6.1.2.1.31.1.1.1.5.1
iso.3.6.1.2.1.31.1.1.1.6.1 = Counter64: 17590663

可以看到它正常返回了下一个 OID 和对应的值:iso.3.6.1.2.1.31.1.1.1.6.1

那 scapy 中正常的报文长什么样呢?我们对 .1 NEXT 试试:

p = sr1(create_getnext_request(ip="192.168.202.152", oid=".1"))

得到的结果为:

Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets

p

<IP  version=4 ihl=5 tos=0x0 len=78 id=29917 flags=DF frag=0 ttl=63 proto=udp chksum=0xc0ea src=192.168.202.152 dst=172.31.206.118 |<UDP  sport=snmp dport=snmp len=58 chksum=0x9381 |<SNMP  version='v2c' 0x1 <ASN1_INTEGER[1]> community=<ASN1_STRING[b'public']> PDU=<SNMPresponse  id=0x0 <ASN1_INTEGER[0]> error='no_error' 0x0 <ASN1_INTEGER[0]> error_index=0x0 <ASN1_INTEGER[0]> varbindlist=[<SNMPvarbind  oid=<ASN1_OID['.1.0.8802.1.1.2.1.3.3.0']> value=<ASN1_STRING[b'ohmytest']> |>] |> |>>>

hexdump(p)

0000  45 00 00 4E 74 DD 40 00 3F 11 C0 EA C0 A8 CA 98  E..Nt.@.?.......
0010  AC 1F CE 76 00 A1 00 A1 00 3A 93 81 30 30 02 01  ...v.....:..00..
0020  01 04 06 70 75 62 6C 69 63 A2 23 02 01 00 02 01  ...public.#.....
0030  00 02 01 00 30 18 30 16 06 0A 28 C4 62 01 01 02  ....0.0...(.b...
0040  01 03 03 00 04 08 6F 68 6D 79 74 65 73 74        ......ohmytest

这是一个正常的 SNMP 报文,但是奇怪的事情发生了:我们注意到,在这次请求中,我们实际上收到了两个包;而在上一次错误的请求中,我们只收到了一个包!

抓包

这里我们抓包看一下 Net-SNMP 和 scapy 构造包的区别。在这里我发了四个包,分别用 上文实现的 create_getnext_request 和 Net-SNMP 的 snmpgetnext 对 oid=".1"oid=1.3.6.1.2.1.31.1.1.1.5.1 进行发送。最终收到了 10 个包,如下图所示,前六个包是上文实现的函数发送接收的。

可以看到,实际上不论是 Net-SNMP 和 scapy 都正常收到了 SNMP 报文,那为什么在 oid=1.3.6.1.2.1.31.1.1.1.5.1 的时候显示为没有接收呢?又为什么 scapy 发送的报文会多一条 Port unreachable 的回复呢?

戳啦,再回头看一下,它们的行为是一致的,只是 scapy 没有解析出来而已。收到的 Raw 包为:

p[Raw].fields['load']

b'0-\x02\x01\x01\x04\x06public\xa2 \x02\x01\x00\x02\x01\x00\x02\x01\x000\x150\x13\x06\x0b+\x06\x01\x02\x01\x1f\x01\x01\x01\x06\x01F\x04\x01\r\x94\xc9'

0000   00 15 5d 98 cb 68 00 15 5d 1c 3d 19 08 00 45 00
0010   00 4b 6a 55 40 00 3f 11 cb 75 c0 a8 ca 98 ac 1f
0020   ce 76 00 a1 00 a1 00 37 3b f1 30 2d 02 01 01 04
0030   06 70 75 62 6c 69 63 a2 20 02 01 00 02 01 00 02
0040   01 00 30 15 30 13 06 0b 2b 06 01 02 01 1f 01 01
0050   01 06 01 46 04 01 0c b7 72

和正常的报文对比没有什么区别

结论

我们可以用 scapy 发包,但是解析包可能有问题。可能是 scapy SNMP 的实现有问题。它的实现为:

class SNMP(ASN1_Packet):
    ASN1_codec = ASN1_Codecs.BER
    ASN1_root = ASN1F_SEQUENCE(
        ASN1F_enum_INTEGER("version", 1, {0: "v1", 1: "v2c", 2: "v2", 3: "v3"}),  # noqa: E501
        ASN1F_STRING("community", "public"),
        ASN1F_CHOICE("PDU", SNMPget(),
                     SNMPget, SNMPnext, SNMPresponse, SNMPset,
                     SNMPtrapv1, SNMPbulk, SNMPinform, SNMPtrapv2)
    )
 
    def answers(self, other):
        return (isinstance(self.PDU, SNMPresponse) and
                isinstance(other.PDU, (SNMPget, SNMPnext, SNMPset)) and
                self.PDU.id == other.PDU.id)

附录 4. Cisco SNMP v3 配置方法

配置访问列表

在下文的命令中,我们创建了一个名称为 snmp-service 的控制列表,它只允许 192.168.100.101 和 192.168.100.102 这两个 IP 访问 snmp 服务。

!
configure terminal 
ip access-list standard snmp-service
  remark SNMP Poller Server #1
  permit 192.168.100.101  
  remark SNMP Poller Server #2
  permit 192.168.100.102 
end
!

创建 SNMP 视图

SNMP 视图(Views)包括只读视图和读写视图,这两种视图都可以访问设备上的所有 OIDs。

在下文的命令中,我们创建了一个名称为 snmp-v3-ReadOnly-View 的只读视图和一个名称为 snmp-v3-ReadWrite-View 的读写视图。

!
configure terminal
snmp-server view snmp-v3-ReadOnly-View iso included 
snmp-server view snmp-v3-ReadWrite-View iso included 
end
!

查询 SNMP 视图

!
show snmp view
!

创建 SNMP 组

在 SNMP v3 中,SNMP 组(Groups)和 SNMP 视图相关联。

下面的命令创建了两个 SNMP 组,分别是只读的 snmp-v3-ReadOnly,它和上文的 snmp-v3-ReadOnly-View 相关联,并且按照 snmp-service 的权限列表控制权限;同理创建了可读写的 snmp-v3-ReadWrite 组。

其中,access snmp-service 可以省略。

!
configure terminal
snmp-server group snmp-v3-ReadOnly v3 priv read snmp-v3-ReadOnly-View access snmp-service
snmp-server group snmp-v3-ReadWrite v3 priv write snmp-v3-ReadWrite-View access snmp-service
end
!

查询 SNMP 组

!
show snmp community
!

创建只读用户

下文创建了两个只读用户,分别称作 cacti-userread-only-user

!
configure terminal
snmp-server user cacti-user snmp-v3-ReadOnly v3 auth sha AaBbCcDdEe1234 priv aes 128 123456789AaBbCc access snmp-service
snmp-server user read-only-user snmp-v3-ReadOnly v3 auth sha 5678MmNnOoPp priv aes 128 MnNnOo7890 access snmp-service
end
!

创建读写用户

!
configure terminal
snmp-server user net-config-user snmp-v3-ReadWrite v3 auth sha 9876QqRrSsTt priv aes 128 TtUuVv3456 access snmp-service
end
!

查询用户

!
show snmp user
!

向 Agent 查询

snmpwalk \
  -v3 \
  -a SHA            `# 验证算法` \
  -A 5678MmNnOoPp   `# 验证密钥` \
  -x AES            `# 加密算法` \
  -X MnNnOo7890     `# 加密密钥` \
  -u read-only-user `# SNMP v3用户名` \
  -l authPriv       `# 安全级别`\
  172.22.148.2      `# target ip` \
  1.3.6.1.2.1.1.1   `# OIDS`

附录 5. SNMPv3 报文解析

附录 4 的基础上对 SNMPv3 的报文进行解析。先简单发送两个报文:

$ snmpgetnext \
  -v3 \
  -a SHA            `# 验证算法` \
  -A 5678MmNnOoPp   `# 验证密钥` \
  -x AES            `# 加密算法` \
  -X MnNnOo7890     `# 加密密钥` \
  -u read-only-user `# SNMP v3用户名` \
  -l authPriv       `# 安全级别`\
  172.22.148.2      `# target ip` \
  1.0.8802.1.1.1.1.1.1.0   `# OIDS`
# iso.0.8802.1.1.1.1.1.2.1.2.1 = Gauge32: 3
$ snmpget \
  -v3 \
  -a SHA            `# 验证算法` \
  -A 5678MmNnOoPp   `# 验证密钥` \
  -x AES            `# 加密算法` \
  -X MnNnOo7890     `# 加密密钥` \
  -u read-only-user `# SNMP v3用户名` \
  -l authPriv       `# 安全级别`\
  172.22.148.2      `# target ip` \
  1.0.8802.1.1.1.1.1.1.0   `# OIDS`
  # iso.0.8802.1.1.1.1.1.1.0 = INTEGER: 2 

在进行实际的报文发送时,会先进行一次设置交换再发送报文:

例如,第一段报文中设置了 Reportable,要求反馈;设置了 msgSecurityModel 为 USM;发送了一个空的 variable-bindings

Simple Network Management Protocol
    msgVersion: snmpv3 (3)
    msgGlobalData
        msgID: 589393291
        msgMaxSize: 65507
        msgFlags: 04
            .... .1.. = Reportable: Set
            .... ..0. = Encrypted: Not set
            .... ...0 = Authenticated: Not set
        msgSecurityModel: USM (3)
    msgAuthoritativeEngineID: <MISSING>
    msgAuthoritativeEngineBoots: 0
    msgAuthoritativeEngineTime: 0
    msgUserName: 
    msgAuthenticationParameters: <MISSING>
    msgPrivacyParameters: <MISSING>
    msgData: plaintext (0)
        plaintext
            contextEngineID: <MISSING>
            contextName: 
            data: get-request (0)
                get-request
                    request-id: 1798557581
                    error-status: noError (0)
                    error-index: 0
                    variable-bindings: 0 items

然后 Agent 向 Manager 返回一个值:

Simple Network Management Protocol
    msgVersion: snmpv3 (3)
    msgGlobalData
        msgID: 589393291
        msgMaxSize: 1500
        msgFlags: 00
            .... .0.. = Reportable: Not set
            .... ..0. = Encrypted: Not set
            .... ...0 = Authenticated: Not set
        msgSecurityModel: USM (3)
    msgAuthoritativeEngineID: 800000090300c4641390eea0
        1... .... = Engine ID Conformance: RFC3411 (SNMPv3)
        Engine Enterprise ID: ciscoSystems (9)
        Engine ID Format: MAC address (3)
        Engine ID Data: Cisco type: Agent (0x00)
        Engine ID Data: MAC address: Cisco_90:ee:a0 (c4:64:13:90:ee:a0)
    msgAuthoritativeEngineBoots: 4
    msgAuthoritativeEngineTime: 622717
    msgUserName: 
    msgAuthenticationParameters: <MISSING>
    msgPrivacyParameters: <MISSING>
    msgData: plaintext (0)
        plaintext
            contextEngineID: 800000090300c4641390eea0
                1... .... = Engine ID Conformance: RFC3411 (SNMPv3)
                Engine Enterprise ID: ciscoSystems (9)
                Engine ID Format: MAC address (3)
                Engine ID Data: Cisco type: Agent (0x00)
                Engine ID Data: MAC address: Cisco_90:ee:a0 (c4:64:13:90:ee:a0)
            contextName: 
            data: report (8)
                report
                    request-id: 1798557581
                    error-status: noError (0)
                    error-index: 0
                    variable-bindings: 1 item
                        1.3.6.1.6.3.15.1.1.4.0: 18

这里返回的 variable-bindings 是一项 OID,似乎是发送过的 SNMPv3 命令的数量?

去翻一下 https://oidref.com/1.3.6.1.6.3.15.1.1.4,介绍是:

The total number of packets received by the SNMP engine which were dropped because they referenced an snmpEngineID that was not known to the SNMP engine.

行吧,再回顾一下第一个包:

    msgAuthoritativeEngineID: <MISSING>

所以 net-snmp 发送的第二个包才是有效的,那第一个包的发送目的是啥?难道是判断目标是否支持 snmp v3 版本?倒是可以阅读一下源码。

那就直接看 net-snmp 发送的第二个报文吧:

Simple Network Management Protocol
    msgVersion: snmpv3 (3)
    msgGlobalData
        msgID: 589393290
        msgMaxSize: 65507
        msgFlags: 07
            .... .1.. = Reportable: Set
            .... ..1. = Encrypted: Set
            .... ...1 = Authenticated: Set
        msgSecurityModel: USM (3)
    msgAuthoritativeEngineID: 800000090300c4641390eea0
        1... .... = Engine ID Conformance: RFC3411 (SNMPv3)
        Engine Enterprise ID: ciscoSystems (9)
        Engine ID Format: MAC address (3)
        Engine ID Data: Cisco type: Agent (0x00)
        Engine ID Data: MAC address: Cisco_90:ee:a0 (c4:64:13:90:ee:a0)
    msgAuthoritativeEngineBoots: 4
    msgAuthoritativeEngineTime: 622717
    msgUserName: read-only-user
    msgAuthenticationParameters: 2d9f084f27aa8d0640c214e8
    msgPrivacyParameters: 58f003461dc6095f
    msgData: encryptedPDU (1)
        encryptedPDU: a5f60016a940b7cc5ddab8a65033d336f4c9a44e26550de73b0a1676215dc96e743b1270c7161c3885ffd0f05c9600c0374b

这个报文指定了 msgAuthoritativeEngineID,这里面包含了 Engine 的一些信息,所以上一个报文其实是用来获取 contextEngineID 用于本次的数据发送的。这一次设置了 msgFlags 中的 ReportableEncryptedAuthenticated

对于 encryptedPDU,我们可以在 Wireshark 中设置其解码方式。参考 How to decrypt SNMPv3 packets using Wireshark,在设置→首选项中设置协议→SNMP 中的各项设置即可。其中的 Engine-ID 就是报文中的 msgAuthoritativeEngineID

在设置完成后就可以解析 encryptedPDU 了:

msgData: encryptedPDU (1)
    encryptedPDU: a5f60016a940b7cc5ddab8a65033d336f4c9a44e26550de73b0a1676215dc96e743b1270c7161c3885ffd0f05c9600c0374b
        Decrypted ScopedPDU: 3030040c800000090300c4641390eea00400a11e02046b33cf8c0201000201003010300e060a28c462010101010101000500
            contextEngineID: 800000090300c4641390eea0
                1... .... = Engine ID Conformance: RFC3411 (SNMPv3)
                Engine Enterprise ID: ciscoSystems (9)
                Engine ID Format: MAC address (3)
                Engine ID Data: Cisco type: Agent (0x00)
                Engine ID Data: MAC address: Cisco_90:ee:a0 (c4:64:13:90:ee:a0)
            contextName: 
            data: get-next-request (1)
                get-next-request
                    request-id: 1798557580
                    error-status: noError (0)
                    error-index: 0
                    variable-bindings: 1 item
                        1.0.8802.1.1.1.1.1.1.0: Value (Null)
            [Response In: 4]