SNMPのパケットキャプチャ＆リプレイ

SNMP(UDP)のパケットキャプチャ＆リプレイのコードを書いてみました。
普通の人はSNMP-Simulatorを使うのが良いかと思いますが。。

# 使い方

1. 192.168.1.119でSNMP-Agentを起動
1. snmp_stub.pyをcaptureモードで実行
2. snmpwalk -c public -v 1 192.168.1.12を実行
3. captureファイルが作成されたことを確認する
1. 192.168.1.119でSNMP-Agentを停止
2. snmp_stub.pyをreplayモードで実行
3. snmpwalk -c public -v 1 192.168.1.12を実行
4. SNMP-Agentが起動している時と同等の結果が返ってくる

# 環境
OS X El Capitan
Python 2.7.11
snmpwalk 5.6.2.1
自PC：192.168.1.12
SNMP取得先：192168.1.119

# 実行結果

```lang:snmp_stub.pyの実行
$ sudo python snmp_stub.py

Requested: 0%26%02%01%00%04%06public%A1%19%02%04%3E%0B%CC%E5%02%01%00%02%01%000%0B0%09%06%05%2B%06%01%02%01%05%00 192.168.1.12 65344
Responsed: 0%81%AA%02%01%00%04%06public%A2%81%9C%02%04%3E%0B%CC%E5%02%01%00%02%01%000%81%8D0%81%8A%06%08%2B%06%01%02%01%01%01%00%04%7EDarwin%20macbook.local%2010.8.0%20Darwin%20Kernel%20Version%2010.8.0%3A%20Tue%20Jun%20%207%2016%3A33%3A36%20PDT%202011%3B%20root%3Axnu-1504.15.3%7E1/RELEASE_I386%20i386 192.168.1.12 65344
---------------------------
Requested: 0%29%02%01%00%04%06public%A1%1C%02%04%3E%0B%CC%E6%02%01%00%02%01%000%0E0%0C%06%08%2B%06%01%02%01%01%01%00%05%00 192.168.1.12 65344
Responsed: 03%02%01%00%04%06public%A2%26%02%04%3E%0B%CC%E6%02%01%00%02%01%000%180%16%06%08%2B%06%01%02%01%01%02%00%06%0A%2B%06%01%04%01%BF%08%03%02%10 192.168.1.12 65344
---------------------------
＜省略＞
```

```lang:snmpwalkの実行
$ snmpwalk -c public -v 1 192.168.1.12
SNMPv2-MIB::sysDescr.0 = STRING: Darwin macbook.local 10.8.0 Darwin Kernel Version 10.8.0: Tue Jun  7 16:33:36 PDT 2011; root:xnu-1504.15.3~1/RELEASE_I386 i386
SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.16
DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (27736720) 3 days, 5:02:47.20
SNMPv2-MIB::sysContact.0 = STRING: SysAdmin
＜省略＞
```

# ソースコード

