snmp v1 get请求响应c++实现

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 代码库

github.com/xcyxiner/sn...

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...

luca.ntop.org/Teaching/Ap...

第一篇链接里有如下的资料,前面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);

4.2.6 测试结果见前文,这里不补充了

相关推荐
jyan_敬言16 分钟前
【C++】入门基础(二)引用、const引用、内联函数inline、nullptr
c语言·开发语言·数据结构·c++·青少年编程·编辑器
UpUpUp……17 分钟前
模拟String基本函数/深浅拷贝/柔性数组
开发语言·c++·算法
攻城狮7号2 小时前
【第九节】windows sdk编程:通用控件的使用
c++·windows·windows编程·windows sdk
曦月逸霜2 小时前
第十次CCF-CSP认证(含C++源码)
数据结构·c++·算法·ccf-csp
眠りたいです3 小时前
Linux:利用System V系列的-共享内存,消息队列实现进程间通信
linux·运维·服务器·c++·进程间通信
moz与京3 小时前
【附JS、Python、C++题解】Leetcode面试150题(9)——三数之和
javascript·c++·leetcode
nqqcat~3 小时前
函数的引用/函数的默认参数/函数的占位参数/函数重载
开发语言·c++·算法
daily_23333 小时前
c++领域展开第十六幕——STL(vector容器的了解以及各种函数的使用)超详细!!!!
开发语言·c++·vector·visual studio code
努力学习的小廉3 小时前
【C++】 —— 笔试刷题day_5
开发语言·c++