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. --- CHANGES | 10 +++ docs/mibs/PYSNMP-SOURCE-MIB.txt | 84 ++++++++++++++++++ .../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 ++++++++++++++++++++++ pysnmp/carrier/asynsock/dgram/base.py | 68 +++++++++++++-- pysnmp/carrier/asynsock/dgram/udp.py | 4 + pysnmp/carrier/asynsock/dgram/udp6.py | 36 +++++--- pysnmp/carrier/asynsock/dgram/unix.py | 5 ++ pysnmp/carrier/base.py | 13 +++ pysnmp/carrier/sockfix.py | 16 ++++ pysnmp/carrier/sockmsg.py | 91 ++++++++++++++++++++ pysnmp/entity/config.py | 19 +++-- pysnmp/entity/rfc3413/config.py | 22 +++-- pysnmp/smi/mibs/PYSNMP-SOURCE-MIB.py | 44 ++++++++++ pysnmp/smi/mibs/TRANSPORT-ADDRESS-MIB.py | 3 +- 17 files changed, 659 insertions(+), 33 deletions(-) create mode 100644 docs/mibs/PYSNMP-SOURCE-MIB.txt 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 create mode 100644 pysnmp/carrier/sockfix.py create mode 100644 pysnmp/carrier/sockmsg.py create mode 100644 pysnmp/smi/mibs/PYSNMP-SOURCE-MIB.py diff --git a/CHANGES b/CHANGES index 1ce6719..9966752 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,16 @@ Revision 4.2.6rc0 will be gradually migrated to this new framework. - Initial support for the asyncio & Trollius frameworks and coroutines-based SNMP Applications interfaces added. +- 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. - Internal oneliner apps configuration cache moved from respective apps objects to [a singular] snmpEngine object. That would allow for better cache reuse and allow for a single app working with diff --git a/docs/mibs/PYSNMP-SOURCE-MIB.txt b/docs/mibs/PYSNMP-SOURCE-MIB.txt new file mode 100644 index 0000000..01e10b0 --- /dev/null +++ b/docs/mibs/PYSNMP-SOURCE-MIB.txt @@ -0,0 +1,84 @@ +PYSNMP-SOURCE-MIB DEFINITIONS ::= BEGIN + +IMPORTS + MODULE-IDENTITY, + OBJECT-TYPE + FROM SNMPv2-SMI + TAddress + FROM SNMPv2-TC + snmpTargetAddrEntry + FROM SNMP-TARGET-MIB + pysnmpModuleIDs + FROM PYSNMP-MIB; + +pysnmpSourceMIB MODULE-IDENTITY + LAST-UPDATED "201501160000Z" -- 16 January 2015, midnight + ORGANIZATION "SNMP Laboratories" + CONTACT-INFO "E-mail: info@snmplabs.com + Subscribe: pysnmp-users-request@lists.sourceforge.net" + DESCRIPTION + "This MIB module defines implementation specific objects + that provide variable source transport endpoints feature to + SNMP Engine and Applications." + REVISION "201501160000Z" -- 16 January 2015, midnight + DESCRIPTION "The Initial Revision" + ::= { pysnmpModuleIDs 8 } + +-- Administrative assignments **************************************** + +pysnmpSourceMIBObjects OBJECT IDENTIFIER ::= { pysnmpSourceMIB 1 } +pysnmpSourceMIBConformance OBJECT IDENTIFIER ::= { pysnmpSourceMIB 2 } + +-- +-- +-- The snmpSourceObjects group +-- +-- + +-- Augments SNMP-TRANSPORT-ADDRESS::snmpSourceAddressTable + +snmpSourceAddrTable OBJECT-TYPE + SYNTAX SEQUENCE OF SnmpSourceAddrEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "A table of transport addresses to be used as a source in the + generation of SNMP messages. This table contains additional + objects for the SNMP-TRANSPORT-ADDRESS::snmpSourceAddressTable." + ::= { pysnmpSourceMIBObjects 1 } + +snmpSourceAddrEntry OBJECT-TYPE + SYNTAX SnmpSourceAddrEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "A transport address to be used as a source in the generation + of SNMP operations. + + An entry containing additional management information + applicable to a particular target." + AUGMENTS { snmpTargetAddrEntry } + ::= { snmpSourceAddrTable 1 } + +SnmpSourceAddrEntry ::= SEQUENCE { + snmpSourceAddrTAddress TAddress +} + +snmpSourceAddrTAddress OBJECT-TYPE + SYNTAX TAddress + MAX-ACCESS read-create + STATUS current + DESCRIPTION + "This object contains a transport address. The format of + this address depends on the value of the + snmpSourceAddrTDomain object." + ::= { snmpSourceAddrEntry 1 } + +-- Conformance Information ******************************************* + +pysnmpSourceMIBCompliances OBJECT IDENTIFIER + ::= { pysnmpSourceMIBConformance 1 } +pysnmpSourceMIBGroups OBJECT IDENTIFIER + ::= { pysnmpSourceMIBConformance 2 } + +END 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() diff --git a/pysnmp/carrier/asynsock/dgram/base.py b/pysnmp/carrier/asynsock/dgram/base.py index 5c3970d..2471f0d 100644 --- a/pysnmp/carrier/asynsock/dgram/base.py +++ b/pysnmp/carrier/asynsock/dgram/base.py @@ -1,7 +1,7 @@ # Implements asyncore-based generic DGRAM transport import socket, errno, sys from pysnmp.carrier.asynsock.base import AbstractSocketTransport -from pysnmp.carrier import error +from pysnmp.carrier import sockfix, sockmsg, error from pysnmp import debug sockErrors = { # Ignore these socket errors @@ -21,6 +21,11 @@ class DgramSocketTransport(AbstractSocketTransport): retryCount = 3; retryInterval = 1 def __init__(self, sock=None, sockMap=None): self.__outQueue = [] + self._sendto = lambda s,b,a: s.sendto(b, a) + def __recvfrom(s, sz): + d, a = s.recvfrom(sz) + return d, self.addressType(a) + self._recvfrom = __recvfrom AbstractSocketTransport.__init__(self, sock, sockMap) def openClientMode(self, iface=None): @@ -38,22 +43,65 @@ class DgramSocketTransport(AbstractSocketTransport): raise error.CarrierError('bind() for %s failed: %s' % (iface, sys.exc_info()[1],)) return self - def enableBroadcast(self, flag=True): + def enableBroadcast(self, flag=1): try: self.socket.setsockopt( socket.SOL_SOCKET, socket.SO_BROADCAST, flag ) except socket.error: raise error.CarrierError('setsockopt() for SO_BROADCAST failed: %s' % (sys.exc_info()[1],)) + debug.logger & debug.flagIO and debug.logger('enableBroadcast: %s option SO_BROADCAST on socket %s' % (turnOn and "enabled" or "disabled", self.socket.fileno())) + return self + + def enablePktInfo(self, flag=1): + if not hasattr(self.socket, 'sendmsg') or \ + not hasattr(self.socket, 'recvmsg'): + raise error.CarrierError('sendmsg()/recvmsg() interface is not supported by this OS and/or Python version') + + try: + if self.socket.family in (socket.AF_INET,socket.AF_INET6): + self.socket.setsockopt(socket.SOL_IP, socket.IP_PKTINFO, flag) + if self.socket.family == socket.AF_INET6: + self.socket.setsockopt(socket.SOL_IPV6, socket.IPV6_RECVPKTINFO, flag) + except socket.error: + raise error.CarrierError('setsockopt() for %s failed: %s' % (self.socket.family == socket.AF_INET6 and "IPV6_RECVPKTINFO" or "IP_PKTINFO", sys.exc_info()[1])) + + self._sendto = sockmsg.getSendTo(self.addressType) + self._recvfrom = sockmsg.getRecvFrom(self.addressType) + + debug.logger & debug.flagIO and debug.logger('enablePktInfo: %s option %s on socket %s' % (self.socket.family == socket.AF_INET6 and "IPV6_RECVPKTINFO" or "IP_PKTINFO", flag and "enabled" or "disabled", self.socket.fileno())) + return self + + def enableTransparent(self, flag=1): + try: + if self.socket.family == socket.AF_INET: + self.socket.setsockopt( + socket.SOL_IP, socket.IP_TRANSPARENT, flag + ) + if self.socket.family == socket.AF_INET6: + self.socket.setsockopt( + socket.SOL_IPV6, socket.IP_TRANSPARENT, flag + ) + except socket.error: + raise error.CarrierError('setsockopt() for IP_TRANSPARENT failed: %s' % sys.exc_info()[1]) + except PermissionError: + raise error.CarrierError('IP_TRANSPARENT socket option requires superusre previleges') + + debug.logger & debug.flagIO and debug.logger('enableTransparent: %s option IP_TRANSPARENT on socket %s' % (flag and "enabled" or "disabled", self.socket.fileno())) return self def sendMessage(self, outgoingMessage, transportAddress): self.__outQueue.append( - (outgoingMessage, transportAddress) + (outgoingMessage, self.normalizeAddress(transportAddress)) ) debug.logger & debug.flagIO and debug.logger('sendMessage: outgoingMessage queued (%d octets) %s' % (len(outgoingMessage), debug.hexdump(outgoingMessage))) - def normalizeAddress(self, transportAddress): return transportAddress + def normalizeAddress(self, transportAddress): + if not isinstance(transportAddress, self.addressType): + transportAddress = self.addressType(transportAddress) + if not transportAddress.getLocalAddress(): + transportAddress.setLocalAddress(self.getLocalAddress()) + return transportAddress def getLocalAddress(self): # one evil OS does not seem to support getsockname() for DGRAM sockets @@ -67,12 +115,14 @@ class DgramSocketTransport(AbstractSocketTransport): def writable(self): return self.__outQueue def handle_write(self): outgoingMessage, transportAddress = self.__outQueue.pop(0) - debug.logger & debug.flagIO and debug.logger('handle_write: transportAddress %r -> %r outgoingMessage (%d octets) %s' % (self.getLocalAddress(), transportAddress, len(outgoingMessage), debug.hexdump(outgoingMessage))) + debug.logger & debug.flagIO and debug.logger('handle_write: transportAddress %r -> %r outgoingMessage (%d octets) %s' % (transportAddress.getLocalAddress(), transportAddress, len(outgoingMessage), debug.hexdump(outgoingMessage))) if not transportAddress: debug.logger & debug.flagIO and debug.logger('handle_write: missing dst address, loosing outgoing msg') return try: - self.socket.sendto(outgoingMessage, transportAddress) + self._sendto( + self.socket, outgoingMessage, transportAddress + ) except socket.error: if sys.exc_info()[1].args[0] in sockErrors: debug.logger & debug.flagIO and debug.logger('handle_write: ignoring socket error %s' % (sys.exc_info()[1],)) @@ -82,9 +132,11 @@ class DgramSocketTransport(AbstractSocketTransport): def readable(self): return 1 def handle_read(self): try: - incomingMessage, transportAddress = self.socket.recvfrom(65535) + incomingMessage, transportAddress = self._recvfrom( + self.socket, 65535 + ) transportAddress = self.normalizeAddress(transportAddress) - debug.logger & debug.flagIO and debug.logger('handle_read: transportAddress %r -> %r incomingMessage (%d octets) %s' % (transportAddress, self.getLocalAddress(), len(incomingMessage), debug.hexdump(incomingMessage))) + debug.logger & debug.flagIO and debug.logger('handle_read: transportAddress %r -> %r incomingMessage (%d octets) %s' % (transportAddress, transportAddress.getLocalAddress(), len(incomingMessage), debug.hexdump(incomingMessage))) if not incomingMessage: self.handle_close() return diff --git a/pysnmp/carrier/asynsock/dgram/udp.py b/pysnmp/carrier/asynsock/dgram/udp.py index dc1d091..1f605ff 100644 --- a/pysnmp/carrier/asynsock/dgram/udp.py +++ b/pysnmp/carrier/asynsock/dgram/udp.py @@ -1,10 +1,14 @@ # Implements asyncore-based UDP transport domain from socket import AF_INET +from pysnmp.carrier.base import AbstractTransportAddress from pysnmp.carrier.asynsock.dgram.base import DgramSocketTransport domainName = snmpUDPDomain = (1, 3, 6, 1, 6, 1, 1) +class UdpTransportAddress(tuple, AbstractTransportAddress): pass + class UdpSocketTransport(DgramSocketTransport): sockFamily = AF_INET + addressType = UdpTransportAddress UdpTransport = UdpSocketTransport diff --git a/pysnmp/carrier/asynsock/dgram/udp6.py b/pysnmp/carrier/asynsock/dgram/udp6.py index c34037d..3b9d9c4 100644 --- a/pysnmp/carrier/asynsock/dgram/udp6.py +++ b/pysnmp/carrier/asynsock/dgram/udp6.py @@ -1,25 +1,35 @@ # Implements asyncore-based UDP6 transport domain -try: - from socket import AF_INET6 -except ImportError: - AF_INET6 = None +from pysnmp.carrier import sockfix +from pysnmp.carrier.base import AbstractTransportAddress from pysnmp.carrier.asynsock.dgram.base import DgramSocketTransport +import socket domainName = snmpUDP6Domain = (1, 3, 6, 1, 2, 1, 100, 1, 2) +class Udp6TransportAddress(tuple, AbstractTransportAddress): pass + class Udp6SocketTransport(DgramSocketTransport): - sockFamily = AF_INET6 + sockFamily = socket.has_ipv6 and socket.AF_INET6 or None + addressType = Udp6TransportAddress def normalizeAddress(self, transportAddress): + if '%' in transportAddress[0]: # strip zone ID - return (transportAddress[0].split('%')[0], - transportAddress[1], - 0, # flowinfo - 0) # scopeid + ta = self.addressType( + (transportAddress[0].split('%')[0], + transportAddress[1], + 0, # flowinfo + 0) # scopeid + ) + else: + ta = self.addressType( + (transportAddress[0], transportAddress[1], 0, 0) + ) + + if isinstance(transportAddress, self.addressType) and \ + transportAddress.getLocalAddress(): + return ta.setLocalAddress(transportAddress.getLocalAddress()) else: - return (transportAddress[0], - transportAddress[1], - 0, # flowinfo - 0) # scopeid + return ta.setLocalAddress(self.getLocalAddress()) Udp6Transport = Udp6SocketTransport diff --git a/pysnmp/carrier/asynsock/dgram/unix.py b/pysnmp/carrier/asynsock/dgram/unix.py index d488f2d..adfc7ec 100644 --- a/pysnmp/carrier/asynsock/dgram/unix.py +++ b/pysnmp/carrier/asynsock/dgram/unix.py @@ -5,14 +5,19 @@ try: from socket import AF_UNIX except ImportError: AF_UNIX = None +from pysnmp.carrier.base import AbstractTransportAddress from pysnmp.carrier.asynsock.dgram.base import DgramSocketTransport domainName = snmpLocalDomain = (1, 3, 6, 1, 2, 1, 100, 1, 13) random.seed() +class UnixTransportAddress(str, AbstractTransportAddress): pass + class UnixSocketTransport(DgramSocketTransport): sockFamily = AF_UNIX + addressType = UnixTransportAddress + def openClientMode(self, iface=None): if iface is None: # UNIX domain sockets must be explicitly bound diff --git a/pysnmp/carrier/base.py b/pysnmp/carrier/base.py index 448b74d..51c76eb 100644 --- a/pysnmp/carrier/base.py +++ b/pysnmp/carrier/base.py @@ -179,8 +179,21 @@ class AbstractTransportDispatcher: self.unregisterRecvCbFun() self.unregisterTimerCbFun() +class AbstractTransportAddress: + _localAddress = None + def setLocalAddress(self, s): + self._localAddress = s + return self + + def getLocalAddress(self): + return self._localAddress + + def clone(self, localAddress=None): + return self.__class__(self).setLocalAddress(localAddress is None and self.getLocalAddress() or localAddress) + class AbstractTransport: protoTransportDispatcher = None + addressType = AbstractTransportAddress _cbFun = None @classmethod def isCompatibleWithDispatcher(cls, transportDispatcher): diff --git a/pysnmp/carrier/sockfix.py b/pysnmp/carrier/sockfix.py new file mode 100644 index 0000000..9d36e28 --- /dev/null +++ b/pysnmp/carrier/sockfix.py @@ -0,0 +1,16 @@ +# +# Add some of the missing socket module symbols +# +import socket + +symbols = { + 'IP_PKTINFO': 8, + 'IP_TRANSPARENT': 19, + 'SOL_IPV6': 41, + 'IPV6_RECVPKTINFO': 49, + 'IPV6_PKTINFO': 50 +} + +for symbol in symbols: + if not hasattr(socket, symbol): + setattr(socket, symbol, symbols[symbol]) diff --git a/pysnmp/carrier/sockmsg.py b/pysnmp/carrier/sockmsg.py new file mode 100644 index 0000000..d0666eb --- /dev/null +++ b/pysnmp/carrier/sockmsg.py @@ -0,0 +1,91 @@ +# +# The following routines act like sendto()/recvfrom() calls but additionally +# support local address retrieval (what can be useful when listening on +# 0.0.0.0 or [::]) and source address spoofing (for transparent proxying). +# +# These routines are based on POSIX sendmsg()/recvmsg() calls which were made +# available since Python 3.3. Therefore this module is only Python 3.x +# compatible. +# +# Parts of the code below is taken from: +# http://carnivore.it/2012/10/12/python3.3_sendmsg_and_recvmsg +# +try: + import ctypes + import ipaddress +except ImportError: + def getRecvFrom(addressType): + raise error.CarrierError('sendmsg()/recvmsg() interface is not supported by this OS and/or Python version') + + def getSendTo(addressType): + raise error.CarrierError('sendmsg()/recvmsg() interface is not supported by this OS and/or Python version') +else: + import sys + import socket + from pysnmp import debug + from pysnmp.carrier import sockfix, error + + uint32_t = ctypes.c_uint32 + in_addr_t = uint32_t + + class in_addr(ctypes.Structure): + _fields_ = [('s_addr', in_addr_t)] + + class in6_addr_U(ctypes.Union): + _fields_ = [ + ('__u6_addr8', ctypes.c_uint8 * 16), + ('__u6_addr16', ctypes.c_uint16 * 8), + ('__u6_addr32', ctypes.c_uint32 * 4), + ] + + class in6_addr(ctypes.Structure): + _fields_ = [ + ('__in6_u', in6_addr_U), + ] + + class in_pktinfo(ctypes.Structure): + _fields_ = [ + ('ipi_ifindex', ctypes.c_int), + ('ipi_spec_dst', in_addr), + ('ipi_addr', in_addr), + ] + + class in6_pktinfo(ctypes.Structure): + _fields_ = [ + ('ipi6_addr', in6_addr), + ('ipi6_ifindex', ctypes.c_uint), + ] + + def getRecvFrom(addressType): + def recvfrom(s, sz): + _to = None + data, ancdata, msg_flags, _from = s.recvmsg(sz, socket.CMSG_LEN(sz)) + for anc in ancdata: + if anc[0] == socket.SOL_IP and anc[1] == socket.IP_PKTINFO: + addr = in_pktinfo.from_buffer_copy(anc[2]) + addr = ipaddress.IPv4Address(memoryview(addr.ipi_addr).tobytes()) + _to = (str(addr),s.getsockname()[1]) + elif anc[0] == socket.SOL_IPV6 and anc[1] == socket.IPV6_PKTINFO: + addr = in6_pktinfo.from_buffer_copy(anc[2]) + addr = ipaddress.ip_address(memoryview(addr.ipi6_addr).tobytes()) + _to = (str(addr),s.getsockname()[1]) + return data, addressType(_from).setLocalAddress(_to) + return recvfrom + + def getSendTo(addressType): + def sendto(s, _data, _to): + ancdata = [] + if type(_to) == addressType: + addr = ipaddress.ip_address(_to.getLocalAddress()[0]) + else: + addr = ipaddress.ip_address(s.getsockname()[0]) + if type(addr) == ipaddress.IPv4Address: + _f = in_pktinfo() + _f.ipi_spec_dst = in_addr.from_buffer_copy(addr.packed) + ancdata = [(socket.SOL_IP, socket.IP_PKTINFO, memoryview(_f).tobytes())] + elif s.family == socket.AF_INET6 and type(addr) == ipaddress.IPv6Address: + _f = in6_pktinfo() + _f.ipi6_addr = in6_addr.from_buffer_copy(addr.packed) + ancdata = [(socket.SOL_IPV6, socket.IPV6_PKTINFO, memoryview(_f).tobytes())] + return s.sendmsg([_data], ancdata, 0, _to) + return sendto diff --git a/pysnmp/entity/config.py b/pysnmp/entity/config.py index 40f49e3..3df3b1f 100644 --- a/pysnmp/entity/config.py +++ b/pysnmp/entity/config.py @@ -261,8 +261,9 @@ def delTargetParams(snmpEngine, name): def __cookTargetAddrInfo(snmpEngine, addrName): snmpTargetAddrEntry, = snmpEngine.msgAndPduDsp.mibInstrumController.mibBuilder.importSymbols('SNMP-TARGET-MIB', 'snmpTargetAddrEntry') + snmpSourceAddrEntry, = snmpEngine.msgAndPduDsp.mibInstrumController.mibBuilder.importSymbols('PYSNMP-SOURCE-MIB', 'snmpSourceAddrEntry') tblIdx = snmpTargetAddrEntry.getInstIdFromIndices(addrName) - return snmpTargetAddrEntry, tblIdx + return snmpTargetAddrEntry, snmpSourceAddrEntry, tblIdx def addTargetAddr( snmpEngine, @@ -272,18 +273,25 @@ def addTargetAddr( params, timeout=None, retryCount=None, - tagList=null + tagList=null, + sourceAddress=None ): - snmpTargetAddrEntry, tblIdx = __cookTargetAddrInfo( + snmpTargetAddrEntry, snmpSourceAddrEntry, tblIdx = __cookTargetAddrInfo( snmpEngine, addrName ) if transportDomain[:len(snmpUDPDomain)] == snmpUDPDomain: SnmpUDPAddress, = snmpEngine.msgAndPduDsp.mibInstrumController.mibBuilder.importSymbols('SNMPv2-TM', 'SnmpUDPAddress') transportAddress = SnmpUDPAddress(transportAddress) + if sourceAddress is None: + sourceAddress = ('0.0.0.0', 0) + sourceAddress = SnmpUDPAddress(sourceAddress) elif transportDomain[:len(snmpUDP6Domain)] == snmpUDP6Domain: TransportAddressIPv6, = snmpEngine.msgAndPduDsp.mibInstrumController.mibBuilder.importSymbols('TRANSPORT-ADDRESS-MIB', 'TransportAddressIPv6') transportAddress = TransportAddressIPv6(transportAddress) + if sourceAddress is None: + sourceAddress = ('::', 0) + sourceAddress = TransportAddressIPv6(sourceAddress) snmpEngine.msgAndPduDsp.mibInstrumController.writeVars( ((snmpTargetAddrEntry.name + (9,) + tblIdx, 'destroy'),) @@ -296,11 +304,12 @@ def addTargetAddr( (snmpTargetAddrEntry.name + (4,) + tblIdx, timeout), (snmpTargetAddrEntry.name + (5,) + tblIdx, retryCount), (snmpTargetAddrEntry.name + (6,) + tblIdx, tagList), - (snmpTargetAddrEntry.name + (7,) + tblIdx, params),) + (snmpTargetAddrEntry.name + (7,) + tblIdx, params), + (snmpSourceAddrEntry.name + (1,) + tblIdx, sourceAddress),) ) def delTargetAddr(snmpEngine, addrName): - snmpTargetAddrEntry, tblIdx = __cookTargetAddrInfo( + snmpTargetAddrEntry, snmpSourceAddrEntry, tblIdx = __cookTargetAddrInfo( snmpEngine, addrName ) snmpEngine.msgAndPduDsp.mibInstrumController.writeVars( diff --git a/pysnmp/entity/rfc3413/config.py b/pysnmp/entity/rfc3413/config.py index 000faec..1124600 100644 --- a/pysnmp/entity/rfc3413/config.py +++ b/pysnmp/entity/rfc3413/config.py @@ -32,6 +32,11 @@ def getTargetAddr(snmpEngine, snmpTargetAddrName): 'snmpTargetAddrRetryCount', 'snmpTargetAddrParams' ) + ( snmpSourceAddrTAddress, ) = mibBuilder.importSymbols( + 'PYSNMP-SOURCE-MIB', + 'snmpSourceAddrTAddress' + ) + tblIdx = snmpTargetAddrEntry.getInstIdFromIndices(snmpTargetAddrName) try: @@ -50,21 +55,28 @@ def getTargetAddr(snmpEngine, snmpTargetAddrName): snmpTargetAddrParams = snmpTargetAddrParams.getNode( snmpTargetAddrParams.name + tblIdx ).syntax + snmpSourceAddrTAddress = snmpSourceAddrTAddress.getNode( + snmpSourceAddrTAddress.name + tblIdx + ).syntax except NoSuchInstanceError: raise SmiError('Target %s not configured to LCD' % snmpTargetAddrName) + transport = snmpEngine.transportDispatcher.getTransport(snmpTargetAddrTDomain) + if snmpTargetAddrTDomain[:len(config.snmpUDPDomain)] == config.snmpUDPDomain: SnmpUDPAddress, = snmpEngine.msgAndPduDsp.mibInstrumController.mibBuilder.importSymbols('SNMPv2-TM', 'SnmpUDPAddress') - snmpTargetAddrTAddress = tuple( + snmpTargetAddrTAddress = transport.addressType( SnmpUDPAddress(snmpTargetAddrTAddress) - ) + ).setLocalAddress(SnmpUDPAddress(snmpSourceAddrTAddress)) elif snmpTargetAddrTDomain[:len(config.snmpUDP6Domain)] == config.snmpUDP6Domain: TransportAddressIPv6, = snmpEngine.msgAndPduDsp.mibInstrumController.mibBuilder.importSymbols('TRANSPORT-ADDRESS-MIB', 'TransportAddressIPv6') - snmpTargetAddrTAddress = tuple( + snmpTargetAddrTAddress = transport.addressType( TransportAddressIPv6(snmpTargetAddrTAddress) - ) + ).setLocalAddress(TransportAddressIPv6(snmpSourceAddrTAddress)) elif snmpTargetAddrTDomain[:len(config.snmpLocalDomain)] == config.snmpLocalDomain: - snmpTargetAddrTAddress = str(snmpTargetAddrTAddress) + snmpTargetAddrTAddress = transport.addressType( + snmpTargetAddrTAddress + ) nameToTargetMap[snmpTargetAddrName] = ( snmpTargetAddrTDomain, diff --git a/pysnmp/smi/mibs/PYSNMP-SOURCE-MIB.py b/pysnmp/smi/mibs/PYSNMP-SOURCE-MIB.py new file mode 100644 index 0000000..f4111a5 --- /dev/null +++ b/pysnmp/smi/mibs/PYSNMP-SOURCE-MIB.py @@ -0,0 +1,44 @@ +# PySNMP SMI module. Autogenerated from smidump -f python PYSNMP-SOURCE-MIB +# by libsmi2pysnmp-0.1.3 at Fri Jan 16 17:45:19 2015, +# Python version sys.version_info(major=2, minor=7, micro=2, releaselevel='final', serial=0) + +# Imports + +( Integer, ObjectIdentifier, OctetString, ) = mibBuilder.importSymbols("ASN1", "Integer", "ObjectIdentifier", "OctetString") +( NamedValues, ) = mibBuilder.importSymbols("ASN1-ENUMERATION", "NamedValues") +( ConstraintsIntersection, ConstraintsUnion, SingleValueConstraint, ValueRangeConstraint, ValueSizeConstraint, ) = mibBuilder.importSymbols("ASN1-REFINEMENT", "ConstraintsIntersection", "ConstraintsUnion", "SingleValueConstraint", "ValueRangeConstraint", "ValueSizeConstraint") +( pysnmpModuleIDs, ) = mibBuilder.importSymbols("PYSNMP-MIB", "pysnmpModuleIDs") +( snmpTargetAddrEntry, ) = mibBuilder.importSymbols("SNMP-TARGET-MIB", "snmpTargetAddrEntry") +( Bits, Integer32, ModuleIdentity, MibIdentifier, MibScalar, MibTable, MibTableRow, MibTableColumn, TimeTicks, ) = mibBuilder.importSymbols("SNMPv2-SMI", "Bits", "Integer32", "ModuleIdentity", "MibIdentifier", "MibScalar", "MibTable", "MibTableRow", "MibTableColumn", "TimeTicks") +( TAddress, ) = mibBuilder.importSymbols("SNMPv2-TC", "TAddress") + +# Objects + +pysnmpSourceMIB = ModuleIdentity((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8)).setRevisions(("2015-01-16 00:00",)) +if mibBuilder.loadTexts: pysnmpSourceMIB.setOrganization("SNMP Laboratories") +if mibBuilder.loadTexts: pysnmpSourceMIB.setContactInfo("E-mail: info@snmplabs.com\nSubscribe: pysnmp-users-request@lists.sourceforge.net") +if mibBuilder.loadTexts: pysnmpSourceMIB.setDescription("This MIB module defines implementation specific objects\nthat provide variable source transport endpoints feature to\nSNMP Engine and Applications.") +pysnmpSourceMIBObjects = MibIdentifier((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8, 1)) +snmpSourceAddrTable = MibTable((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8, 1, 1)) +if mibBuilder.loadTexts: snmpSourceAddrTable.setDescription("A table of transport addresses to be used as a source in the\ngeneration of SNMP messages. This table contains additional\nobjects for the SNMP-TRANSPORT-ADDRESS::snmpSourceAddressTable.") +snmpSourceAddrEntry = MibTableRow((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8, 1, 1, 1)) +if mibBuilder.loadTexts: snmpSourceAddrEntry.setDescription("A transport address to be used as a source in the generation\nof SNMP operations.\n\nAn entry containing additional management information\napplicable to a particular target.") +snmpSourceAddrTAddress = MibTableColumn((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8, 1, 1, 1, 1), TAddress()).setMaxAccess("readcreate") +if mibBuilder.loadTexts: snmpSourceAddrTAddress.setDescription("This object contains a transport address. The format of\nthis address depends on the value of the\nsnmpSourceAddrTDomain object.") +pysnmpSourceMIBConformance = MibIdentifier((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8, 2)) +pysnmpSourceMIBCompliances = MibIdentifier((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8, 2, 1)) +pysnmpSourceMIBGroups = MibIdentifier((1, 3, 6, 1, 4, 1, 20408, 3, 1, 8, 2, 2)) + +# Augmentions +snmpTargetAddrEntry, = mibBuilder.importSymbols("SNMP-TARGET-MIB", "snmpTargetAddrEntry") +snmpTargetAddrEntry.registerAugmentions(("PYSNMP-SOURCE-MIB", "snmpSourceAddrEntry")) +snmpSourceAddrEntry.setIndexNames(*snmpTargetAddrEntry.getIndexNames()) + +# Exports + +# Module identity +mibBuilder.exportSymbols("PYSNMP-SOURCE-MIB", PYSNMP_MODULE_ID=pysnmpSourceMIB) + +# Objects +mibBuilder.exportSymbols("PYSNMP-SOURCE-MIB", pysnmpSourceMIB=pysnmpSourceMIB, pysnmpSourceMIBObjects=pysnmpSourceMIBObjects, snmpSourceAddrTable=snmpSourceAddrTable, snmpSourceAddrEntry=snmpSourceAddrEntry, snmpSourceAddrTAddress=snmpSourceAddrTAddress, pysnmpSourceMIBConformance=pysnmpSourceMIBConformance, pysnmpSourceMIBCompliances=pysnmpSourceMIBCompliances, pysnmpSourceMIBGroups=pysnmpSourceMIBGroups) + diff --git a/pysnmp/smi/mibs/TRANSPORT-ADDRESS-MIB.py b/pysnmp/smi/mibs/TRANSPORT-ADDRESS-MIB.py index e4ece3d..d40efb2 100644 --- a/pysnmp/smi/mibs/TRANSPORT-ADDRESS-MIB.py +++ b/pysnmp/smi/mibs/TRANSPORT-ADDRESS-MIB.py @@ -4,9 +4,10 @@ from pyasn1.compat.octets import int2oct, oct2int from pysnmp import error +from pysnmp.carrier import sockfix import socket -has_ipv6 = socket.has_ipv6 and hasattr(socket, 'AF_INET6') +has_ipv6 = socket.has_ipv6 if hasattr(socket, 'inet_ntop') and hasattr(socket, 'inet_pton'): inet_ntop = socket.inet_ntop -- cgit v1.2.1