1 | """ |
---|
2 | Utilities for getting IP addresses. |
---|
3 | """ |
---|
4 | |
---|
5 | from future.utils import native_str |
---|
6 | |
---|
7 | from typing import Callable |
---|
8 | |
---|
9 | import os, socket |
---|
10 | |
---|
11 | from zope.interface import implementer |
---|
12 | |
---|
13 | import attr |
---|
14 | |
---|
15 | from netifaces import ( |
---|
16 | interfaces, |
---|
17 | ifaddresses, |
---|
18 | ) |
---|
19 | |
---|
20 | # from Twisted |
---|
21 | from twisted.python.reflect import requireModule |
---|
22 | from twisted.python import log |
---|
23 | from twisted.internet.endpoints import AdoptedStreamServerEndpoint |
---|
24 | from twisted.internet.interfaces import ( |
---|
25 | IReactorSocket, |
---|
26 | IStreamServerEndpoint, |
---|
27 | ) |
---|
28 | |
---|
29 | from .gcutil import ( |
---|
30 | fileDescriptorResource, |
---|
31 | ) |
---|
32 | |
---|
33 | fcntl = requireModule("fcntl") |
---|
34 | |
---|
35 | allocate_tcp_port: Callable[[], int] |
---|
36 | from foolscap.util import allocate_tcp_port # re-exported |
---|
37 | |
---|
38 | try: |
---|
39 | import resource |
---|
40 | def increase_rlimits(): |
---|
41 | # We'd like to raise our soft resource.RLIMIT_NOFILE, since certain |
---|
42 | # systems (OS-X, probably solaris) start with a relatively low limit |
---|
43 | # (256), and some unit tests want to open up more sockets than this. |
---|
44 | # Most linux systems start with both hard and soft limits at 1024, |
---|
45 | # which is plenty. |
---|
46 | |
---|
47 | # unfortunately the values to pass to setrlimit() vary widely from |
---|
48 | # one system to another. OS-X reports (256, HUGE), but the real hard |
---|
49 | # limit is 10240, and accepts (-1,-1) to mean raise it to the |
---|
50 | # maximum. Cygwin reports (256, -1), then ignores a request of |
---|
51 | # (-1,-1): instead you have to guess at the hard limit (it appears to |
---|
52 | # be 3200), so using (3200,-1) seems to work. Linux reports a |
---|
53 | # sensible (1024,1024), then rejects (-1,-1) as trying to raise the |
---|
54 | # maximum limit, so you could set it to (1024,1024) but you might as |
---|
55 | # well leave it alone. |
---|
56 | |
---|
57 | try: |
---|
58 | current = resource.getrlimit(resource.RLIMIT_NOFILE) |
---|
59 | except AttributeError: |
---|
60 | # we're probably missing RLIMIT_NOFILE |
---|
61 | return |
---|
62 | |
---|
63 | if current[0] >= 1024: |
---|
64 | # good enough, leave it alone |
---|
65 | return |
---|
66 | |
---|
67 | try: |
---|
68 | if current[1] > 0 and current[1] < 1000000: |
---|
69 | # solaris reports (256, 65536) |
---|
70 | resource.setrlimit(resource.RLIMIT_NOFILE, |
---|
71 | (current[1], current[1])) |
---|
72 | else: |
---|
73 | # this one works on OS-X (bsd), and gives us 10240, but |
---|
74 | # it doesn't work on linux (on which both the hard and |
---|
75 | # soft limits are set to 1024 by default). |
---|
76 | resource.setrlimit(resource.RLIMIT_NOFILE, (-1,-1)) |
---|
77 | new = resource.getrlimit(resource.RLIMIT_NOFILE) |
---|
78 | if new[0] == current[0]: |
---|
79 | # probably cygwin, which ignores -1. Use a real value. |
---|
80 | resource.setrlimit(resource.RLIMIT_NOFILE, (3200,-1)) |
---|
81 | |
---|
82 | except ValueError: |
---|
83 | log.msg("unable to set RLIMIT_NOFILE: current value %s" |
---|
84 | % (resource.getrlimit(resource.RLIMIT_NOFILE),)) |
---|
85 | except: |
---|
86 | # who knows what. It isn't very important, so log it and continue |
---|
87 | log.err() |
---|
88 | except ImportError: |
---|
89 | def _increase_rlimits(): |
---|
90 | # TODO: implement this for Windows. Although I suspect the |
---|
91 | # solution might be "be running under the iocp reactor and |
---|
92 | # make this function be a no-op". |
---|
93 | pass |
---|
94 | # pyflakes complains about two 'def FOO' statements in the same time, |
---|
95 | # since one might be shadowing the other. This hack appeases pyflakes. |
---|
96 | increase_rlimits = _increase_rlimits |
---|
97 | |
---|
98 | |
---|
99 | def get_local_addresses_sync(): |
---|
100 | """ |
---|
101 | Get locally assigned addresses as dotted-quad native strings. |
---|
102 | |
---|
103 | :return [str]: A list of IPv4 addresses which are assigned to interfaces |
---|
104 | on the local system. |
---|
105 | """ |
---|
106 | return list( |
---|
107 | native_str(address["addr"]) |
---|
108 | for iface_name |
---|
109 | in interfaces() |
---|
110 | for address |
---|
111 | in ifaddresses(iface_name).get(socket.AF_INET, []) |
---|
112 | ) |
---|
113 | |
---|
114 | |
---|
115 | def _foolscapEndpointForPortNumber(portnum): |
---|
116 | """ |
---|
117 | Create an endpoint that can be passed to ``Tub.listen``. |
---|
118 | |
---|
119 | :param portnum: Either an integer port number indicating which TCP/IPv4 |
---|
120 | port number the endpoint should bind or ``None`` to automatically |
---|
121 | allocate such a port number. |
---|
122 | |
---|
123 | :return: A two-tuple of the integer port number allocated and a |
---|
124 | Foolscap-compatible endpoint object. |
---|
125 | """ |
---|
126 | if portnum is None: |
---|
127 | # Bury this reactor import here to minimize the chances of it having |
---|
128 | # the effect of installing the default reactor. |
---|
129 | from twisted.internet import reactor |
---|
130 | if fcntl is not None and IReactorSocket.providedBy(reactor): |
---|
131 | # On POSIX we can take this very safe approach of binding the |
---|
132 | # actual socket to an address. Once the bind succeeds here, we're |
---|
133 | # no longer subject to any future EADDRINUSE problems. |
---|
134 | s = socket.socket() |
---|
135 | try: |
---|
136 | s.bind(('', 0)) |
---|
137 | portnum = s.getsockname()[1] |
---|
138 | s.listen(1) |
---|
139 | # File descriptors are a relatively scarce resource. The |
---|
140 | # cleanup process for the file descriptor we're about to dup |
---|
141 | # is unfortunately complicated. In particular, it involves |
---|
142 | # the Python garbage collector. See CleanupEndpoint for |
---|
143 | # details of that. Here, we need to make sure the garbage |
---|
144 | # collector actually runs frequently enough to make a |
---|
145 | # difference. Normally, the garbage collector is triggered by |
---|
146 | # allocations. It doesn't know about *file descriptor* |
---|
147 | # allocation though. So ... we'll "teach" it about those, |
---|
148 | # here. |
---|
149 | fileDescriptorResource.allocate() |
---|
150 | fd = os.dup(s.fileno()) |
---|
151 | flags = fcntl.fcntl(fd, fcntl.F_GETFD) |
---|
152 | flags = flags | os.O_NONBLOCK | fcntl.FD_CLOEXEC |
---|
153 | fcntl.fcntl(fd, fcntl.F_SETFD, flags) |
---|
154 | endpoint = AdoptedStreamServerEndpoint(reactor, fd, socket.AF_INET) |
---|
155 | return (portnum, CleanupEndpoint(endpoint, fd)) |
---|
156 | finally: |
---|
157 | s.close() |
---|
158 | else: |
---|
159 | # Get a random port number and fall through. This is necessary on |
---|
160 | # Windows where Twisted doesn't offer IReactorSocket. This |
---|
161 | # approach is error prone for the reasons described on |
---|
162 | # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2787 |
---|
163 | portnum = allocate_tcp_port() |
---|
164 | return (portnum, "tcp:%d" % portnum) |
---|
165 | |
---|
166 | |
---|
167 | @implementer(IStreamServerEndpoint) |
---|
168 | @attr.s |
---|
169 | class CleanupEndpoint(object): |
---|
170 | """ |
---|
171 | An ``IStreamServerEndpoint`` wrapper which closes a file descriptor if the |
---|
172 | wrapped endpoint is never used. |
---|
173 | |
---|
174 | :ivar IStreamServerEndpoint _wrapped: The wrapped endpoint. The |
---|
175 | ``listen`` implementation is delegated to this object. |
---|
176 | |
---|
177 | :ivar int _fd: The file descriptor to close if ``listen`` is never called |
---|
178 | by the time this object is garbage collected. |
---|
179 | |
---|
180 | :ivar bool _listened: A flag recording whether or not ``listen`` has been |
---|
181 | called. |
---|
182 | """ |
---|
183 | _wrapped = attr.ib() |
---|
184 | _fd = attr.ib() |
---|
185 | _listened = attr.ib(default=False) |
---|
186 | |
---|
187 | def listen(self, protocolFactory): |
---|
188 | self._listened = True |
---|
189 | return self._wrapped.listen(protocolFactory) |
---|
190 | |
---|
191 | def __del__(self): |
---|
192 | """ |
---|
193 | If ``listen`` was never called then close the file descriptor. |
---|
194 | """ |
---|
195 | if not self._listened: |
---|
196 | os.close(self._fd) |
---|
197 | fileDescriptorResource.release() |
---|
198 | |
---|
199 | |
---|
200 | def listenOnUnused(tub, portnum=None): |
---|
201 | """ |
---|
202 | Start listening on an unused TCP port number with the given tub. |
---|
203 | |
---|
204 | :param portnum: Either an integer port number indicating which TCP/IPv4 |
---|
205 | port number the endpoint should bind or ``None`` to automatically |
---|
206 | allocate such a port number. |
---|
207 | |
---|
208 | :return: An integer indicating the TCP port number on which the tub is now |
---|
209 | listening. |
---|
210 | """ |
---|
211 | portnum, endpoint = _foolscapEndpointForPortNumber(portnum) |
---|
212 | tub.listenOn(endpoint) |
---|
213 | tub.setLocation("localhost:%d" % portnum) |
---|
214 | return portnum |
---|
215 | |
---|
216 | |
---|
217 | __all__ = ["allocate_tcp_port", |
---|
218 | "increase_rlimits", |
---|
219 | "get_local_addresses_sync", |
---|
220 | "listenOnUnused", |
---|
221 | ] |
---|