fixed indentation
[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)