From f32853c8324048a2e87e0ecad98c762f5d4ef3bb Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Sun, 8 Sep 2019 12:18:48 +0200 Subject: [PATCH] Add SNMP Context Engine ID support Added support for SNMP Context Engine ID mapping to the file system paths. This feature allows for every single SNMP engine to simulate multiple SNMP entities each identified by Context Engine ID and Context Name pair mapped onto different .snmprec files. On top of that, this feature simplifies the mapping of empty SNMP community and context names to .snmprec files. --- CHANGES.txt | 5 ++ .../documentation/addressing-agents.rst | 53 ++++++++---- scripts/snmpsimd.py | 84 ++++++++++++++----- snmpsim/record/search/database.py | 1 - 4 files changed, 104 insertions(+), 39 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c927988..1f9085c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,11 @@ Revision 0.4.8, released XX-08-2019 ----------------------------------- - Code base PEP8'ed +- Added support for SNMP Context Engine ID mapping to the file system paths. + This feature allows for every single SNMP engine to simulate multiple + SNMP contexts based on different .snmprec files. On top of that, this + feature simplifies the mapping of empty SNMP community and context names + to .snmprec files. - Added support for simulation based on compressed .snmprec files using bzip2 compression algorithm (.snmprec.bz2), as well as recording straight into this compressed format. diff --git a/docs/source/documentation/addressing-agents.rst b/docs/source/documentation/addressing-agents.rst index dc4c447..ce9371b 100644 --- a/docs/source/documentation/addressing-agents.rst +++ b/docs/source/documentation/addressing-agents.rst @@ -14,13 +14,25 @@ SNMP managers differently. Legacy mode ----------- -When running in the *--v2c-arch* mode, SNMP Simulator attempts to find a *.snmprec* -file to fulfill a request by probing files in paths constructed from pieces of request -data. The path construction occurs by these rules and in this order: +When running in the *--v2c-arch* mode, SNMP Simulator is not using SNMPv3 +framework. That implies that SNMPv3 infrastructure can't be used for agent +addressing. In this mode, only SNMP v1 and v2c versions can be served. The +main reason for legacy mode support is higher performance. -1. *community / transport-ID / source-address* .snmprec -2. *community / transport-ID* .snmprec -3. *community* .snmprec +In *--v2c-arch* mode, SNMP Simulator attempts to find a *.snmprec* file to +fulfill the request by probing files by paths constructed from pieces of +SNMPv1/v2c request data. Path construction occurs by these rules and +in this order: + +1. *self / / / * .snmprec +2. *self / / * .snmprec +3. *self / * .snmprec +4. *self* .snmprec + +The *self* component is just a constant which conceptually refers to the SNMP +engine serving current request. + +One of the use-cases is to serve requests with empty SNMP community name. In other words, SNMP Simulator first tries to take community name, destination and source addresses into account. If that does not match @@ -63,7 +75,7 @@ For example, to make Simulator reporting from particular file to a Manager at 192.168.1.10 whenever community name "public" is used and queries are sent to Simulator over UDP/IPv4 to 192.168.1.1 interface (which is reported by Simulator under transport ID 1.3.6.1.6.1.1.0), -device file *public/1.3.6.1.6.1.1.0/192.168.1.10.snmprec* would be used +device file *self/public/1.3.6.1.6.1.1.0/192.168.1.10.snmprec* would be used for building responses. .. _v3-style-variation: @@ -71,20 +83,31 @@ for building responses. SNMPv3 mode ----------- -When Simulator is NOT running in *--v2c-arch* mode, e.g. SNMPv3 engine is -used, similar rules apply to SNMPv3 context name rather than to SNMPv1/2c -community name. In that case device file path construction would work -like this: +When Simulator is NOT running in *--v2c-arch* mode, e.g. SNMPv3 framework is +used. In this mode, all SNMP versions can be served. + +The same filesystem mapping rules apply to SNMP community name, but also to SNMPv3 +context name. The path to .snmprec file for fulfilling response is probed at these +locations in the following order: + +1. *context-engine-id / context-name / transport-ID / source-address* .snmprec +2. *context-engine-id / context-name / transport-ID* .snmprec +3. *context-engine-id / context-name* .snmprec +4. *context-engine-id* .snmprec + +The *context-engine-id* component is taken from SNMP Context Engine ID field +of the SNMP command request. If it happens to be equal to local SNMP engine ID +value, then the constant literal *self* will be looked up on the file system +instead. Conceptually, *self* refers to the SNMP engine serving current request. -1. *context-name / transport-ID / source-address* .snmprec -2. *context-name / transport-ID* .snmprec -3. *context-name* .snmprec +One of the side-effects of supporting *context-engine-id* is to serve requests +with empty SNMP context/community name (i.e. *self.snmprec*). For example, to make Simulator reporting from particular file to a Manager at 192.168.1.10 whenever context-name is an empty string and queries are sent to Simulator over UDP/IPv4 to 192.168.1.1 interface (which is reported by Simulator under transport ID 1.3.6.1.6.1.1.0), -device file *1.3.6.1.6.1.1.0/192.168.1.10.snmprec* would be used +device file *self/1.3.6.1.6.1.1.0/192.168.1.10.snmprec* would be used for building responses. .. _sharing-snmprec-files: diff --git a/scripts/snmpsimd.py b/scripts/snmpsimd.py index f7fc7d6..ef71ec1 100644 --- a/scripts/snmpsimd.py +++ b/scripts/snmpsimd.py @@ -22,6 +22,7 @@ from pyasn1.codec.ber import decoder from pyasn1.codec.ber import encoder from pyasn1.compat.octets import str2octs +from pyasn1.compat.octets import null from pyasn1.error import PyAsn1Error from pysnmp.entity import config from pysnmp.entity import engine @@ -91,6 +92,9 @@ walk.WalkRecord.ext: walk.WalkRecord(), } +SELF_LABEL = 'self' + + # Settings forceIndexBuild = False @@ -527,6 +531,12 @@ def getDataFiles(tgtDir, topLen=None): continue # just the file name would serve for agent identification + if relPath[0] == SELF_LABEL: + relPath = relPath[1:] + + if len(relPath) == 1 and relPath[0] == SELF_LABEL + os.path.extsep + dExt: + relPath[0] = relPath[0][4:] + ident = os.path.join(*relPath) ident = ident[:-len(dExt) - 1] ident = ident.replace(os.path.sep, '/') @@ -654,9 +664,18 @@ def addDataFile(self, *args): # Suggest variations of context name based on request data -def probeContext(transportDomain, transportAddress, contextName): - candidate = [ - contextName, '.'.join([str(x) for x in transportDomain])] +def probeContext(transportDomain, transportAddress, + contextEngineId, contextName): + if contextEngineId: + candidate = [ + contextEngineId, contextName, '.'.join( + [str(x) for x in transportDomain])] + + else: + # try legacy layout w/o contextEnginId in the path + candidate = [ + contextName, '.'.join( + [str(x) for x in transportDomain])] if transportDomain[:len(udp.domainName)] == udp.domainName: candidate.append(transportAddress[0]) @@ -674,6 +693,12 @@ def probeContext(transportDomain, transportAddress, contextName): os.path.normpath( os.path.sep.join(candidate)).replace(os.path.sep, '/')).asOctets() del candidate[-1] + + # try legacy layout w/o contextEnginId in the path + if contextEngineId: + for candidate in probeContext( + transportDomain, transportAddress, None, contextName): + yield candidate # main script body starts here @@ -1208,11 +1233,14 @@ def commandResponderCbFun(transportDispatcher, transportDomain, communityName = reqMsg.getComponentByPosition(1) - for candidate in probeContext(transportDomain, transportAddress, communityName): + for candidate in probeContext(transportDomain, transportAddress, + contextEngineId=SELF_LABEL, + contextName=communityName): if candidate in contexts: log.info( 'Using %s selected by candidate %s; transport ID %s, ' - 'source address %s, community name ' + 'source address %s, context engine ID , ' + 'community name ' '"%s"' % (contexts[candidate], candidate, univ.ObjectIdentifier(transportDomain), transportAddress[0], communityName)) @@ -1307,16 +1335,30 @@ def backendFun(varBinds): else: # v3arch - def probeHashContext(self, snmpEngine, stateReference, contextName): + def probeHashContext(self, snmpEngine): # this API is first introduced in pysnmp 4.2.6 execCtx = snmpEngine.observer.getExecutionContext( 'rfc3412.receiveMessage:request') - transportDomain, transportAddress = ( - execCtx['transportDomain'], execCtx['transportAddress']) + (transportDomain, + transportAddress, + contextEngineId, + contextName) = ( + execCtx['transportDomain'], + execCtx['transportAddress'], + execCtx['contextEngineId'], + execCtx['contextName'].prettyPrint() + ) + + if contextEngineId == snmpEngine.snmpEngineID: + contextEngineId = SELF_LABEL + + else: + contextEngineId = contextEngineId.prettyPrint() for candidate in probeContext( - transportDomain, transportAddress, contextName): + transportDomain, transportAddress, + contextEngineId, contextName): if len(candidate) > 32: probedContextName = md5(candidate).hexdigest() @@ -1333,10 +1375,12 @@ def probeHashContext(self, snmpEngine, stateReference, contextName): else: log.info( 'Using %s selected by candidate %s; transport ID %s, ' - 'source address %s, context name ' + 'source address %s, context engine ID %s, ' + 'community name ' '"%s"' % (mibInstrum, candidate, univ.ObjectIdentifier(transportDomain), - transportAddress[0], probedContextName)) + transportAddress[0], contextEngineId, + probedContextName)) contextName = probedContextName break else: @@ -1360,9 +1404,7 @@ def handleMgmtOperation(self, snmpEngine, stateReference, contextName, PDU, acIn try: cmdrsp.GetCommandResponder.handleMgmtOperation( self, snmpEngine, stateReference, - probeHashContext( - self, snmpEngine, stateReference, contextName - ), + probeHashContext(self, snmpEngine), PDU, (None, snmpEngine) # custom acInfo ) @@ -1375,9 +1417,7 @@ def handleMgmtOperation(self, snmpEngine, stateReference, contextName, PDU, acIn try: cmdrsp.SetCommandResponder.handleMgmtOperation( self, snmpEngine, stateReference, - probeHashContext( - self, snmpEngine, stateReference, contextName - ), + probeHashContext(self, snmpEngine), PDU, (None, snmpEngine) # custom acInfo ) @@ -1390,9 +1430,7 @@ def handleMgmtOperation(self, snmpEngine, stateReference, contextName, PDU, acIn try: cmdrsp.NextCommandResponder.handleMgmtOperation( self, snmpEngine, stateReference, - probeHashContext( - self, snmpEngine, stateReference, contextName - ), + probeHashContext(self, snmpEngine), PDU, (None, snmpEngine) # custom acInfo ) @@ -1405,9 +1443,7 @@ def handleMgmtOperation(self, snmpEngine, stateReference, contextName, PDU, acIn try: cmdrsp.BulkCommandResponder.handleMgmtOperation( self, snmpEngine, stateReference, - probeHashContext( - self, snmpEngine, stateReference, contextName - ), + probeHashContext(self, snmpEngine), PDU, (None, snmpEngine) # custom acInfo ) @@ -1554,6 +1590,8 @@ def registerTransportDispatcher(snmpEngine, transportDispatcher, for v3ContextEngineId, ctxDataDirs in v3ContextEngineIds: snmpContext = context.SnmpContext(snmpEngine, v3ContextEngineId) + # unregister default context + snmpContext.unregisterContextName(null) log.msg( 'SNMPv3 Context Engine ID: ' diff --git a/snmpsim/record/search/database.py b/snmpsim/record/search/database.py index 882af0a..2e5254c 100644 --- a/snmpsim/record/search/database.py +++ b/snmpsim/record/search/database.py @@ -6,7 +6,6 @@ # import os import sys -import bz2 if sys.version_info[0] < 3: import anydbm as dbm