upnp port forwarding
[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 retreive local IP")
127         return LocalNetworkMulticast().discover().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 addres returned"
146         else:
147                 return (is_rfc1918_ip(localip), localip)
148                 
149 class LocalNetworkMulticast(DatagramProtocol, object):
150         """
151         Local IP discovery protocol via multicast:
152                 - Broadcast 3 ping multicast packet with "ping" in it
153                 - Wait for an answer
154                 - Retreive the ip address from the returning packet, which is ours
155         """
156         def __init__(self, *args, **kwargs):
157                 super(LocalNetworkMulticast, self).__init__(*args, **kwargs)
158                 
159                 # Nothing found yet
160                 self.completed = False
161                 self.result = defer.Deferred()
162                 
163         def discover(self):
164                 """
165                 Launch the discovery of an UPnP device on the local network. You should
166                 always call this after having created the object.
167                 
168                 >>> result = LocalNetworkMulticast().discover().addCallback(got_upnpdevice_ip)
169                 
170                 @return: A deferred called with the IP address of the UPnP device
171                 @rtype: L{twisted.internet.defer.Deferred}
172                 """
173                 #The port we listen on
174                 mcast_port = 0
175                 #The result of listenMulticast
176                 mcast = None
177
178                 # 5 different UDP ports
179                 ports = [11000+random.randint(0, 5000) for port in range(5)]
180                 for attempt, port in enumerate(ports):
181                         try:
182                                 mcast = reactor.listenMulticast(port, self)
183                                 mcast_port = port
184                                 break
185                         except CannotListenError:
186                                 if attempt < 5:
187                                         print "Trying another multicast UDP port", port
188                                 else:
189                                         raise
190                 
191                 logging.debug("Sending multicast ping")
192                 mcast.joinGroup('239.255.255.250', socket.INADDR_ANY)
193                 self.transport.write('ping', ('239.255.255.250', mcast_port))
194                 self.transport.write('ping', ('239.255.255.250', mcast_port))
195                 self.transport.write('ping', ('239.255.255.250', mcast_port))
196                 return self.result
197
198         def datagramReceived(self, dgram, addr):
199                 """Datagram received, we callback the IP address."""
200                 logging.debug("Received multicast pong: %s; addr:%r", dgram, addr)
201                 if self.completed or dgram != 'ping':
202                         # Result already handled, do nothing
203                         return
204                 else:
205                         self.completed = True
206                         # Return the upn device ip address
207                         self.result.callback(addr[0])