#!/usr/bin/env python # Copyright (C) 2004 Anthony Baxter # $Id: stun.py,v 1.15 2004/03/02 14:22:31 anthony Exp $ import struct, socket, time from twisted.internet import reactor, defer from twisted.internet.protocol import DatagramProtocol from twisted.python import log import warnings from shtoom.interfaces import NATMapper as INATMapper from shtoom.interfaces import StunPolicy as IStunPolicy from shtoom.defcache import DeferredCache from shtoom.nat import BaseMapper STUNVERBOSE = False # If we're going to follow RFC recommendation, make this 7 MAX_RETRANSMIT = 7 # Work to be done: # - Reverse engineer the C code stun client - why is it giving up so fast? # - Do the shared secret stuff? looks like it needs TLS and crap like that. # - Move the StunPolicy code to become nat.NATPolicy - we still don't want to # use a NAT if both addresses are RFC1918 addresses. # - Make the STUN discovery code use a deferred for it's result! # - Cache the first responding STUN server after discovery (in the NatType # object, maybe?) and only use it? Then need to handle it failing later. # # What happens to any delayed STUN packets that arrive after the Hook gives # the transport back? The code that uses the transport should be forgiving, # I guess. # This should be replaced with lookups of # _stun._udp.divmod.com and _stun._udp.wirlab.net DefaultServers = [ ('stun.xten.net', 3478), ('sip.iptel.org', 3478), ('stun2.wirlab.net', 3478), # ('stun.fwdnet.net', 3478), # ('stun2.fwdnet.net', 3478), # ('stun.wirlab.net', 3478), # # ('stun1.vovida.org', 3478), # # ('tesla.divmod.net', 3478), # # ('erlang.divmod.net', 3478), # # ('69.90.168.13', 3478), # stun.fednet.net # ('69.90.168.14', 3478), # stun2.fednet.net # ('64.69.76.23', 3478), # stun.xten.net # ('195.37.77.99', 3478), # sip.iptel.org # ('192.98.81.87', 3478), # stun2.wirlab.net # ('192.98.81.66', 3478), # stun.wirlab.net # ('128.107.250.38', 3478), # stun1.vovida.org # ('204.91.10.94', 3478), # tesla.divmod.net # ('204.91.10.93', 3478), # erlang.divmod.net ] StunTypes = { 0x0001: 'MAPPED-ADDRESS', 0x0002: 'RESPONSE-ADDRESS ', 0x0003: 'CHANGE-REQUEST', 0x0004: 'SOURCE-ADDRESS', 0x0005: 'CHANGED-ADDRESS', 0x0006: 'USERNAME', 0x0007: 'PASSWORD', 0x0008: 'MESSAGE-INTEGRITY', 0x0009: 'ERROR-CODE', 0x000a: 'UNKNOWN-ATTRIBUTES', 0x000b: 'REFLECTED-FROM', } CHANGE_NONE = struct.pack('!i',0) CHANGE_PORT = struct.pack('!i',2) CHANGE_IP = struct.pack('!i',4) CHANGE_BOTH = struct.pack('!i',6) for k,v in StunTypes.items(): StunTypes[v] = k del k, v class _NatType: def __init__(self, name, useful=True, blocked=False): self.name = name self.useful = useful self.blocked = blocked def __repr__(self): return ''%(self.name) NatTypeUDPBlocked = _NatType('UDPBlocked', useful=False, blocked=True) NatTypeNone = _NatType('None') NatTypeSymUDP = _NatType('SymUDP') NatTypeFullCone = _NatType('FullCone') NatTypeSymmetric = _NatType('Symmetric', useful=False) NatTypeRestrictedCone = _NatType('RestrictedCone') NatTypePortRestricted = _NatType('PortRestricted') # For testing - always return this STUN type _ForceStunType = None import os if hasattr(os, 'urandom'): def getRandomTID(): return os.urandom(16) elif os.path.exists('/dev/urandom'): def getRandomTID(): return open('/dev/urandom').read(16) else: def getRandomTID(): # It's not absolutely necessary to have a particularly strong TID here import random tid = [ chr(random.randint(0,255)) for x in range(16) ] tid = ''.join(tid) return tid def _parseStunResponse(dgram, address, expectedTID=None): mt, pktlen, tid = struct.unpack('!hh16s', dgram[:20]) if expectedTID is not None and expectedTID != tid: # a response from an earlier request log.msg("got unexpected STUN response %r != %r from %s"% (expectedTID, tid, repr(address),), system='stun') return resdict = {} if mt == 0x0101: log.msg("got STUN response from %s"%repr(address), system='stun') # response remainder = dgram[20:] while remainder: avtype, avlen = struct.unpack('!hh', remainder[:4]) val = remainder[4:4+avlen] avtype = StunTypes.get(avtype, '(Unknown type %04x)'%avtype) remainder = remainder[4+avlen:] if avtype in ('MAPPED-ADDRESS', 'CHANGED-ADDRESS', 'SOURCE-ADDRESS'): dummy,family,port,addr = struct.unpack('!ccH4s', val) addr = socket.inet_ntoa(addr) if STUNVERBOSE: print avtype, addr, port if avtype == 'MAPPED-ADDRESS': resdict['externalAddress'] = (addr, port) elif avtype == 'CHANGED-ADDRESS': resdict['_altStunAddress'] = (addr, address[1]) elif address[0] != addr: # Some son of a bitch is rewriting packets on the way # back. AAARGH. log.msg('WARNING: packets are being rewritten %r != %r'% (address, (addr,port)), system='stun') return else: log.msg("STUN: unhandled AV %s, val %r"%(avtype, repr(val)), system='stun') elif mt == 0x0111: log.error("STUN got an error response") return resdict class _StunBase(object): def sendRequest(self, server, tid=None, avpairs=()): 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 with %d avpairs to %r (in state %s)"%( 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.natType = None self.servers = servers super(StunDiscoveryProtocol, self).__init__(*args, **kwargs) def initialStunRequest(self, address): tid = getRandomTID() delayed = reactor.callLater(0.100, self.retransmitInitial, address, tid) self._potentialStuns[tid] = delayed self.sendRequest(address, tid=tid) def retransmitInitial(self, address, tid, count=1): if count <= MAX_RETRANSMIT: t = 0.1 * 2**min(count,4) 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) 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) 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.state2DelayedCall = reactor.callLater(0.1, 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): if count <= MAX_RETRANSMIT: t = 0.1 * 2**min(count,4) 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(0.1, self.retransmitStunState3, address, tid) self.expectedTID = tid = getRandomTID() 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.state4DelayedCall = reactor.callLater(0.1, self.retransmitStunState4, address, tid) self.expectedTID = tid = getRandomTID() self.sendRequest(address, tid, avpairs=( ('CHANGE-REQUEST', CHANGE_PORT),)) else: self.natType = NatTypeSymmetric self._finishedStun() def retransmitStunState3(self, address, tid, count=1): if count <= (2 * MAX_RETRANSMIT): t = 0.1 * 2**min(count,4) 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): if count < MAX_RETRANSMIT: t = 0.1 * 2**min(count,4) 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))) class StunHook(_StunBase): """Hook a StunHook into a UDP protocol object, and it will discover STUN settings for it. You should probably use the NATMapper approach rather than using StunHook directly. """ def __init__(self, prot, servers=DefaultServers, *args, **kwargs): self._protocol = prot self._pending = {} self.servers = servers self.expectedTID = None self._stunState = 'hook' super(StunHook, self).__init__(*args, **kwargs) def initialStunRequest(self, address): tid = getRandomTID() delayed = reactor.callLater(0.100, self.retransmitInitial, address, tid) self._pending[tid] = delayed self.sendRequest(address, tid=tid) def retransmitInitial(self, address, tid, count=1): if count <= MAX_RETRANSMIT: t = 0.1 * 2**min(count,4) delayed = reactor.callLater(t, self.retransmitInitial, address, tid, count+1) self._pending[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.deferred is None: # We're already done return mt, pktlen, tid = struct.unpack('!hh16s', dgram[:20]) if self._pending.has_key(tid): delayed = self._pending[tid] if delayed is not None: delayed.cancel() del self._pending[tid] resdict = _parseStunResponse(dgram, address) if not resdict or not resdict.get('externalAddress'): # Crap response, ignore it. return # Got a valid response. Clean up around here first. self.uninstallStun() # kill any pending retransmits for delayed in self._pending.values(): if delayed is not None: delayed.cancel() # send response d, self.deferred = self.deferred, None d.callback(resdict['externalAddress']) def installStun(self): self._protocol._mp_datagramReceived = self._protocol.datagramReceived self._protocol.datagramReceived = self.datagramReceived self.transport = self._protocol.transport def discoverAddress(self): """ Sniff out external address. Returns a deferred with the external address as a 2-tuple (ip, port) """ from twisted.internet import defer self.installStun() self.deferred = defer.Deferred() for host, port in self.servers[:3]: d = reactor.resolve(host) d.addCallback(lambda x, p=port: self.initialStunRequest((x, p))) return self.deferred def uninstallStun(self): self._protocol.datagramReceived = self._protocol._mp_datagramReceived del self.transport # XXX should move this class somewhere else. class NetAddress: """ A class that represents a net address of the form foo/nbits, e.g. 10/8, or 192.168/16, or whatever """ def __init__(self, netaddress): parts = netaddress.split('/') if len(parts) > 2: raise ValueError, "should be of form address/mask" if len(parts) == 1: ip, mask = parts[0], 32 else: ip, mask = parts[0], int(parts[1]) if mask < 0 or mask > 32: raise ValueError, "mask should be between 0 and 32" self.net = self.inet_aton(ip) self.mask = ( 2L**32 -1 ) ^ ( 2L**(32-mask) - 1 ) self.start = self.net self.end = self.start | (2L**(32-mask) - 1) def inet_aton(self, ipstr): "A sane inet_aton" net = [ int(x) for x in ipstr.split('.') ] + [ 0,0,0 ] net = net[:4] return ((((((0L+net[0])<<8) + net[1])<<8) + net[2])<<8) +net[3] def inet_ntoa(self, ip): import socket, struct return socket.inet_ntoa(struct.pack('!I',ip)) def __repr__(self): return ''%(self.inet_ntoa(self.net), self.inet_ntoa(self.mask), self.inet_ntoa(self.start), self.inet_ntoa(self.end), id(self)) def check(self, ip): "Check if an IP or network is contained in this network address" if isinstance(ip, NetAddress): return self.check(ip.start) and self.check(ip.end) if isinstance(ip, basestring): ip = self.inet_aton(ip) if ip & self.mask == self.net: return True else: return False __contains__ = check class AlwaysStun: __implements__ = IStunPolicy def checkStun(self, localip, remoteip): return True class NeverStun: __implements__ = IStunPolicy def checkStun(self, localip, remoteip): return False class RFC1918Stun: "A sane default policy" __implements__ = IStunPolicy addresses = ( NetAddress('10/8'), NetAddress('172.16/12'), NetAddress('192.168/16'), NetAddress('127/8') ) localhost = NetAddress('127/8') def checkStun(self, localip, remoteip): localIsRFC1918 = False remoteIsRFC1918 = False remoteIsLocalhost = False # Yay. getPeer() returns a name, not an IP # XXX tofix: grab radix's goodns.py until it # lands in twisted proper. # Until then, use this getaddrinfo() hack. if not remoteip: return None if remoteip[0] not in '0123456789': import socket try: ai = socket.getaddrinfo(remoteip, None) except (socket.error, socket.gaierror): return None remoteips = [x[4][0] for x in ai] else: remoteips = [remoteip,] for net in self.addresses: if localip in net: localIsRFC1918 = True # See comments above. Worse, if the host has an address that's # RFC1918, and externally advertised (which is wrong, and broken), # the STUN check will be incorrect. Bah. for remoteip in remoteips: if remoteip in net: remoteIsRFC1918 = True if remoteip in self.localhost: remoteIsLocalhost = True if localIsRFC1918 and not (remoteIsRFC1918 or remoteIsLocalhost): return True else: return False _defaultPolicy = RFC1918Stun() def installPolicy(policy): global _defaultPolicy _defaultPolicy = policy def getPolicy(): return _defaultPolicy class _DetectSTUNProt(StunDiscoveryProtocol): d = None def finishedStun(self): self.d.callback(self.natType) def _getSTUN(): stunClient = _DetectSTUNProt() stunClient.d = defer.Deferred() l = reactor.listenUDP(0, stunClient) reactor.callLater(0, stunClient.startDiscovery) def _stundone(x, l=l): l.stopListening() return x stunClient.d.addCallback(_stundone) return stunClient.d getSTUN = DeferredCache(_getSTUN) _cached_mapper = None def getMapper(): global _cached_mapper if _cached_mapper is None: _cached_mapper = STUNMapper() return _cached_mapper def clearCache(): global _cached_mapper _cached_mapper = None getSTUN.clearCache() class STUNMapper(BaseMapper): __implements__ = INATMapper _ptypes = [ 'UDP', ] def __init__(self): self._mapped = {} def map(self, port): "See shtoom.interfaces.NATMapper.map" self._checkValidPort(port) cd = defer.Deferred() self._mapped[port] = cd d = getSTUN() d.addCallback(lambda x: self._cb_map_gotSTUN(x, port)) return cd map = DeferredCache(map, inProgressOnly=True) def _cb_map_gotSTUN(self, stun, port): if not stun.useful: cd = self._mapped[port] del self._mapped[port] cd.errback(ValueError('%r means STUN is useless'%(stun,))) return SH = StunHook(port.protocol) d = SH.discoverAddress() d.addCallback(lambda x: self._cb_map_discoveredAddress(x, port)) map = DeferredCache(map, inProgressOnly=True) def _cb_map_discoveredAddress(self, addr, port): cd = self._mapped[port] self._mapped[port] = addr cd.callback(addr) def info(self, port): "See shtoom.interfaces.NATMapper.info" if port in self._mapped: return self._mapped[port] else: raise ValueError('Port %r is not currently mapped'%(port)) def unmap(self, port): "See shtoom.interfaces.NATMapper.unmap" # A no-op for STUN if port not in self._mapped: raise ValueError('Port %r is not currently mapped'%(port)) del self._mapped[port] return defer.succeed(None) if __name__ == "__main__": STUNVERBOSE=True log.FileLogObserver.timeFormat = "%H:%M:%S" import sys class TestStunDiscoveryProtocol(StunDiscoveryProtocol): def finishedStun(self): print "STUN finished, results:" print "You're behind a %r"%(self.natType) if self.natType is NatTypeSymmetric: print "You're going to have to use an outbound proxy" else: print "and external address is %r"%(self.externalAddress,) reactor.stop() stunClient = TestStunDiscoveryProtocol() log.startLogging(sys.stdout) try: reactor.listenUDP(5061, stunClient) except: reactor.listenUDP(0, stunClient) #reactor.callLater(20, stunClient.stunTimedOut) reactor.callLater(0, stunClient.startDiscovery) reactor.run() """ Copied from RFC 3489 +--------+ | Test | S:1 | I | +--------+ | | V S:1a /\ /\ N / \ Y / \ Y +--------+ UDP <-------/Resp\--------->/ IP \------------->| Test | S:2a Blocked \ ? / \Same/ | II | \ / \? / +--------+ \/ \/ | | N | | V V /\ +--------+ Sym. N / \ S:2b | Test | UDP <---/Resp\ | II | Firewall \ ? / +--------+ \ / | \/ V |Y /\ S:3 /\ | Symmetric N / \ +--------+ N / \ V NAT <--- / IP \<-----| Test |<--- /Resp\ Open \Same/ | III | \ ? / Internet \? / +--------+ \ / \/ \/ | |Y | | | V | Full | Cone V /\ +--------+ / \ Y S:4 | Test |------>/Resp\---->Restricted | IV | \ ? / +--------+ \ / \/ |N | Port +------>Restricted Figure 2: Flow for type discovery process Test I: A Binding Request with no change port/change ip Test II: A Binding Request with change port & change ip Test III: A Binding Request with no change port/change ip, sent to the address from CHANGED-ADDRESS in response to Test I. Test IV: A Binding Request with change port """