实验要求:
- 实现一个 MIB 信息读取程序(功能与 MIB Browser 类似)
- 输入 IP 地址和 OID,能够连接指定网络设备(可以把自己机器上的 SNMP Agent 配置好,或者链接本机上的虚拟机),获取对应 OID 的 Value。
- 输入 IP 地址和 OID,返回该 OID 对应的子树。
- 输入 IP 地址,检索返回地址段内所有机器的机器名。
编程平台和环境
本实验编程工具使用的是PyCharm Community Edition 2023.3.4,python的环境安装的是Python 3.10.4
因为本实验涉及到要调用SNMP 服务相关的库来实现功能,所以需要提前安装好snmp的库pysnmp
其次本实验要求面向用户 GUI 界面展示,所以需要安装好Tkinter 库
程序结构
该程序只有一个代码文件(mib-try.py),代码整体上有四个模块(3个MIB信息获取程序的功能函数,1个实现GUI界面展示的模块)
SNMP GET 请求(snmp_get(ip, oid))
通过 SNMP 协议从网络设备上获取特定 OID 的值。它接受目标设备的 IP 地址和要获取的 OID 作为参数,然后发送 SNMP GET 请求,并处理返回的结果。如果请求成功并且没有错误,它会将获取到的值作为字符串返回;如果发生了错误,它会返回相应的错误信息。
"""SNMP GET 请求"""
def snmp_get(ip, oid):
try:
errorIndication, errorStatus, errorIndex, varBinds = next(
#调用 next() 函数获取 getCmd() 的下一次执行结果
getCmd(SnmpEngine(),
CommunityData('public', mpModel=0), #SNMP 引擎
#指定目标设备的 IP 地址和 SNMP 的标准 UDP 端口161
UdpTransportTarget((ip, 161)),
ContextData(),
#用于指定要获取的 OID
ObjectType(ObjectIdentity(oid)))
)
if errorIndication:
result = str(errorIndication)
elif errorStatus:
result = '%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'
)
else:
result = ""
for varBind in varBinds: #遍历变量绑定列表
# Only take the value part
result += varBind[1].prettyPrint() + '\n'
# 去除字符串两端的空白字符和换行符
result = result.strip()
# 检查结果字符串是否以 ‘0x’ 开头
if result.startswith('0x'):
# 去除开头的 '0x'
result = result[2:]
# 将 16 进制字符串解码为字节对象
result_bytes = bytes.fromhex(result)
# 将字节对象解码为字符串
result = result_bytes.decode('utf-8')
return result
except Exception as e:
return str(e)
- getCmd: 这个函数是 PySNMP 库中的一个函数,用于发送 SNMP GET 请求,并返回响应。创建一个 SnmpEngine 实例,是 SNMP 引擎,负责处理SNMP通讯。
- SnmpEngine 实例:SnmpEngine 是 PySNMP 库中用于处理 SNMP 通讯的核心类之一。在 SNMP中,SNMP 引擎负责管理和执行 SNMP 操作,包括发送和接收 SNMP 消息,处理消息的编码和解码等
- CommunityData:CommunityData 实例用于指定 SNMP 操作中所使用的社区字符串(Community String),它是 SNMP 协议中一种简单的认证机制。在 SNMPv1 和 SNMPv2c 中,社区字符串类似于密码,用于验证请求的合法性
- UdpTransportTarget((ip, 161):UdpTransportTarget 实例用于指定 SNMP 操作中目标设备的 IP 地址和端号,以便与目标设备建立通信连接。在 SNMP 中通常使用 UDP 协议进行通讯,而标准的 SNMP 端口号是 161。
- ObjectType:ObjectType 类:用于表示 SNMP 操作中要获取或设置的一个或多个 OID 值。它通常用于构建 SNMP 操作的请求消息,以指定要操作的数据对象。ObjectType 实例可以包含一个或多个 OID,每个 OID 都可以有一个或多个相关联的值。
在完成的snmp_get函数时遇到了输出结果是ASCII码的问题,通过print检查是在一开始函数得到result的时候就是ASCII码。
所以我选择在snmp_get函数直接添加ASCII码转字符串的代码,使得函数的返回值就是ASCII码。
SNMP WALK 操作遍历实体(snmp_walk(ip, oid))
通过 SNMP 协议与网络设备交互,以执行 SNMP WALK 操作。自动遍历特定设备的管理信息基 (MIB) 树,从开始的 OID(由输入参数 oid 指定)开始,收集所有相关的信息,直到该分支结束。主要使用 PySNMP 库实现,通过 SNMP WALK 获取设备的详细配置和状态信息。函数接受两个参数:目标设备的 IP 地址 (ip) 和起始 OID (oid)。它首先建立与目标设备的连接,然后从指定的起始 OID 开始遍历 MIB 树,收集每个 OID 及其对应值。
最后返回的result是一个多行文本(在GUI窗口中涉及多汗文本输出的问题)
"""使用 SNMP WALK 操作遍历实体"""
def snmp_walk(ip, oid):
try:
results = ""
#使用 nextCmd() 函数开始一个 SNMP WALK 操作
for (errorIndication, errorStatus, errorIndex, varBinds) in nextCmd(
SnmpEngine(), #SNMP 引擎
#创建 CommunityData 实例进行认证
CommunityData('public', mpModel=0),
UdpTransportTarget((ip, 161)),
ContextData(),
ObjectType(ObjectIdentity(oid)), #指定开始的 OID
lexicographicMode=False #防止 SNMP WALK 操作越过所需的 MIB 树范围
):
if errorIndication:
results = str(errorIndication)
break # Exit on first error
elif errorStatus:
results = '%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'
)
break # Exit on first error
else:
for varBind in varBinds:
# 解码OID和值,并将它们以所需格式添加到结果中
oid_part = varBind[0].prettyPrint()
value_part = varBind[1].prettyPrint()
# 去除值部分两端的引号
value_part = value_part.strip('"')
# 如果值是十六进制编码,则解码为字符串
if value_part.startswith('0x'):
value_part = bytes.fromhex(value_part[2:]).decode('utf-8')
results += f"{oid_part} = \"{value_part}\"\n"
return results
except Exception as e:
return str(e)
- nextCmd():nextCmd() 函数属于 PySNMP 库,用于非阻塞式地遍历指定的 OID 范围,并可以继续进行直到目标设备的 MIB 树中没有更多的继承数据为止。这个函数在每次迭代中返回一个四元组 (errorIndication, errorStatus, errorIndex, varBinds)
- UdpTransportTarget((ip, 161)):设置 lexicographicMode用于控制和限制遍历的行为,确保操作不会越过预定的 MIB(管理信息基)树范围。当 lexicographicMode 设置为 False 时,这将指导 SNMP WALK 操作仅在特定的分支或区域内进行遍历,而不是整棵树。
在实现snmp_walk函数时仍然需要增加ASCII码转换的功能-
在一个给定的 IP 范围内扫描设备(snmp_scan(start_ip, end_ip))
通过SNMP(简单网络管理协议)扫描一个指定的IP地址范围,尝试收集各设备的主机名。首先,它将传入的起始和终止IP地址转换成可以进行数值操作的IP地址对象。然后,代码遍历这个范围内的每个IP地址,对每一个地址使用SNMP的GET请求来查询设备的系统名称(通过OID '1.3.6.1.2.1.1.5.0’标识)。根据查询结果,代码会记录下每个IP地址的主机名或标记为未知(如果查询失败或设备不响应)。
"""在一个给定的 IP 范围内扫描设备"""
def snmp_scan(start_ip, end_ip):
result = ""
"""使用 ipaddress.ip_address() 函数
将 start_ip 字符串转换为一个 IP 地址对象,
便于进行数值计算和格式转换"""
start = ipaddress.ip_address(start_ip)
end = ipaddress.ip_address(end_ip)
oid = '1.3.6.1.2.1.1.5.0' #获取设备主机名的对象标识符
#遍历每个 IP 地址
for ip_int in range(int(start), int(end) + 1):
ip_str = str(ipaddress.ip_address(ip_int))#获取其字符串表示形式,以便于后续的 SNMP 调用
#获取当前 ip_str 设备上与 oid 对应的主机名
hostname = snmp_get(ip_str, oid)
if "No Such" not in hostname: #没有从目标设备获取到任何信息
result += f"IP: {ip_str}, Hostname: {hostname}\n"
else:
result += f"IP: {ip_str}, Hostname: Unknown\n"
return result
- ipaddress.ip_address():在Python的 ipaddress 模块中,ip_address() 函数用于将一个字符串形式的IP地址转换为一个IP地址对象。这样的处理对于进行IP地址的数值计算和格式转换非常方便和重要。
- range(int(start), int(end) + 1):将起始IP转换为一个IP地址对象后,为了能在起始IP和结束IP之间进行遍历,需要先将这些IP地址对象转换成整数,然后就可以使用range(int(start), int(end) + 1)生成一个整数范围,来遍历每一个可能的IP地址值
- 字符串内插(f-string):使用f-string时,只需在字符串前加上f或F前缀,并将需要插入的变量或表达式放入花括号{}中。Python运行时会自动替换这些花括号及其内容为相应变量的值或表达式的结果。
图形用户界面(class SNMPToolApp)
定义了一个简单的图形用户界面(GUI)应用程序用于执行与简单网络管理协议(SNMP)操作有关的任务。
是使用Python的Tkinter库构建的,允许用户输入目标IP地址、对象标识符(OID)以及IP范围,然后选择要执行的操作,包括查询单个OID、查询OID子树或扫描一系列IP地址。 应用程序的主界面通过标签、文本框、单选按钮和按钮组件来收集用户的输入。用户利用这些输入可以执行三中类型的SNMP操作:query_oid(查询单个OID的值)、query_subtree(查询OID子树中的多个值)和scan_ips(扫描IP范围内的设备)。执行操作后的结果会显示在一个文本区域中,为用户提供即时的反馈。
具体来说,当用户按下“Run Operation”按钮时,程序会根据用户选择的操作(通过单选按钮选择),调用相应的方法——query_oid,query_subtree,或scan_ips。这些方法负责执行实际的SNMP操作,并将结果返回,最后通过display_result方法把结果输出到GUI的文本显示区域。
# GUI Application
class SNMPToolApp:
def __init__(self, master):
self.master = master
master.title("SNMP Tool")
master.configure(bg="#ADD8E6")
master.geometry("578x600") # 主窗口大小
# IP和OID的输入
tk.Label(master, text="Target IP:", bg="#ADD8E6", fg="black").grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.target_ip_entry = tk.Entry(master, width=40)
self.target_ip_entry.grid(row=0, column=1, padx=10, pady=5)
tk.Label(master, text="OID:", bg="#ADD8E6", fg="black").grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.oid_entry = tk.Entry(master, width=40)
self.oid_entry.grid(row=1, column=1, padx=10, pady=5)
tk.Label(master, text="Start IP:", bg="#ADD8E6", fg="black").grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.start_ip_entry = tk.Entry(master, width=40)
self.start_ip_entry.grid(row=2, column=1, padx=10, pady=5)
tk.Label(master, text="End IP:", bg="#ADD8E6", fg="black").grid(row=3, column=0, sticky="w", padx=10, pady=5)
self.end_ip_entry = tk.Entry(master, width=40)
self.end_ip_entry.grid(row=3, column=1, padx=10, pady=5)
# Radio buttons for selecting the operation
self.operation = tk.StringVar()
self.operation.set("query_oid")
tk.Radiobutton(master, text="Query OID", variable=self.operation, value="query_oid", bg="#ADD8E6").grid(row=4, column=0, padx=10, sticky="w")
tk.Radiobutton(master, text="Query Subtree", variable=self.operation, value="query_subtree", bg="#ADD8E6").grid(row=4, column=1, padx=10, sticky="w")
tk.Radiobutton(master, text="Scan IP Range", variable=self.operation, value="scan_ips", bg="#ADD8E6").grid(row=4, column=2, padx=10, sticky="w")
# Run button
tk.Button(master, text="Run Operation", command=self.run_operation, bg="#90EE90").grid(row=5, column=0, columnspan=3, padx=10, pady=10, sticky="ew")
# Result display
self.result_text = tk.Text(master, height=28, width=79)
self.result_text.grid(row=6, column=0, columnspan=3, padx=10, pady=10)
def run_operation(self):
operation = self.operation.get()
if operation == "query_oid":
self.query_oid()
elif operation == "query_subtree":
self.query_subtree()
elif operation == "scan_ips":
self.scan_ips()
def query_oid(self):
ip = self.target_ip_entry.get()
oid = self.oid_entry.get()
result = snmp_get(ip, oid)
self.display_result(result)
def query_subtree(self):
ip = self.target_ip_entry.get()
oid = self.oid_entry.get()
result = snmp_walk(ip, oid)
self.display_result(result)
def scan_ips(self):
start_ip = self.start_ip_entry.get()
end_ip = self.end_ip_entry.get()
result = snmp_scan(start_ip, end_ip)
self.display_result(result)
def display_result(self, result):
self.result_text.delete(1.0, tk.END)
self.result_text.insert("end", "结果: {}\n".format(result))
def main():
root = tk.Tk()
app = SNMPToolApp(root)
root.mainloop()
if __name__ == "__main__":
main()
尤其需要注意的是当结果有多条记录时就涉及到多行文本的输出,在调试代码的过程中,我反复遇到只显示一条记录或者记录首尾相连的情况,通过注释掉GUI窗口的代码,正常打印结果可以发现时可以整长打印结果的,所以判断问题出在GUI窗口部分。
.insert("end", "结果: {}\n".format(result))是一个方法调用,用于将文本插入到Text控件中。这个方法同样接受两个参数:插入的位置和要插入的文本。
"end"指定了文本的插入位置,表示新的文本将被添加到现有文本的末尾。这是为了保持结果的顺序,让新的结果总是出现在最后。
"结果: {}\n".format(result)是要插入的文本字符串。这里使用了.format()方法进行字符串格式化,{}作为占位符被result的值所替代。结果字符串后面跟着一个换行符\n,确保后续的结果(如果有)将从新的一行开始。
测试结果
如下图所示,是运行代码出现的窗口,提供了目标IP地址、OID、起始IP地址、结束IP地址的四个输入框,以及Query OID、Query Subtree、Scan IP Range三个功能选项按钮,一个Run Operation的运行按钮和一个结果显示窗。
SNMP GET 请求(单个OID查询)
输入目标主机的IP地址以及想要查询的OID,选择Query OID,点击Run Operation,可以显示查询到的OID 的value。
这里注意OID必须是某个具体实例的OID。否则会报错。
SNMP WALK 操作(OID子树查询)
输入目标主机的IP地址以及想要查询的OID,选择Query Subtree,点击Run Operation,可以显示查询到的该OID下面的子树,举例如图所示,输入的OID是{ 1.3.6.1.2.1.2.2.1.2 },这个对象是指网络接口的描述信息,它的子树就应该是网络设备上每个接口的描述,有多少个接口就有多少条记录。
这里输入的OID必须是有子树才可以看到结果,也即是不能输入某一个实例的OID。
扫描IP范围内的设备
当我开启手机热点,一台主机(192.168.43.19)连接到我的热点上,输入扫描的范围192.168.43.18~ 192.168.43.20,选择Scan IP Range,点击Run Operation,可以看到得到了IP地址为192.168.43.19的设备名称“DESKTOP-EO97O97”,而并没有ip地址为192.168.43.18和192.168.43.20的主机连接到我的热点,所以不会得到回复。