fixed resource leak and incorrect exception handling in nattraverso upnp library
[p2pool.git] / nattraverso / ipdiscover.py
1 """
2 Generic methods to retreive the IP address of the local machine.
3
4 TODO: Example
5
6 @author: Raphael Slinckx
7 @copyright: Copyright 2005
8 @license: LGPL
9 @contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
10 @version: 0.1.0
11 """
12
13 __revision__ = "$id"
14
15 import random, socket, logging
16
17 from twisted.internet import defer, reactor
18
19 from twisted.internet.protocol import DatagramProtocol
20 from twisted.internet.error import CannotListenError
21 from twisted.internet.interfaces import IReactorMulticast
22
23 from nattraverso.utils import is_rfc1918_ip, is_bogus_ip
24
25 def get_local_ip():
26         """
27         Returns a deferred which will be called with a
28         2-uple (lan_flag, ip_address) :
29                 - lan_flag:
30                         - True if it's a local network (RFC1918)
31                         - False if it's a WAN address
32                               
33                 - ip_address is the actual ip address
34                 
35         @return: A deferred called with the above defined tuple
36         @rtype: L{twisted.internet.defer.Deferred}
37         """
38         # first we try a connected udp socket, then via multicast
39         logging.debug("Resolving dns to get udp ip")
40         result = reactor.resolve('A.ROOT-SERVERS.NET')
41         result.addCallbacks(_get_via_connected_udp, lambda x:_get_via_multicast())
42         return result
43
44 def get_external_ip():  
45         """
46         Returns a deferred which will be called with a
47         2-uple (wan_flag, ip_address):
48                 - wan_flag:
49                         - True if it's a WAN address
50                         - False if it's a LAN address
51                         - None if it's a localhost (127.0.0.1) address
52                 - ip_address: the most accessible ip address of this machine
53         
54         @return: A deferred called with the above defined tuple
55         @rtype: L{twisted.internet.defer.Deferred}
56         """
57         return get_local_ip().addCallbacks(_on_local_ip, _on_no_local_ip)
58         
59 #Private----------
60 def _on_upnp_external_found(ipaddr):
61         """
62         Called when an external ip is found through UPNP.
63         
64         @param ipaddr: The WAN ip address
65         @type ipaddr: an IP string "x.x.x.x"
66         """
67         return (True, ipaddr)
68
69 def _on_no_upnp_external_found(error, ipaddr):
70         """
71         Called when the UPnP device failed to return external address.
72         
73         @param ipaddr: The LAN ip address
74         @type ipaddr: an IP string "x.x.x.x"
75         """
76         return (False, ipaddr)
77         
78 def _on_local_ip(result):
79         """
80         Called when we got the local ip of this machine. If we have a WAN address,
81         we return immediately, else we try to discover ip address through UPnP.
82         
83         @param result: a tuple (lan_flag, ip_addr)
84         @type result: a tuple (bool, ip string)
85         """
86         local, ipaddr = result
87         if not local:
88                 return (True, ipaddr)
89         else:
90                 logging.debug("Got local ip, trying to use upnp to get WAN ip")
91                 import nattraverso.pynupnp
92                 return nattraverso.pynupnp.get_external_ip().addCallbacks(
93                         _on_upnp_external_found,
94                         lambda x: _on_no_upnp_external_found(x, ipaddr))
95
96 def _on_no_local_ip(error):
97         """
98         Called when we could not retreive by any mean the ip of this machine.
99         We simply assume there is no connectivity, and return localhost address.
100         """
101         return (None, "127.0.0.1")
102         
103 def _got_multicast_ip(ipaddr):
104         """
105         Called when we received the ip address via udp multicast.
106         
107         @param ipaddr: an ip address
108         @type ipaddr: a string "x.x.x.x"
109         """
110         return (is_rfc1918_ip(ipaddr), ipaddr)
111
112 def _get_via_multicast():
113         """
114         Init a multicast ip address discovery.
115         
116         @return: A deferred called with the discovered ip address
117         @rtype: L{twisted.internet.defer.Deferred}
118         @raise Exception: When an error occurs during the multicast engine init
119         """
120         try:
121                 # Init multicast engine
122                 IReactorMulticast(reactor)
123         except:
124                 raise
125
126         logging.debug("Multicast ping to retrieve local IP")
127         return _discover_multicast().addCallback(_got_multicast_ip)
128
129 def _get_via_connected_udp(ipaddr):
130         """
131         Init a UDP socket ip discovery. We do a dns query, and retreive our
132         ip address from the connected udp socket.
133         
134         @param ipaddr: The ip address of a dns server
135         @type ipaddr: a string "x.x.x.x"
136         @raise RuntimeError: When the ip is a bogus ip (0.0.0.0 or alike)
137         """
138         udpprot = DatagramProtocol()
139         port = reactor.listenUDP(0, udpprot)
140         udpprot.transport.connect(ipaddr, 7)
141         localip = udpprot.transport.getHost().host
142         port.stopListening()
143         
144         if is_bogus_ip(localip):
145                 raise RuntimeError, "Invalid IP address returned"
146         else:
147                 return (is_rfc1918_ip(localip), localip)
148                 
149 class _LocalNetworkMulticast(DatagramProtocol):
150         def __init__(self, nonce):
151                 from p2pool.util import variable
152                 
153                 self.nonce = nonce
154                 self.address_received = variable.Event()
155         
156         def datagramReceived(self, dgram, addr):
157                 """Datagram received, we callback the IP address."""
158                 logging.debug("Received multicast pong: %s; addr:%r", dgram, addr)
159                 if dgram != self.nonce:
160                         return
161                 self.address_received.happened(addr[0])
162
163 @defer.inlineCallbacks
164 def _discover_multicast():
165         """
166         Local IP discovery protocol via multicast:
167                 - Broadcast 3 ping multicast packet with "ping" in it
168                 - Wait for an answer
169                 - Retrieve the ip address from the returning packet, which is ours
170         """
171         
172         nonce = str(random.randrange(2**64))
173         p = _LocalNetworkMulticast(nonce)
174         
175         # 5 different UDP ports
176         ports = [11000+random.randint(0, 5000) for port in range(5)]
177         for attempt, port in enumerate(ports):
178                 try:
179                         mcast = reactor.listenMulticast(port, p)
180                         mcast_port = port
181                 except CannotListenError:
182                         if attempt < 5:
183                                 print "Trying another multicast UDP port", port
184                         else:
185                                 raise
186                 else:
187                         break
188         
189         try:
190                 yield mcast.joinGroup('239.255.255.250', socket.INADDR_ANY)
191                 
192                 try:
193                         logging.debug("Sending multicast ping")
194                         for i in xrange(3):
195                                 p.transport.write(nonce, ('239.255.255.250', mcast_port))
196                         
197                         address, = yield p.address_received.get_deferred(5)
198                 finally:
199                         mcast.leaveGroup('239.255.255.250', socket.INADDR_ANY)
200         finally:
201                 mcast.stopListening()
202         
203         defer.returnValue(address)