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