1、前言
最近遇到了个问题,SNMPv3 Trap上报,在snmp agent侧修改了用户密码,管理站mibbroswer上没有修改trap用户的密码,仍然可接收到trap上报消息。通过Wireshark抓包,(编辑–首选项–Protocols–SNMP–Users Table编辑填写Engine ID、Username、Authentication model、Password、Privacy protocol、Privacy password),可以在抓包的时候将SNMP包解析出来,发现使用旧密码可以解析,但是新密码却无法解析,说明上报的trap消息SNMP报文是用旧密码加密的。<–c语言开发发送trap–>分析trap消息组包过程,可以发现用户信息及加密密钥是填写在session中,说明在用户配置改变时,会清理掉之前session然后重新打开一个新session(在项目中的方式)的流程没有生效,也即是旧session没有关掉仍然在使用。
以上就是遇到问题的简单分析过程,记录此文主要是想梳理一下snmp agent trap上报的过程,是怎么把trap消息发送出去的,以及接收端如何解包接收。(查询了很多文档,算是一点归纳记录)
2、SNMPv3安全传输机制
相关博客:
SNMP开发过程中的一些积累:https://blog.csdn.net/yyw794/article/details/78091545
SNMPv3的加密和认证:https://www.lmlphp.com/user/57883/article/item/600007/
3、SNMP Trap发送
3.1、使用 snmptrap 发送 SNMP Trap
本部分内容为摘自其他博客,本人并没有亲自测试,主要目的是表达如何利用snmptrap发送和接收trap报文,大致知道原理即可,如果要用snmptrap来进行trap发收,可以在查查其他博客详细学习。
(1)一台机器接收trap
利用 Net-snmp 提供的 snmptrapd 应用程序作为后台 SNMP Trap 服务器,负责接收被管理设备发送过来的 Trap 消息。
命令行窗口中输入以下命令:
snmptrapd – c mysnmptrad.conf udp:162
该命令启动了一个 snmptrapd 进程,守候在 UDP 端口 162上,侦听 SNMP Trap 消息。-c 命令行指定了名为 mysnmptrapd.conf 的配置文件。文件内容如下:
$cat mysnmptrapd.conf
traphandle default lognotify IBM-DW-SAMPLE::nodeDown
authCommunity log,execute,net public
其中,以 traphandle 开头的一行定义了 snmptrapd 进程接收到 Trap 消息后应该执行的动作。在本例中,lognotify 是一个 shell 脚本,功能是将接收到的 Trap 信息写入文件 checkfile。
以 authCommunity 开头的一行配置了 snmptrapd 的安全设置,表示可以接收 community 为”public”的 SNMP Trap,并且本进程可以有 log,net 和 execute 的权限。
Log 权限表明收到 Trap 之后 snmptrapd 可以记录日志;execute 表明收到 Trap 之后可以执行 traphandle 中所指定的操作。Net 表示 snmptrapd 可以将接收到的 Trap 信息转发到其他的 Receiver 去。(假如需要转发,还需要对给定的 OID 指定以 forward 为开始的处理细节:forward OID|default DESTINATION)
Lognotify 脚本
$cat lognotify
#!/bin/sh
read host
read ip
vars=
while read oid val
do
if [ "$vars" = "" ]
then
vars="$oid = $val"
else
vars="$vars, $oid = $val"
fi
done
echo trap: $1 $host $ip $vars >checkfile
————————————————
原文链接:https://blog.csdn.net/LANGGUANGCHENG/article/details/88948727
(2)另一台机器发送trap
假定 Trap 定义在文件 sample-trap.mib 中,可以使用命令行:
snmptrap -m ./sample- trap.mib -v 2c -c public 16.157.76.227:1622 \
"" IBM-DW-SAMPLE::nodeDown IBM-DW-SAMPLE::nodeDown.1 s "M1"
定义了一个 Trap 消息:nodeDown
Trap nodeDown 的 MIB 定义:
nodeDown NOTIFICATION-TYPE
STATUS current
DESCRIPTION "node down notification"
::= { notification 1 }
nodeDown 被定义为 Notification 类型,即 SNMPv2 类型的 Trap。NodeDown 对象位于 notification 节点下,子 OID 为 1。字母 s 表示该 OID 的类型为 string。“M1”为该 OID 变量的值。
打开文件 checkfile,应该能够看到如下信息:
Trap: IBM-DW-SAMPLE::nodeDown UDP: [16.157.76.221]:
54329->[16.157.76.227]:1622 DISMAN-EVENT-MIB::sysUpTimeInstance=
5:2:22:26.99, SNMPv2-MIB::snmpTrapOID.0 = SNMPv2-SMI::
enterprises.10234.100.1 SNMPv2-SMI::enterprises.10234.100.1.1 =
“M1”
表示已经能够用 snmptrap 发送 Trap;并且能够使用 snmptrapd 来接收 Trap 消息了。此例虽然简单,但却是 SNMP Trap 的典型应用。实际生产环境中的 SNMP Trap 应用的基本模型和本例是类似的。
snmptrapd使用类似博客:
SNMP Trap (V1,V2, V3)总结:https://blog.csdn.net/Java_Vicky/article/details/120518497
3.2、C语言 SNMP Trap开发
有时可能不仅需要 net-snmp 所提供的现成工具,还需要在自己开发的应用程序中编码发送 Trap。Net-snmp 不仅提供了现成的工具,也提供了开发库,下面看看如何在 C 程序中调用 net-snmp 的库函数进行 SNMP Trap 的开发。
一次trap消息上报大致的流程为: 包含必要的netsnmp库头文件、初始化(主要是session)、创建pdu(填充信息内容)、发送trap、结束清理。
Note:项目中遇到的bug,就在于trap发送线程作为订阅者,while一直等待观察者是否有消息需要上报,如果有就组包发送。trap发送线程会去检查snmpv3 trap用户配置(包括用户名、鉴权加密算法、密码、EngineID等)是否改变,如果有改变则需要关闭之前的session,新建session。正是这个过程没有使新session生效,才导致开头提到的问题出现。
3.2.1 头文件
C 语言直接调用 API 进行 SNMP Trap 处理,所需要的头文件为了使用 netsnmp 的 API,必须 include 以下这些头文件:
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
3.2.2 初始化
在使用 netsnmp 库之前,先要做一些必要的初始化工作。函数 init_snmp 初始化 SNMP Library。假如在调用 init_snmp 函数时指定了文件名,init_snmp 函数将读取配置文件,设置诸如 Access Control 等具体配置。否则会使用默认的 /etc 下面的配置文件。
– init_snmp(“snmpapp”);没有太理解初始化SNMP Library的实质是啥,反正snmp_close(session)没有解决清除旧session的问题,就使用了snmp_shutdown( “snmpapp” );关闭之前的初始化, 然后使用新session之前再重新init_snmp(“snmpapp”);。有时间再研究一下…
//对 snmp 协议栈进行初始化,init_snmp();
初始化 SNMP 库之后,就可以打开一个会话 session。此后所有和 NMS 的信息交互都在该 session 内进行,因为可能在同一台机器上运行多个 SNMP 进程,每个进程都需要自己独立的 session 来和 NMS 进行信息交互。
调用函数 snmp_sess_init 成功后将返回一个 session 数据结构。用该数据结构来设置 session 的属性,比如 peer 的 IP 信息。还可以设置其它的必要信息,比如 Community,即用于 SNMP 安全的社区设置,前面 snmptrapd 设置为 public,因此这里也设置为 public。这类似于通行密码,不过安全性的确比较弱。(snmpv3 初始化session过程,可参考另一篇博客:https://blog.csdn.net/chengwenyao18/article/details/7319732)
Session 属性设置好之后便可以使用函数 snmp_open 打开 session。示例代码如下:
init_snmp("myexample");
struct snmp_session session;
snmp_sess_init(&session);
session.version = SNMP_VERSION_2c;
strcpy(peername,"16.157.76.227:1622");
strcpy(commu,"public");
session.peername = peername;
session.community = (unsigned char*)commu;
session.community_len = strlen(commu);
ss = snmp_open(&session);
打开一个会话之后,程序可以通过该会话发送 Trap 给 NMS,也可以从 NMS 接受 SNMP get/set 操作。
3.2.3 创建pdu报文
每个 Trap 都由 PDU 承载,PDU 有固定的格式。 为了简化描述,本文只描述 SNMPv2 的 Trap 发送方法。SNMP v2c 和 v1 、v3的 Trap 有所不同。
SNMPv2 的 Trap PDU 定义如下:
详情见“TCP/IP详解 卷一 --------SNMP”:https://blog.csdn.net/yao_fairy/article/details/51325698
各个组成部分的解释如下: sysUpTime– 被管理设备上一次初始化网络到本 Trap 发送以来的累积时间。 snmpTrapOID– 表示本 PDU 是一个 Trap,有固定的值。对于一般的 Trap,RFC1907 给出了通用的定义。用户自定义的 Trap 通常是由以下几个部分连接而成:SNMPv1 Enterprise parameter + ‘0’ + SNMPv1 Specific trap code。 VarBindList– 变量列表,所谓变量就是 Trap 消息中所携带的信息单元。
//下面的代码片断用来填充如图三所示的一个 PDU。
oid objid_sysuptime[] = { 1, 3, 6, 1, 2, 1, 1, 3, 0 };
oid objid_snmptrap[] = { 1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0 };
netsnmp_pdu * pdu = NULL;
oid tmpOID[MAX_OID_LEN];
size_t tmpOID_len;
in_addr_t addr;
pdu = snmp_pdu_create(SNMP_MSG_TRAP2);
long sysuptime;
char tempbuf[128];
memset(tempbuf,128,0);
sprintf(tempbuf,"%ld",sysuptime);
sysuptime = get_uptime();
oid tmpOID[MAX_OID_LEN];
size_t tmpOID_len;
tmpOID_len = MAX_OID_LEN;
if(!snmp_parse_oid(TRAP_NAME_1, tmpOID, &tmpOID_len))
snmp_error(“snmp_parse_oid”);
snmp_add_var(pdu, objid_sysuptime, sizeof(objid_sysuptime)/sizeof(oid), 't', tempbuf);
snmp_add_var(pdu, objid_snmptrap, sizeof(objid_snmptrap)/sizeof(oid),'o',
“IBM-DW-SAMPLE::nodeDown”);
首先调用函数 snmp_pdu_create 创建一个 SNMPv2 的 Trap PDU。然后调用 snmp_add_var 向该 PDU 中添加图三所示的三个部分。 sysUpTime 在 SNMPv2-MIB 中定义,其 OID 为”1.3.6.1.2.1.1.3.0”。我们只需要通过 get_uptime() 函数获得该值,然后调用 snmp_add_var 将该变量加入刚才创建的 PDU 中。
同理,snmpTrapOID 也是固定的 :” 1.3.6.1.6.3.1.1.4.1.0”。同样利用 snmp_add_var 函数将我们定义的 OID 为“IBM-DW-SAMPLE::nodeDown”的 Trap 加入该 PDU。稍稍留意一下,您应该可以发现,sysUpTime 的类型为’t’,即 timestamp;而 snmpTrapOID 的类型为’ o ’,即 OID 类型。PDU 内的每一个元素都拥有自己的类型。在该 Trap 中,我们还打算携带两个变量:IBM-DW-SAMPLE::host 和 IBM-DW-SAMPLE::ip。表示一些真正有趣的信息。添加这两个变量的代码如下所示:
HP-DW-SAMPLE::ip",varId,&varIdLen))
{
snmp_perror("ip");
}
else
{
printf("Success snmp_parse_oid\n");
snmp_add_var(pdu, varId, sizeof(varId)/sizeof(oid),'s',"2.2.2.2");
}
if(!snmp_parse_oid("HP-DW-SAMPLE::host",varId,&varIdLen))
{
snmp_perror("host");
}
else
{
retv = snmp_add_var(pdu, varId, sizeof(varId)/sizeof(oid),'s',”M1”);
}
用 snmp_parse_oid 解析相应变量的 OID,然后就可以调用 snmp_add_var 将您想设置的值加入 PDU 中了。假如您直接在代码中使用”HP-DW-SAMPLE::ip”的 OID,(即 1.3.6.1.4.1.10234.10.2),那么可以不需要调用 snmp_parse_oid。该函数只是把一个好记的字符串翻译为一串数字的 OID。不过这就好比用 16.157.1.2 而不使用 www.ibm.com 一样。
3.2.4 发送Trap
至此,一个 SNMP Trap PDU 就创建成功了。将该 PDU 发送出去即可:
if( !snmp_send(session, pdu) )
{
snmp_error("Send pdu error \n");
}
3.2.5 结束清理
程序结束之前需要做清理工作,代码如下:
snmp_close(sptr);
snmp_shutdown( “myexample” );
SOCK_CLEANUP;
3.2.6 小结
利用netsnmp开发,就是调用snmp api库函数实现即可,参照标准的流程,可实现set、get、trap等开发。没有仔细梳理,把看过有用的都记录下来了,反正核心思想就是 参考流程 一步步开发就是了,类似于建立 socket 发送数据道理一样。有时间再仔细整理一下…
流程:https://www.cnblogs.com/YBhello/articles/5465507.html
SNMPv3 Get报文构建可参考:https://blog.csdn.net/chengwenyao18/article/details/7319732
net-snmp配置开发:https://dandelioncloud.cn/article/details/1572239455459438593
net-snmp简单例子:https://blog.csdn.net/wangcg123/article/details/52949285
介绍头文件#include<net-snmp-includes.h>库函数:https://www.cnblogs.com/cqx6388/p/17306491.html
其他使用netsnmp库开发的博客:
Qt 使用 net-snmp 包的过程记录:https://www.cnblogs.com/pied/p/7501248.html
snmp学习笔记:https://www.cnblogs.com/beautifullife4e/p/4760361.html
学习笔记 trap部分:https://blog.csdn.net/weixin_39897267/article/details/115988004
snmptrap动作学习:
与snmpget动作相差无几,只不过调用函数时参数不同。参考:net-snmp-5.5/apps/snmptrap.c
只需要将open_session换为snmp_add和netsnmp_transport_open_client即可。
netsnmp_session *snmp_add(netsnmp_session *, struct netsnmp_transport_s *, int (*fpre_parse) (netsnmp_session *, struct netsnmp_transport_s*, void *, int), int (*fpost_parse) (netsnmp_session *,netsnmp_pdu *, int));
将一些设置添加到session中,
netsnmp_transport* netsnmp_transport_open_client(const char* application, const char* str);
打开trap的客户端,产生netsnmp_transport结构体用于trap。
void snmp_shutdown(const char *type);
关闭应用程序,保存任何需要持久性存储,并进行适当的清理。参数:config文件中的"type"使用的类型标签
snmptarp和snmptarpd的基本流程为:
1.初始化session(snmp_sess_init)
2.打开session(snmp_open)
3.打开trap客户端或者服务端(netsnmp_transport_open_client or netsnmp_transport_open_server)
4.客户端发送trap pdu。服务端等待接收trap
5.关闭应用程序(snmp_shutdown)
Trap报文解析:https://javaforall.cn/171446.html
SNMPv2原始报文内容:
00 23 5a 9e 58b9 00 4c 41 49 50 55 08 00 45 00 00 7b 00 00 40 00 40 11 a5 1b c0 a8 0a01 c0 a8 0a05 0c01 00 a2 0067 04 bb 305d 02 01 01 04 06 70 75 62 6c 69 63a7 50 02 04 1773 2c fb 02 01 00 02 01 00 30 42 30 0d 06 08 2b 06 01 02 01 01 03 00 43 01 0e 30 17 060a 2b 06 01 06 03 01 01 04 01 00 06 09 2b 06 01 0603 01 01 05 01 3018 06 0a2b 06 01 06 03 01 01 04 0300 06 0a2b 06 01 04 01 bf 08 03 02 0a
目的MAC:00 23 5a 9e 58 b9
源MAC:00 4c 41 49 50 55
协议类型:08 00,IP报文
IP头:45 00 00 7b 00 00 40 00 40 11 a5 1b c0 a80a 01 c0 a80a 05
UDP头:0c01 00 a2 00 67 04 bb
余下部分全为SNMP报文内容,这里我们做一下简单的约定:
xx 标注类型;xx 标注长度;xx 标注真正的数据。
这样一来上面这串原始数据就好分析多了J
n 30 5d 整个SNMP报文的编码方式为30,即SEQUENCE类型,报文长度93(0x5d)字节;
n 02 01 01 版本号01即v2版本;
n 04 06 70 75 62 6c 69 63 团体名70 75 62 6c69 63 即英文的“public”;
n a7 50 a7表示trap类型为7,即厂商自定义trap;50表示PDU区段占80(0x50)字节;
n 02 04 17 73 2c fb 请求ID为17 73 2c fb 十进制的393424123;
n 02 01 00 错误状态0;
n 02 01 00 错误索引0;
n 30 42 “变量名-值”对编码类型30 即SEQUENCE类型;“变量名-值”所占总字节0x42,即66字节;
n 30 0d 06 08 2b 06 01 02 01 01 03 00 43 01 0e 第一个“名-值”对区段编码方式30 即SEQUENCE类型;第一个“名-值”对总长度0x0d,13字节;第一个变量名的编码类型0x06,时间标签;第一个变量名占0x08个字节;第一个变量名2b 06 01 02 01 01 03 00,为1.3.6.1.2.1.1.3.0;第一个变量值为0x0e,即14;
n 30 17 06 0a2b 06 01 06 03 01 01 04 0100 06 09 2b 06 01 06 03 01 01 05 01 第二个“名-值”对;变量名1.3.6.1.6.3.1.1.4.1.0;变量值1.3.6.1.6.3.1.1.5.1;
n 30 18 06 0a2b 06 01 06 03 01 01 04 0300 06 0a2b 06 01 04 01 bf 08 03 02 0a 第三个“名-值”对;变量名1.3.6.1.6.3.1.1.4.3.0;变量值1.3.6.1.4.1.8072.3.2.10;