1背景
第一篇文章查了rfc的文档,第二篇用wireshark来抓包,这一篇轮到具体实现了。
2最终效果
2.1 IReasoning MIB Browser 测试
js
模拟 localhost,v1,OID 为 .1.3.6.1.2.1.1.5.0 的get请求 (system 下的 sysName)
返回 Name/OID: sysName.0; Value (OctetString): Simple SNMP Agent v1.0
2.2 snmpget 命令行测试
www.net-snmp.org/docs/man/sn...
js
$ snmpget -v1 -c public localhost system.sysName
SNMPv2-MIB::sysName.0 = STRING: Simple SNMP Agent v1.0
3 github 代码库
PS:使用了vscode的 c++插件(需要借助vs2019),具体可以看欢迎页的 c++演练
运行后的效果
4 详细步骤
4.1 winsock 教程还是直接抄微软官方文档就行,这个基本都一样。
learn.microsoft.com/zh-cn/windo...
主要步骤
markdown
- [为服务器](https://learn.microsoft.com/zh-cn/windows/win32/winsock/creating-a-socket-for-the-server) 创建套接字
- [绑定套接字](https://learn.microsoft.com/zh-cn/windows/win32/winsock/binding-a-socket)
- [侦听套接字](https://learn.microsoft.com/zh-cn/windows/win32/winsock/listening-on-a-socket)
- [接受连接](https://learn.microsoft.com/zh-cn/windows/win32/winsock/accepting-a-connection)
- [在服务器上接收和发送数据](https://learn.microsoft.com/zh-cn/windows/win32/winsock/receiving-and-sending-data-on-the-server)
- [断开服务器](https://learn.microsoft.com/zh-cn/windows/win32/winsock/disconnecting-the-server) 的连接
c++
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_PORT "161"
#define DEFAULT_IP "127.0.0.1"
#define DEFAULT_COMMUNITY "public"
#define RECV_BUFFER_SIZE 1024
#define SEND_BUFFER_SIZE 1024
int main(void)
{
WSADATA wsaData;
SOCKET sockfd;
struct addrinfo hints, *servinfo;
int rv;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
std::cout << "Error in WSAStartup" << WSAGetLastError() << std::endl;
return 1;
}
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
if ((rv = getaddrinfo(NULL, DEFAULT_PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo:%d\n", rv);
WSACleanup();
return 1;
}
if ((sockfd = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)) == INVALID_SOCKET)
{
std::cout << "Error in socket" << WSAGetLastError() << std::endl;
freeaddrinfo(servinfo);
WSACleanup();
return 1;
}
if (bind(sockfd, servinfo->ai_addr, (int)servinfo->ai_addrlen) == SOCKET_ERROR)
{
std::cout << "Error in bind" << WSAGetLastError() << std::endl;
closesocket(sockfd);
freeaddrinfo(servinfo);
WSACleanup();
return 1;
}
freeaddrinfo(servinfo);
std::cout << "Waiting for a client..." << std::endl;
struct sockaddr_storage client_addr;
int addr_len;
unsigned char recv_buffer[RECV_BUFFER_SIZE];
unsigned char send_buffer[SEND_BUFFER_SIZE];
while (1)
{
addr_len = sizeof(client_addr);
int numbytes = recvfrom(sockfd, (char *)recv_buffer, RECV_BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);
if (numbytes == SOCKET_ERROR)
{
std::cout << "Error in recvfrom" << WSAGetLastError() << std::endl;
continue;
}
int resp_len = build_snpm_response(recv_buffer, numbytes, send_buffer);
if (resp_len > 0)
{
if (sendto(sockfd, (char *)send_buffer, resp_len, 0, (struct sockaddr *)&client_addr, addr_len) == SOCKET_ERROR)
{
std::cout << "Error in sendTo" << WSAGetLastError() << std::endl;
}
}
}
closesocket(sockfd);
WSACleanup();
return 0;
}
4.2 重点解析 build_snpm_response
4.2.1 从wireshark上看完整的接收数据如下所示
4.2.2 按照字节填写对应的response
这里第一个是十六进制的30,这个翻了下rfc没找到,然后找到一点ASN1的资料。
learn.microsoft.com/en-us/windo...
从下面的图片能看到这里0x30是一个固定标签编码
所以在代码里我记录了下用到的几个基础类型
arduino
// 跨协议标准
#define ASN1_SEQUENCE 0x30
#define ASN1_INTEGER 0x02
#define ASN1_OCTET_STRING 0x04
#define ASN1_OBJECT_ID 0x06
按这个套路,可以走到下图的data前
30 29 02 01 00 04 06 70 75 62 6c 69 63 分别标记 TLV
scss
30(SEQUENCE) 29(字节长度)
02(INTEGER) 01(字节长度) 00(版本号-1)
04(OCTET_STRING) 06(字节长度) 70 75 62 6c 69 63 (public)
对应的代码如下
c++
unsigned char *ptr = response;
int total_len = 0;
*ptr++ = ASN1_SEQUENCE;
unsigned char *len_ptr = ptr++;
// version -- version-1 for this RFC
// INTEGER {
// version-1(0)
// },
*ptr++ = ASN1_INTEGER;
*ptr++ = 0x01;
*ptr++ = SNMP_VERSION_1;
// community -- community name
// OCTET STRING,
*ptr++ = ASN1_OCTET_STRING;
*ptr++ = (unsigned char)strlen(DEFAULT_COMMUNITY);
memcpy(ptr, DEFAULT_COMMUNITY, strlen(DEFAULT_COMMUNITY));
ptr += strlen(DEFAULT_COMMUNITY);
// data skip
4.2.3 处理pdu
从前面的wireshark上能看到pdu开头的字节是A0,对应的get请求,但是怎么来的,从4.2.2中的微软的文档没找到来源,又找到另外两篇资料
letsencrypt.org/docs/a-warm...
第一篇链接里有如下的资料,前面4.2.2用的其实是Universal(所以前面定义的时候 我补了一个 跨协议标准,pdu实际上用了一个 上下文的标签)
第一篇用谷歌翻译后的截图如下,再差下之前的rfc协议,能看到没有APPLICATION限定词,那就是上下文了
上面用了8和7位,按二进制10来计算实际的值如下,最终值为80。
然后是第6位,PDU查看RFC,是 SEQUENCE,并且有IMPLICIT,构造位这里也是1
综合上面8和7位,以及6位,最终的值如下所示,最终值A0,这个也是wireshark中看到的标签。
按上面的规则,我在代码声明的时候补充了如下,剩下的5位是索引,这个没啥好解释的,直接抄就行
c++
// 临时字段标识 7-8
#define ASN1_CONTEXT_SPECIFIC (0B10 << 6)
// 结构化 6
#define ASN1_CONSTRUCTED (0B01 << 5)
#define SNMP_VERSION_1 0
// GetRequest-PDU ::=
// [0]
// IMPLICIT PDU
#define SNMP_GET_REQUEST ASN1_CONTEXT_SPECIFIC + ASN1_CONSTRUCTED + 0
// GetResponse-PDU ::=
// [2]
// IMPLICIT PDU
#define SNMP_GET_RESPONSE ASN1_CONTEXT_SPECIFIC + ASN1_CONSTRUCTED + 2
下面的 SNMP_GET_RESPONSE 就是 0xA2,(wireshark索引是get的,之后还有next,然后才是response)
ini
// PDU
*ptr++ = SNMP_GET_RESPONSE;
unsigned char *pdu_len_ptr = ptr++;
上面的处理完后,就是之前默认的ASAN1的标签了
4.2.4 PDU下的request_id
为啥提这个,因为 IReasoning MIB Browser 是4个字节长度,而 snmpget 是两个字节的长度,因为时间来不及处理,就直接读取了原有的get请求下的长度,然后再拷贝. 正常应该有个解析专门处理request的,但是没来得及处理
c++
// request id
*ptr++ = ASN1_INTEGER;
if(request[16] == 0x02){
*ptr++ = 0x02;
*ptr++ = request[17];
*ptr++ = request[18];
}
if(request[16] == 0x04){
*ptr++ = 0x04;
*ptr++ = request[17];
*ptr++ = request[18];
*ptr++ = request[19];
*ptr++ = request[20];
}
4.2.5 PDU下的 OID
OID有一套字节的规则,RFC里提到的MIB就有。转换和查询的还没来及检查,先随机绑定一个字符串回去。
c++
// variable-bindings -- values are sometimes ignored
// VarBindList
// -- variable bindings
// VarBind ::=
// SEQUENCE {
// name
// ObjectName,
// value
// ObjectSyntax
// }
// VarBindList ::=
// SEQUENCE OF
// VarBind
*ptr++ = ASN1_SEQUENCE;
unsigned char *var_len_ptr = ptr++;
// 单个变量绑定
*ptr++ = ASN1_SEQUENCE; // SEQUENCE
unsigned char *item_len_ptr = ptr++; // 项长度
// OID
*ptr++ = ASN1_OBJECT_ID;
*ptr++ = sizeof(sysDescrOID);
memcpy(ptr, sysDescrOID, sizeof(sysDescrOID));
ptr += sizeof(sysDescrOID);
// 值
*ptr++ = 0x04; // OCTET STRING
*ptr++ = (unsigned char)strlen(sysDescr);
memcpy(ptr, sysDescr, strlen(sysDescr));
ptr += strlen(sysDescr);