[Shtoom] STUN retransmit delayed calls not cleaned up
Antoine Pitrou
solipsis at pitrou.net
Fri Sep 16 13:03:22 CEST 2005
Hi,
(is shtoom still active? "svn up" has not given changes for a long time)
I've found a potential problem with the STUN implementation.
The delayed calls which handle message restranmission are not cleaned up
at the end. This means if you contact several servers at once, and one
of them does not answer readily (or the DNS resolution takes some time),
the STUN protocol handler will still try to contact it even after it has
finished with another server.
It seems to cause problems with Twisted 2.0 in that the "self.transport"
in a DatagramProtocol is reset to None when we stopListening(). It leads
to errors like "NoneType object has no attribute 'write'" when the
_StunBase tries to send a request.
Here is the correction I've done to stun.py (modifications inside
<AP> ... </AP>):
class _StunBase(object):
def sendRequest(self, server, tid=None, avpairs=()):
# <AP>
if not self.transport:
print "No transport defined, cannot send STUN request"
return
# </AP>
if tid is None:
tid = getRandomTID()
mt = 0x1 # binding request
avstr = ''
# add any attributes
if not avpairs:
avpairs = ('CHANGE-REQUEST', CHANGE_NONE),
for a,v in avpairs:
avstr = avstr + struct.pack('!hh', StunTypes[a], len(v)) + v
pktlen = len(avstr)
if pktlen > 65535:
raise ValueError, "stun request too big (%d bytes)"%pktlen
pkt = struct.pack('!hh16s', mt, pktlen, tid) + avstr
if STUNVERBOSE:
print "sending request %r with %d avpairs to %r (in state %s)"%(
hexify(tid), len(avpairs), server, self._stunState)
self.transport.write(pkt, server)
class StunDiscoveryProtocol(DatagramProtocol, _StunBase):
stunDiscoveryRetries = 0
def __init__(self, servers=DefaultServers, *args, **kwargs):
# Potential STUN servers
self._potentialStuns = {}
# See flowchart ascii art at bottom of file.
self._stunState = '1'
self._finished = False
self._altStunAddress = None
self.externalAddress = None
self.localAddress = None
self.expectedTID = None
self.oldTIDs = sets.Set()
self.natType = None
# <AP>
# self.servers = [(socket.gethostbyname(host), port)
# for host, port in servers]
self.servers = [(host, port) for host, port in servers]
# </AP>
super(StunDiscoveryProtocol, self).__init__(*args, **kwargs)
def initialStunRequest(self, address):
# <AP>
if self._finished:
return
# </AP>
tid = getRandomTID()
delayed = reactor.callLater(INITIAL_TIMEOUT,
self.retransmitInitial, address, tid)
self._potentialStuns[tid] = delayed
self.oldTIDs.add(tid)
self.sendRequest(address, tid=tid)
def retransmitInitial(self, address, tid, count=1):
# <AP>
if self._finished:
return
# </AP>
if count <= MAX_RETRANSMIT:
t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF)
delayed = reactor.callLater(t, self.retransmitInitial,
address, tid, count+1)
self._potentialStuns[tid] = delayed
self.sendRequest(address, tid=tid)
else:
if STUNVERBOSE:
print "giving up on %r"%(address,)
del self._potentialStuns[tid]
if not self._potentialStuns:
if STUNVERBOSE:
print "stun state 1 timeout - no internet UDP possible"
self.natType = NatTypeUDPBlocked
self._finishedStun()
def datagramReceived(self, dgram, address):
if self._finished:
return
mt, pktlen, tid = struct.unpack('!hh16s', dgram[:20])
# Check tid is one we sent and haven't had a reply to yet
if tid in self._potentialStuns:
delayed = self._potentialStuns.get(tid)
if delayed is not None:
delayed.cancel()
del self._potentialStuns[tid]
if self._stunState == '1':
# We got a (potentially) working STUN server!
# Cancel the retransmit timers for the other ones
for k in self._potentialStuns.keys():
self._potentialStuns[k].cancel()
self._potentialStuns[k] = None
resdict = _parseStunResponse(dgram, address, self.expectedTID,
self.oldTIDs)
if not resdict:
return
self.handleStunState1(resdict, address)
else:
# We already have a working STUN server to play with.
pass
return
resdict = _parseStunResponse(dgram, address, self.expectedTID,
self.oldTIDs)
if not resdict:
return
if STUNVERBOSE: print 'calling handleStunState%s'%(self._stunState)
getattr(self, 'handleStunState%s'%(self._stunState))(resdict, address)
def handleStunState1(self, resdict, address):
self.__dict__.update(resdict)
if self.externalAddress and self._altStunAddress:
if self.localAddress == self.externalAddress[0]:
self._stunState = '2a'
else:
self._stunState = '2b'
self.expectedTID = tid = getRandomTID()
self.oldTIDs.add(tid)
self.state2DelayedCall = reactor.callLater(INITIAL_TIMEOUT,
self.retransmitStunState2,
address, tid)
self.sendRequest(address, tid, avpairs=(
('CHANGE-REQUEST', CHANGE_BOTH),))
def handleStunState2a(self, resdict, address):
self.state2DelayedCall.cancel()
del self.state2DelayedCall
if STUNVERBOSE:
print "2a", resdict
self.natType = NatTypeNone
self._finishedStun()
def handleStunState2b(self, resdict, address):
self.state2DelayedCall.cancel()
del self.state2DelayedCall
if STUNVERBOSE:
print "2b", resdict
self.natType = NatTypeFullCone
self._finishedStun()
def retransmitStunState2(self, address, tid, count=1):
# <AP>
if self._finished:
return
# </AP>
if count <= MAX_RETRANSMIT:
t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF)
self.state2DelayedCall = reactor.callLater(t,
self.retransmitStunState2,
address, tid, count+1)
self.sendRequest(address, tid, avpairs=(
('CHANGE-REQUEST', CHANGE_BOTH),))
elif self._stunState == '2a':
self.natType = NatTypeSymUDP
self._finishedStun()
else: # 2b
# Off to state 3 we go!
self._stunState = '3'
self.state3DelayedCall = reactor.callLater(INITIAL_TIMEOUT,
self.retransmitStunState3,
address, tid)
self.expectedTID = tid = getRandomTID()
self.oldTIDs.add(tid)
self.sendRequest(self._altStunAddress, tid)
def handleStunState3(self, resdict, address):
self.state3DelayedCall.cancel()
del self.state3DelayedCall
if STUNVERBOSE:
print "3", resdict
if self.externalAddress == resdict['externalAddress']:
# State 4! wheee!
self._stunState = '4'
self.expectedTID = tid = getRandomTID()
self.oldTIDs.add(tid)
self.state4DelayedCall = reactor.callLater(INITIAL_TIMEOUT,
self.retransmitStunState4,
address, tid)
self.expectedTID = tid = getRandomTID()
self.oldTIDs.add(tid)
self.sendRequest(address, tid, avpairs=(
('CHANGE-REQUEST', CHANGE_PORT),))
else:
self.natType = NatTypeSymmetric
self._finishedStun()
def retransmitStunState3(self, address, tid, count=1):
# <AP>
if self._finished:
return
# </AP>
if count <= (2 * MAX_RETRANSMIT):
t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF)
self.state3DelayedCall = reactor.callLater(t,
self.retransmitStunState3,
address, tid, count+1)
self.sendRequest(self._altStunAddress, tid)
else:
log.err("STUN Failed in state 3, retrying")
# We should do _something_ here. a new type BrokenNAT?
self.stunDiscoveryRetries = self.stunDiscoveryRetries + 1
if self.stunDiscoveryRetries < 5:
reactor.callLater(0.2, self.startDiscovery)
def handleStunState4(self, resdict, address):
self.state4DelayedCall.cancel()
del self.state4DelayedCall
self.natType = NatTypeRestrictedCone
self._finishedStun()
def retransmitStunState4(self, address, tid, count = 1):
# <AP>
if self._finished:
return
# </AP>
if count < MAX_RETRANSMIT:
t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF)
self.state4DelayedCall = reactor.callLater(t,
self.retransmitStunState4,
address, tid, count+1)
self.sendRequest(address, tid, avpairs=(
('CHANGE-REQUEST', CHANGE_PORT),))
else:
self.natType = NatTypePortRestricted
self._finishedStun()
def _finishedStun(self):
self._finished = True
self.finishedStun()
def finishedStun(self):
# Override in a subclass
if STUNVERBOSE:
print "firewall type is", self.natType
def startDiscovery(self):
from shtoom.nat import isBogusAddress, getLocalIPAddress
if _ForceStunType is not None:
self.natType = _ForceStunType
reactor.callLater(0, self._finishedStun)
return
localAddress = self.transport.getHost().host
if isBogusAddress(localAddress):
d = getLocalIPAddress()
d.addCallback(self._resolveStunServers)
else:
self._resolveStunServers(localAddress)
def _resolveStunServers(self, localAddress):
self.localAddress = localAddress
# reactor.resolve the hosts!
for host, port in self.servers:
d = reactor.resolve(host)
d.addCallback(lambda x,p=port: self.initialStunRequest((x, p)))
More information about the Shtoom
mailing list