├── setup.py ├── pytest_snmpserver ├── pytest_plugin.py ├── config.py ├── .gitignore ├── tests │ ├── test_plugin.py │ └── test_server.py └── snmp_server.py ├── LICENSE ├── README.rst └── snmp-server.py /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name="pytest_snmpserver", 7 | version="0.1.6", 8 | packages=["pytest_snmpserver"], 9 | long_description="SNMP server as a pytest plugin", 10 | long_description_content_type="text/markdown", 11 | python_requires=">=3.6", 12 | install_requires=[], 13 | entry_points={"pytest11": ["pytest_snmpserver = pytest_snmpserver.pytest_plugin"]}, 14 | classifiers=[ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /pytest_snmpserver/pytest_plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | 4 | import pytest 5 | from .snmp_server import SNMPServer 6 | 7 | @pytest.fixture 8 | def snmpserver(): 9 | host = os.environ.get("PYTEST_SNMPSERVER_HOST") 10 | port = os.environ.get("PYTEST_SNMPSERVER_PORT") 11 | if port: 12 | port = int(port) 13 | 14 | if not host: 15 | host = SNMPServer.DEFAULT_LISTEN_HOST 16 | if not port: 17 | port = SNMPServer.DEFAULT_LISTEN_PORT 18 | 19 | with SNMPServer(host, port) as server: 20 | t = threading.Thread(target=server.process_request) 21 | t.daemon = True 22 | t.start() 23 | yield server 24 | -------------------------------------------------------------------------------- /pytest_snmpserver/config.py: -------------------------------------------------------------------------------- 1 | # SNMP server response config example 2 | 3 | def my_response(oid): 4 | res = '|'.join(oid.split('.')) 5 | return octet_string('response: {}'.format(res)) 6 | 7 | 8 | DATA = { 9 | '1.3.6.1.4.1.1.1.0': integer(12345), 10 | '1.3.6.1.4.1.1.2.0': bit_string('\x12\x34\x56\x78'), 11 | '1.3.6.1.4.1.1.3.0': octet_string('test'), 12 | '1.3.6.1.4.1.1.4.0': null(), 13 | '1.3.6.1.4.1.1.5.0': object_identifier('1.3.6.7.8.9'), 14 | '1.3.6.1.4.1.1.6.0': real(1.2345), 15 | '1.3.6.1.4.1.1.7.0': double(12345.2345), 16 | # integer enumeration 17 | '1.3.6.1.4.1.1.8.0': integer(1, enum=[1, 2, 3]), 18 | # notice the wildcards in the next OIDs: 19 | '1.3.6.1.4.1.1.?.0': lambda oid: octet_string('? {}'.format(oid)), 20 | '1.3.6.1.4.1.2.*': lambda oid: octet_string('* {}'.format(oid)), 21 | # lambda or function, with single oid argument, can be used for response generation 22 | '1.3.6.1.4.1.1001.1.0': my_response, 23 | '1.3.6.1.4.1.1002.1.0': lambda oid: octet_string('-'.join(oid.split('.'))), 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dmitry Alimov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pytest_snmpserver/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /pytest_snmpserver/tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import shlex 3 | 4 | 5 | snmpget_command = '/usr/bin/snmpget -Oq -v2c -t 1 -c public 127.0.0.1:{port}' 6 | snmpset_command = '/usr/bin/snmpset -Oq -v2c -t 1 -c public 127.0.0.1:{port}' 7 | snmpwalk_command = '/usr/bin/snmpwalk -Oq -v2c -t 1 -c public 127.0.0.1:{port}' 8 | 9 | 10 | def test_request_replies_correctly(snmpserver): 11 | snmpserver.expect_request("1.3.6.1.2.1.2.2.1.2", "qqq") 12 | p = subprocess.Popen(shlex.split(f'{snmpget_command.format(port=snmpserver.port)} IF-MIB::ifDescr'), stdout=subprocess.PIPE) 13 | p.wait() 14 | assert 'IF-MIB::ifDescr qqq' == p.stdout.read().decode('utf-8').strip() 15 | 16 | 17 | def test_dual_request_replies_correctly(snmpserver): 18 | snmpserver.expect_request("1.3.6.1.2.1.2.2.1.8.1005", 1) # 1==up 19 | snmpserver.expect_request("1.3.6.1.2.1.2.2.1.2.1004", "some description") 20 | 21 | p = subprocess.Popen(shlex.split(f'{snmpget_command.format(port=snmpserver.port)} IF-MIB::ifOperStatus.1005 IF-MIB::ifDescr.1004'), stdout=subprocess.PIPE) 22 | p.wait() 23 | stdout = p.stdout.read().decode('utf-8') 24 | print(f'stdout: {stdout}') 25 | assert 'IF-MIB::ifOperStatus.1005 up\nIF-MIB::ifDescr.1004 some description' == stdout.strip() 26 | 27 | 28 | def test_snmp_range_correctly(snmpserver): 29 | snmpserver.expect_request("1.3.6.1.2.1.2.2.1.7.1004", [2, 3]) 30 | p = subprocess.Popen(shlex.split(f'{snmpset_command.format(port=snmpserver.port)} IF-MIB::ifAdminStatus.1004 i 7'), 31 | stdout=subprocess.PIPE, 32 | stderr=subprocess.PIPE) 33 | p.wait() 34 | assert p.returncode != 0 35 | stderr = p.stderr.read().decode('utf-8') 36 | assert 'wrongValue' in stderr 37 | 38 | def test_snmp_walk(snmpserver): 39 | snmpserver.expect_request("1.3.6.1.2.1.2.2.1.1.1004", [2, 3]) 40 | snmpserver.expect_request("1.3.6.1.2.1.2.2.1.1.1005", [2, 3]) 41 | p = subprocess.Popen(shlex.split(f'{snmpwalk_command.format(port=snmpserver.port)} IF-MIB::ifIndex'), 42 | stdout=subprocess.PIPE, 43 | stderr=subprocess.PIPE) 44 | p.wait() 45 | stdout = p.stdout.read().decode('utf-8') 46 | assert '1004' in stdout 47 | assert '1005' in stdout 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | SNMP server 2 | =========== 3 | 4 | |MIT license badge| 5 | 6 | Description: 7 | ------------ 8 | Simple SNMP server in pure Python 9 | 10 | Usage with pytest: 11 | ------------------ 12 | 13 | It is possible to use snmpserver as pytest plugin. This option requires Python >=3.6. 14 | 15 | The fixture ``snmpserver`` has the ``host`` and ``port`` attributes (which can be set via environment variables ``PYTEST_SNMPSERVER_HOST`` and ``PYTEST_SNMPSERVER_PORT``), along with the ``expect_request`` method: 16 | 17 | :: 18 | 19 | def test_request_replies_correctly(snmpserver): 20 | snmpserver.expect_request("1.3.6.1.2.1.2.2.1.2", "some description") 21 | command = shlex.split(f'{snmpget_command} {snmpserver.host}:{snmpserver.port} IF-MIB::ifDescr') 22 | p = subprocess.Popen(command, stdout=subprocess.PIPE) 23 | p.wait() 24 | assert 'IF-MIB::ifDescr some description' == p.stdout.read().decode('utf-8').strip() 25 | 26 | 27 | Standalone usage: 28 | ----------------- 29 | 30 | It is also possible to use standalone version of SNMP server, which works as an echo server if no config is passed. This version supports Python 2 and 3. 31 | 32 | :: 33 | 34 | Standalone usage: snmp-server.py [-h] [-p PORT] [-c CONFIG] [-d] [-v] 35 | 36 | SNMP server 37 | 38 | optional arguments: 39 | -h, --help show this help message and exit 40 | -p PORT, --port PORT port (by default 161 - requires root privileges) 41 | -c CONFIG, --config CONFIG 42 | OIDs config file 43 | -d, --debug run in debug mode 44 | -v, --version show program's version number and exit 45 | 46 | **Examples:** 47 | 48 | :: 49 | 50 | # ./snmp-server.py -p 12345 51 | SNMP server listening on 0.0.0.0:12345 52 | # ./snmp-server.py 53 | SNMP server listening on 0.0.0.0:161 54 | 55 | Without config file SNMP server works as a simple SNMP echo server: 56 | 57 | :: 58 | 59 | # snmpget -v 2c -c public 0.0.0.0:161 1.2.3.4.5.6.7.8.9.10.11 60 | iso.2.3.4.5.6.7.8.9.10.11 = STRING: "1.2.3.4.5.6.7.8.9.10.11" 61 | 62 | It is possible to create a config file with values for specific OIDs. 63 | 64 | Config file - is a Python script and must have DATA dictionary with string OID keys and values. 65 | 66 | Values can be either ASN.1 types (e.g. :code:`integer(...)`, :code:`octet_string(...)`, etc) or any Python lambda/functions (with single argument - OID string), returning ASN.1 type. 67 | 68 | :: 69 | 70 | DATA = { 71 | '1.3.6.1.4.1.1.1.0': integer(12345), 72 | '1.3.6.1.4.1.1.2.0': bit_string('\x12\x34\x56\x78'), 73 | '1.3.6.1.4.1.1.3.0': octet_string('test'), 74 | '1.3.6.1.4.1.1.4.0': null(), 75 | '1.3.6.1.4.1.1.5.0': object_identifier('1.3.6.7.8.9'), 76 | # notice the wildcards: 77 | '1.3.6.1.4.1.1.6.*': lambda oid: octet_string('* {}'.format(oid)), 78 | '1.3.6.1.4.1.1.?.0': lambda oid: octet_string('? {}'.format(oid)), 79 | '1.3.6.1.4.1.2.1.0': real(1.2345), 80 | '1.3.6.1.4.1.3.1.0': double(12345.2345), 81 | } 82 | 83 | :: 84 | 85 | # ./snmp-server.py -c config.py 86 | SNMP server listening on 0.0.0.0:161 87 | 88 | With config file :code:`snmpwalk` command as well as :code:`snmpget` can be used: 89 | 90 | :: 91 | 92 | # snmpwalk -v 2c -c public 0.0.0.0:161 .1.3.6.1.4.1 93 | iso.3.6.1.4.1.1.1.0 = INTEGER: 12345 94 | iso.3.6.1.4.1.1.2.0 = BITS: 12 34 56 78 3 6 10 11 13 17 19 21 22 25 26 27 28 95 | iso.3.6.1.4.1.1.3.0 = STRING: "test" 96 | iso.3.6.1.4.1.1.4.0 = NULL 97 | iso.3.6.1.4.1.1.5.0 = OID: iso.3.6.7.8.9 98 | iso.3.6.1.4.1.1.6.4294967295 = STRING: "* 1.3.6.1.4.1.1.6.4294967295" 99 | iso.3.6.1.4.1.1.9.0 = STRING: "? 1.3.6.1.4.1.1.9.0" 100 | iso.3.6.1.4.1.2.1.0 = Opaque: Float: 1.234500 101 | iso.3.6.1.4.1.3.1.0 = Opaque: Float: 12345.234500 102 | iso.3.6.1.4.1.4.1.0 = No more variables left in this MIB View (It is past the end of the MIB tree) 103 | 104 | Also :code:`snmpset` command can be used: 105 | 106 | :: 107 | 108 | # snmpset -v2c -c public 0.0.0.0:161 .1.3.6.1.4.1.1.3.0 s "new value" 109 | iso.3.6.1.4.1.1.3.0 = STRING: "new value" 110 | # 111 | # snmpget -v2c -c public 0.0.0.0:161 .1.3.6.1.4.1.1.3.0 112 | iso.3.6.1.4.1.1.3.0 = STRING: "new value" 113 | 114 | License: 115 | -------- 116 | Released under `The MIT License`_. 117 | 118 | .. |MIT license badge| image:: http://img.shields.io/badge/license-MIT-brightgreen.svg 119 | .. _The MIT License: https://github.com/delimitry/snmp-server/blob/master/LICENSE 120 | -------------------------------------------------------------------------------- /pytest_snmpserver/tests/test_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf8 -*- 3 | 4 | import unittest 5 | from pytest_snmpserver.snmp_server import * 6 | from pytest_snmpserver.snmp_server import (_parse_asn1_length, _parse_snmp_asn1, _read_byte, 7 | _read_int_len, _write_asn1_length, _write_int) 8 | 9 | try: 10 | from StringIO import StringIO 11 | except ImportError: 12 | from io import StringIO 13 | 14 | 15 | class Test(unittest.TestCase): 16 | """ 17 | Test SNMP server functions 18 | """ 19 | 20 | def test_encode_to_7bit(self): 21 | """Test encode_to_7bit""" 22 | self.assertEqual(encode_to_7bit(0x00), [0x00]) 23 | self.assertEqual(encode_to_7bit(0x7f), [0x7f]) 24 | self.assertEqual(encode_to_7bit(0x80), [0x81, 0x00]) 25 | self.assertEqual(encode_to_7bit(0xffff), [0x83, 0xff, 0x7f]) 26 | self.assertEqual(encode_to_7bit(0xffffff), [0x87, 0xff, 0xff, 0x7f]) 27 | self.assertEqual(encode_to_7bit(0xffffffff), [0x8f, 0xff, 0xff, 0xff, 0x7f]) 28 | 29 | def test_oid_to_bytes_list(self): 30 | """Test oid_to_bytes_list""" 31 | self.assertEqual(oid_to_bytes_list('iso.3.6'), [43, 6]) 32 | self.assertEqual(oid_to_bytes_list('1.3.6'), [43, 6]) 33 | self.assertEqual(oid_to_bytes_list('.1.3.6'), [43, 6]) 34 | self.assertEqual(oid_to_bytes_list('iso.3'), [43]) 35 | self.assertEqual(oid_to_bytes_list('1.3'), [43]) 36 | self.assertEqual(oid_to_bytes_list('.1.3'), [43]) 37 | with self.assertRaises(Exception): 38 | oid_to_bytes_list('') 39 | with self.assertRaises(Exception): 40 | oid_to_bytes_list('1') 41 | with self.assertRaises(Exception): 42 | oid_to_bytes_list('iso') 43 | 44 | def test_oid_to_bytes(self): 45 | """Test oid_to_bytes""" 46 | self.assertEqual(oid_to_bytes('iso.3.6'), '\x2b\x06') 47 | self.assertEqual(oid_to_bytes('1.3.6'), '\x2b\x06') 48 | self.assertEqual(oid_to_bytes('.1.3.6'), '\x2b\x06') 49 | self.assertEqual(oid_to_bytes('1.3.6.0.255'), '\x2b\x06\x00\x81\x7f') 50 | self.assertEqual(oid_to_bytes('1.3.6.0.1.2.3.4'), '\x2b\x06\x00\x01\x02\x03\x04') 51 | 52 | def test_bytes_to_oid(self): 53 | """Test bytes_to_oid""" 54 | self.assertEqual(bytes_to_oid('\x2b\x06'), '1.3.6') 55 | self.assertEqual(bytes_to_oid('\x2b\x06\x00'), '1.3.6.0') 56 | self.assertEqual(bytes_to_oid('\x2b\x06\x00\x7f'), '1.3.6.0.127') 57 | self.assertEqual(bytes_to_oid('\x2b\x06\x00\x7f\x81\x7f'), '1.3.6.0.127.255') 58 | 59 | def test_timeticks_to_str(self): 60 | """Test timeticks_to_str""" 61 | self.assertEqual(timeticks_to_str(0), '00:00:00.00') 62 | self.assertEqual(timeticks_to_str(1), '00:00:00.01') 63 | self.assertEqual(timeticks_to_str(10), '00:00:00.10') 64 | self.assertEqual(timeticks_to_str(100), '00:00:01.00') 65 | self.assertEqual(timeticks_to_str(6000), '00:01:00.00') 66 | self.assertEqual(timeticks_to_str(360000), '01:00:00.00') 67 | 68 | def test_int_to_ip(self): 69 | """Test int_to_ip""" 70 | self.assertEqual(int_to_ip(0x00000000), '0.0.0.0') 71 | self.assertEqual(int_to_ip(0x7f000001), '127.0.0.1') 72 | self.assertEqual(int_to_ip(0xffffffff), '255.255.255.255') 73 | 74 | def test_twos_complement(self): 75 | """Test twos_complement""" 76 | self.assertEqual(twos_complement(0, 1), 0) 77 | self.assertEqual(twos_complement(1, 8), 1) 78 | self.assertEqual(twos_complement(0b1111, 8), 0b1111) 79 | 80 | def test_read_byte(self): 81 | """Test _read_byte""" 82 | self.assertEqual(_read_byte(StringIO('\x00')), 0x00) 83 | self.assertEqual(_read_byte(StringIO('\xaa\xbb')), 0xaa) 84 | with self.assertRaises(Exception): 85 | _read_byte(StringIO('')) 86 | 87 | def test_read_int_len(self): 88 | """Test _read_int_len""" 89 | self.assertEqual(_read_int_len(StringIO('\x00'), 1), 0x00) 90 | self.assertEqual(_read_int_len(StringIO('\x7f'), 1), 0x7f) 91 | self.assertEqual(_read_int_len(StringIO('\x80'), 1, signed=True), -0x80) 92 | self.assertEqual(_read_int_len(StringIO('\xff\xff\xff\xff'), 4), 0xffffffff) 93 | 94 | def test_write_int(self): 95 | """Test _write_int""" 96 | self.assertEqual(_write_int(0x00), b'\x00') 97 | self.assertEqual(_write_int(-0x01), b'\xff') 98 | self.assertEqual(_write_int(0x80), b'\x80') 99 | self.assertEqual(_write_int(0xffff), b'\xff\xff') 100 | self.assertEqual(_write_int(0xffffffff), b'\xff\xff\xff\xff') 101 | self.assertEqual(_write_int(0xffffffffff), b'\xff\xff\xff\xff\xff') 102 | 103 | def test_write_asn1_length(self): 104 | """Test _parse_asn1_length""" 105 | self.assertEqual(_write_asn1_length(0x00), b'\x00') 106 | self.assertEqual(_write_asn1_length(0x7f), b'\x7f') 107 | self.assertEqual(_write_asn1_length(0x80), b'\x81\x80') 108 | self.assertEqual(_write_asn1_length(0xffff), b'\x82\xff\xff') 109 | self.assertEqual(_write_asn1_length(0xffffff), b'\x83\xff\xff\xff') 110 | self.assertEqual(_write_asn1_length(0xffffffff), b'\x84\xff\xff\xff\xff') 111 | with self.assertRaises(Exception): 112 | _write_asn1_length(0x100000000) 113 | 114 | def test_parse_asn1_length(self): 115 | """Test _parse_asn1_length""" 116 | self.assertEqual(_parse_asn1_length(StringIO('\x00')), 0x00) 117 | self.assertEqual(_parse_asn1_length(StringIO('\x01')), 0x01) 118 | self.assertEqual(_parse_asn1_length(StringIO('\x7f')), 0x7f) 119 | self.assertEqual(_parse_asn1_length(StringIO('\x81\x00')), 0x00) 120 | self.assertEqual(_parse_asn1_length(StringIO('\x81\xff')), 0xff) 121 | self.assertEqual(_parse_asn1_length(StringIO('\x82\x00\x00')), 0x00) 122 | self.assertEqual(_parse_asn1_length(StringIO('\x82\xff\x00')), 0xff00) 123 | self.assertEqual(_parse_asn1_length(StringIO('\x83\x00\x00\x00')), 0x00) 124 | self.assertEqual(_parse_asn1_length(StringIO('\x83\x12\x34\x56')), 0x123456) 125 | self.assertEqual(_parse_asn1_length(StringIO('\x84\x00\x00\x00\x00')), 0x00) 126 | self.assertEqual(_parse_asn1_length(StringIO('\x84\x12\x34\x56\x78')), 0x12345678) 127 | with self.assertRaises(Exception): 128 | _parse_asn1_length(StringIO('\x80\x00')) 129 | with self.assertRaises(Exception): 130 | _parse_asn1_length(StringIO('\x85\x00\x00\x00\x00\x00')) 131 | self.assertEqual(_parse_asn1_length(StringIO(_write_asn1_length(12345678).decode('latin'))), 12345678) 132 | 133 | def test_parse_snmp_asn1(self): 134 | """Test _parse_snmp_asn1""" 135 | with self.assertRaises(ProtocolError): 136 | _parse_snmp_asn1(StringIO('')) 137 | with self.assertRaises(ProtocolError): 138 | _parse_snmp_asn1(StringIO('\x30\x27\x02\x01\x01\x04\x06public')) 139 | self.assertEqual( 140 | _parse_snmp_asn1( 141 | StringIO( 142 | '\x30\x27' 143 | '\x02\x01\x01' # version 144 | '\x04\x06public' # community 145 | '\xa1\x1a' # GetNextRequest PDU 146 | '\x02\x01\x05' # request id 147 | '\x02\x01\x00' # error status 148 | '\x02\x01\x00' # error index 149 | '\x30\x0c\x30\x0a' 150 | '\x06\x06\x2b\x06\x01\x02\x03\x04' # OID 151 | '\x05\x00' 152 | ) 153 | ), [ 154 | ('INTEGER', 0x01), 155 | ('STRING', 'public'), 156 | ('ASN1_GET_NEXT_REQUEST_PDU', ASN1_GET_NEXT_REQUEST_PDU), 157 | ('INTEGER', 0x05), 158 | ('INTEGER', 0x00), 159 | ('INTEGER', 0x00), 160 | ('OID', '1.3.6.1.2.3.4') 161 | ] 162 | ) 163 | with self.assertRaises(ProtocolError): # swap community and version fields 164 | _parse_snmp_asn1( 165 | StringIO( 166 | '\x30\x27' 167 | '\x04\x06public' # community 168 | '\x02\x01\x01' # version 169 | '\xa1\x1a' # GetNextRequest PDU 170 | '\x02\x01\x05' # request id 171 | '\x02\x01\x00' # error status 172 | '\x02\x01\x00' # error index 173 | '\x30\x0c\x30\x0a' 174 | '\x06\x06\x2b\x06\x01\x02\x03\x04' # OID 175 | '\x05\x00' 176 | ) 177 | ) 178 | 179 | def test_get_next_oid(self): 180 | """Test get_next_oid""" 181 | self.assertEqual(get_next_oid('1.3.6.1.1'), '1.3.6.2.1') 182 | self.assertEqual(get_next_oid('1.3.6.1'), '1.3.7.1') 183 | self.assertEqual(get_next_oid('1.3.6'), '1.4.1') 184 | self.assertEqual(get_next_oid('1.3'), '2.1') 185 | self.assertEqual(get_next_oid('1'), '2') 186 | self.assertEqual(get_next_oid('0'), '1') 187 | 188 | def test_write_tlv(self): 189 | """Test write_tlv""" 190 | self.assertEqual(write_tlv(0, 0, b''), b'\x00\x00') 191 | self.assertEqual(write_tlv(255, 5, b'value'), b'\xff\x05value') 192 | self.assertEqual(write_tlv(255, 0x7f, b'value'), b'\xff\x7fvalue') 193 | self.assertEqual(write_tlv(255, 0x80, b'value'), b'\xff\x81\x80value') 194 | self.assertEqual(write_tlv(255, 0xff, b'value'), b'\xff\x81\xffvalue') 195 | self.assertEqual(write_tlv(255, 0x0100, b'value'), b'\xff\x82\x01\x00value') 196 | self.assertEqual(write_tlv(255, 0xffff, b'value'), b'\xff\x82\xff\xffvalue') 197 | self.assertEqual(write_tlv(255, 0xffff, b'value'), b'\xff\x82\xff\xffvalue') 198 | 199 | def test_write_tv(self): 200 | """Test write_tv""" 201 | self.assertEqual(write_tv(0, b''), b'\x00\x00') 202 | self.assertEqual(write_tv(255, b'value'), b'\xff\x05value') 203 | 204 | def test_boolean(self): 205 | """Test boolean""" 206 | self.assertEqual(boolean(True), b'\x01\x01\xff') 207 | self.assertEqual(boolean(False), b'\x01\x01\x00') 208 | 209 | def test_integer(self): 210 | """Test integer""" 211 | self.assertEqual(integer(0), b'\x02\x08\x00\x00\x00\x00\x00\x00\x00\x00') 212 | self.assertEqual(integer(0xffff), b'\x02\x08\x00\x00\x00\x00\x00\x00\xff\xff') 213 | self.assertEqual(integer(0x12345678), b'\x02\x08\x00\x00\x00\x00\x124Vx') 214 | self.assertEqual(integer(-1), b'\x02\x01\xff') 215 | self.assertEqual(integer(-0x12345678), b'\x02\x04\xed\xcb\xa9\x88') 216 | 217 | def test_octet_string(self): 218 | """Test octet string""" 219 | self.assertEqual(octet_string(''), b'\x04\x00') 220 | self.assertEqual(octet_string('abc'), b'\x04\x03abc') 221 | self.assertEqual(octet_string('\x00\x01\x02'), b'\x04\x03\x00\x01\x02') 222 | 223 | def test_null(self): 224 | """Test null""" 225 | self.assertEqual(null(), b'\x05\x00') 226 | 227 | def test_object_identifier(self): 228 | """Test OID""" 229 | self.assertEqual(object_identifier('1.3.6'), b'\x06\x02\x2b\x06') 230 | self.assertEqual(object_identifier('1.3.6.7.8.9'), b'\x06\x05\x2b\x06\x07\x08\x09') 231 | 232 | def test_real(self): 233 | """Test real""" 234 | self.assertEqual(real(0.0), b'\x44\x07\x9f\x78\x04\x00\x00\x00\x00') 235 | self.assertEqual(real(float('inf')), b'\x44\x07\x9f\x78\x04\x7f\x80\x00\x00') 236 | self.assertEqual(real(float('-inf')), b'\x44\x07\x9f\x78\x04\xff\x80\x00\x00') 237 | self.assertEqual(real(float('nan')), b'\x44\x07\x9f\x78\x04\x7f\xc0\x00\x00') 238 | self.assertEqual(real(float('-nan')), b'\x44\x07\x9f\x78\x04\xff\xc0\x00\x00') 239 | self.assertEqual(real(123.456), b'\x44\x07\x9f\x78\x04\x42\xf6\xe9\x79') 240 | 241 | def test_double(self): 242 | """Test double""" 243 | self.assertEqual(double(0.0), b'\x44\x0b\x9f\x79\x08\x00\x00\x00\x00\x00\x00\x00\x00') 244 | self.assertEqual(double(float('inf')), b'\x44\x0b\x9f\x79\x08\x7f\xf0\x00\x00\x00\x00\x00\x00') 245 | self.assertEqual(double(float('-inf')), b'\x44\x0b\x9f\x79\x08\xff\xf0\x00\x00\x00\x00\x00\x00') 246 | self.assertEqual(double(float('nan')), b'\x44\x0b\x9f\x79\x08\x7f\xf8\x00\x00\x00\x00\x00\x00') 247 | self.assertEqual(double(float('-nan')), b'\x44\x0b\x9f\x79\x08\xff\xf8\x00\x00\x00\x00\x00\x00') 248 | self.assertEqual(double(123.456), b'\x44\x0b\x9f\x79\x08\x40\x5e\xdd\x2f\x1a\x9f\xbe\x77') 249 | 250 | def test_ip_address(self): 251 | """Test IP address""" 252 | self.assertEqual(ip_address('0.0.0.0'), b'\x40\x04\x00\x00\x00\x00') 253 | self.assertEqual(ip_address('127.0.0.1'), b'\x40\x04\x7f\x00\x00\x01') 254 | self.assertEqual(ip_address('255.254.253.252'), b'\x40\x04\xff\xfe\xfd\xfc') 255 | 256 | def test_timeticks(self): 257 | """Test timeticks""" 258 | self.assertEqual(timeticks(0), b'\x43\x01\x00') 259 | self.assertEqual(timeticks(255), b'\x43\x01\xff') 260 | self.assertEqual(timeticks(0xffff), b'\x43\x02\xff\xff') 261 | self.assertEqual(timeticks(0xffffffff), b'\x43\x04\xff\xff\xff\xff') 262 | with self.assertRaises(Exception): 263 | timeticks(0xffffffffff) 264 | 265 | def test_gauge32(self): 266 | """Test gauge32""" 267 | self.assertEqual(gauge32(0), b'\x42\x08\x00\x00\x00\x00\x00\x00\x00\x00') 268 | self.assertEqual(gauge32(255), b'\x42\x08\x00\x00\x00\x00\x00\x00\x00\xff') 269 | self.assertEqual(gauge32(0xffff), b'\x42\x08\x00\x00\x00\x00\x00\x00\xff\xff') 270 | self.assertEqual(gauge32(0xffffffff), b'\x42\x08\x00\x00\x00\x00\xff\xff\xff\xff') 271 | with self.assertRaises(Exception): 272 | gauge32(0xffffffffff) 273 | 274 | def test_counter32(self): 275 | """Test counter32""" 276 | self.assertEqual(counter32(0), b'\x41\x01\x00') 277 | self.assertEqual(counter32(255), b'\x41\x01\xff') 278 | self.assertEqual(counter32(0xffff), b'\x41\x02\xff\xff') 279 | self.assertEqual(counter32(0xffffffff), b'\x41\x04\xff\xff\xff\xff') 280 | with self.assertRaises(Exception): 281 | counter32(0xffffffffff) 282 | 283 | def test_counter64(self): 284 | """Test counter64""" 285 | self.assertEqual(counter64(0), b'\x46\x01\x00') 286 | self.assertEqual(counter64(255), b'\x46\x01\xff') 287 | self.assertEqual(counter64(0xffff), b'\x46\x02\xff\xff') 288 | self.assertEqual(counter64(0xffffffff), b'\x46\x04\xff\xff\xff\xff') 289 | with self.assertRaises(Exception): 290 | gauge32(0xffffffffffffffffff) 291 | 292 | -------------------------------------------------------------------------------- /pytest_snmpserver/snmp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Simple SNMP server in pure Python 5 | """ 6 | 7 | 8 | import argparse 9 | import fnmatch 10 | import functools 11 | import logging 12 | import socket 13 | import string 14 | import struct 15 | import sys 16 | import threading 17 | import time 18 | import types 19 | 20 | from collections import Iterable 21 | 22 | try: 23 | from StringIO import StringIO 24 | except ImportError: 25 | from io import StringIO 26 | 27 | __version__ = '1.0.5' 28 | 29 | PY3 = sys.version_info[0] == 3 30 | 31 | logging.basicConfig(format='[%(levelname)s] %(message)s') 32 | logger = logging.getLogger() 33 | logger.setLevel(logging.WARNING) 34 | 35 | # ASN.1 tags 36 | ASN1_BOOLEAN = 0x01 37 | ASN1_INTEGER = 0x02 38 | ASN1_BIT_STRING = 0x03 39 | ASN1_OCTET_STRING = 0x04 40 | ASN1_NULL = 0x05 41 | ASN1_OBJECT_IDENTIFIER = 0x06 42 | ASN1_UTF8_STRING = 0x0c 43 | ASN1_PRINTABLE_STRING = 0x13 44 | ASN1_IA5_STRING = 0x16 45 | ASN1_BMP_STRING = 0x1e 46 | ASN1_SEQUENCE = 0x30 47 | ASN1_SET = 0x31 48 | ASN1_IPADDRESS = 0x40 49 | ASN1_COUNTER32 = 0x41 50 | ASN1_GAUGE32 = 0x42 51 | ASN1_TIMETICKS = 0x43 52 | ASN1_OPAQUE = 0x44 53 | ASN1_COUNTER64 = 0x46 54 | ASN1_NO_SUCH_OBJECT = 0x80 55 | ASN1_NO_SUCH_INSTANCE = 0x81 56 | ASN1_END_OF_MIB_VIEW = 0x82 57 | ASN1_GET_REQUEST_PDU = 0xA0 58 | ASN1_GET_NEXT_REQUEST_PDU = 0xA1 59 | ASN1_GET_RESPONSE_PDU = 0xA2 60 | ASN1_SET_REQUEST_PDU = 0xA3 61 | ASN1_GET_BULK_REQUEST_PDU = 0xA5 62 | 63 | # error statuses 64 | ASN1_ERROR_STATUS_NO_ERROR = 0x00 65 | ASN1_ERROR_STATUS_TOO_BIG = 0x01 66 | ASN1_ERROR_STATUS_NO_SUCH_NAME = 0x02 67 | ASN1_ERROR_STATUS_BAD_VALUE = 0x03 68 | ASN1_ERROR_STATUS_READ_ONLY = 0x04 69 | ASN1_ERROR_STATUS_GEN_ERR = 0x05 70 | ASN1_ERROR_STATUS_WRONG_VALUE = 0x0A 71 | 72 | # some ASN.1 opaque special types 73 | ASN1_CONTEXT = 0x80 # context-specific 74 | ASN1_EXTENSION_ID = 0x1F # 0b11111 (fill tag in first octet) 75 | ASN1_OPAQUE_TAG1 = ASN1_CONTEXT | ASN1_EXTENSION_ID # 0x9f 76 | ASN1_OPAQUE_TAG2 = 0x30 # base tag value 77 | ASN1_APPLICATION = 0x40 78 | ASN1_APP_FLOAT = ASN1_APPLICATION | 0x08 # application-specific type 0x08 79 | ASN1_APP_DOUBLE = ASN1_APPLICATION | 0x09 # application-specific type 0x09 80 | ASN1_APP_INT64 = ASN1_APPLICATION | 0x0A # application-specific type 0x0A 81 | ASN1_APP_UINT64 = ASN1_APPLICATION | 0x0B # application-specific type 0x0B 82 | ASN1_OPAQUE_FLOAT = ASN1_OPAQUE_TAG2 | ASN1_APP_FLOAT 83 | ASN1_OPAQUE_DOUBLE = ASN1_OPAQUE_TAG2 | ASN1_APP_DOUBLE 84 | ASN1_OPAQUE_INT64 = ASN1_OPAQUE_TAG2 | ASN1_APP_INT64 85 | ASN1_OPAQUE_UINT64 = ASN1_OPAQUE_TAG2 | ASN1_APP_UINT64 86 | ASN1_OPAQUE_FLOAT_BER_LEN = 7 87 | ASN1_OPAQUE_DOUBLE_BER_LEN = 11 88 | ASN1_OPAQUE_INT64_BER_LEN = 4 89 | ASN1_OPAQUE_UINT64_BER_LEN = 4 90 | 91 | SNMP_VERSIONS = { 92 | 1: 'v1', 93 | 2: 'v2c', 94 | 3: 'v3', 95 | } 96 | 97 | SNMP_PDUS = ( 98 | 'version', 99 | 'community', 100 | 'PDU-type', 101 | 'request-id', 102 | 'error-status', 103 | 'error-index', 104 | 'variable bindings', 105 | ) 106 | 107 | 108 | class ProtocolError(Exception): 109 | """Raise when SNMP protocol error occured""" 110 | 111 | 112 | class ConfigError(Exception): 113 | """Raise when config error occured""" 114 | 115 | 116 | class BadValueError(Exception): 117 | """Raise when bad value error occured""" 118 | 119 | 120 | class WrongValueError(Exception): 121 | """Raise when wrong value (e.g. value not in available range) error occured""" 122 | 123 | 124 | def encode_to_7bit(value): 125 | """Encode to 7 bit""" 126 | if value > 0x7f: 127 | res = [] 128 | res.insert(0, value & 0x7f) 129 | while value > 0x7f: 130 | value >>= 7 131 | res.insert(0, (value & 0x7f) | 0x80) 132 | return res 133 | return [value] 134 | 135 | 136 | def oid_to_bytes_list(oid): 137 | """Convert OID str to bytes list""" 138 | if oid.startswith('iso'): 139 | oid = oid.replace('iso', '1') 140 | try: 141 | oid_values = [int(x) for x in oid.split('.') if x] 142 | first_val = 40 * oid_values[0] + oid_values[1] 143 | except (ValueError, IndexError): 144 | raise Exception('Could not parse OID value "{}"'.format(oid)) 145 | result_values = [first_val] 146 | for node_num in oid_values[2:]: 147 | result_values += encode_to_7bit(node_num) 148 | return result_values 149 | 150 | 151 | def oid_to_bytes(oid): 152 | """Convert OID str to bytes""" 153 | return ''.join([chr(x) for x in oid_to_bytes_list(oid)]) 154 | 155 | 156 | def bytes_to_oid(data): 157 | """Convert bytes to OID str""" 158 | values = [ord(x) for x in data] 159 | first_val = values.pop(0) 160 | res = [] 161 | res += divmod(first_val, 40) 162 | while values: 163 | val = values.pop(0) 164 | if val > 0x7f: 165 | huges = [] 166 | huges.append(val) 167 | while True: 168 | next_val = values.pop(0) 169 | huges.append(next_val) 170 | if next_val < 0x80: 171 | break 172 | huge = 0 173 | for i, huge_byte in enumerate(huges): 174 | huge += (huge_byte & 0x7f) << (7 * (len(huges) - i - 1)) 175 | res.append(huge) 176 | else: 177 | res.append(val) 178 | return '.'.join(str(x) for x in res) 179 | 180 | 181 | def timeticks_to_str(ticks): 182 | """Return "days, hours, minutes, seconds and ms" string from ticks""" 183 | days, rem1 = divmod(ticks, 24 * 60 * 60 * 100) 184 | hours, rem2 = divmod(rem1, 60 * 60 * 100) 185 | minutes, rem3 = divmod(rem2, 60 * 100) 186 | seconds, milliseconds = divmod(rem3, 100) 187 | ending = 's' if days > 1 else '' 188 | days_fmt = '{} day{}, '.format(days, ending) if days > 0 else '' 189 | return '{}{:-02}:{:-02}:{:-02}.{:-02}'.format(days_fmt, hours, minutes, seconds, milliseconds) 190 | 191 | 192 | def int_to_ip(value): 193 | """Int to IP""" 194 | return socket.inet_ntoa(struct.pack("!I", value)) 195 | 196 | 197 | def twos_complement(value, bits): 198 | """Calculate two's complement""" 199 | mask = 2 ** (bits - 1) 200 | return -(value & mask) + (value & ~mask) 201 | 202 | 203 | def _read_byte(stream): 204 | """Read byte from stream""" 205 | read_byte = stream.read(1) 206 | if not read_byte: 207 | raise Exception('No more bytes!') 208 | return ord(read_byte) 209 | 210 | 211 | def _read_int_len(stream, length, signed=False): 212 | """Read int with length""" 213 | result = 0 214 | sign = None 215 | for _ in range(length): 216 | value = _read_byte(stream) 217 | if sign is None: 218 | sign = value & 0x80 219 | result = (result << 8) + value 220 | if signed and sign: 221 | result = twos_complement(result, 8 * length) 222 | return result 223 | 224 | 225 | def _write_int(value, strip_leading_zeros=True): 226 | """Write int""" 227 | if abs(value) > 0xffffffffffffffff: 228 | raise Exception('Int value must be in [0..18446744073709551615]') 229 | if value < 0: 230 | if abs(value) <= 0x7f: 231 | result = struct.pack('>b', value) 232 | elif abs(value) <= 0x7fff: 233 | result = struct.pack('>h', value) 234 | elif abs(value) <= 0x7fffffff: 235 | result = struct.pack('>i', value) 236 | elif abs(value) <= 0x7fffffffffffffff: 237 | result = struct.pack('>q', value) 238 | else: 239 | result = struct.pack('>Q', value) 240 | # strip first null bytes, if all are null - leave one 241 | result = result.lstrip(b'\x00') if strip_leading_zeros else result 242 | return result or b'\x00' 243 | 244 | 245 | def _write_asn1_length(length): 246 | """Write ASN.1 length""" 247 | if length > 0x7f: 248 | if length <= 0xff: 249 | packed_length = 0x81 250 | elif length <= 0xffff: 251 | packed_length = 0x82 252 | elif length <= 0xffffff: 253 | packed_length = 0x83 254 | elif length <= 0xffffffff: 255 | packed_length = 0x84 256 | else: 257 | raise Exception('Length is too big!') 258 | return struct.pack('B', packed_length) + _write_int(length) 259 | return struct.pack('B', length) 260 | 261 | 262 | def _parse_asn1_length(stream): 263 | """Parse ASN.1 length""" 264 | length = _read_byte(stream) 265 | # handle long length 266 | if length > 0x7f: 267 | data_length = length - 0x80 268 | if not 0 < data_length <= 4: 269 | raise Exception('Data length must be in [1..4]') 270 | length = _read_int_len(stream, data_length) 271 | return length 272 | 273 | 274 | def _parse_asn1_octet_string(stream): 275 | """Parse ASN.1 octet string""" 276 | length = _parse_asn1_length(stream) 277 | value = stream.read(length) 278 | # if any char is not printable - convert string to hex 279 | if any([c not in string.printable for c in value]): 280 | return ' '.join(['%02X' % ord(x) for x in value]) 281 | return value 282 | 283 | 284 | def _parse_asn1_opaque_float(stream): 285 | """Parse ASN.1 opaque float""" 286 | length = _parse_asn1_length(stream) 287 | value = _read_int_len(stream, length, signed=True) 288 | # convert int to float 289 | float_value = struct.unpack('>f', struct.pack('>l', value))[0] 290 | logger.debug('ASN1_OPAQUE_FLOAT: %s', round(float_value, 5)) 291 | return 'FLOAT', round(float_value, 5) 292 | 293 | 294 | def _parse_asn1_opaque_double(stream): 295 | """Parse ASN.1 opaque double""" 296 | length = _parse_asn1_length(stream) 297 | value = _read_int_len(stream, length, signed=True) 298 | # convert long long to double 299 | double_value = struct.unpack('>d', struct.pack('>q', value))[0] 300 | logger.debug('ASN1_OPAQUE_DOUBLE: %s', round(double_value, 5)) 301 | return 'DOUBLE', round(double_value, 5) 302 | 303 | 304 | def _parse_asn1_opaque_int64(stream): 305 | """Parse ASN.1 opaque int64""" 306 | length = _parse_asn1_length(stream) 307 | value = _read_int_len(stream, length, signed=True) 308 | logger.debug('ASN1_OPAQUE_INT64: %s', value) 309 | return 'INT64', value 310 | 311 | 312 | def _parse_asn1_opaque_uint64(stream): 313 | """Parse ASN.1 opaque uint64""" 314 | length = _parse_asn1_length(stream) 315 | value = _read_int_len(stream, length) 316 | logger.debug('ASN1_OPAQUE_UINT64: %s', value) 317 | return 'UINT64', value 318 | 319 | 320 | def _parse_asn1_opaque(stream): 321 | """Parse ASN.1 opaque""" 322 | length = _parse_asn1_length(stream) 323 | opaque_tag = _read_byte(stream) 324 | opaque_type = _read_byte(stream) 325 | if (length == ASN1_OPAQUE_FLOAT_BER_LEN and 326 | opaque_tag == ASN1_OPAQUE_TAG1 and 327 | opaque_type == ASN1_OPAQUE_FLOAT): 328 | return _parse_asn1_opaque_float(stream) 329 | elif (length == ASN1_OPAQUE_DOUBLE_BER_LEN and 330 | opaque_tag == ASN1_OPAQUE_TAG1 and 331 | opaque_type == ASN1_OPAQUE_DOUBLE): 332 | return _parse_asn1_opaque_double(stream) 333 | elif (length >= ASN1_OPAQUE_INT64_BER_LEN and 334 | opaque_tag == ASN1_OPAQUE_TAG1 and 335 | opaque_type == ASN1_OPAQUE_INT64): 336 | return _parse_asn1_opaque_int64(stream) 337 | elif (length >= ASN1_OPAQUE_UINT64_BER_LEN and 338 | opaque_tag == ASN1_OPAQUE_TAG1 and 339 | opaque_type == ASN1_OPAQUE_UINT64): 340 | return _parse_asn1_opaque_uint64(stream) 341 | # for simple opaque - rewind 2 bytes back (opaque tag and type) 342 | stream.seek(stream.tell() - 2) 343 | return stream.read(length) 344 | 345 | 346 | def _parse_snmp_asn1(stream): 347 | """Parse SNMP ASN.1 348 | After |IP|UDP| headers and "sequence" tag, SNMP protocol data units (PDUs) are the next: 349 | |version|community|PDU-type|request-id|error-status|error-index|variable bindings| 350 | """ 351 | result = [] 352 | wait_oid_value = False 353 | pdu_index = 0 354 | while True: 355 | read_byte = stream.read(1) 356 | if not read_byte: 357 | if pdu_index < 7: 358 | raise ProtocolError('Not all SNMP protocol data units are read!') 359 | return result 360 | tag = ord(read_byte) 361 | # check protocol's tags at indices 362 | if ( 363 | pdu_index in [1, 4, 5, 6] and tag != ASN1_INTEGER or 364 | pdu_index == 2 and tag != ASN1_OCTET_STRING or 365 | pdu_index == 3 and tag not in [ 366 | ASN1_GET_REQUEST_PDU, 367 | ASN1_GET_NEXT_REQUEST_PDU, 368 | ASN1_SET_REQUEST_PDU, 369 | ASN1_GET_BULK_REQUEST_PDU, 370 | ] 371 | ): 372 | raise ProtocolError('Invalid tag for PDU unit "{}"'.format(SNMP_PDUS[pdu_index])) 373 | if tag == ASN1_SEQUENCE: 374 | length = _parse_asn1_length(stream) 375 | logger.debug('ASN1_SEQUENCE: %s', 'length = {}'.format(length)) 376 | elif tag == ASN1_INTEGER: 377 | length = _read_byte(stream) 378 | value = _read_int_len(stream, length, True) 379 | logger.debug('ASN1_INTEGER: %s', value) 380 | # pdu_index is version, request-id, error-status, error-index 381 | if wait_oid_value or pdu_index in [1, 4, 5, 6]: 382 | result.append(('INTEGER', value)) 383 | wait_oid_value = False 384 | elif tag == ASN1_OCTET_STRING: 385 | value = _parse_asn1_octet_string(stream) 386 | logger.debug('ASN1_OCTET_STRING: %s', value) 387 | if wait_oid_value or pdu_index == 2: # community 388 | result.append(('STRING', value)) 389 | wait_oid_value = False 390 | elif tag == ASN1_OBJECT_IDENTIFIER: 391 | length = _read_byte(stream) 392 | value = stream.read(length) 393 | logger.debug('ASN1_OBJECT_IDENTIFIER: %s', bytes_to_oid(value)) 394 | result.append(('OID', bytes_to_oid(value))) 395 | wait_oid_value = True 396 | elif tag == ASN1_PRINTABLE_STRING: 397 | length = _parse_asn1_length(stream) 398 | value = stream.read(length) 399 | logger.debug('ASN1_PRINTABLE_STRING: %s', value) 400 | elif tag == ASN1_GET_REQUEST_PDU: 401 | length = _parse_asn1_length(stream) 402 | logger.debug('ASN1_GET_REQUEST_PDU: %s', 'length = {}'.format(length)) 403 | if pdu_index == 3: # PDU-type 404 | result.append(('ASN1_GET_REQUEST_PDU', tag)) 405 | elif tag == ASN1_GET_NEXT_REQUEST_PDU: 406 | length = _parse_asn1_length(stream) 407 | logger.debug('ASN1_GET_NEXT_REQUEST_PDU: %s', 'length = {}'.format(length)) 408 | if pdu_index == 3: # PDU-type 409 | result.append(('ASN1_GET_NEXT_REQUEST_PDU', tag)) 410 | elif tag == ASN1_GET_BULK_REQUEST_PDU: 411 | length = _parse_asn1_length(stream) 412 | logger.debug('ASN1_GET_BULK_REQUEST_PDU: %s', 'length = {}'.format(length)) 413 | if pdu_index == 3: # PDU-type 414 | result.append(('ASN1_GET_BULK_REQUEST_PDU', tag)) 415 | elif tag == ASN1_GET_RESPONSE_PDU: 416 | length = _parse_asn1_length(stream) 417 | logger.debug('ASN1_GET_RESPONSE_PDU: %s', 'length = {}'.format(length)) 418 | elif tag == ASN1_SET_REQUEST_PDU: 419 | length = _parse_asn1_length(stream) 420 | logger.debug('ASN1_SET_REQUEST_PDU: %s', 'length = {}'.format(length)) 421 | if pdu_index == 3: # PDU-type 422 | result.append(('ASN1_SET_REQUEST_PDU', tag)) 423 | elif tag == ASN1_TIMETICKS: 424 | length = _read_byte(stream) 425 | value = _read_int_len(stream, length) 426 | logger.debug('ASN1_TIMETICKS: %s (%s)', value, timeticks_to_str(value)) 427 | if wait_oid_value: 428 | result.append(('TIMETICKS', value)) 429 | wait_oid_value = False 430 | elif tag == ASN1_IPADDRESS: 431 | length = _read_byte(stream) 432 | value = _read_int_len(stream, length) 433 | logger.debug('ASN1_IPADDRESS: %s (%s)', value, int_to_ip(value)) 434 | if wait_oid_value: 435 | result.append(('IPADDRESS', int_to_ip(value))) 436 | wait_oid_value = False 437 | elif tag == ASN1_COUNTER32: 438 | length = _read_byte(stream) 439 | value = _read_int_len(stream, length) 440 | logger.debug('ASN1_COUNTER32: %s', value) 441 | if wait_oid_value: 442 | result.append(('COUNTER32', value)) 443 | wait_oid_value = False 444 | elif tag == ASN1_GAUGE32: 445 | length = _read_byte(stream) 446 | value = _read_int_len(stream, length) 447 | logger.debug('ASN1_GAUGE32: %s', value) 448 | if wait_oid_value: 449 | result.append(('GAUGE32', value)) 450 | wait_oid_value = False 451 | elif tag == ASN1_OPAQUE: 452 | value = _parse_asn1_opaque(stream) 453 | logger.debug('ASN1_OPAQUE: %r', value) 454 | if wait_oid_value: 455 | result.append(('OPAQUE', value)) 456 | wait_oid_value = False 457 | elif tag == ASN1_COUNTER64: 458 | length = _read_byte(stream) 459 | value = _read_int_len(stream, length) 460 | logger.debug('ASN1_COUNTER64: %s', value) 461 | if wait_oid_value: 462 | result.append(('COUNTER64', value)) 463 | wait_oid_value = False 464 | elif tag == ASN1_NULL: 465 | value = _read_byte(stream) 466 | logger.debug('ASN1_NULL: %s', value) 467 | elif tag == ASN1_NO_SUCH_OBJECT: 468 | value = _read_byte(stream) 469 | logger.debug('ASN1_NO_SUCH_OBJECT: %s', value) 470 | result.append('No Such Object') 471 | elif tag == ASN1_NO_SUCH_INSTANCE: 472 | value = _read_byte(stream) 473 | logger.debug('ASN1_NO_SUCH_INSTANCE: %s', value) 474 | result.append('No Such Instance with OID') 475 | elif tag == ASN1_END_OF_MIB_VIEW: 476 | value = _read_byte(stream) 477 | logger.debug('ASN1_END_OF_MIB_VIEW: %s', value) 478 | return (('', ''), ('', '')) 479 | else: 480 | logger.debug('?: %s', hex(ord(read_byte))) 481 | pdu_index += 1 482 | return result 483 | 484 | 485 | def get_next_oid(oid): 486 | """Get the next OID parent's node""" 487 | # increment pre last node, e.g.: "1.3.6.1.1" -> "1.3.6.2.1" 488 | oid_vals = oid.rsplit('.', 2) 489 | if len(oid_vals) < 2: 490 | oid_vals[-1] = str(int(oid_vals[-1]) + 1) 491 | else: 492 | oid_vals[-2] = str(int(oid_vals[-2]) + 1) 493 | oid_vals[-1] = '1' 494 | oid_next = '.'.join(oid_vals) 495 | return oid_next 496 | 497 | 498 | def write_tlv(tag, length, value): 499 | """Write TLV (Tag-Length-Value)""" 500 | return struct.pack('B', tag) + _write_asn1_length(length) + value 501 | 502 | 503 | def write_tv(tag, value): 504 | """Write TV (Tag-Value) and calculate length from value""" 505 | return write_tlv(tag, len(value), value) 506 | 507 | 508 | def boolean(value): 509 | """Get Boolean""" 510 | return write_tlv(ASN1_BOOLEAN, 1, b'\xff' if value else b'\x00') 511 | 512 | 513 | def integer(value, enum=None): 514 | """Get Integer""" 515 | if enum and isinstance(enum, Iterable): 516 | if not value in enum: 517 | raise WrongValueError('Integer value {} is outside the range of enum values'.format(value)) 518 | if not (-2147483648 <= value <= 2147483647): 519 | raise Exception('Integer value must be in [-2147483648..2147483647]') 520 | if not enum: 521 | return write_tv(ASN1_INTEGER, _write_int(value, False)) 522 | return write_tv(ASN1_INTEGER, _write_int(value, False)), enum 523 | 524 | 525 | def bit_string(value): 526 | """ 527 | Get BitString 528 | For example, if the input value is '\xF0\xF0' 529 | F0 F0 in hex = 11110000 11110000 in binary 530 | And in binary bits 0, 1, 2, 3, 8, 9, 10, 11 are set, so these bits are added to the output 531 | Therefore the SNMP response is: F0 F0 0 1 2 3 8 9 10 11 532 | """ 533 | return write_tlv(ASN1_BIT_STRING, len(value), value.encode('latin') if PY3 else value) 534 | 535 | 536 | def octet_string(value): 537 | """Get OctetString""" 538 | return write_tv(ASN1_OCTET_STRING, value.encode('latin') if PY3 else value) 539 | 540 | 541 | def null(): 542 | """Get Null""" 543 | return write_tv(ASN1_NULL, b'') 544 | 545 | 546 | def object_identifier(value): 547 | """Get OID""" 548 | value = oid_to_bytes(value) 549 | return write_tv(ASN1_OBJECT_IDENTIFIER, value.encode('latin') if PY3 else value) 550 | 551 | 552 | def real(value): 553 | """Get real""" 554 | # opaque tag | len | tag1 | tag2 | len | data 555 | float_value = struct.pack('>f', value) 556 | opaque_type_value = struct.pack( 557 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_FLOAT 558 | ) + _write_asn1_length(len(float_value)) + float_value 559 | return write_tv(ASN1_OPAQUE, opaque_type_value) 560 | 561 | 562 | def double(value): 563 | """Get double""" 564 | # opaque tag | len | tag1 | tag2 | len | data 565 | double_value = struct.pack('>d', value) 566 | opaque_type_value = struct.pack( 567 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_DOUBLE 568 | ) + _write_asn1_length(len(double_value)) + double_value 569 | return write_tv(ASN1_OPAQUE, opaque_type_value) 570 | 571 | 572 | def int64(value): 573 | """Get int64""" 574 | # opaque tag | len | tag1 | tag2 | len | data 575 | int64_value = struct.pack('>q', value) 576 | opaque_type_value = struct.pack( 577 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_INT64 578 | ) + _write_asn1_length(len(int64_value)) + int64_value 579 | return write_tv(ASN1_OPAQUE, opaque_type_value) 580 | 581 | 582 | def uint64(value): 583 | """Get uint64""" 584 | # opaque tag | len | tag1 | tag2 | len | data 585 | uint64_value = struct.pack('>Q', value) 586 | opaque_type_value = struct.pack( 587 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_UINT64 588 | ) + _write_asn1_length(len(uint64_value)) + uint64_value 589 | return write_tv(ASN1_OPAQUE, opaque_type_value) 590 | 591 | 592 | 593 | def utf8_string(value): 594 | """Get UTF8String""" 595 | return write_tv(ASN1_UTF8_STRING, value.encode('latin') if PY3 else value) 596 | 597 | 598 | def printable_string(value): 599 | """Get PrintableString""" 600 | return write_tv(ASN1_PRINTABLE_STRING, value.encode('latin') if PY3 else value) 601 | 602 | 603 | def ia5_string(value): 604 | """Get IA5String""" 605 | return write_tv(ASN1_IA5_STRING, value.encode('latin') if PY3 else value) 606 | 607 | 608 | def bmp_string(value): 609 | """Get BMPString""" 610 | return write_tv(ASN1_BMP_STRING, value.encode('utf-16-be')) 611 | 612 | 613 | def ip_address(value): 614 | """Get IPAddress""" 615 | return write_tv(ASN1_IPADDRESS, socket.inet_aton(value)) 616 | 617 | 618 | def timeticks(value): 619 | """Get Timeticks""" 620 | if value > 0xffffffff: 621 | raise Exception('Timeticks value must be in [0..4294967295]') 622 | return write_tv(ASN1_TIMETICKS, _write_int(value)) 623 | 624 | 625 | def gauge32(value): 626 | """Get Gauge32""" 627 | if value > 0xffffffff: 628 | raise Exception('Gauge32 value must be in [0..4294967295]') 629 | return write_tv(ASN1_GAUGE32, _write_int(value, strip_leading_zeros=False)) 630 | 631 | 632 | def counter32(value): 633 | """Get Counter32""" 634 | if value > 0xffffffff: 635 | raise Exception('Counter32 value must be in [0..4294967295]') 636 | return write_tv(ASN1_COUNTER32, _write_int(value)) 637 | 638 | 639 | def counter64(value): 640 | """Get Counter64""" 641 | if value > 0xffffffffffffffff: 642 | raise Exception('Counter64 value must be in [0..18446744073709551615]') 643 | return write_tv(ASN1_COUNTER64, _write_int(value)) 644 | 645 | 646 | def replace_wildcards(value): 647 | """Replace wildcards with some possible big values""" 648 | return value.replace('?', '9').replace('*', str(0xffffffff)) 649 | 650 | 651 | def oid_cmp(oid1, oid2): 652 | """OIDs comparator function""" 653 | oid1 = replace_wildcards(oid1) 654 | oid2 = replace_wildcards(oid2) 655 | oid1 = [int(x) for x in oid1.replace('iso', '1').strip('.').split('.')] 656 | oid2 = [int(x) for x in oid2.replace('iso', '1').strip('.').split('.')] 657 | if oid1 < oid2: 658 | return -1 659 | elif oid1 > oid2: 660 | return 1 661 | return 0 662 | 663 | 664 | def get_next(oids, oid): 665 | """Get next OID from the OIDs list""" 666 | for val in sorted(oids, key=functools.cmp_to_key(oid_cmp)): 667 | # return first if compared with empty oid 668 | if not oid: 669 | return val 670 | # if oid < val, return val (i.e. first oid value after oid) 671 | elif oid_cmp(oid, val) < 0: 672 | return val 673 | # return empty when no more oids available 674 | return '' 675 | 676 | 677 | def parse_config(filename): 678 | """Read and parse a config""" 679 | oids = {} 680 | try: 681 | with open(filename, 'rb') as conf_file: 682 | data = conf_file.read() 683 | out_locals = {} 684 | exec(data, globals(), out_locals) 685 | oids = out_locals['DATA'] 686 | for value in oids.values(): 687 | if isinstance(value, types.FunctionType): 688 | if value.__code__.co_argcount != 1: 689 | raise ConfigError('"{}" must have one argument'.format(value.__name__)) 690 | return oids 691 | except Exception as ex: 692 | raise ConfigError('Config parsing error: {}'.format(ex)) 693 | return oids 694 | 695 | 696 | def find_oid_and_value_with_wildcard(oids, oid): 697 | """Find OID and OID value with wildcards""" 698 | wildcard_keys = [x for x in oids.keys() if '*' in x or '?' in x] 699 | out = [] 700 | for wck in wildcard_keys: 701 | if fnmatch.filter([oid], wck): 702 | value = oids[wck](oid) 703 | out.append((wck, value,)) 704 | return out 705 | 706 | 707 | def handle_get_request(oids, oid): 708 | """Handle GetRequest PDU""" 709 | error_status = ASN1_ERROR_STATUS_NO_ERROR 710 | error_index = 0 711 | oid_value = null() 712 | found = oid in oids 713 | if found: 714 | # TODO: check this 715 | oid_value = oids[oid] 716 | if not oid_value: 717 | oid_value = struct.pack('BB', ASN1_NO_SUCH_OBJECT, 0) 718 | else: 719 | # now check wildcards 720 | results = find_oid_and_value_with_wildcard(oids, oid) 721 | if len(results) > 1: 722 | logger.warning('Several results found with wildcards for OID: %s', oid) 723 | if results: 724 | _, oid_value = results[0] 725 | if oid_value: 726 | found = True 727 | if not found: 728 | error_status = ASN1_ERROR_STATUS_NO_SUCH_NAME 729 | error_index = 1 730 | # TODO: check this 731 | oid_value = struct.pack('BB', ASN1_NO_SUCH_INSTANCE, 0) 732 | return error_status, error_index, oid_value 733 | 734 | 735 | def handle_get_next_request(oids, oid): 736 | """Handle GetNextRequest""" 737 | error_status = ASN1_ERROR_STATUS_NO_ERROR 738 | error_index = 0 739 | oid_value = null() 740 | new_oid = None 741 | if oid in oids: 742 | new_oid = get_next(oids, oid) 743 | if not new_oid: 744 | oid_value = struct.pack('BB', ASN1_END_OF_MIB_VIEW, 0) #null() 745 | else: 746 | oid_value = oids.get(new_oid) 747 | else: 748 | # now check wildcards 749 | results = find_oid_and_value_with_wildcard(oids, oid) 750 | if len(results) > 1: 751 | logger.warning('Several results found with wildcards for OID: %s', oid) 752 | if results: 753 | # if found several results get first one 754 | oid_key, oid_value = results[0] 755 | # and get the next oid from oids 756 | new_oid = get_next(oids, oid_key) 757 | else: 758 | new_oid = get_next(oids, oid) 759 | oid_value = oids.get(new_oid) 760 | if not oid_value: 761 | oid_value = null() 762 | # if new oid is found - get it, otherwise calculate possible next one 763 | if new_oid: 764 | oid = new_oid 765 | else: 766 | oid = get_next_oid(oid.rstrip('.0')) + '.0' 767 | # if wildcards are used in oid - replace them 768 | final_oid = replace_wildcards(oid) 769 | return error_status, error_index, final_oid, oid_value 770 | 771 | 772 | def handle_set_request(oids, oid, type_and_value): 773 | """Handle SetRequest PDU""" 774 | error_status = ASN1_ERROR_STATUS_NO_ERROR 775 | error_index = 0 776 | value_type, value = type_and_value 777 | if value_type == 'INTEGER': 778 | enum_values = None 779 | if isinstance(oids[oid], tuple) and len(oids[oid]) > 1: 780 | enum_values = oids[oid][1] 781 | oids[oid] = integer(value, enum=enum_values) 782 | elif value_type == 'STRING': 783 | oids[oid] = octet_string(value if PY3 else value.encode('latin')) 784 | elif value_type == 'OID': 785 | oids[oid] = object_identifier(value) 786 | elif value_type == 'TIMETICKS': 787 | oids[oid] = timeticks(value) 788 | elif value_type == 'IPADDRESS': 789 | oids[oid] = ip_address(value) 790 | elif value_type == 'COUNTER32': 791 | oids[oid] = counter32(value) 792 | elif value_type == 'COUNTER64': 793 | oids[oid] = counter64(value) 794 | elif value_type == 'GAUGE32': 795 | oids[oid] = gauge32(value) 796 | elif value_type == 'OPAQUE': 797 | if value[0] == 'FLOAT': 798 | oids[oid] = real(value[1]) 799 | elif value[0] == 'DOUBLE': 800 | oids[oid] = double(value[1]) 801 | elif value[0] == 'UINT64': 802 | oids[oid] = uint64(value[1]) 803 | elif value[0] == 'INT64': 804 | oids[oid] = int64(value[1]) 805 | else: 806 | raise Exception('Unsupported type: {} ({})'.format(value_type, repr(value))) 807 | oid_value = oids[oid] 808 | return error_status, error_index, oid_value 809 | 810 | 811 | def craft_response(version, community, request_id, error_status, error_index, oid_items): 812 | """Craft SNMP response""" 813 | response = write_tv( 814 | ASN1_SEQUENCE, 815 | # add version and community from request 816 | write_tv(ASN1_INTEGER, _write_int(version)) + 817 | write_tv(ASN1_OCTET_STRING, community.encode('latin') if PY3 else str(community)) + 818 | # add GetResponse PDU with get response fields 819 | write_tv( 820 | ASN1_GET_RESPONSE_PDU, 821 | # add response id, error status and error index 822 | write_tv(ASN1_INTEGER, _write_int(request_id)) + 823 | write_tlv(ASN1_INTEGER, 1, _write_int(error_status)) + 824 | write_tlv(ASN1_INTEGER, 1, _write_int(error_index)) + 825 | # add variable bindings 826 | write_tv( 827 | ASN1_SEQUENCE, 828 | b''.join( 829 | # add OID and OID value 830 | write_tv( 831 | ASN1_SEQUENCE, 832 | write_tv( 833 | ASN1_OBJECT_IDENTIFIER, 834 | oid_key.encode('latin') if PY3 else oid_key 835 | ) + 836 | oid_value 837 | ) for (oid_key, oid_value) in oid_items 838 | ) 839 | ) 840 | ) 841 | ) 842 | return response 843 | 844 | 845 | def generate_response(request_result, oids): 846 | 847 | # get required fields from request 848 | version = request_result[0][1] 849 | community = request_result[1][1] 850 | pdu_type = request_result[2][1] 851 | request_id = request_result[3][1] 852 | max_repetitions = request_result[5][1] 853 | logger.debug('max_repetitions %i', max_repetitions) 854 | 855 | error_status = ASN1_ERROR_STATUS_NO_ERROR 856 | error_index = 0 857 | oid_items = [] 858 | oid_value = null() 859 | 860 | # handle protocol data units 861 | if pdu_type == ASN1_GET_REQUEST_PDU: 862 | requested_oids = request_result[6:] 863 | for _, oid in requested_oids: 864 | _, _, oid_value = handle_get_request(oids, oid) 865 | # if oid value is a function - call it to get the value 866 | if isinstance(oid_value, types.FunctionType): 867 | oid_value = oid_value(oid) 868 | if isinstance(oid_value, tuple): 869 | oid_value = oid_value[0] 870 | oid_items.append((oid_to_bytes(oid), oid_value)) 871 | 872 | elif pdu_type == ASN1_GET_NEXT_REQUEST_PDU: 873 | oid = request_result[6][1] 874 | error_status, error_index, oid, oid_value = handle_get_next_request(oids, oid) 875 | if isinstance(oid_value, types.FunctionType): 876 | oid_value = oid_value(oid) 877 | if isinstance(oid_value, tuple): 878 | oid_value = oid_value[0] 879 | oid_items.append((oid_to_bytes(oid), oid_value)) 880 | 881 | elif pdu_type == ASN1_GET_BULK_REQUEST_PDU: 882 | requested_oids = request_result[6:] 883 | for _ in range(0, max_repetitions): 884 | for idx, val in enumerate(requested_oids): 885 | oid = val[1] 886 | error_status, error_index, oid, oid_value = handle_get_next_request(oids, oid) 887 | if isinstance(oid_value, types.FunctionType): 888 | oid_value = oid_value(oid) 889 | if isinstance(oid_value, tuple): 890 | oid_value = oid_value[0] 891 | oid_items.append((oid_to_bytes(oid), oid_value)) 892 | requested_oids[idx] = ('OID', oid) 893 | 894 | elif pdu_type == ASN1_SET_REQUEST_PDU: 895 | if len(request_result) < 8: 896 | raise Exception('Invalid ASN.1 parsed request length for SNMP set request!') 897 | oid = request_result[6][1] 898 | type_and_value = request_result[7] 899 | try: 900 | if isinstance(oids[oid], tuple) and len(oids[oid]) > 1: 901 | enum_values = oids[oid][1] 902 | new_value = type_and_value[1] 903 | if isinstance(enum_values, Iterable) and new_value not in enum_values: 904 | raise WrongValueError('Value {} is outside the range of enum values'.format(new_value)) 905 | error_status, error_index, oid_value = handle_set_request(oids, oid, type_and_value) 906 | except WrongValueError as ex: 907 | logger.error(ex) 908 | error_status = ASN1_ERROR_STATUS_WRONG_VALUE 909 | error_index = 0 910 | except Exception as ex: 911 | logger.error(ex) 912 | error_status = ASN1_ERROR_STATUS_BAD_VALUE 913 | error_index = 0 914 | # if oid value is a function - call it to get the value 915 | if isinstance(oid_value, types.FunctionType): 916 | oid_value = oid_value(oid) 917 | if isinstance(oid_value, tuple): 918 | oid_value = oid_value[0] 919 | oid_items.append((oid_to_bytes(oid), oid_value)) 920 | 921 | # craft SNMP response 922 | response = craft_response( 923 | version, community, request_id, error_status, error_index, oid_items) 924 | return response 925 | 926 | def send_response(sock, response, address): 927 | logger.debug('Sending %d bytes of response', len(response)) 928 | try: 929 | sock.sendto(response, address) 930 | except socket.error as ex: 931 | logger.error('Failed to send %d bytes of response: %s', len(response), ex) 932 | logger.debug('') 933 | 934 | 935 | class SNMPServer: 936 | DEFAULT_LISTEN_HOST = '0.0.0.0' 937 | DEFAULT_LISTEN_PORT = 0 938 | DELAY_BEFORE_REPLY = 0 939 | 940 | def __init__(self, host, port): 941 | self.host = host 942 | self.port = port 943 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 944 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 945 | self.expected_messages = dict() 946 | self._is_running = True 947 | 948 | def start(self): 949 | self.sock.bind((self.host, self.port)) 950 | if self.port == 0: 951 | self.port = self.sock.getsockname()[1] 952 | logger.info('SNMP server listening on {}:{}'.format(self.host, self.port)) 953 | 954 | def stop(self): 955 | self._is_running = False 956 | self.sock.close() 957 | 958 | def __enter__(self): 959 | self.start() 960 | return self 961 | 962 | def __exit__(self, exception_type, exception_vale, traceback): 963 | self.stop() 964 | 965 | def process_request(self): 966 | while self._is_running: 967 | request_data, address = self.sock.recvfrom(4096) 968 | logger.debug('Received %d bytes from %s', len(request_data), address) 969 | 970 | request_stream = StringIO(request_data.decode('latin')) 971 | request_result = _parse_snmp_asn1(request_stream) 972 | 973 | if len(request_result) < 7: 974 | raise Exception('Invalid ASN.1 parsed request length!') 975 | request = dict(request_result) 976 | print(request) 977 | 978 | _id = request['OID'] 979 | if _id not in self.expected_messages: 980 | self.sock.close() 981 | raise ValueError(f'Request OID ({_id}) was not expected') 982 | 983 | response = generate_response(request_result, self.expected_messages) 984 | time.sleep(self.DELAY_BEFORE_REPLY) 985 | send_response(self.sock, response, address) 986 | 987 | def expect_request(self, request_id, reply_with, populate_parent=True): 988 | if isinstance(reply_with, str): 989 | reply = octet_string(reply_with) 990 | elif isinstance(reply_with, int): 991 | reply = integer(reply_with) 992 | elif isinstance(reply_with, list): 993 | reply = integer(reply_with[0], enum=reply_with) 994 | else: 995 | print('wtf!!', reply_with) 996 | reply = reply_with 997 | 998 | print(request_id, reply) 999 | self.expected_messages[request_id] = reply 1000 | if populate_parent: 1001 | parent = request_id.rpartition('.')[0] 1002 | if parent not in self.expected_messages: 1003 | self.expected_messages[parent] = None 1004 | 1005 | 1006 | def main(): 1007 | host = '0.0.0.0' 1008 | port = 1234 1009 | s = SNMPServer(host, port) 1010 | s.start() 1011 | print(s.expect_request('1.3.6.1.2.1.2.2.1.2')) 1012 | s.stop() 1013 | 1014 | 1015 | if __name__ == '__main__': 1016 | main() 1017 | 1018 | -------------------------------------------------------------------------------- /snmp-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Simple SNMP server in pure Python 5 | """ 6 | 7 | from __future__ import print_function 8 | 9 | import argparse 10 | import fnmatch 11 | import functools 12 | import logging 13 | import socket 14 | import string 15 | import struct 16 | import sys 17 | import types 18 | from contextlib import closing 19 | 20 | try: 21 | from collections import Iterable 22 | except ImportError: 23 | from collections.abc import Iterable 24 | 25 | try: 26 | from StringIO import StringIO 27 | except ImportError: 28 | from io import StringIO 29 | 30 | __version__ = '1.0.5' 31 | 32 | PY3 = sys.version_info[0] == 3 33 | 34 | logging.basicConfig(format='[%(levelname)s] %(message)s') 35 | logger = logging.getLogger() 36 | logger.setLevel(logging.WARNING) 37 | 38 | # ASN.1 tags 39 | ASN1_BOOLEAN = 0x01 40 | ASN1_INTEGER = 0x02 41 | ASN1_BIT_STRING = 0x03 42 | ASN1_OCTET_STRING = 0x04 43 | ASN1_NULL = 0x05 44 | ASN1_OBJECT_IDENTIFIER = 0x06 45 | ASN1_UTF8_STRING = 0x0c 46 | ASN1_PRINTABLE_STRING = 0x13 47 | ASN1_IA5_STRING = 0x16 48 | ASN1_BMP_STRING = 0x1e 49 | ASN1_SEQUENCE = 0x30 50 | ASN1_SET = 0x31 51 | ASN1_IPADDRESS = 0x40 52 | ASN1_COUNTER32 = 0x41 53 | ASN1_GAUGE32 = 0x42 54 | ASN1_TIMETICKS = 0x43 55 | ASN1_OPAQUE = 0x44 56 | ASN1_COUNTER64 = 0x46 57 | ASN1_NO_SUCH_OBJECT = 0x80 58 | ASN1_NO_SUCH_INSTANCE = 0x81 59 | ASN1_END_OF_MIB_VIEW = 0x82 60 | ASN1_GET_REQUEST_PDU = 0xA0 61 | ASN1_GET_NEXT_REQUEST_PDU = 0xA1 62 | ASN1_GET_RESPONSE_PDU = 0xA2 63 | ASN1_SET_REQUEST_PDU = 0xA3 64 | ASN1_TRAP_REQUEST_PDU = 0xA4 65 | ASN1_GET_BULK_REQUEST_PDU = 0xA5 66 | ASN1_INFORM_REQUEST_PDU = 0xA6 67 | ASN1_SNMPv2_TRAP_REQUEST_PDU = 0xA7 68 | ASN1_REPORT_REQUEST_PDU = 0xA8 69 | 70 | # error statuses 71 | ASN1_ERROR_STATUS_NO_ERROR = 0x00 72 | ASN1_ERROR_STATUS_TOO_BIG = 0x01 73 | ASN1_ERROR_STATUS_NO_SUCH_NAME = 0x02 74 | ASN1_ERROR_STATUS_BAD_VALUE = 0x03 75 | ASN1_ERROR_STATUS_READ_ONLY = 0x04 76 | ASN1_ERROR_STATUS_GEN_ERR = 0x05 77 | ASN1_ERROR_STATUS_WRONG_VALUE = 0x0A 78 | 79 | # some ASN.1 opaque special types 80 | ASN1_CONTEXT = 0x80 # context-specific 81 | ASN1_EXTENSION_ID = 0x1F # 0b11111 (fill tag in first octet) 82 | ASN1_OPAQUE_TAG1 = ASN1_CONTEXT | ASN1_EXTENSION_ID # 0x9f 83 | ASN1_OPAQUE_TAG2 = 0x30 # base tag value 84 | ASN1_APPLICATION = 0x40 85 | ASN1_APP_FLOAT = ASN1_APPLICATION | 0x08 # application-specific type 0x08 86 | ASN1_APP_DOUBLE = ASN1_APPLICATION | 0x09 # application-specific type 0x09 87 | ASN1_APP_INT64 = ASN1_APPLICATION | 0x0A # application-specific type 0x0A 88 | ASN1_APP_UINT64 = ASN1_APPLICATION | 0x0B # application-specific type 0x0B 89 | ASN1_OPAQUE_FLOAT = ASN1_OPAQUE_TAG2 | ASN1_APP_FLOAT 90 | ASN1_OPAQUE_DOUBLE = ASN1_OPAQUE_TAG2 | ASN1_APP_DOUBLE 91 | ASN1_OPAQUE_INT64 = ASN1_OPAQUE_TAG2 | ASN1_APP_INT64 92 | ASN1_OPAQUE_UINT64 = ASN1_OPAQUE_TAG2 | ASN1_APP_UINT64 93 | ASN1_OPAQUE_FLOAT_BER_LEN = 7 94 | ASN1_OPAQUE_DOUBLE_BER_LEN = 11 95 | ASN1_OPAQUE_INT64_BER_LEN = 4 96 | ASN1_OPAQUE_UINT64_BER_LEN = 4 97 | 98 | SNMP_VERSIONS = { 99 | 1: 'v1', 100 | 2: 'v2c', 101 | 3: 'v3', 102 | } 103 | 104 | SNMP_PDUS = ( 105 | 'version', 106 | 'community', 107 | 'PDU-type', 108 | 'request-id', 109 | 'error-status', 110 | 'error-index', 111 | 'variable bindings', 112 | ) 113 | 114 | 115 | class ProtocolError(Exception): 116 | """Raise when SNMP protocol error occurred""" 117 | 118 | 119 | class ConfigError(Exception): 120 | """Raise when config error occurred""" 121 | 122 | 123 | class BadValueError(Exception): 124 | """Raise when bad value error occurred""" 125 | 126 | 127 | class WrongValueError(Exception): 128 | """Raise when wrong value (e.g. value not in available range) error occurred""" 129 | 130 | 131 | def encode_to_7bit(value): 132 | """Encode to 7 bit""" 133 | if value > 0x7f: 134 | res = [] 135 | res.insert(0, value & 0x7f) 136 | while value > 0x7f: 137 | value >>= 7 138 | res.insert(0, (value & 0x7f) | 0x80) 139 | return res 140 | return [value] 141 | 142 | 143 | def oid_to_bytes_list(oid): 144 | """Convert OID str to bytes list""" 145 | if oid.startswith('iso'): 146 | oid = oid.replace('iso', '1') 147 | try: 148 | oid_values = [int(x) for x in oid.split('.') if x] 149 | first_val = 40 * oid_values[0] + oid_values[1] 150 | except (ValueError, IndexError): 151 | raise Exception('Could not parse OID value "{}"'.format(oid)) 152 | result_values = [first_val] 153 | for node_num in oid_values[2:]: 154 | result_values += encode_to_7bit(node_num) 155 | return result_values 156 | 157 | 158 | def oid_to_bytes(oid): 159 | """Convert OID str to bytes""" 160 | return ''.join([chr(x) for x in oid_to_bytes_list(oid)]) 161 | 162 | 163 | def bytes_to_oid(data): 164 | """Convert bytes to OID str""" 165 | values = [ord(x) for x in data] 166 | first_val = values.pop(0) 167 | res = [] 168 | res += divmod(first_val, 40) 169 | while values: 170 | val = values.pop(0) 171 | if val > 0x7f: 172 | huge_vals = [val] 173 | while True: 174 | next_val = values.pop(0) 175 | huge_vals.append(next_val) 176 | if next_val < 0x80: 177 | break 178 | huge = 0 179 | for i, huge_byte in enumerate(huge_vals): 180 | huge += (huge_byte & 0x7f) << (7 * (len(huge_vals) - i - 1)) 181 | res.append(huge) 182 | else: 183 | res.append(val) 184 | return '.'.join(str(x) for x in res) 185 | 186 | 187 | def timeticks_to_str(ticks): 188 | """Return "days, hours, minutes, seconds and ms" string from ticks""" 189 | days, rem1 = divmod(ticks, 24 * 60 * 60 * 100) 190 | hours, rem2 = divmod(rem1, 60 * 60 * 100) 191 | minutes, rem3 = divmod(rem2, 60 * 100) 192 | seconds, milliseconds = divmod(rem3, 100) 193 | ending = 's' if days > 1 else '' 194 | days_fmt = '{} day{}, '.format(days, ending) if days > 0 else '' 195 | return '{}{:-02}:{:-02}:{:-02}.{:-02}'.format(days_fmt, hours, minutes, seconds, milliseconds) 196 | 197 | 198 | def int_to_ip(value): 199 | """Int to IP""" 200 | return socket.inet_ntoa(struct.pack("!I", value)) 201 | 202 | 203 | def twos_complement(value, bits): 204 | """Calculate two's complement""" 205 | mask = 2 ** (bits - 1) 206 | return -(value & mask) + (value & ~mask) 207 | 208 | 209 | def _read_byte(stream): 210 | """Read byte from stream""" 211 | read_byte = stream.read(1) 212 | if not read_byte: 213 | raise Exception('No more bytes!') 214 | return ord(read_byte) 215 | 216 | 217 | def _read_int_len(stream, length, signed=False): 218 | """Read int with length""" 219 | result = 0 220 | sign = None 221 | for _ in range(length): 222 | value = _read_byte(stream) 223 | if sign is None: 224 | sign = value & 0x80 225 | result = (result << 8) + value 226 | if signed and sign: 227 | result = twos_complement(result, 8 * length) 228 | return result 229 | 230 | def _write_int(value, strip_leading_zeros=True): 231 | """Write int while ensuring correct sign representation.""" 232 | if abs(value) > 0xffffffffffffffff: 233 | raise Exception('Int value must be in [0..18446744073709551615]') 234 | 235 | # Determine the correct format specifier based on the value's magnitude and sign. 236 | if value < 0: 237 | if abs(value) <= 0x7f: 238 | result = struct.pack('>b', value) 239 | elif abs(value) <= 0x7fff: 240 | result = struct.pack('>h', value) 241 | elif abs(value) <= 0x7fffffff: 242 | result = struct.pack('>i', value) 243 | elif abs(value) <= 0x7fffffffffffffff: 244 | result = struct.pack('>q', value) 245 | else: 246 | raise Exception('Min signed int value') 247 | else: 248 | if not strip_leading_zeros: 249 | # Always pack as the largest size to simplify leading zero handling. 250 | if value <= 0x7fffffff: 251 | result = struct.pack('>I', value) 252 | else: 253 | result = struct.pack('>Q', value) 254 | else: 255 | # Always pack as the largest size to simplify leading zero handling. 256 | result = struct.pack('>Q', value) 257 | # Check if the first relevant byte (ignoring leading zeros for now) would be misinterpreted as negative. 258 | if (result[0] == 0x00 and (result[1] & 0x80) != 0): 259 | # If not stripping leading zeros, or if stripping them would cause a misinterpretation, 260 | # leave the result as is. This branch might need revisiting based on specific needs. 261 | pass 262 | else: 263 | # Here's the core of the adjustment: only strip leading zeros if it does not lead to misinterpretation. 264 | # This means checking if the first byte would make it look negative and adjusting accordingly. 265 | first_non_zero_byte = next((i for i, byte in enumerate(result) if byte != 0), len(result) - 1) 266 | if result[first_non_zero_byte] & 0x80: 267 | # If the first non-zero byte's MSB is set, prepend a 0x00 to keep it positive. 268 | result = b'\x00' + result[first_non_zero_byte:] 269 | else: 270 | # Otherwise, strip all leading zeros except the last one, if all are zeros. 271 | result = result[first_non_zero_byte:] 272 | 273 | return result or b'\x00' 274 | 275 | def _write_asn1_length(length): 276 | """Write ASN.1 length""" 277 | if length > 0x7f: 278 | if length <= 0xff: 279 | packed_length = 0x81 280 | elif length <= 0xffff: 281 | packed_length = 0x82 282 | elif length <= 0xffffff: 283 | packed_length = 0x83 284 | elif length <= 0xffffffff: 285 | packed_length = 0x84 286 | else: 287 | raise Exception('Length is too big!') 288 | return struct.pack('B', packed_length) + _write_int(length) 289 | return struct.pack('B', length) 290 | 291 | 292 | def _parse_asn1_length(stream): 293 | """Parse ASN.1 length""" 294 | length = _read_byte(stream) 295 | # handle long length 296 | if length > 0x7f: 297 | data_length = length - 0x80 298 | if not 0 < data_length <= 4: 299 | raise Exception('Data length must be in [1..4]') 300 | length = _read_int_len(stream, data_length) 301 | return length 302 | 303 | 304 | def _parse_asn1_octet_string(stream): 305 | """Parse ASN.1 octet string""" 306 | length = _parse_asn1_length(stream) 307 | value = stream.read(length) 308 | # if any char is not printable - convert string to hex 309 | if any(c not in string.printable for c in value): 310 | return ' '.join(['%02X' % ord(x) for x in value]) 311 | return value 312 | 313 | 314 | def _parse_asn1_opaque_float(stream): 315 | """Parse ASN.1 opaque float""" 316 | length = _parse_asn1_length(stream) 317 | value = _read_int_len(stream, length, signed=True) 318 | # convert int to float 319 | float_value = struct.unpack('>f', struct.pack('>l', value))[0] 320 | logger.debug('ASN1_OPAQUE_FLOAT: %s', round(float_value, 5)) 321 | return 'FLOAT', round(float_value, 5) 322 | 323 | 324 | def _parse_asn1_opaque_double(stream): 325 | """Parse ASN.1 opaque double""" 326 | length = _parse_asn1_length(stream) 327 | value = _read_int_len(stream, length, signed=True) 328 | # convert long long to double 329 | double_value = struct.unpack('>d', struct.pack('>q', value))[0] 330 | logger.debug('ASN1_OPAQUE_DOUBLE: %s', round(double_value, 5)) 331 | return 'DOUBLE', round(double_value, 5) 332 | 333 | 334 | def _parse_asn1_opaque_int64(stream): 335 | """Parse ASN.1 opaque int64""" 336 | length = _parse_asn1_length(stream) 337 | value = _read_int_len(stream, length, signed=True) 338 | logger.debug('ASN1_OPAQUE_INT64: %s', value) 339 | return 'INT64', value 340 | 341 | 342 | def _parse_asn1_opaque_uint64(stream): 343 | """Parse ASN.1 opaque uint64""" 344 | length = _parse_asn1_length(stream) 345 | value = _read_int_len(stream, length) 346 | logger.debug('ASN1_OPAQUE_UINT64: %s', value) 347 | return 'UINT64', value 348 | 349 | 350 | def _parse_asn1_opaque(stream): 351 | """Parse ASN.1 opaque""" 352 | length = _parse_asn1_length(stream) 353 | opaque_tag = _read_byte(stream) 354 | opaque_type = _read_byte(stream) 355 | if (length == ASN1_OPAQUE_FLOAT_BER_LEN and 356 | opaque_tag == ASN1_OPAQUE_TAG1 and 357 | opaque_type == ASN1_OPAQUE_FLOAT): 358 | return _parse_asn1_opaque_float(stream) 359 | elif (length == ASN1_OPAQUE_DOUBLE_BER_LEN and 360 | opaque_tag == ASN1_OPAQUE_TAG1 and 361 | opaque_type == ASN1_OPAQUE_DOUBLE): 362 | return _parse_asn1_opaque_double(stream) 363 | elif (length >= ASN1_OPAQUE_INT64_BER_LEN and 364 | opaque_tag == ASN1_OPAQUE_TAG1 and 365 | opaque_type == ASN1_OPAQUE_INT64): 366 | return _parse_asn1_opaque_int64(stream) 367 | elif (length >= ASN1_OPAQUE_UINT64_BER_LEN and 368 | opaque_tag == ASN1_OPAQUE_TAG1 and 369 | opaque_type == ASN1_OPAQUE_UINT64): 370 | return _parse_asn1_opaque_uint64(stream) 371 | # for simple opaque - rewind 2 bytes back (opaque tag and type) 372 | stream.seek(stream.tell() - 2) 373 | return stream.read(length) 374 | 375 | 376 | def _is_trap_request(result): 377 | """Checks if it is Trap-PDU request.""" 378 | return len(result) > 2 and result[2][1] == ASN1_TRAP_REQUEST_PDU 379 | 380 | 381 | def _validate_protocol(pdu_index, tag, result): 382 | """Validates the protocol and returns True if valid, or False otherwise.""" 383 | if _is_trap_request(result): 384 | if ( 385 | pdu_index == 4 and tag != ASN1_OBJECT_IDENTIFIER or 386 | pdu_index == 5 and tag != ASN1_IPADDRESS or 387 | pdu_index in [6, 7] and tag != ASN1_INTEGER or 388 | pdu_index == 8 and tag != ASN1_TIMETICKS 389 | ): 390 | return False 391 | elif ( 392 | pdu_index in [1, 4, 5, 6] and tag != ASN1_INTEGER or 393 | pdu_index == 2 and tag != ASN1_OCTET_STRING or 394 | pdu_index == 3 and tag not in [ 395 | ASN1_GET_REQUEST_PDU, 396 | ASN1_GET_NEXT_REQUEST_PDU, 397 | ASN1_SET_REQUEST_PDU, 398 | ASN1_GET_BULK_REQUEST_PDU, 399 | ASN1_TRAP_REQUEST_PDU, 400 | ASN1_INFORM_REQUEST_PDU, 401 | ASN1_SNMPv2_TRAP_REQUEST_PDU, 402 | ] 403 | ): 404 | return False 405 | return True 406 | 407 | 408 | def _parse_snmp_asn1(stream): 409 | """Parse SNMP ASN.1 410 | After |IP|UDP| headers and "sequence" tag, SNMP protocol data units (PDUs) are the next: 411 | |version|community|PDU-type|request-id|error-status|error-index|variable bindings| 412 | but for ASN1_TRAP_REQUEST_PDU next: 413 | |version|community|PDU-type|enterprise-oid|agent|trap-type|specific-type|uptime| 414 | """ 415 | result = [] 416 | wait_oid_value = False 417 | pdu_index = 0 418 | while True: 419 | read_byte = stream.read(1) 420 | if not read_byte: 421 | if pdu_index < 7: 422 | raise ProtocolError('Not all SNMP protocol data units are read!') 423 | return result 424 | tag = ord(read_byte) 425 | # check protocol's tags at indices 426 | if not _validate_protocol(pdu_index, tag, result): 427 | raise ProtocolError('Invalid tag for PDU unit "{}"'.format(SNMP_PDUS[pdu_index])) 428 | if tag == ASN1_SEQUENCE: 429 | length = _parse_asn1_length(stream) 430 | logger.debug('ASN1_SEQUENCE: %s', 'length = {}'.format(length)) 431 | elif tag == ASN1_INTEGER: 432 | length = _read_byte(stream) 433 | value = _read_int_len(stream, length, True) 434 | logger.debug('ASN1_INTEGER: %s', value) 435 | # pdu_index is version, request-id, error-status, error-index 436 | if wait_oid_value or pdu_index in [1, 4, 5, 6] or _is_trap_request(result): 437 | result.append(('INTEGER', value)) 438 | wait_oid_value = False 439 | elif tag == ASN1_OCTET_STRING: 440 | value = _parse_asn1_octet_string(stream) 441 | logger.debug('ASN1_OCTET_STRING: %s', value) 442 | if wait_oid_value or pdu_index == 2: # community 443 | result.append(('STRING', value)) 444 | wait_oid_value = False 445 | elif tag == ASN1_OBJECT_IDENTIFIER: 446 | length = _read_byte(stream) 447 | value = stream.read(length) 448 | logger.debug('ASN1_OBJECT_IDENTIFIER: %s', bytes_to_oid(value)) 449 | result.append(('OID', bytes_to_oid(value))) 450 | wait_oid_value = True 451 | elif tag == ASN1_PRINTABLE_STRING: 452 | length = _parse_asn1_length(stream) 453 | value = stream.read(length) 454 | logger.debug('ASN1_PRINTABLE_STRING: %s', value) 455 | elif tag == ASN1_GET_REQUEST_PDU: 456 | length = _parse_asn1_length(stream) 457 | logger.debug('ASN1_GET_REQUEST_PDU: %s', 'length = {}'.format(length)) 458 | if pdu_index == 3: # PDU-type 459 | result.append(('ASN1_GET_REQUEST_PDU', tag)) 460 | elif tag == ASN1_GET_NEXT_REQUEST_PDU: 461 | length = _parse_asn1_length(stream) 462 | logger.debug('ASN1_GET_NEXT_REQUEST_PDU: %s', 'length = {}'.format(length)) 463 | if pdu_index == 3: # PDU-type 464 | result.append(('ASN1_GET_NEXT_REQUEST_PDU', tag)) 465 | elif tag == ASN1_GET_BULK_REQUEST_PDU: 466 | length = _parse_asn1_length(stream) 467 | logger.debug('ASN1_GET_BULK_REQUEST_PDU: %s', 'length = {}'.format(length)) 468 | if pdu_index == 3: # PDU-type 469 | result.append(('ASN1_GET_BULK_REQUEST_PDU', tag)) 470 | elif tag == ASN1_GET_RESPONSE_PDU: 471 | length = _parse_asn1_length(stream) 472 | logger.debug('ASN1_GET_RESPONSE_PDU: %s', 'length = {}'.format(length)) 473 | elif tag == ASN1_SET_REQUEST_PDU: 474 | length = _parse_asn1_length(stream) 475 | logger.debug('ASN1_SET_REQUEST_PDU: %s', 'length = {}'.format(length)) 476 | if pdu_index == 3: # PDU-type 477 | result.append(('ASN1_SET_REQUEST_PDU', tag)) 478 | elif tag == ASN1_TRAP_REQUEST_PDU: 479 | length = _parse_asn1_length(stream) 480 | logger.debug('ASN1_TRAP_REQUEST_PDU: %s', 'length = {}'.format(length)) 481 | if pdu_index == 3: # PDU-type 482 | result.append(('ASN1_TRAP_REQUEST_PDU', tag)) 483 | elif tag == ASN1_INFORM_REQUEST_PDU: 484 | if result and result[0][1] == 0: 485 | raise Exception('INFORM request PDU is not supported in SNMPv1!') 486 | length = _parse_asn1_length(stream) 487 | logger.debug('ASN1_INFORM_REQUEST_PDU: %s', 'length = {}'.format(length)) 488 | if pdu_index == 3: # PDU-type 489 | result.append(('ASN1_INFORM_REQUEST_PDU', tag)) 490 | elif tag == ASN1_SNMPv2_TRAP_REQUEST_PDU: 491 | if result and result[0][1] == 0: 492 | raise Exception('SNMPv2 TRAP PDU request is not supported in SNMPv1!') 493 | length = _parse_asn1_length(stream) 494 | logger.debug('ASN1_SNMPv2_TRAP_REQUEST_PDU: %s', 'length = {}'.format(length)) 495 | if pdu_index == 3: # PDU-type 496 | result.append(('ASN1_SNMPv2_TRAP_REQUEST_PDU', tag)) 497 | elif tag == ASN1_REPORT_REQUEST_PDU: 498 | raise Exception('Report request PDU is not supported!') 499 | elif tag == ASN1_TIMETICKS: 500 | length = _read_byte(stream) 501 | value = _read_int_len(stream, length) 502 | logger.debug('ASN1_TIMETICKS: %s (%s)', value, timeticks_to_str(value)) 503 | if wait_oid_value or _is_trap_request(result): 504 | result.append(('TIMETICKS', value)) 505 | wait_oid_value = False 506 | elif tag == ASN1_IPADDRESS: 507 | length = _read_byte(stream) 508 | value = _read_int_len(stream, length) 509 | logger.debug('ASN1_IPADDRESS: %s (%s)', value, int_to_ip(value)) 510 | if wait_oid_value or _is_trap_request(result): 511 | result.append(('IPADDRESS', int_to_ip(value))) 512 | wait_oid_value = False 513 | elif tag == ASN1_COUNTER32: 514 | length = _read_byte(stream) 515 | value = _read_int_len(stream, length) 516 | logger.debug('ASN1_COUNTER32: %s', value) 517 | if wait_oid_value: 518 | result.append(('COUNTER32', value)) 519 | wait_oid_value = False 520 | elif tag == ASN1_GAUGE32: 521 | length = _read_byte(stream) 522 | value = _read_int_len(stream, length) 523 | logger.debug('ASN1_GAUGE32: %s', value) 524 | if wait_oid_value: 525 | result.append(('GAUGE32', value)) 526 | wait_oid_value = False 527 | elif tag == ASN1_OPAQUE: 528 | value = _parse_asn1_opaque(stream) 529 | logger.debug('ASN1_OPAQUE: %r', value) 530 | if wait_oid_value: 531 | result.append(('OPAQUE', value)) 532 | wait_oid_value = False 533 | elif tag == ASN1_COUNTER64: 534 | length = _read_byte(stream) 535 | value = _read_int_len(stream, length) 536 | logger.debug('ASN1_COUNTER64: %s', value) 537 | if wait_oid_value: 538 | result.append(('COUNTER64', value)) 539 | wait_oid_value = False 540 | elif tag == ASN1_NULL: 541 | value = _read_byte(stream) 542 | logger.debug('ASN1_NULL: %s', value) 543 | elif tag == ASN1_NO_SUCH_OBJECT: 544 | value = _read_byte(stream) 545 | logger.debug('ASN1_NO_SUCH_OBJECT: %s', value) 546 | result.append('No Such Object') 547 | elif tag == ASN1_NO_SUCH_INSTANCE: 548 | value = _read_byte(stream) 549 | logger.debug('ASN1_NO_SUCH_INSTANCE: %s', value) 550 | result.append('No Such Instance with OID') 551 | elif tag == ASN1_END_OF_MIB_VIEW: 552 | value = _read_byte(stream) 553 | logger.debug('ASN1_END_OF_MIB_VIEW: %s', value) 554 | return ('', ''), ('', '') 555 | else: 556 | logger.debug('?: %s', hex(ord(read_byte))) 557 | pdu_index += 1 558 | 559 | 560 | def get_next_oid(oid): 561 | """Get the next OID parent's node""" 562 | # increment pre last node, e.g.: "1.3.6.1.1" -> "1.3.6.2.1" 563 | oid_vals = oid.rsplit('.', 2) 564 | if len(oid_vals) < 2: 565 | oid_vals[-1] = str(int(oid_vals[-1]) + 1) 566 | else: 567 | oid_vals[-2] = str(int(oid_vals[-2]) + 1) 568 | oid_vals[-1] = '1' 569 | oid_next = '.'.join(oid_vals) 570 | return oid_next 571 | 572 | 573 | def write_tlv(tag, length, value): 574 | """Write TLV (Tag-Length-Value)""" 575 | return struct.pack('B', tag) + _write_asn1_length(length) + value 576 | 577 | 578 | def write_tv(tag, value): 579 | """Write TV (Tag-Value) and calculate length from value""" 580 | return write_tlv(tag, len(value), value) 581 | 582 | 583 | def boolean(value): 584 | """Get Boolean""" 585 | return write_tlv(ASN1_BOOLEAN, 1, b'\xff' if value else b'\x00') 586 | 587 | 588 | def integer(value, enum=None): 589 | """Get Integer""" 590 | if enum and isinstance(enum, Iterable) and value not in enum: 591 | raise WrongValueError('Integer value {} is outside the range of enum values'.format(value)) 592 | if not (-2147483648 <= value <= 2147483647): 593 | raise Exception('Integer value must be in [-2147483648..2147483647]') 594 | if not enum: 595 | return write_tv(ASN1_INTEGER, _write_int(value, False)) 596 | return write_tv(ASN1_INTEGER, _write_int(value, False)), enum 597 | 598 | 599 | def bit_string(value): 600 | """ 601 | Get BitString 602 | For example, if the input value is '\xF0\xF0' 603 | F0 F0 in hex = 11110000 11110000 in binary 604 | And in binary bits 0, 1, 2, 3, 8, 9, 10, 11 are set, so these bits are added to the output 605 | Therefore the SNMP response is: F0 F0 0 1 2 3 8 9 10 11 606 | """ 607 | return write_tlv(ASN1_BIT_STRING, len(value), value.encode('latin') if PY3 else value) 608 | 609 | 610 | def octet_string(value): 611 | """Get OctetString""" 612 | return write_tv(ASN1_OCTET_STRING, value.encode('latin') if PY3 else value) 613 | 614 | 615 | def null(): 616 | """Get Null""" 617 | return write_tv(ASN1_NULL, b'') 618 | 619 | 620 | def object_identifier(value): 621 | """Get OID""" 622 | value = oid_to_bytes(value) 623 | return write_tv(ASN1_OBJECT_IDENTIFIER, value.encode('latin') if PY3 else value) 624 | 625 | 626 | def real(value): 627 | """Get real""" 628 | # opaque tag | len | tag1 | tag2 | len | data 629 | float_value = struct.pack('>f', value) 630 | opaque_type_value = struct.pack( 631 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_FLOAT 632 | ) + _write_asn1_length(len(float_value)) + float_value 633 | return write_tv(ASN1_OPAQUE, opaque_type_value) 634 | 635 | 636 | def double(value): 637 | """Get double""" 638 | # opaque tag | len | tag1 | tag2 | len | data 639 | double_value = struct.pack('>d', value) 640 | opaque_type_value = struct.pack( 641 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_DOUBLE 642 | ) + _write_asn1_length(len(double_value)) + double_value 643 | return write_tv(ASN1_OPAQUE, opaque_type_value) 644 | 645 | 646 | def int64(value): 647 | """Get int64""" 648 | # opaque tag | len | tag1 | tag2 | len | data 649 | int64_value = struct.pack('>q', value) 650 | opaque_type_value = struct.pack( 651 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_INT64 652 | ) + _write_asn1_length(len(int64_value)) + int64_value 653 | return write_tv(ASN1_OPAQUE, opaque_type_value) 654 | 655 | 656 | def uint64(value): 657 | """Get uint64""" 658 | # opaque tag | len | tag1 | tag2 | len | data 659 | uint64_value = struct.pack('>Q', value) 660 | opaque_type_value = struct.pack( 661 | 'BB', ASN1_OPAQUE_TAG1, ASN1_OPAQUE_UINT64 662 | ) + _write_asn1_length(len(uint64_value)) + uint64_value 663 | return write_tv(ASN1_OPAQUE, opaque_type_value) 664 | 665 | 666 | def utf8_string(value): 667 | """Get UTF8String""" 668 | return write_tv(ASN1_UTF8_STRING, value.encode('latin') if PY3 else value) 669 | 670 | 671 | def printable_string(value): 672 | """Get PrintableString""" 673 | return write_tv(ASN1_PRINTABLE_STRING, value.encode('latin') if PY3 else value) 674 | 675 | 676 | def ia5_string(value): 677 | """Get IA5String""" 678 | return write_tv(ASN1_IA5_STRING, value.encode('latin') if PY3 else value) 679 | 680 | 681 | def bmp_string(value): 682 | """Get BMPString""" 683 | return write_tv(ASN1_BMP_STRING, value.encode('utf-16-be')) 684 | 685 | 686 | def ip_address(value): 687 | """Get IPAddress""" 688 | return write_tv(ASN1_IPADDRESS, socket.inet_aton(value)) 689 | 690 | 691 | def timeticks(value): 692 | """Get Timeticks""" 693 | if value > 0xffffffff: 694 | raise Exception('Timeticks value must be in [0..4294967295]') 695 | return write_tv(ASN1_TIMETICKS, _write_int(value)) 696 | 697 | 698 | def gauge32(value): 699 | """Get Gauge32""" 700 | if value > 0xffffffff: 701 | raise Exception('Gauge32 value must be in [0..4294967295]') 702 | return write_tv(ASN1_GAUGE32, _write_int(value, strip_leading_zeros=False)) 703 | 704 | 705 | def counter32(value): 706 | """Get Counter32""" 707 | if value > 0xffffffff: 708 | raise Exception('Counter32 value must be in [0..4294967295]') 709 | return write_tv(ASN1_COUNTER32, _write_int(value)) 710 | 711 | 712 | def counter64(value): 713 | """Get Counter64""" 714 | if value > 0xffffffffffffffff: 715 | raise Exception('Counter64 value must be in [0..18446744073709551615]') 716 | return write_tv(ASN1_COUNTER64, _write_int(value)) 717 | 718 | 719 | def replace_wildcards(value): 720 | """Replace wildcards with some possible big values""" 721 | return value.replace('?', '9').replace('*', str(0xffffffff)) 722 | 723 | 724 | def oid_cmp(oid1, oid2): 725 | """OIDs comparator function""" 726 | oid1 = replace_wildcards(oid1) 727 | oid2 = replace_wildcards(oid2) 728 | oid1 = [int(x) for x in oid1.replace('iso', '1').strip('.').split('.')] 729 | oid2 = [int(x) for x in oid2.replace('iso', '1').strip('.').split('.')] 730 | if oid1 < oid2: 731 | return -1 732 | elif oid1 > oid2: 733 | return 1 734 | return 0 735 | 736 | 737 | def get_next(oids, oid): 738 | """Get next OID from the OIDs list""" 739 | for val in sorted(oids, key=functools.cmp_to_key(oid_cmp)): 740 | # return first if compared with empty oid 741 | if not oid: 742 | return val 743 | # if oid < val, return val (i.e. first oid value after oid) 744 | elif oid_cmp(oid, val) < 0: 745 | return val 746 | # return empty when no more oids available 747 | return '' 748 | 749 | 750 | def parse_config(filename): 751 | """Read and parse a config""" 752 | try: 753 | with open(filename, 'rb') as conf_file: 754 | data = conf_file.read() 755 | out_locals = {} 756 | exec(data, globals(), out_locals) 757 | oids = out_locals['DATA'] 758 | for value in oids.values(): 759 | if isinstance(value, types.FunctionType) and value.__code__.co_argcount != 1: 760 | raise ConfigError('"{}" must have one argument'.format(value.__name__)) 761 | return oids 762 | except Exception as ex: 763 | raise ConfigError('Config parsing error: {}'.format(ex)) 764 | return oids 765 | 766 | 767 | def find_oid_and_value_with_wildcard(oids, oid): 768 | """Find OID and OID value with wildcards""" 769 | wildcard_keys = [x for x in oids.keys() if '*' in x or '?' in x] 770 | out = [] 771 | for wck in wildcard_keys: 772 | if fnmatch.filter([oid], wck): 773 | value = oids[wck](oid) 774 | out.append((wck, value,)) 775 | return out 776 | 777 | 778 | def handle_get_request(oids, oid): 779 | """Handle GetRequest PDU""" 780 | error_status = ASN1_ERROR_STATUS_NO_ERROR 781 | error_index = 0 782 | oid_value = null() 783 | found = oid in oids 784 | if found: 785 | # TODO: check this 786 | oid_value = oids[oid] 787 | if not oid_value: 788 | oid_value = struct.pack('BB', ASN1_NO_SUCH_OBJECT, 0) 789 | else: 790 | # now check wildcards 791 | results = find_oid_and_value_with_wildcard(oids, oid) 792 | if len(results) > 1: 793 | logger.warning('Several results found with wildcards for OID: %s', oid) 794 | if results: 795 | _, oid_value = results[0] 796 | if oid_value: 797 | found = True 798 | if not found: 799 | error_status = ASN1_ERROR_STATUS_NO_SUCH_NAME 800 | error_index = 1 801 | # TODO: check this 802 | oid_value = struct.pack('BB', ASN1_NO_SUCH_INSTANCE, 0) 803 | return error_status, error_index, oid_value 804 | 805 | 806 | def handle_get_next_request(oids, oid, limit_to_last_in_config=True): 807 | """Handle GetNextRequest""" 808 | error_status = ASN1_ERROR_STATUS_NO_ERROR 809 | error_index = 0 810 | if oid in oids: 811 | new_oid = get_next(oids, oid) 812 | if not new_oid: 813 | oid_value = struct.pack('BB', ASN1_END_OF_MIB_VIEW, 0) 814 | else: 815 | oid_value = oids.get(new_oid) 816 | else: 817 | # now check wildcards 818 | results = find_oid_and_value_with_wildcard(oids, oid) 819 | if len(results) > 1: 820 | logger.warning('Several results found with wildcards for OID: %s', oid) 821 | if results: 822 | # if found several results get first one 823 | oid_key, oid_value = results[0] 824 | # and get the next oid from oids 825 | new_oid = get_next(oids, oid_key) 826 | else: 827 | new_oid = get_next(oids, oid) 828 | oid_value = oids.get(new_oid) 829 | if not oid_value: 830 | oid_value = null() 831 | # if new oid is found - get it, otherwise calculate possible next one 832 | if new_oid: 833 | oid = new_oid 834 | else: 835 | oid = get_next_oid(oid.rstrip('.0')) + '.0' 836 | # if wildcards are used in oid - replace them 837 | final_oid = replace_wildcards(oid) 838 | # to prevent loop - check a new oid and if it is more than the last in config - stop 839 | if oids and limit_to_last_in_config: 840 | last_oid_in_config = sorted(oids, key=functools.cmp_to_key(oid_cmp))[-1] 841 | if oid_cmp(final_oid, last_oid_in_config) > 0: 842 | oid_value = struct.pack('BB', ASN1_END_OF_MIB_VIEW, 0) 843 | return error_status, error_index, final_oid, oid_value 844 | 845 | 846 | def handle_set_request(oids, oid, type_and_value): 847 | """Handle SetRequest PDU""" 848 | error_status = ASN1_ERROR_STATUS_NO_ERROR 849 | error_index = 0 850 | value_type, value = type_and_value 851 | 852 | res = None 853 | if value_type == 'INTEGER': 854 | enum_values = None 855 | if oid in oids and isinstance(oids[oid], tuple) and len(oids[oid]) > 1: 856 | enum_values = oids[oid][1] 857 | res = integer(value, enum=enum_values) 858 | elif value_type == 'STRING': 859 | res = octet_string(value if PY3 else value.encode('latin')) 860 | elif value_type == 'OID': 861 | res = object_identifier(value) 862 | elif value_type == 'TIMETICKS': 863 | res = timeticks(value) 864 | elif value_type == 'IPADDRESS': 865 | res = ip_address(value) 866 | elif value_type == 'COUNTER32': 867 | res = counter32(value) 868 | elif value_type == 'COUNTER64': 869 | res = counter64(value) 870 | elif value_type == 'GAUGE32': 871 | res = gauge32(value) 872 | elif value_type == 'OPAQUE': 873 | if value[0] == 'FLOAT': 874 | res = real(value[1]) 875 | elif value[0] == 'DOUBLE': 876 | res = double(value[1]) 877 | elif value[0] == 'UINT64': 878 | res = uint64(value[1]) 879 | elif value[0] == 'INT64': 880 | res = int64(value[1]) 881 | else: 882 | raise Exception('Unsupported type: {} ({})'.format(value_type, repr(value))) 883 | 884 | if res is None: 885 | error_status = ASN1_ERROR_STATUS_WRONG_VALUE 886 | error_index = 1 887 | res = null() 888 | else: 889 | if oid in oids and isinstance(oids[oid], types.FunctionType): 890 | oids[oid](oid, value) 891 | else: 892 | oids[oid] = res 893 | 894 | oid_value = res 895 | return error_status, error_index, oid_value 896 | 897 | def handle_trap_request(oids, oid, type_and_value, details): 898 | """Handle Trap Request""" 899 | logger.info(type_and_value) 900 | value_type, value = type_and_value 901 | 902 | res = None 903 | if value_type == 'INTEGER': 904 | enum_values = None 905 | if oid in oids and isinstance(oids[oid], tuple) and len(oids[oid]) > 1: 906 | enum_values = oids[oid][1] 907 | res = integer(value, enum=enum_values) 908 | elif value_type == 'STRING': 909 | res = octet_string(value if PY3 else value.encode('latin')) 910 | elif value_type == 'OID': 911 | res = object_identifier(value) 912 | elif value_type == 'TIMETICKS': 913 | res = timeticks(value) 914 | elif value_type == 'IPADDRESS': 915 | res = ip_address(value) 916 | elif value_type == 'COUNTER32': 917 | res = counter32(value) 918 | elif value_type == 'COUNTER64': 919 | res = counter64(value) 920 | elif value_type == 'GAUGE32': 921 | res = gauge32(value) 922 | elif value_type == 'OPAQUE': 923 | if value[0] == 'FLOAT': 924 | res = real(value[1]) 925 | elif value[0] == 'DOUBLE': 926 | res = double(value[1]) 927 | elif value[0] == 'UINT64': 928 | res = uint64(value[1]) 929 | elif value[0] == 'INT64': 930 | res = int64(value[1]) 931 | else: 932 | raise Exception('Unsupported type: {} ({})'.format(value_type, repr(value))) 933 | 934 | if res is not None: 935 | if oid in oids and isinstance(oids[oid], types.FunctionType): 936 | oids[oid](oid, value, details) 937 | else: 938 | oids[oid] = res 939 | 940 | def craft_response(version, community, request_id, error_status, error_index, oid_items): 941 | """Craft SNMP response""" 942 | response = write_tv( 943 | ASN1_SEQUENCE, 944 | # add version and community from request 945 | write_tv(ASN1_INTEGER, _write_int(version)) + 946 | write_tv(ASN1_OCTET_STRING, community.encode('latin') if PY3 else str(community)) + 947 | # add GetResponse PDU with get response fields 948 | write_tv( 949 | ASN1_GET_RESPONSE_PDU, 950 | # add response id, error status and error index 951 | write_tv(ASN1_INTEGER, _write_int(request_id)) + 952 | write_tlv(ASN1_INTEGER, 1, _write_int(error_status)) + 953 | write_tlv(ASN1_INTEGER, 1, _write_int(error_index)) + 954 | # add variable bindings 955 | write_tv( 956 | ASN1_SEQUENCE, 957 | b''.join( 958 | # add OID and OID value 959 | write_tv( 960 | ASN1_SEQUENCE, 961 | write_tv( 962 | ASN1_OBJECT_IDENTIFIER, 963 | oid_key.encode('latin') if PY3 else oid_key 964 | ) + 965 | oid_value 966 | ) for (oid_key, oid_value) in oid_items 967 | ) 968 | ) 969 | ) 970 | ) 971 | return response 972 | 973 | 974 | def snmp_server(host, port, oids): 975 | """Main SNMP server loop""" 976 | with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)) as sock: 977 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 978 | sock.bind((host, port)) 979 | print('SNMP server listening on {}:{}'.format(host, port)) 980 | 981 | # SNMP server main loop 982 | while True: 983 | request_data, address = sock.recvfrom(4096) 984 | logger.debug('Received %d bytes from %s', len(request_data), address) 985 | 986 | request_stream = StringIO(request_data.decode('latin')) 987 | try: 988 | request_result = _parse_snmp_asn1(request_stream) 989 | except ProtocolError as ex: 990 | logger.error('SNMP request parsing failed: %s', ex) 991 | continue 992 | 993 | # get required fields from request 994 | version = request_result[0][1] 995 | community = request_result[1][1] 996 | pdu_type = request_result[2][1] 997 | request_id = request_result[3][1] 998 | 999 | expected_length = 8 if pdu_type == ASN1_TRAP_REQUEST_PDU else 7 1000 | if len(request_result) < expected_length: 1001 | raise Exception('Invalid ASN.1 parsed request length! %s' % str(request_result)) 1002 | 1003 | error_status = ASN1_ERROR_STATUS_NO_ERROR 1004 | error_index = 0 1005 | oid_items = [] 1006 | oid_value = null() 1007 | 1008 | # handle protocol data units 1009 | if pdu_type == ASN1_GET_REQUEST_PDU: 1010 | requested_oids = request_result[6:] 1011 | for _, oid in requested_oids: 1012 | _, _, oid_value = handle_get_request(oids, oid) 1013 | # if oid value is a function - call it to get the value 1014 | if isinstance(oid_value, types.FunctionType): 1015 | oid_value = oid_value(oid) 1016 | if isinstance(oid_value, tuple): 1017 | oid_value = oid_value[0] 1018 | oid_items.append((oid_to_bytes(oid), oid_value)) 1019 | elif pdu_type == ASN1_GET_NEXT_REQUEST_PDU: 1020 | oid = request_result[6][1] 1021 | error_status, error_index, oid, oid_value = handle_get_next_request(oids, oid) 1022 | if isinstance(oid_value, types.FunctionType): 1023 | oid_value = oid_value(oid) 1024 | if isinstance(oid_value, tuple): 1025 | oid_value = oid_value[0] 1026 | oid_items.append((oid_to_bytes(oid), oid_value)) 1027 | elif pdu_type == ASN1_GET_BULK_REQUEST_PDU: 1028 | max_repetitions = request_result[5][1] 1029 | logger.debug('max_repetitions: %i', max_repetitions) 1030 | requested_oids = request_result[6:] 1031 | for _ in range(0, max_repetitions): 1032 | for idx, val in enumerate(requested_oids): 1033 | oid = val[1] 1034 | error_status, error_index, oid, oid_value = handle_get_next_request(oids, oid) 1035 | if isinstance(oid_value, types.FunctionType): 1036 | oid_value = oid_value(oid) 1037 | if isinstance(oid_value, tuple): 1038 | oid_value = oid_value[0] 1039 | oid_items.append((oid_to_bytes(oid), oid_value)) 1040 | requested_oids[idx] = ('OID', oid) 1041 | elif pdu_type == ASN1_SET_REQUEST_PDU: 1042 | if len(request_result) < 8: 1043 | raise Exception('Invalid ASN.1 parsed request length for SNMP set request!') 1044 | oid = request_result[6][1] 1045 | type_and_value = request_result[7] 1046 | try: 1047 | if oid in oids and isinstance(oids[oid], tuple) and len(oids[oid]) > 1: 1048 | enum_values = oids[oid][1] 1049 | new_value = type_and_value[1] 1050 | if isinstance(enum_values, Iterable) and new_value not in enum_values: 1051 | raise WrongValueError('Value {} is outside the range of enum values'.format(new_value)) 1052 | logger.debug('oid: %s, type_and_value: %s', oid, type_and_value) 1053 | error_status, error_index, oid_value = handle_set_request(oids, oid, type_and_value) 1054 | except WrongValueError as ex: 1055 | logger.error(f"Wrong value error: {ex}") 1056 | error_status = ASN1_ERROR_STATUS_WRONG_VALUE 1057 | error_index = 0 1058 | except Exception as ex: 1059 | logger.error(f"Unknown exception error: {ex}") 1060 | error_status = ASN1_ERROR_STATUS_BAD_VALUE 1061 | error_index = 0 1062 | # if oid value is a function - call it to get the value 1063 | if isinstance(oid_value, types.FunctionType): 1064 | oid_value = oid_value(oid) 1065 | if isinstance(oid_value, tuple): 1066 | oid_value = oid_value[0] 1067 | oid_items.append((oid_to_bytes(oid), oid_value)) 1068 | elif pdu_type == ASN1_INFORM_REQUEST_PDU: 1069 | if len(request_result) < 8: 1070 | raise Exception('Invalid ASN.1 parsed request length for SNMP set request!') 1071 | requested_oids = request_result[6:] 1072 | if len(requested_oids) % 2: 1073 | raise Exception('Invalid length of OID and value items in SNMP inform request!') 1074 | for i in range(0, len(requested_oids), 2): 1075 | oid = requested_oids[i][1] 1076 | type_and_value = requested_oids[i + 1] 1077 | error_status, error_index, oid_value = handle_set_request(oids, oid, type_and_value) 1078 | if isinstance(oid_value, types.FunctionType): 1079 | oid_value = oid_value(oid) 1080 | if isinstance(oid_value, tuple): 1081 | oid_value = oid_value[0] 1082 | oid_items.append((oid_to_bytes(oid), oid_value)) 1083 | elif pdu_type == ASN1_TRAP_REQUEST_PDU: 1084 | if len(request_result) < 10: 1085 | raise Exception('Invalid ASN.1 parsed request length for SNMP trap request!') 1086 | logger.debug('Trap request length %d', len(request_result)) 1087 | agent_community = request_result[1][1] 1088 | enterprise_oid = request_result[3][1] 1089 | agent_addr = request_result[4][1] 1090 | trap_type = request_result[5][1] 1091 | specific_type = request_result[6][1] 1092 | uptime = request_result[7][1] 1093 | oid = request_result[8][1] 1094 | type_and_value = request_result[9] 1095 | handle_trap_request(oids, oid, type_and_value, { 1096 | 'agent_community': agent_community, 1097 | 'enterprise_oid': enterprise_oid, 1098 | 'agent_addr': agent_addr, 1099 | 'trap_type': trap_type, 1100 | 'specific_type': specific_type, 1101 | 'uptime': uptime, 1102 | }) 1103 | continue # traps do not require a response 1104 | else: 1105 | continue 1106 | 1107 | # craft SNMP response 1108 | logger.info('Crafting response') 1109 | logger.info(request_id) 1110 | response = craft_response( 1111 | version, community, request_id, error_status, error_index, oid_items) 1112 | logger.debug('Sending %d bytes of response', len(response)) 1113 | try: 1114 | sock.sendto(response, address) 1115 | except socket.error as ex: 1116 | logger.error('Failed to send %d bytes of response: %s', len(response), ex) 1117 | logger.debug('') 1118 | 1119 | def main(): 1120 | """Main""" 1121 | parser = argparse.ArgumentParser(description='SNMP server') 1122 | parser.add_argument( 1123 | '-p', '--port', dest='port', type=int, 1124 | help='port (by default 161 - requires root privileges)', default=161, required=False) 1125 | parser.add_argument( 1126 | '-c', '--config', type=str, 1127 | help='OIDs config file', required=False) 1128 | parser.add_argument( 1129 | '-d', '--debug', 1130 | help='run in debug mode', action='store_true') 1131 | parser.add_argument( 1132 | '-v', '--version', action='version', 1133 | version='SNMP server v{}'.format(__version__)) 1134 | 1135 | args = parser.parse_args() 1136 | 1137 | if args.debug: 1138 | logger.setLevel(logging.DEBUG) 1139 | 1140 | # work as an echo server if no config is passed 1141 | oids = { 1142 | '*': lambda oid: octet_string(oid), 1143 | } 1144 | # read a config 1145 | config_filename = args.config 1146 | if config_filename: 1147 | try: 1148 | oids = parse_config(config_filename) 1149 | except ConfigError as ex: 1150 | logger.error(ex) 1151 | sys.exit(-1) 1152 | 1153 | host = '0.0.0.0' 1154 | port = args.port 1155 | try: 1156 | snmp_server(host, port, oids) 1157 | except KeyboardInterrupt: 1158 | logger.debug('Interrupted by Ctrl+C') 1159 | 1160 | 1161 | if __name__ == '__main__': 1162 | main() 1163 | --------------------------------------------------------------------------------