From fd457f8135120a10b6789bafe0d84f943eea893d Mon Sep 17 00:00:00 2001 From: elie Date: Tue, 20 Jan 2015 16:57:59 +0000 Subject: - The asyncore-based transport subsystem extended to support POSIX sendmsg()/recvmsg() based socket communication what could be used, among other things, in the context of a transparent SNMP proxy application. Technically, the following features were brought into pysnmp with this update: * Sending SNMP packets from a non-local IP address * Receiving IP packets for non-local IP addresses * Responding to SNMP requests from exactly the same IP address the query was sent to. This proves to be useful when listening on both primary and secondary IP interfaces. --- .../v1arch/manager/get-v2c-spoof-source-address.py | 87 +++++++++++++++++++ .../agent/cmdrsp/v3-observe-request-processing.py | 2 +- .../v3-preserve-original-destination-address.py | 89 +++++++++++++++++++ .../manager/cmdgen/get-v2c-spoof-source-address.py | 99 ++++++++++++++++++++++ 4 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 examples/v1arch/manager/get-v2c-spoof-source-address.py create mode 100644 examples/v3arch/agent/cmdrsp/v3-preserve-original-destination-address.py create mode 100644 examples/v3arch/manager/cmdgen/get-v2c-spoof-source-address.py (limited to 'examples') diff --git a/examples/v1arch/manager/get-v2c-spoof-source-address.py b/examples/v1arch/manager/get-v2c-spoof-source-address.py new file mode 100644 index 0000000..28f2190 --- /dev/null +++ b/examples/v1arch/manager/get-v2c-spoof-source-address.py @@ -0,0 +1,87 @@ +from pysnmp.carrier.asynsock.dispatch import AsynsockDispatcher +from pysnmp.carrier.asynsock.dgram import udp +from pysnmp.proto import api +from pyasn1.codec.ber import encoder, decoder +from time import time + +# Send request message to this address +transportAddress = udp.UdpTransportAddress(('195.218.195.228', 161)) + +# Send request message from this non-local (!) IP address +transportAddress.setLocalAddress(('1.2.3.4', 0)) + +# Protocol version to use +#pMod = api.protoModules[api.protoVersion1] +pMod = api.protoModules[api.protoVersion2c] + +# Build PDU +reqPDU = pMod.GetRequestPDU() +pMod.apiPDU.setDefaults(reqPDU) +pMod.apiPDU.setVarBinds( + reqPDU, ( ('1.3.6.1.2.1.1.1.0', pMod.Null('')), + ('1.3.6.1.2.1.1.3.0', pMod.Null('')) ) + ) + +# Build message +reqMsg = pMod.Message() +pMod.apiMessage.setDefaults(reqMsg) +pMod.apiMessage.setCommunity(reqMsg, 'public') +pMod.apiMessage.setPDU(reqMsg, reqPDU) + +startedAt = time() + +class StopWaiting(Exception): pass + +def cbTimerFun(timeNow): + if timeNow - startedAt > 3: + raise StopWaiting() + +def cbRecvFun(transportDispatcher, transportDomain, transportAddress, + wholeMsg, reqPDU=reqPDU): + while wholeMsg: + rspMsg, wholeMsg = decoder.decode(wholeMsg, asn1Spec=pMod.Message()) + rspPDU = pMod.apiMessage.getPDU(rspMsg) + # Match response to request + if pMod.apiPDU.getRequestID(reqPDU)==pMod.apiPDU.getRequestID(rspPDU): + # Check for SNMP errors reported + errorStatus = pMod.apiPDU.getErrorStatus(rspPDU) + if errorStatus: + print(errorStatus.prettyPrint()) + else: + for oid, val in pMod.apiPDU.getVarBinds(rspPDU): + print('%s = %s' % (oid.prettyPrint(), val.prettyPrint())) + transportDispatcher.jobFinished(1) + return wholeMsg + +transportDispatcher = AsynsockDispatcher() + +transportDispatcher.registerRecvCbFun(cbRecvFun) +transportDispatcher.registerTimerCbFun(cbTimerFun) + +# Initialize UDP/IPv4 transport +udpSocketTransport = udp.UdpSocketTransport().openClientMode() + +# Use sendmsg()/recvmsg() for socket communication (required for +# IP source spoofing functionality) +udpSocketTransport.enablePktInfo() + +# Enable IP source spoofing (requires root privileges) +udpSocketTransport.enableTransparent() + +transportDispatcher.registerTransport(udp.domainName, udpSocketTransport) + +# Pass message to dispatcher +transportDispatcher.sendMessage( + encoder.encode(reqMsg), udp.domainName, transportAddress +) + +# We might never receive any response as we sent request with fake source IP +transportDispatcher.jobStarted(1) + +# Dispatcher will finish as all jobs counter reaches zero +try: + transportDispatcher.runDispatcher() +except StopWaiting: + transportDispatcher.closeDispatcher() +else: + raise diff --git a/examples/v3arch/agent/cmdrsp/v3-observe-request-processing.py b/examples/v3arch/agent/cmdrsp/v3-observe-request-processing.py index 98ec64a..842f1d7 100644 --- a/examples/v3arch/agent/cmdrsp/v3-observe-request-processing.py +++ b/examples/v3arch/agent/cmdrsp/v3-observe-request-processing.py @@ -32,7 +32,7 @@ snmpEngine = engine.SnmpEngine() def requestObserver(snmpEngine, execpoint, variables, cbCtx): print('Execution point: %s' % execpoint) print('* transportDomain: %s' % '.'.join([str(x) for x in variables['transportDomain']])) - print('* transportAddress: %s' % '@'.join([str(x) for x in variables['transportAddress']])) + print('* transportAddress: %s (local %s)' % ('@'.join([str(x) for x in variables['transportAddress']]), '@'.join([str(x) for x in variables['transportAddress'].getLocalAddress()]))) print('* securityModel: %s' % variables['securityModel']) print('* securityName: %s' % variables['securityName']) print('* securityLevel: %s' % variables['securityLevel']) diff --git a/examples/v3arch/agent/cmdrsp/v3-preserve-original-destination-address.py b/examples/v3arch/agent/cmdrsp/v3-preserve-original-destination-address.py new file mode 100644 index 0000000..a371cd6 --- /dev/null +++ b/examples/v3arch/agent/cmdrsp/v3-preserve-original-destination-address.py @@ -0,0 +1,89 @@ +# +# Command Responder +# +# Listen on all local IPv4 interfaces respond to SNMP GET/SET/GETNEXT/GETBULK +# queries with the following options: +# +# * SNMPv3 +# * with USM user 'usr-md5-des', auth: MD5, priv DES +# * allow access to SNMPv2-MIB objects (1.3.6.1.2.1) +# * over IPv4/UDP, listening at 0.0.0.0:161 +# * preserve local IP address when responding (Python 3.3+ required) +# +# The following Net-SNMP's command will walk this Agent: +# +# $ snmpwalk -v3 -u usr-md5-des -l authPriv -A authkey1 -X privkey1 localhost .1.3.6 +# +# In the situation when UDP responder receives a datagram targeted to +# a secondary (AKA virtial) IP interface or a non-local IP interface +# (e.g. routed through policy routing or iptables TPROXY facility), +# OS stack will by default put primary local IP interface address into +# the IP source field of the response IP packet. Such datagram may not +# reach the sender as either the sender itself or a stateful firewall +# somewhere in between would not be able to match response to original +# request. +# +# The following script solves this problem by preserving original request +# destination IP address and put it back into response IP packet's source +# address field. +# +# To respond from a non-local (e.g. spoofed) IP address, uncomment the +# .enableTransparent() method call and run this script as root. +# +from pysnmp.entity import engine, config +from pysnmp.entity.rfc3413 import cmdrsp, context +from pysnmp.carrier.asynsock.dgram import udp + +# Create SNMP engine +snmpEngine = engine.SnmpEngine() + +# Transport setup + +# Initialize asyncore-based UDP/IPv4 transport +udpSocketTransport = udp.UdpSocketTransport().openServerMode(('0.0.0.0', 161)) + +# Use sendmsg()/recvmsg() for socket communication (used for preserving +# original destination IP address when responding) +udpSocketTransport.enablePktInfo() + +# Enable IP source spoofing (requires root privileges) +# udpSocketTransport.enableTransparent() + +# Register this transport at SNMP Engine +config.addTransport( + snmpEngine, + udp.domainName, + udpSocketTransport +) + +# SNMPv3/USM setup + +# user: usr-md5-des, auth: MD5, priv DES +config.addV3User( + snmpEngine, 'usr-md5-des', + config.usmHMACMD5AuthProtocol, 'authkey1', + config.usmDESPrivProtocol, 'privkey1' +) + +# Allow full MIB access for each user at VACM +config.addVacmUser(snmpEngine, 3, 'usr-md5-des', 'authPriv', (1,3,6,1,2,1), (1,3,6,1,2,1)) + +# Get default SNMP context this SNMP engine serves +snmpContext = context.SnmpContext(snmpEngine) + +# Register SNMP Applications at the SNMP engine for particular SNMP context +cmdrsp.GetCommandResponder(snmpEngine, snmpContext) +cmdrsp.SetCommandResponder(snmpEngine, snmpContext) +cmdrsp.NextCommandResponder(snmpEngine, snmpContext) +cmdrsp.BulkCommandResponder(snmpEngine, snmpContext) + +# Register an imaginary never-ending job to keep I/O dispatcher running forever +snmpEngine.transportDispatcher.jobStarted(1) + +# Run I/O dispatcher which would receive queries and send responses +try: + snmpEngine.transportDispatcher.runDispatcher() +except: + snmpEngine.observer.unregisterObserver() + snmpEngine.transportDispatcher.closeDispatcher() + raise diff --git a/examples/v3arch/manager/cmdgen/get-v2c-spoof-source-address.py b/examples/v3arch/manager/cmdgen/get-v2c-spoof-source-address.py new file mode 100644 index 0000000..036cf3f --- /dev/null +++ b/examples/v3arch/manager/cmdgen/get-v2c-spoof-source-address.py @@ -0,0 +1,99 @@ +# +# GET Command Generator +# +# Send a SNMP GET request +# with SNMPv2c, community 'public' +# over IPv4/UDP +# to an Agent at 195.218.195.228:161 +# from a non-local, spoofed IP 1.2.3.4 (root and Python 3.3+ required) +# for an OID in tuple form +# +# This script performs similar to the following Net-SNMP command: +# +# $ snmpget -v2c -c public -ObentU 195.218.195.228 1.3.6.1.2.1.1.1.0 +# +# But unlike the above command, this script issues SNMP request from +# a non-default, non-local IP address. +# +# It is indeed possible to originate SNMP traffic from any valid local +# IP addresses. It could be a secondary IP interface, for instance. +# Superuser privileges are only required to send spoofed packets. +# Alternatively, sending from local interface could also be achieved by +# binding to it (via openClientMode() parameter). +# +# +from pysnmp.entity import engine, config +from pysnmp.carrier.asynsock.dgram import udp +from pysnmp.entity.rfc3413 import cmdgen + +# Create SNMP engine instance +snmpEngine = engine.SnmpEngine() + +# +# SNMPv1 setup +# + +# SecurityName <-> CommunityName mapping +config.addV1System(snmpEngine, 'my-area', 'public') + +# Specify security settings per SecurityName (SNMPv1 - 0, SNMPv2c - 1) +config.addTargetParams(snmpEngine, 'my-creds', 'my-area', 'noAuthNoPriv', 0) + +# +# Setup transport endpoint and bind it with security settings yielding +# a target name +# + +# Initialize asyncore-based UDP/IPv4 transport +udpSocketTransport = udp.UdpSocketTransport().openClientMode() + +# Use sendmsg()/recvmsg() for socket communication (required for +# IP source spoofing functionality) +udpSocketTransport.enablePktInfo() + +# Enable IP source spoofing (requires root privileges) +udpSocketTransport.enableTransparent() + +# Register this transport at SNMP Engine +config.addTransport( + snmpEngine, + udp.domainName, + udpSocketTransport +) + +# Configure destination IPv4 address as well as source IPv4 address +config.addTargetAddr( + snmpEngine, 'my-router', + udp.domainName, ('195.218.195.228', 161), + 'my-creds', + sourceAddress=('1.2.3.4', 0) +) + +# Error/response receiver +def cbFun(snmpEngine, sendRequestHandle, errorIndication, + errorStatus, errorIndex, varBinds, cbCtx): + if errorIndication: + print(errorIndication) + # SNMPv1 response may contain noSuchName error *and* SNMPv2c exception, + # so we ignore noSuchName error here + elif errorStatus and errorStatus != 2: + print('%s at %s' % ( + errorStatus.prettyPrint(), + errorIndex and varBinds[int(errorIndex)-1][0] or '?' + ) + ) + else: + for oid, val in varBinds: + print('%s = %s' % (oid.prettyPrint(), val.prettyPrint())) + +# Prepare and send a request message +cmdgen.GetCommandGenerator().sendVarBinds( + snmpEngine, + 'my-router', + None, '', # contextEngineId, contextName + [ ((1,3,6,1,2,1,1,1,0), None) ], + cbFun +) + +# Run I/O dispatcher which would send pending queries and process responses +snmpEngine.transportDispatcher.runDispatcher() -- cgit v1.2.1