Mercurial > libervia-backend
comparison src/plugins/plugin_misc_nat-port.py @ 1554:e281ed2c21db
plugin NAT port: added UPnP IGD mapping + automatic unmapping on backend shut down
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Nov 2015 22:02:41 +0100 |
parents | 04a13d9ae265 |
children | 7d91dff71067 |
comparison
equal
deleted
inserted
replaced
1553:ebf97c1ac14a | 1554:e281ed2c21db |
---|---|
22 from sat.core.log import getLogger | 22 from sat.core.log import getLogger |
23 log = getLogger(__name__) | 23 log = getLogger(__name__) |
24 from sat.core import exceptions | 24 from sat.core import exceptions |
25 from twisted.internet import threads | 25 from twisted.internet import threads |
26 from twisted.internet import defer | 26 from twisted.internet import defer |
27 from twisted.python import failure | |
28 import threading | |
27 | 29 |
28 try: | 30 try: |
29 import miniupnpc | 31 import miniupnpc |
30 except ImportError: | 32 except ImportError: |
31 raise exceptions.MissingModule(u"Missing module MiniUPnPc, please download/install it (and its Python binding) at http://miniupnp.free.fr/") | 33 raise exceptions.MissingModule(u"Missing module MiniUPnPc, please download/install it (and its Python binding) at http://miniupnp.free.fr/") |
38 "main": "NatPort", | 40 "main": "NatPort", |
39 "handler": "no", | 41 "handler": "no", |
40 "description": _("""Automatic NAT port mapping using UPnP"""), | 42 "description": _("""Automatic NAT port mapping using UPnP"""), |
41 } | 43 } |
42 | 44 |
45 STARTING_PORT = 6000 # starting point to automatically find a port | |
46 DEFAULT_DESC = u'SaT port mapping' # we don't use "à" here as some bugged NAT don't manage charset correctly | |
47 | |
48 | |
49 class MappingError(Exception): | |
50 pass | |
51 | |
43 | 52 |
44 class NatPort(object): | 53 class NatPort(object): |
45 # TODO: refresh data if a new connection is detected (see plugin_misc_ip) | 54 # TODO: refresh data if a new connection is detected (see plugin_misc_ip) |
46 | 55 |
47 def __init__(self, host): | 56 def __init__(self, host): |
49 self.host = host | 58 self.host = host |
50 self._external_ip = None | 59 self._external_ip = None |
51 self._initialised = defer.Deferred() | 60 self._initialised = defer.Deferred() |
52 self._upnp = miniupnpc.UPnP() # will be None if no device is available | 61 self._upnp = miniupnpc.UPnP() # will be None if no device is available |
53 self._upnp.discoverdelay=200 | 62 self._upnp.discoverdelay=200 |
63 self._mutex = threading.Lock() # used to protect access to self._upnp | |
64 self._starting_port_cache = None # used to cache the first available port | |
65 self._to_unmap = [] # list of tuples (ext_port, protocol) of ports to unmap on unload | |
54 discover_d = threads.deferToThread(self._discover) | 66 discover_d = threads.deferToThread(self._discover) |
55 discover_d.chainDeferred(self._initialised) | 67 discover_d.chainDeferred(self._initialised) |
56 self._initialised.addErrback(self._init_failed) | 68 self._initialised.addErrback(self._init_failed) |
57 | 69 |
58 def _init_failed(self, failure): | 70 def unload(self): |
59 log.info(u"UPnP-GID not available") | 71 if self._to_unmap: |
72 log.info(u"Cleaning mapped ports") | |
73 return threads.deferToThread(self._unmapPortsBlocking) | |
74 | |
75 def _init_failed(self, failure_): | |
76 e = failure_.trap(exceptions.NotFound, exceptions.FeatureNotFound) | |
77 if e == exceptions.FeatureNotFound: | |
78 log.info(u"UPnP-IGD seems to be not activated on the device") | |
79 else: | |
80 log.info(u"UPnP-IGD not available") | |
60 self._upnp = None | 81 self._upnp = None |
61 | 82 |
62 def _discover(self): | 83 def _discover(self): |
63 devices = self._upnp.discover() | 84 devices = self._upnp.discover() |
64 if devices: | 85 if devices: |
65 log.info(u"{nb} UPnP-IGD device(s) found".format(nb=devices)) | 86 log.info(u"{nb} UPnP-IGD device(s) found".format(nb=devices)) |
66 else: | 87 else: |
67 log.info(u"Can't find UPnP-IGD device on the local network") | 88 log.info(u"Can't find UPnP-IGD device on the local network") |
68 raise exceptions.NotFound | 89 raise failure.Failure(exceptions.NotFound()) |
69 self._upnp.selectigd() | 90 self._upnp.selectigd() |
70 self._external_ip = self._upnp.externalipaddress() | 91 try: |
92 self._external_ip = self._upnp.externalipaddress() | |
93 except Exception: | |
94 raise failure.Failure(exceptions.FeatureNotFound()) | |
71 | 95 |
72 def getIP(self, local=False): | 96 def getIP(self, local=False): |
73 """Return IP address found with UPnP-IGD | 97 """Return IP address found with UPnP-IGD |
74 | 98 |
75 @param local(bool): True to get external IP address, False to get local network one | 99 @param local(bool): True to get external IP address, False to get local network one |
80 return None | 104 return None |
81 # lanaddr can be the empty string if not found, | 105 # lanaddr can be the empty string if not found, |
82 # we need to return None in this case | 106 # we need to return None in this case |
83 return (self._upnp.lanaddr or None) if local else self._external_ip | 107 return (self._upnp.lanaddr or None) if local else self._external_ip |
84 return self._initialised.addCallback(getIP) | 108 return self._initialised.addCallback(getIP) |
109 | |
110 def _unmapPortsBlocking(self): | |
111 """Unmap ports mapped in this session""" | |
112 self._mutex.acquire() | |
113 try: | |
114 for port, protocol in self._to_unmap: | |
115 log.info(u"Unmapping port {}".format(port)) | |
116 unmapping = self._upnp.deleteportmapping( | |
117 # the last parameter is remoteHost, we don't use it | |
118 port, protocol, '') | |
119 | |
120 if not unmapping: | |
121 log.error(u"Can't unmap port {port} ({protocol})".format( | |
122 port=port, protocol=protocol)) | |
123 del self._to_unmap[:] | |
124 finally: | |
125 self._mutex.release() | |
126 | |
127 def _mapPortBlocking(self, int_port, ext_port, protocol, desc): | |
128 """Internal blocking method to map port | |
129 | |
130 @param int_port(int): internal port to use | |
131 @param ext_port(int): external port to use, or None to find one automatically | |
132 @param protocol(str): 'TCP' or 'UDP' | |
133 @param desc(str): description of the mapping | |
134 @param return(int, None): external port used in case of success, otherwise None | |
135 """ | |
136 # we use mutex to avoid race condition if 2 threads | |
137 # try to acquire a port at the same time | |
138 self._mutex.acquire() | |
139 try: | |
140 if ext_port is None: | |
141 # find a free port | |
142 starting_port = self._starting_port_cache | |
143 ext_port = STARTING_PORT if starting_port is None else starting_port | |
144 ret = self._upnp.getspecificportmapping(ext_port, protocol) | |
145 while ret != None and ext_port < 65536: | |
146 ext_port += 1 | |
147 ret = self._upnp.getspecificportmapping(ext_port, protocol) | |
148 if starting_port is None: | |
149 # XXX: we cache the first successfuly found external port | |
150 # to avoid testing again the first series the next time | |
151 self._starting_port_cache = ext_port | |
152 | |
153 mapping = self._upnp.addportmapping( | |
154 # the last parameter is remoteHost, we don't use it | |
155 ext_port, protocol, self._upnp.lanaddr, int_port, desc, '') | |
156 | |
157 if not mapping: | |
158 raise MappingError | |
159 else: | |
160 self._to_unmap.append((ext_port, protocol)) | |
161 finally: | |
162 self._mutex.release() | |
163 | |
164 return ext_port | |
165 | |
166 def mapPort(self, int_port, ext_port=None, protocol='TCP', desc=DEFAULT_DESC): | |
167 """Add a port mapping | |
168 | |
169 @param int_port(int): internal port to use | |
170 @param ext_port(int,None): external port to use, or None to find one automatically | |
171 @param protocol(str): 'TCP' or 'UDP' | |
172 @param desc(unicode): description of the mapping | |
173 Some UPnP IGD devices have broken encoding. It's probably a good idea to avoid non-ascii chars here | |
174 @return (D(int, None)): external port used in case of success, otherwise None | |
175 """ | |
176 if self._upnp is None: | |
177 return defer.succeed(None) | |
178 def mappingCb(ext_port): | |
179 log.info(u"{protocol} mapping from {int_port} to {ext_port} successful".format( | |
180 protocol = protocol, | |
181 int_port = int_port, | |
182 ext_port = ext_port, | |
183 )) | |
184 return ext_port | |
185 def mappingEb(failure): | |
186 failure.trap(MappingError) | |
187 log.warning(u"Can't map internal {int_port}".format(int_port=int_port)) | |
188 d = threads.deferToThread(self._mapPortBlocking, int_port, ext_port, protocol, desc) | |
189 d.addCallbacks(mappingCb, mappingEb) | |
190 return d |