Skip to content

Commit

Permalink
Add SNMP Context Engine ID support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
etingof committed Sep 8, 2019
1 parent ca5e7ea commit f32853c
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 39 deletions.
5 changes: 5 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
53 changes: 38 additions & 15 deletions docs/source/documentation/addressing-agents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 / <community> / <transport-ID> / <source-address>* .snmprec
2. *self / <community> / <transport-ID>* .snmprec
3. *self / <community>* .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
Expand Down Expand Up @@ -63,28 +75,39 @@ 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:

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:
Expand Down
84 changes: 61 additions & 23 deletions scripts/snmpsimd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -91,6 +92,9 @@
walk.WalkRecord.ext: walk.WalkRecord(),
}

SELF_LABEL = 'self'


# Settings

forceIndexBuild = False
Expand Down Expand Up @@ -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, '/')
Expand Down Expand Up @@ -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])
Expand All @@ -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

Expand Down Expand Up @@ -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 <empty>, '
'community name '
'"%s"' % (contexts[candidate], candidate,
univ.ObjectIdentifier(transportDomain),
transportAddress[0], communityName))
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand Down Expand Up @@ -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: '
Expand Down
1 change: 0 additions & 1 deletion snmpsim/record/search/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#
import os
import sys
import bz2

if sys.version_info[0] < 3:
import anydbm as dbm
Expand Down

0 comments on commit f32853c

Please sign in to comment.