UPnP: randomize external port
[re6stnet.git] / re6st / upnpigd.py
1 from functools import wraps
2 import logging, random, socket, time
3 import miniupnpc
4
5
6 class UPnPException(Exception):
7 pass
8
9
10 class Forwarder(object):
11 """
12 External port is chosen randomly between 32768 & 49151 included.
13 """
14
15 next_refresh = 0
16 _next_retry = -1
17 _next_port = random.randrange(0, 8192)
18
19 @classmethod
20 def _getExternalPort(cls):
21 port = cls._next_port = (cls._next_port + 1) % 8192
22 return 32768 + port
23
24 def __init__(self, description):
25 self._description = description
26 self._u = miniupnpc.UPnP()
27 self._u.discoverdelay = 200
28 self._rules = []
29
30 def __getattr__(self, name):
31 wrapped = getattr(self._u, name)
32 def wrapper(*args, **kw):
33 try:
34 return wrapped(*args, **kw)
35 except Exception, e:
36 raise UPnPException(str(e))
37 return wraps(wrapped)(wrapper)
38
39 def select(self, r, w, t):
40 t.append((self.next_refresh, self.refresh))
41
42 def checkExternalIp(self, ip=None):
43 if ip:
44 try:
45 socket.inet_aton(ip)
46 except socket.error:
47 ip = None
48 else:
49 ip = self.refresh()
50 # If port is None, we assume we're not NATed.
51 return socket.AF_INET, [(ip, str(port or local), proto)
52 for local, proto, port in self._rules] if ip else ()
53
54 def addRule(self, local_port, proto):
55 self._rules.append([local_port, proto, None])
56
57 def refresh(self):
58 if self._next_retry:
59 if time.time() < self._next_retry:
60 return
61 self._next_retry = 0
62 else:
63 try:
64 return self._refresh()
65 except UPnPException, e:
66 logging.debug("UPnP failure", exc_info=1)
67 self.clear()
68 try:
69 self.discover()
70 self.selectigd()
71 return self._refresh()
72 except UPnPException, e:
73 self.next_refresh = self._next_retry = time.time() + 60
74 logging.info(str(e))
75 self.clear()
76
77 def _refresh(self):
78 force = self.next_refresh < time.time()
79 if force:
80 self.next_refresh = time.time() + 500
81 logging.debug('Refreshing port forwarding')
82 ip = self.externalipaddress()
83 lanaddr = self._u.lanaddr
84 retry = 8191
85 for r in self._rules:
86 local, proto, port = r
87 if port and not force:
88 continue
89 desc = '%s (%u/%s)' % (self._description, local, proto)
90 args = proto.upper(), lanaddr, local, desc, ''
91 while True:
92 if port is None:
93 if not retry:
94 raise UPnPException('No free port to redirect %s'
95 % desc)
96 retry -= 1
97 port = self._getExternalPort()
98 try:
99 self.addportmapping(port, *args)
100 break
101 except UPnPException, e:
102 if str(e) != 'ConflictInMappingEntry':
103 raise
104 port = None
105 if r[2] != port:
106 logging.debug('%s forwarded from %s:%u', desc, ip, port)
107 r[2] = port
108 return ip
109
110 def clear(self):
111 for r in self._rules:
112 port = r[2]
113 if port:
114 r[2] = None
115 try:
116 self.deleteportmapping(port, r[1].upper())
117 except UPnPException:
118 pass