summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorelie <elie>2015-01-20 16:57:59 +0000
committerelie <elie>2015-01-20 16:57:59 +0000
commitfd457f8135120a10b6789bafe0d84f943eea893d (patch)
tree9ba83ae52f221cccbf5fce433b8f16ff48bdbae5
parent61d145ab7be929790beba74760db4e0ce9e70ef1 (diff)
downloadpysnmp-fd457f8135120a10b6789bafe0d84f943eea893d.tar.gz
- 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.
-rw-r--r--CHANGES10
-rw-r--r--docs/mibs/PYSNMP-SOURCE-MIB.txt84
-rw-r--r--examples/v1arch/manager/get-v2c-spoof-source-address.py87
-rw-r--r--examples/v3arch/agent/cmdrsp/v3-observe-request-processing.py2
-rw-r--r--examples/v3arch/agent/cmdrsp/v3-preserve-original-destination-address.py89
-rw-r--r--examples/v3arch/manager/cmdgen/get-v2c-spoof-source-address.py99
-rw-r--r--pysnmp/carrier/asynsock/dgram/base.py68
-rw-r--r--pysnmp/carrier/asynsock/dgram/udp.py4
-rw-r--r--pysnmp/carrier/asynsock/dgram/udp6.py36
-rw-r--r--pysnmp/carrier/asynsock/dgram/unix.py5
-rw-r--r--pysnmp/carrier/base.py13
-rw-r--r--pysnmp/carrier/sockfix.py16
-rw-r--r--pysnmp/carrier/sockmsg.py91
-rw-r--r--pysnmp/entity/config.py19
-rw-r--r--pysnmp/entity/rfc3413/config.py22
-rw-r--r--pysnmp/smi/mibs/PYSNMP-SOURCE-MIB.py44
-rw-r--r--pysnmp/smi/mibs/TRANSPORT-ADDRESS-MIB.py3
17 files changed, 659 insertions, 33 deletions
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