```lang:snmp_stub.py
import socket
import base64
import urllib

UDP_IP_LISTEN = "192.168.1.12"
UDP_PORT_LISTEN = 161
UDP_IP_TO = "192.168.1.119"
UDP_PORT_TO = 161
BUFFER_SIZE = 4096

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
sock.bind((UDP_IP_LISTEN, UDP_PORT_LISTEN))

cache = []
captured_data = []
captured_data_dic = {}

stub_mode = 'replay'
#stub_mode = 'capture'

if stub_mode == 'capture':
    f = open('capture', 'w')
elif stub_mode == 'replay':
    f = open('capture', 'r')
    captured_data = f.readlines()
    for i in range(len(captured_data)):
        if 'Requested' in captured_data[i]:
           key = urllib.unquote(captured_data[i+3].rstrip())
           value = urllib.unquote(captured_data[i+3+4].rstrip())
           key = key[:17] + 'XXXX' + key[21:]
           captured_data_dic[key] = value
           #print value, key

while True:
    if stub_mode == 'capture':
        # send to remote snmp
        data_request, (host_request, port_request) = sock.recvfrom(BUFFER_SIZE)
        while data_request in cache:
            data_request, (host_request, port_request) = sock.recvfrom(BUFFER_SIZE)
            print 'skipped due to duplicated'
        cache.append(data_request)
        if len(cache) > 10:
            cache = cache[1:]
        print 'Requested:', urllib.quote(data_request), host_request, port_request

        f.write(urllib.quote('Requested data') + '\n')
        f.write(urllib.quote(host_request) + '\n')
        f.write(urllib.quote(str(port_request)) + '\n')
        f.write(urllib.quote(data_request) + '\n')
        sock.sendto(data_request, (UDP_IP_TO, UDP_PORT_TO))

        # receive from remote snmp
        data_response, (host_response, port_response) = sock.recvfrom(BUFFER_SIZE)
        while data_response in cache:
            data_response, (host_response, port_response) = sock.recvfrom(BUFFER_SIZE)
            print 'skipped due to duplicated'
        cache.append(data_response)
        if len(cache) > 10:
            cache = cache[1:]
      
        f.write(urllib.quote('Responsed data') + '\n')
        f.write(urllib.quote(host_response) + '\n')
        f.write(urllib.quote(str(port_response)) + '\n')
        f.write(urllib.quote(data_response) + '\n')
        f.write('\n')
        sock.sendto(data_response, (UDP_IP_LISTEN, port_request))
        print 'responsed:', urllib.quote(data_response), UDP_IP_LISTEN, port_request
        print '---------------------------'

    elif stub_mode == 'replay':
        data_request, (host_request, port_request) = sock.recvfrom(BUFFER_SIZE)
        while data_request in cache:
          data_request, (host_request, port_request) = sock.recvfrom(BUFFER_SIZE)
          print 'skipped due to duplicated'
        cache.append(data_request)
        if len(cache) > 0:
            cache = cache[1:]
        print 'Requested:', urllib.quote(data_request), host_request, port_request
      
        request_id = data_request[17:21]

        data_request = data_request[:17] + 'XXXX' + data_request[21:]
        ret_data = captured_data_dic[data_request]
        if ord(ret_data[1]) >= 128:
            length = ord(ret_data[1]) - 128
        else:
            length = 0
      
        if ord(ret_data[1+length+13]) >= 128:
            length2 = ord(ret_data[1+length+13]) - 128
        else:
            length2 = 0
      
        pos = 1+length+13+length2+3
        ret_data = ret_data[:pos] + request_id + ret_data[pos+4:]

        sock.sendto(ret_data, (UDP_IP_LISTEN, port_request))
        print 'Responsed:', urllib.quote(ret_data), UDP_IP_LISTEN, port_request
        print '---------------------------'

    else:
        break
```

共通の解説
解説1
今回は1つのネットワークしか使わないので、ソケットは1つのみ作成。
もしLISTENとTOで分けたいのであれば、それぞれのソケットを作成し、それに合わせたフォワーディングルールを書く必要があります。

capture modeの解説

解説1
処理概要は、1リクエストずつ処理することを想定しています。
つまり、snmpwalkから1リクエストが来たら、本物のsnmp-agentに1リクエストを転送し、
本物のsnmp-agentから応答が来たら、snmpwalkに1リクエストを転送する、を
1パケットずつ処理しています。
snmpwalkのパケットの流れを見たところ、1リクエストずつ処理をしていたので、このような処理方法で問題なさそうです。
ただし、応答が遅いとすぐに再送されてしまうため、再送対策は必要です。

解説2
cacheは、UDP固有のされたパケットを無視する為。
とりあえず、過去10件のパケットを記録し、同じリクエストが来たら再送とみなし
次のリクエストを待つようにしています。

replay modeの解説

解説1
再送対策のコードをここにも入れていますが、実は無効化されています。もし、再送に悩まされたら0を適宜増やしてください。



# 参考
https://tools.ietf.org/html/rfc1592