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