API Reference
Every public name, generated from the package's own docstrings.
For the contracts and gotchas in prose form — including the platform
differences that are easy to get wrong — see src/netimps/AGENTS.md in the
repository, which lists the per-package headers beside the code (_dns,
_sockets, _udp and the others).
netimps -- small, self-contained network utilities.
A thin, typed convenience layer over the standard library's :mod:ipaddress
plus a handful of host helpers (DNS lookup, ping, interface discovery). One
flat import surface; no hard runtime dependencies. :func:resolve chains
dnspython/OS-resolver/nslookup backends, using whichever is available
-- dnspython (install with the dns extra) is optional but gives the
fullest one.
::
import netimps
netimps.parse("10.0.0.5") # -> IPv4Address
netimps.MACAddress("AA:BB:CC:DD:EE:FF").format("-")
netimps.resolve("example.com", "aaaa")
for iface in netimps.get_interfaces():
print(iface.name, iface.mac, iface.ips)
Types and parsing
IPAddress/IPInterface/IPNetwork are the v4/v6 unions you annotate
with, reading the way :class:ipaddress.IPv4Address does::
def get_route(dst: netimps.IPAddress, via: netimps.IPNetwork) -> None: ...
The same names are what you parse into, via one entry point::
parse(value, IPNetwork) # raises on bad input
try_parse(value, IPNetwork) # None instead
is_valid(value, IPNetwork) # bool convertibility check
All IP/network values are the concrete :mod:ipaddress classes, so
.exploded, .network_address, .netmask and addr in network
membership all behave exactly as the stdlib does.
AddressInUseError
Bases: NetimpsError, OSError
The address is taken -- one stable type, whatever the platform called it.
"The port is already bound" surfaces as three different shapes depending on the flags and the interpreter. Measured on Windows 11 ARM64 against an exclusive holder:
========================== ================== ======== ==========
call type errno winerror
========================== ================== ======== ==========
3.14, allow_takeover PermissionError 13 10013
3.9, allow_takeover OSError 10013 10013
either, plain OSError 10048 10048
========================== ================== ======== ==========
The 3.14 row is the harmful one: PermissionError says "privilege
problem", and on Windows there is no such thing for a port -- the address is
simply held. A caller branching on the type then sends its user after an
elevation problem that cannot exist -- and the alternative is a wrapper that
re-derives the fact by string-matching :func:bind_error_hint's message,
which is worse.
So :func:bind raises this instead, with errno normalised to
EADDRINUSE, the hint as the message, and the original exception chained
as __cause__ -- so winerror and the platform's own code stay
reachable for anyone who wants them.
Subclasses :class:OSError and deliberately not
:class:PermissionError: every existing except OSError keeps working,
while except PermissionError stops catching a case that was never about
permission. A genuine privilege failure -- POSIX EACCES on a port below
1024 -- is left exactly as it was.
DeviceBindingUnsupportedError
Bases: NetimpsError, OSError
bind(device=...) on a platform with no way to restrict a socket to a device.
Raised before any socket is opened, with errno set to ENOPROTOOPT
and a message naming :func:has_device_binding, which says in advance
whether a platform can. An :class:OSError, so an except OSError around
a bind keeps catching it.
DNSDecodeError
Bases: NetimpsValueError
Bytes or a name that the DNS message codec cannot read or write: a truncated or looping reply, a reply for another query, an empty or over-long label, a record type the codec does not handle.
NetimpsError
Bases: Exception
The base of every exception netimps raises on its own account.
NetimpsValueError
Bases: NetimpsError, ValueError
Text that is not the value it was asked to become: a malformed address,
MAC, domain name or host:port.
Also a :class:ValueError, so except ValueError keeps catching it.
NoAnswerError
Bases: ResolutionError
The lookup completed and there is no such name or no such record.
What check=True on :class:~netimps.Host and :class:~netimps.FQDN
raises for an empty answer, as opposed to a plain :class:ResolutionError
for a resolver that could not be asked. The difference matters to
:func:netimps.retry: an outage is worth another attempt, a name that does
not exist is not.
ResolutionError
Bases: NetimpsError, OSError
A backend could not even attempt the query (missing binary, unsupported
rdtype, a server that did not answer, a transport or setup failure).
Distinct from a definitive DNS answer of "no such record", which is [],
not an exception.
An :class:OSError, as :class:socket.gaierror is: a caller who writes
except OSError around a connect already expects name failures in it, and
:func:netimps.retry retries it by default.
ResolutionTimeoutError
Bases: ResolutionError, TimeoutError
A resolution backend's deadline expired before it had an answer.
Caught by except ResolutionError (so the :func:netimps.resolve chain
moves on) and by except TimeoutError.
Host(value)
A host named by either an address or a hostname.
Config files and URLs hold "the host" as a string that may be either, and the useful operations differ. This keeps the original text and resolves on demand::
host = Host("db.internal")
host.ip() # IPv4Address(...) once DNS answers
str(host) # 'db.internal' -- always the original
Host("10.0.0.5").is_address # True, no DNS involved
The point is that str(host) is always what was given, so a URL can
still be rebuilt when resolution fails -- the case a bare lookup handles
badly, since it returns None and loses the name.
Source code in src/netimps/_ip/_host.py
109 110 111 112 113 114 115 116 117 118 | |
is_address
property
True if the value is already an IP literal -- no DNS needed.
parse(text)
classmethod
Build a Host from text naming an address or a hostname.
A Host keeps what it was given, so no spelling is rejected for its
form; text with nothing in it is, since there is no host to name.
The constructor is the lenient entry that takes None and "".
:raises NetimpsValueError: for empty or blank text. It is a
ValueError.
:raises TypeError: for anything that is not str.
Source code in src/netimps/_ip/_host.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
try_parse(text, default=None)
classmethod
try_parse(text: str) -> 'Optional[_H]'
try_parse(text: str, default: _D) -> 'Union[_H, _D]'
Return parse(text), or default for empty or blank text.
:raises TypeError: for anything that is not str; only bad text
is answered with default. :func:netimps.try_parse is the
entry that answers default for any object.
Source code in src/netimps/_ip/_host.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
fqdn(*, check=False, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None, source=None, cache=False, deadline=None)
fqdn(
*,
check: Literal[True],
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "FQDN"
fqdn(
*,
check: Literal[False] = False,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Optional[FQDN]"
fqdn(
*,
check: bool,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Optional[FQDN]"
This host as an :class:FQDN: the name itself, or the name an address
reverses to.
A name is returned as written, with no lookup; an address costs a reverse (PTR) lookup, which can block::
Host("www.example.com").fqdn().domain # FQDN('example.com'), no I/O
Host("10.0.0.5").fqdn() # FQDN('db.internal'), or None
The bridge between the two types: :class:Host is "an address or a
name", while :class:FQDN is a name algebra that refuses an address.
The name is the one written, not the canonical name after
search-list expansion, and a reverse answer comes back without its root
dot.
None is the answer when nothing was found, and also for text that is
not a syntactically possible name (over-long, an empty inner label).
With check=True the first raises :class:ResolutionError and the
second :class:NetimpsValueError.
The resolver options mean what they do for :meth:ip. There is no
ipv6: the address decides which reverse zone is asked. Nothing is
memoised.
Source code in src/netimps/_ip/_host.py
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
ip(*, check=False, ipv6=None, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None, source=None, cache=False, deadline=None, refresh=False)
ip(
*,
check: Literal[True],
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None,
refresh: bool = False
) -> "IPAddress"
ip(
*,
check: Literal[False] = False,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None,
refresh: bool = False
) -> "Optional[IPAddress]"
ip(
*,
check: bool,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None,
refresh: bool = False
) -> "Optional[IPAddress]"
Resolve to an address, or None.
A literal is returned as parsed, with no lookup; a name is looked up, which can block.
:param check: raise :class:ResolutionError instead of returning
None when nothing was found -- an empty answer, an outage, or an
empty host.
:param ipv6: True asks for AAAA, False for A, None for either
in one lookup, in the order the OS chose.
:param ns: nameserver(s) to ask instead of the OS's.
:param timeout: seconds per backend attempt; what it bounds differs by
backend, see :func:netimps.resolve.
:param deadline: seconds for the whole lookup, every backend and
candidate included; None sets no overall limit.
:param port: nameserver port.
:param tcp: query over TCP.
:param search: expand an unqualified name through a search list: the
system's (True), none (False) or the list given.
:param backends: which of "dnspython", "wire", "system",
"nslookup", and in what order; see :func:netimps.resolve.
:param source: the local address the query is sent from.
:param cache: reuse a recent answer, a miss included, from the shared
resolution cache: False (the default) never reads or writes it,
True keeps an answer for :data:RESOLUTION_CACHE_TTL seconds, a
number is that many. Unlike the memo below it is shared by every
:class:Host and :class:FQDN, and is keyed on the name and every
option; see :func:netimps.resolve.
:param refresh: ask again, and replace the memo with the new answer --
a name that failed once may resolve later.
With none of ns, port, tcp, source or backends the
OS resolver alone answers, as the standard library's lookups do. A
missed name then costs milliseconds, where the full chain behind
:func:netimps.resolve costs seconds. Naming any of them hands the
choice to that chain.
A call that passes no option memoises its answer, a miss included,
because the common use is several lookups in a row on the same object.
A call that passes any option, cache= included, neither reads nor
writes the memo.
Source code in src/netimps/_ip/_host.py
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 | |
resolve(*, check=False, ipv6=None, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None, source=None, cache=False, deadline=None)
resolve(
*,
check: Literal[True],
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Tuple[FQDN, IPAddress]"
resolve(
*,
check: Literal[False] = False,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Tuple[Optional[FQDN], Optional[IPAddress]]"
resolve(
*,
check: bool,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Tuple[Optional[FQDN], Optional[IPAddress]]"
The pair (fqdn, ip); whichever half was not found is None.
===================== ================================= ==================
host is fqdn ip
===================== ================================= ==================
a name the name as written forward lookup
an address reverse lookup the literal
===================== ================================= ==================
The result is always a pair, so fqdn, ip = host.resolve() never
fails to unpack. With check=True a half that could not be found
raises :class:ResolutionError instead. The options are
:meth:ip's, and a name does its one lookup only.
Source code in src/netimps/_ip/_host.py
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 | |
MACAddress(value)
An IEEE 802 MAC address.
Accepts the common textual forms on construction -- colon
(AA:BB:CC:DD:EE:FF), hyphen (AA-BB-CC-DD-EE-FF), dot per octet
(aa.bb.cc.dd.ee.ff), dot/Cisco triplets (aabb.ccdd.eeff) or bare
(AABBCCDDEEFF) -- as well as an int, six raw bytes or
bytearray, or another MACAddress. Separators may not be mixed
(00-11:22-33:44-55 is rejected), and a bool is not taken as an
integer even though Python says it is one.
The value is normalised to lowercase and compared/hashed by its canonical bytes, so instances are usable as dict keys and set members.
Equality holds only against another MACAddress: comparing with
text is False, because no hash can agree with str's across every
spelling of one address. Parse the text first::
MACAddress.try_parse(text) == mac
format(sep) renders the address with an arbitrary separator between
octets; sep="" produces the bare form, and every separator the
constructor accepts round-trips through it. f"{mac:-X}" is the same
call spelled as a format spec, and bytes(mac) is the six raw octets.
Source code in src/netimps/_mac.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
packed
property
The 6 raw bytes of the address.
The escape hatch for wire formats and syscalls, mirroring
:attr:ipaddress.IPv4Address.packed. MACAddress deliberately is
not a :class:bytes subclass -- see the class docstring.
oui
property
The first three octets -- the vendor prefix -- verbatim.
The I/G (group) and U/L (locally administered) flags live in the low
two bits of octet 0 and are not masked out here, so for a multicast
or locally administered address this is not a registered IEEE OUI:
01:00:5e:00:00:01 reports 01:00:5e where the registered
assignment is 00:00:5e. Mask them before a registry lookup::
bytes([mac.oui[0] & 0xFC]) + mac.oui[1:]
The bits are kept because this is the prefix as it appears on the wire
and in this package's own output; :attr:is_multicast and
:attr:is_local say when the masked form would differ.
is_multicast
property
True if the group bit (low bit of the first octet) is set.
Multicast MACs are destinations only -- a NIC never has one -- so this is the check for "did I mistake a group address for a host?".
is_local
property
True if locally administered (the U/L bit is set).
Locally administered addresses are assigned by software -- VMs, containers, and MAC-randomising clients -- rather than burned in by the vendor, so they are not stable identifiers.
is_universal
property
True if universally administered (vendor-assigned). Inverse of :attr:is_local.
is_valid(value)
classmethod
Return True if value can be parsed as a MAC. Never raises.
The type-local spelling of netimps.is_valid(value, MACAddress),
which is often what reads best at a call site::
if MACAddress.is_valid(user_input):
...
A classmethod rather than a staticmethod so a subclass validates against itself.
Returns a plain bool and deliberately does not narrow
value: a :data:typing.TypeGuard here would let a checker certify
user_input.packed on something that is still a str. Use
:meth:try_parse when you want the parsed object.
Source code in src/netimps/_mac.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
parse(text)
classmethod
Build a MACAddress from text, in any spelling the class accepts.
:raises NetimpsValueError: for text that is not a MAC address. It is a
ValueError.
:raises TypeError: for anything that is not str. The constructor
is the entry for int and bytes.
Source code in src/netimps/_mac.py
128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
try_parse(text, default=None)
classmethod
try_parse(text: str) -> 'Optional[_M]'
try_parse(text: str, default: _D) -> 'Union[_M, _D]'
Return parse(text), or default for text that is not a MAC.
Prefer it to :meth:is_valid followed by construction -- one call, and
no window in which the two disagree. It is also how text is compared
against a MAC, since :meth:__eq__ does not coerce a str::
MACAddress.try_parse(user_input) == known_mac
:raises TypeError: for anything that is not str; only bad text
is answered with default. :func:netimps.try_parse is the
entry that answers default for any object.
Source code in src/netimps/_mac.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
hex(sep=None, bytes_per_sep=1)
The octets as hex, exactly like :meth:bytes.hex.
sep and bytes_per_sep mean what they do there; omitting sep
gives the unseparated form::
MACAddress("aa:bb:cc:dd:ee:ff").hex() # 'aabbccddeeff'
MACAddress("aa:bb:cc:dd:ee:ff").hex(":") # 'aa:bb:cc:dd:ee:ff'
MACAddress("aa:bb:cc:dd:ee:ff").hex("-", 2) # 'aabb-ccdd-eeff'
A pure passthrough to self.packed.hex, which exists because this is
a value object rather than a bytes subclass, so the method is not
inherited -- and callers reasonably expect it, to the point of
subclassing the type to add it back.
For the conventional colon- or hyphen-separated forms prefer
:meth:format, which also offers the uppercase rendering; this is the
one to reach for when the target wants bytes.hex semantics
specifically, such as bytes_per_sep grouping.
Source code in src/netimps/_mac.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
format(sep=':', *, upper=False)
Return the MAC as a string with sep between octets.
Lowercase by default (the canonical form used by str(mac) and by
equality/hashing); pass upper=True for the uppercase rendering
favoured by Windows tooling and much vendor output::
mac.format("-") # 'aa-bb-cc-dd-ee-ff'
mac.format("-", upper=True) # 'AA-BB-CC-DD-EE-FF'
sep always goes between octets, "." included: the Cisco
triplet spelling aabb.ccdd.eeff is accepted on input but never
produced, so that one separator does not quietly mean something
different from the others. ":", "-", "." and "" all
parse back through the constructor; any other separator renders but
does not round-trip.
Case affects only this rendering -- two MACAddress values that
differ solely in the case they were parsed from remain equal.
Source code in src/netimps/_mac.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | |
Interface(name, index=0, *, mac=None, ips=None, mtu=None, raw=None, is_loopback=None, is_up=None, is_multicast=None, is_point_to_point=None)
One network interface, normalised to be identical across platforms.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Human-usable adapter name ( |
index |
int
|
:func: |
mac |
'Optional[MACAddress]'
|
The hardware address, or |
ips |
'Tuple[_IPInterface, ...]'
|
Every address bound to the interface, each as an
|
mtu |
'Optional[int]'
|
Link MTU in bytes, or |
is_up |
'Optional[bool]'
|
Whether the interface is usable: |
is_multicast |
'Optional[bool]'
|
Whether the interface can carry multicast: |
is_point_to_point |
'Optional[bool]'
|
Whether the interface joins two endpoints with no
broadcast domain (a tunnel, a PPP or VPN link): |
is_loopback |
bool
|
The kernel's own loopback flag ( |
raw |
'Optional[Mapping[str, Any]]'
|
|
The constructor raises :class:TypeError for a field of the wrong type
(mac must be a :class:MACAddress, each of ips an
IPv4Interface/IPv6Interface).
Source code in src/netimps/_ifaddrs/_model.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
is_loopback
property
True when this is the loopback interface.
The kernel's own answer when there is one: IFF_LOOPBACK on POSIX,
IF_TYPE_SOFTWARE_LOOPBACK on Windows, captured during
enumeration. Never the name -- lo (Linux),
lo0 (macOS) and Loopback Pseudo-Interface 1 (Windows) share no
common spelling.
Falls back to the addresses only when the flag was not reported (the
degraded enumeration path reports no flags, and neither do hand-built
objects). That fallback requires a loopback address and no routable
one, which is a guess rather than an answer: WSL2 binds a routable
10.255.255.254/32 to lo on every installation, and binding a
service address to the loopback interface is the standard
keepalived/anycast pattern -- on such a host the heuristic reports that
there is no loopback interface at all. Link-local addresses are ignored
by it, since macOS's lo0 also carries fe80::1/64 and a
non-routable address cannot make an interface non-loopback.
ipv4
property
Just the IPv4 addresses, as a tuple like :attr:ips.
ipv6
property
Just the IPv6 addresses, as a tuple like :attr:ips.
primary_ip(ipv6=False, *, loopback_ok=True)
Pick the one entry that best represents this interface, or None.
Answers "which of this adapter's addresses do I use?" -- the question
IP_MULTICAST_IF, a bind target and ping -S all ask::
iface.primary_ip() # IPv4Interface('10.0.0.5/24')
iface.primary_ip().ip # IPv4Address('10.0.0.5')
iface.primary_ip(ipv6=True) # its IPv6 entry instead
Ranked, not first-non-loopback: routable, then loopback, then
link-local, keeping the OS order within a rank. The rank matters
because an interface commonly lists a link-local address before its
routable one -- fe80:: is configured first on Linux and macOS NICs
-- and "the first entry that is not loopback" therefore picked an
address that is useless as a bind target and unreachable off-link.
Loopback outranks link-local deliberately, which is not the obvious
order. The only interface that carries both is the loopback adapter, and
there ::1 is the address every caller means; a real NIC has no
loopback entry, so the rank never takes anything from it. A NIC holding
only a link-local address -- before SLAAC completes, say -- still
yields it, because there is nothing else to yield. With
loopback_ok=False the loopback rank is skipped entirely, so such a
caller still gets the link-local in preference to None.
Measured on a macOS loopback adapter, whose entries are
127.0.0.1/8, ::1/128, fe80::1/64: taking the first IPv6
entry returns fe80::1 for ipv6=True where ::1 is wanted,
and bind(interface=...) then fails with "Can't assign requested
address". Likewise, on a NIC listing fe80:: before a global
address, first-entry order picks the link-local over the global one.
Link-local covers fe80::/10 and IPv4 169.254.0.0/16
(LINK_LOCAL_V4): the same problem in the other family, and an
interface holding both a LINK_LOCAL_V4 address and a DHCP lease
should answer with the lease.
Named primary rather than ip because this is a selection, not
"the" address: an interface routinely has several, and the full lists
remain on :attr:ips / :attr:ipv4 / :attr:ipv6.
:param ipv6: pick from :attr:ipv6 rather than :attr:ipv4.
:param loopback_ok: when False, an interface holding only loopback
addresses yields None instead -- for callers that need a
routable address specifically.
Returns an IPv4Interface/IPv6Interface, the same element type
as :attr:ips -- one of them, not a different shape. Use .ip for
the bare address that socket options take.
Source code in src/netimps/_ifaddrs/_model.py
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | |
PingResult(ok, dst, *, rtt=None, ttl=None, src=None, attempts=1)
Outcome of a :func:ping, usable directly as a boolean.
ping() answers "did it reply?", so this is truthy on success and falsy
on failure -- if ping(host): works -- while carrying the details a
caller would otherwise re-run ping to scrape.
Attributes:
| Name | Type | Description |
|---|---|---|
ok |
bool
|
whether the destination replied. |
dst |
'HostLike'
|
the destination as given. |
rtt |
Optional[float]
|
round-trip time in seconds, or |
ttl |
Optional[int]
|
TTL/hop-limit of the reply, or |
src |
'Optional[IPAddress]'
|
address that answered, which on success is the destination. |
attempts |
int
|
how many probes were sent before this outcome. |
Source code in src/netimps/_ping/_result.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
FQDN(*parts)
A domain name, with label algebra. Immutable, hashable and ordered.
Built from a dotted string or from separate labels, leftmost first -- the order they appear in the text::
FQDN("www.example.com")
FQDN("www", "example", "com") # the same name
FQDN("www", FQDN("example.com")) # composition works too
A trailing dot marks the name fully qualified and is preserved by
str(). See the module docstring for the pathlib inversion, the
absoluteness rule, and the two things this type deliberately omits.
:raises ValueError: for an address literal, an empty name, an over-long
name or label, or an empty inner label (a..b).
Source code in src/netimps/_fqdn/_name.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
labels
property
The labels, leftmost first, without the root.
FQDN("www.example.com").labels == ("www", "example", "com"). The
root is carried by :meth:is_fully_qualified rather than as an empty
final label, because an empty string in this tuple would be a trap for
every caller that iterates it.
parts
property
Alias of :attr:labels, for readers coming from pathlib.
hostname
property
The leftmost label -- www in www.example.com.
The opposite end from PurePath.name, which is the whole point of
the inversion warning. For a single-label name this is the whole name.
name
property
Alias of :attr:hostname, for readers coming from pathlib.
tld
property
The rightmost label -- com in www.example.com.
Deliberately has no .suffix alias: a filesystem suffix is part of a
name (.txt) while a TLD is a whole label, so the analogy misleads.
domain
property
The name with its leftmost label removed, or None at the top.
FQDN("www.example.com").domain == FQDN("example.com"). Absoluteness
is preserved.
None for a single-label name, rather than a self-reference: pathlib
makes Path("/").parent return itself, which is right for a
filesystem root but would make while f.domain: loop forever here.
This is not the registrable domain. The .domain of
example.com is com. See the module docstring.
parent
property
Alias of :attr:domain, for readers coming from pathlib.
domains
property
Every enclosing domain, nearest first.
FQDN("a.b.example.com").domains is
(FQDN('b.example.com'), FQDN('example.com'), FQDN('com')).
parents
property
Alias of :attr:domains, for readers coming from pathlib.
is_wildcard
property
Whether the leftmost label is * -- a DNS wildcard name.
A predicate only. Deliberately no matching method: DNS wildcards
(RFC 4592) synthesise for names at any depth below the wildcard's
parent, while TLS certificate matching (RFC 6125) allows exactly one
label. Those are different answers for a.b.example.com against
*.example.com, and picking one silently would be wrong for half the
callers. Write the rule you need against :meth:is_subdomain_of.
wire_length
property
Octets this name occupies on the wire, root terminator included.
This is the figure the 255-octet protocol limit applies to, while the 253 limit checked at construction is on the printable form -- the difference being one length prefix per label plus the root. Worth reaching for when a name is going into a packet you are sizing.
is_valid(value)
classmethod
True if value can be parsed as a domain name. Never raises.
A classmethod rather than a staticmethod so a subclass validates
against itself. Returns a plain bool and deliberately does not
narrow value -- see :meth:MACAddress.is_valid for why a
TypeGuard here would be unsound.
Source code in src/netimps/_fqdn/_name.py
190 191 192 193 194 195 196 197 198 199 200 201 202 203 | |
parse(text)
classmethod
Build an :class:FQDN from dotted text.
:raises NetimpsValueError: for text that is not a domain name: an
address literal, an empty or over-long name or label, an empty
inner label. It is a ValueError.
:raises TypeError: for anything that is not str. The constructor
also takes labels and another FQDN.
Source code in src/netimps/_fqdn/_name.py
205 206 207 208 209 210 211 212 213 214 215 216 217 | |
try_parse(text, default=None)
classmethod
try_parse(text: str) -> 'Optional[_F]'
try_parse(text: str, default: _D) -> 'Union[_F, _D]'
Return parse(text), or default for text that is not a name.
Prefer it to :meth:is_valid followed by construction -- one call, and
no window in which the two disagree.
:raises TypeError: for anything that is not str; only bad text
is answered with default. :func:netimps.try_parse is the
entry that answers default for any object.
Source code in src/netimps/_fqdn/_name.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | |
is_fully_qualified()
Whether the name carries the root dot, so needs no search list.
The DNS reading of PurePath.is_absolute. A relative name resolves
against the resolver's search domains and can therefore mean different
things on different hosts.
Source code in src/netimps/_fqdn/_name.py
334 335 336 337 338 339 340 341 | |
is_absolute()
Alias of :meth:is_fully_qualified.
Source code in src/netimps/_fqdn/_name.py
343 344 345 | |
fully_qualified()
This name with the root dot, unchanged if it already has one.
Source code in src/netimps/_fqdn/_name.py
347 348 349 350 351 | |
relative()
This name without the root dot, unchanged if it has none.
Source code in src/netimps/_fqdn/_name.py
353 354 355 356 357 | |
child(*labels)
Spelled-out form of :meth:__truediv__, for several labels at once.
Source code in src/netimps/_fqdn/_name.py
379 380 381 382 383 384 | |
with_hostname(hostname)
Replace the leftmost label.
FQDN("www.example.com").with_hostname("mail") is
FQDN('mail.example.com'). For a single-label name this replaces the
whole name.
Source code in src/netimps/_fqdn/_name.py
386 387 388 389 390 391 392 393 394 395 396 | |
with_name(name)
Alias of :meth:with_hostname, for readers coming from pathlib.
Source code in src/netimps/_fqdn/_name.py
398 399 400 | |
is_subdomain_of(other)
Whether this name sits under other.
A name is not a subdomain of itself, matching the ordinary reading
of the word -- use == for that, or
f == other or f.is_subdomain_of(other) for "at or under".
Qualification is ignored, since example.com and example.com.
describe the same place in the tree.
Source code in src/netimps/_fqdn/_name.py
402 403 404 405 406 407 408 409 410 411 412 413 414 | |
relative_to(other)
The labels of this name that are not part of other.
FQDN("www.example.com").relative_to("example.com") is
FQDN('www'), always relative (never fully qualified -- a fragment
of a name has no root). Labels compare case-blind and the remainder
keeps this name's spelling.
:raises ValueError: if this name is not under other, mirroring
PurePath.relative_to.
Source code in src/netimps/_fqdn/_name.py
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 | |
reverse()
The same labels in reverse order -- com.example.www.
A mechanical flip, for display and for building keys. The result is not a resolvable name; it is the labels the other way round. Note this is unrelated to a reverse DNS pointer, which is built from an address and so has no place on this type.
Source code in src/netimps/_fqdn/_name.py
432 433 434 435 436 437 438 439 440 | |
resolve(*, check=False, ipv6=None, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None, source=None, cache=False, deadline=None)
resolve(
*,
check: Literal[True],
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Tuple[FQDN, _IPAddress]"
resolve(
*,
check: Literal[False] = False,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Tuple[FQDN, Optional[_IPAddress]]"
resolve(
*,
check: bool,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Tuple[FQDN, Optional[_IPAddress]]"
The pair (self, ip): this name, and the address it resolves to.
The same shape as :meth:netimps.Host.resolve, so the two types
answer .resolve() alike. The name is returned as it is -- no
reverse lookup, and not the canonical name after search-list expansion.
ip is None when nothing was found, or :class:ResolutionError
is raised with check=True. The fully-qualified form is asked as
such, so a name with a trailing dot keeps bypassing the search list.
The resolver options are those of :meth:netimps.Host.ip, including
the rule that with none of ns, port, tcp, source or
backends only the OS resolver answers. DNS records of any type come
from :func:netimps.resolve.
Source code in src/netimps/_fqdn/_name.py
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 | |
ping(**kwargs)
Ping this name. Straight through to :func:netimps.ping.
Source code in src/netimps/_fqdn/_name.py
529 530 531 532 533 | |
ip(*, check=False, ipv6=None, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None, source=None, cache=False, deadline=None)
ip(
*,
check: Literal[True],
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "_IPAddress"
ip(
*,
check: Literal[False] = False,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Optional[_IPAddress]"
ip(
*,
check: bool,
ipv6: "Optional[bool]" = None,
ns: "Optional[Union[str, List[str]]]" = None,
timeout: "Optional[float]" = 5.0,
port: int = 53,
tcp: bool = False,
search: "Union[bool, List[str]]" = True,
backends: "Optional[Union[str, List[str]]]" = None,
source: "Optional[Union[str, List[str]]]" = None,
cache: "Union[bool, float]" = False,
deadline: "Optional[float]" = None
) -> "Optional[_IPAddress]"
The first address this name resolves to, or None.
Always a lookup, which can block, and never memoised: this type is
immutable, and a cache on it would be a lie about freshness.
:meth:netimps.Host.ip documents the options.
Source code in src/netimps/_fqdn/_name.py
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 | |
to_unicode()
The name in its display form, decoding punycode back to Unicode.
Labels are stored ASCII-encoded, because that is what goes on the wire and what comparisons must use. This is the other direction, for showing a name to a person::
FQDN("münchen.de").to_unicode() # 'münchen.de'
str(FQDN("münchen.de")) # 'xn--mnchen-3ya.de'
A label that is not valid punycode is passed through unchanged rather
than raising -- xn-- on its own is undecodable, and a display helper
that throws is worse than one that shows the stored form.
Source code in src/netimps/_fqdn/_name.py
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 | |
is_hostname()
Whether every label is a legal host name label (RFC 1123 LDH).
Letters, digits and hyphens only, and no leading or trailing hyphen.
This is narrower than what this type accepts, and deliberately so:
plenty of real DNS names are not hostnames. _dmarc.example.com,
_sip._tcp.example.com and _acme-challenge.example.com all carry
an underscore, and a wildcard carries *. Rejecting them at
construction would make the type useless for SRV, DMARC and ACME work,
so the constructor takes the broad DNS rule and this reports the narrow
one::
FQDN("_dmarc.example.com").is_hostname() # False -- but valid DNS
FQDN("www.example.com").is_hostname() # True
Source code in src/netimps/_fqdn/_name.py
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 | |
common_ancestor(other)
The deepest domain enclosing both names, or None if unrelated.
FQDN("a.example.com").common_ancestor("b.example.com") is
FQDN('example.com'). Compared from the right, since that is the end
names share. Qualification follows this name's.
None rather than an empty name when the two share no label at all:
there is no such thing as a zero-label name, and the root is not a
useful answer.
Source code in src/netimps/_fqdn/_name.py
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 | |
encode()
The DNS wire encoding: each label length-prefixed, root terminated.
FQDN("www.example.com").encode() is
b'\x03www\x07example\x03com\x00', uncompressed. Delegates to
the package's own encoder, so it cannot drift from what
:func:netimps.resolve_wire actually sends.
Always absolute -- the root terminator is present whether or not this
name carries a trailing dot, because there is no relative wire form.
So :meth:decode of the result equals :meth:fully_qualified, which is
this name only when it already is.
Source code in src/netimps/_fqdn/_name.py
703 704 705 706 707 708 709 710 711 712 713 714 715 716 | |
decode(data)
classmethod
The name in a buffer that holds exactly one, in wire form.
:raises DNSDecodeError: for a malformed name, a compression loop, or
bytes left over after the name. It is a ValueError.
Source code in src/netimps/_fqdn/_name.py
721 722 723 724 725 726 727 728 729 730 731 732 | |
decode_at(data, offset)
classmethod
The name starting at offset in a DNS message, and where it ends.
Follows compression pointers, with loop detection. The returned offset is the first byte after the name in the record -- just past the two-byte pointer when the name was compressed -- so the caller can carry on parsing from it. The name is always fully qualified.
:raises DNSDecodeError: for an offset outside the message, a name that
runs past it, a label over 63 octets, a name over 255, a compression
loop, the root alone, or a label no :class:FQDN can hold (a
non-printable byte or a dot). It is a ValueError.
Source code in src/netimps/_fqdn/_name.py
734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 | |
Backoff(delay=0.5, *, multiplier=2.0, max_delay=30.0, jitter=0.0, jitter_seconds=None, symmetric=False)
A retransmission timer: grows on loss, resets on progress.
:func:backoff_delays is a one-shot schedule for "retry this call a few
times". A long-lived session needs the other shape -- a current delay that
advances when a reply does not come and goes back to the base when the peer
moves the transfer forward. The loop it replaces is hand-written in every
protocol client::
timer = Backoff(delay=timeout, max_delay=timeout * 8, jitter=0)
while not done:
deadline = now() + timer.delay
if replied:
timer.reset() # progress: fresh timer
elif timer.attempt >= retries:
raise TransferTimeout(...)
else:
timer.advance() # loss: back off
delay is stable between calls to :meth:advance or :meth:reset,
so arming a deadline, logging it and comparing against it all see one value;
a property that re-jittered on each read would be a trap here.
Two guard rails a hand-rolled version tends to miss, both refused with
:class:ValueError when the timer is built: a multiplier below 1,
which would shrink the wait on loss, and a max_delay below delay,
which would truncate the first wait. :func:backoff_delays and
:func:retry refuse the same arguments.
Jitter is off by default here, the opposite of
:func:backoff_delays. Its purpose is to desynchronise many clients
retrying together; a point-to-point session retransmitting to one peer has
no herd to avoid, and TFTP and TCP both specify plain doubling. Set
jitter, jitter_seconds or symmetric for the protocols that do
ask for it -- the arguments mean exactly what they mean on
:func:backoff_delays.
Source code in src/netimps/_retry.py
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | |
delay
property
The wait for the current attempt. Stable until advance/reset.
attempt
property
How many times :meth:advance has been called since the last reset.
advance()
Back off one step and return the new :attr:delay.
Source code in src/netimps/_retry.py
319 320 321 322 323 324 | |
reset()
Return to the base delay -- the peer made progress.
Source code in src/netimps/_retry.py
326 327 328 329 330 331 | |
Datagram
Bases: NamedTuple
One received datagram and where it came from.
Attributes:
| Name | Type | Description |
|---|---|---|
data |
bytes
|
the payload. |
sender |
'SocketAddress'
|
|
destination |
'Optional[IPAddress]'
|
the address the datagram was sent to, or |
interface_index |
int
|
receiving interface index, or |
interface |
'Optional[Interface]'
|
the resolved :class: |
control_truncated |
bool
|
the kernel had more ancillary data than the buffer
held ( |
truncated |
bool
|
the payload did not fit |
is_unicast
property
Whether the datagram was addressed to one host: not a broadcast, a multicast group or the wildcard.
None when destination is unknown -- there was no pktinfo -- since
a guess in either direction would make a server answer, or ignore, the
wrong packets. The subnet broadcast is judged against the arrival
interface, so this costs no enumeration when that is set; see
:func:netimps.is_unicast.
reply_address
property
sender, in the family a :meth:UDPEndpoint.reply_socket will use.
Use this, not sender, to answer a datagram. On a dual-stack
AF_INET6 listener a v4 client's sender is the v6 4-tuple
('::ffff:127.0.0.1', port, 0, 0), while reply_socket correctly
hands back an AF_INET socket -- so reply.sendto(answer,
packet.sender) raises TypeError: AF_INET address must be a pair
(host, port). reply_address is the sender already in the family
the library chose, so the caller does not unmap it::
with endpoint.reply_socket(packet) as reply:
reply.sendto(answer, packet.reply_address)
A v4-mapped sender becomes the plain (host, port) pair; everything
else is returned unchanged, so this is the right thing to pass on a
single-family listener too.
UDPEndpoint(sock, *, pktinfo=True, interfaces=())
Bases: _SendMixin, _ReplyMixin, _AdmitMixin
A UDP socket that can report which interface each datagram arrived on.
::
endpoint = UDPEndpoint(netimps.bind("", 67, broadcast=True))
while True:
packet = endpoint.recv(2048)
if packet.interface is not None:
reply_on(packet.interface, packet.data)
Wraps rather than subclasses socket.socket: the raw socket stays
reachable as :attr:socket for anything this does not cover.
:param sock: an already-bound UDP socket -- build it with
:func:netimps.bind. Its family decides which pktinfo option is
used; an AF_INET6 socket gets the v6 one, including when it is
dual-stack.
:param pktinfo: request arrival-interface data. True (the default)
enables it where supported and is a no-op elsewhere. This governs
receiving only -- :meth:send's src needs no socket option.
:param interfaces: the interfaces this endpoint serves, each an
:class:Interface with an index; empty (the default) serves all.
:meth:admits tests a datagram against them; receiving never filters.
Two flags report what this socket can actually do, so a caller never has to infer it from an empty result:
:ivar has_pktinfo: recv will report the arrival interface. This
is False -- not an optimistic True -- whenever the option for
this socket's family is missing or refused.
:ivar has_src_pinning: :meth:send can honour src. False
where the platform exports no pktinfo cmsg for this family, and for an
IPv4 endpoint on FreeBSD that is bound to an address (the kernel pins
only on a wildcard-bound socket); src is then ignored, and the
kernel picks the source as it always would.
Source code in src/netimps/_udp/_endpoint.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
recv(bufsize=65535, *, resolve_interface=True)
Receive one datagram.
:param resolve_interface: look the arrival index up in
:func:netimps.get_interfaces to populate .interface. Pass
False in a hot loop and use .interface_index directly --
enumeration is not free.
When pktinfo is unavailable this still works; the interface fields are
simply empty. When it is available -- :attr:has_pktinfo --
the interface fields are filled for both address families, and
.control_truncated says whether anything was dropped for want of
buffer space.
A timeout set on the socket raises the builtin :class:TimeoutError
on every supported Python.
Source code in src/netimps/_udp/_endpoint.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
arecv(bufsize=65535, *, resolve_interface=True)
async
:meth:recv, awaited. Same arguments, same :class:Datagram.
Works on every asyncio loop, including the Windows default
ProactorEventLoop, which has no add_reader and whose
recvfrom would discard the ancillary data this class exists for::
async def serve(endpoint):
while True:
packet = await endpoint.arecv()
with endpoint.reply_socket(packet) as reply:
reply.sendto(answer(packet), packet.reply_address)
The read itself happens on the loop, not in a helper thread, so
bufsize stays a per-call argument and
:attr:Datagram.truncated keeps meaning what it means. Only the
readability notification is platform-specific -- add_reader where the
loop has it, a thread where it does not. See
:class:netimps._udp._notifier.ReadNotifier.
One waiter at a time: this is a receive loop's method, and two coroutines awaiting the same endpoint would race for the same datagram regardless of how the waiting were arranged.
Every call suspends once, datagram waiting or not, so a loop that reads as fast as datagrams arrive still lets other tasks run.
The socket's timeout is left as it is: the read is made non-blocking
only for the duration of one call, so a synchronous :meth:recv on the
same endpoint is unaffected.
:raises RuntimeError: if the endpoint is closed, or is closed while this is waiting.
Source code in src/netimps/_udp/_endpoint.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 | |
asend(data, dst, port, *, src=None)
async
:meth:send, awaited. Same arguments, same return value.
The datagram is handed to the kernel without blocking; when the send
buffer is full this waits (without blocking the loop) until the socket
is writable and tries again. The socket's timeout is left as it was
found, as for :meth:arecv.
:raises RuntimeError: if the endpoint is closed.
Source code in src/netimps/_udp/_endpoint.py
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | |
datagrams(bufsize=65535, *, resolve_interface=True, on_error=None)
async
Yield datagrams until the endpoint is closed -- async for sugar.
::
async for packet in endpoint.datagrams():
...
Stops cleanly on :meth:close. By default any other error propagates and
ends the loop, because a receive loop that swallows them is how a dead
server looks healthy.
:param on_error: called with the exception when a receive fails. Return true to carry on with the next datagram, false (or raise) to stop with that error. The caller decides, so log or count there; nothing is dropped silently. Not called for the error a close causes.
Source code in src/netimps/_udp/_endpoint.py
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 | |
close()
Close the wrapped socket, and stop the async notifier if one was started.
The notifier goes first: its thread selects on this socket, and closing
the socket underneath it would turn an orderly shutdown into a caught
OSError. Complete on return, and harmless when called again. From a
coroutine use :meth:aclose, which does not block the loop.
A task awaiting :meth:arecv is woken with :class:RuntimeError, and
:meth:datagrams finishes.
Source code in src/netimps/_udp/_endpoint.py
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 | |
aclose()
async
:meth:close, awaited: the loop keeps running while the notifier's
thread leaves.
Complete on return -- the thread is gone and the socket is closed --
and harmless when called again, or after :meth:close. Use it from a
coroutine; :meth:close would block the loop for as long as the thread
takes to stop.
Source code in src/netimps/_udp/_endpoint.py
470 471 472 473 474 475 476 477 478 479 480 481 482 483 | |
ListenAddress
Bases: NamedTuple
One socket a specification names: an address, a port and the interfaces limiting it.
interfaces is empty for a socket that is not limited; otherwise address
is the wildcard of the socket's family.
SocketOption
Bases: NamedTuple
One setsockopt triple, for :func:bind's options=.
bind accepts bare (level, name, value) tuples as well -- this only
gives the triple a name, so a caller building a list of them reads as
something other than Iterable[Tuple[int, int, Any]]::
bind("", 67, options=[SocketOption(SOL_SOCKET, SO_RCVBUF, 1 << 20)])
A :class:typing.NamedTuple, so it is a tuple: plain tuples are
accepted and these unpack like any other.
Route(dst, *, src=None, gateway=None, interface_index=0, on_link=None)
How traffic to a destination leaves this host.
Attributes:
| Name | Type | Description |
|---|---|---|
dst |
'Union[str, IPAddress]'
|
the destination this route was computed for. |
src |
'Optional[IPAddress]'
|
local address the kernel would use (see :func: |
gateway |
'Optional[IPAddress]'
|
next-hop router, or |
interface_index |
int
|
index of the outgoing interface, |
on_link |
'Optional[bool]'
|
|
on_link is three-state deliberately. gateway is None would turn "we
never looked" into a confident True: on macOS, where the lookup has no
source to read, get_route('1.1.1.1') from a 192.168.64.3/24 host
would report on_link=True. None is falsy, so if route.on_link:
takes the safe branch, and route.on_link is True asks the exact
question.
Source code in src/netimps/_sockets/_route.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
on_link
property
True without a router, False with one, None if unknown.
A gateway is proof on its own, so it wins over whatever was recorded. Its absence proves nothing by itself, which is why the constructor takes the flag separately.
collapse(networks)
Merge an iterable of networks into the smallest equivalent list.
Adjacent and overlapping networks are combined; the result is sorted and covers exactly the same addresses::
collapse(["10.0.0.0/25", "10.0.0.128/25"]) # [IPv4Network('10.0.0.0/24')]
collapse(["10.0.0.0/24", "10.0.0.8/29"]) # [IPv4Network('10.0.0.0/24')]
Accepts anything :func:parse does, mixed v4 and v6 -- the families are
collapsed independently and returned v4 first. Raises :class:ValueError
on malformed input.
Source code in src/netimps/_ip/_cidr.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
get_hostname(*, fqdn=False)
This machine's host name, asked for when called.
:func:platform.node by default. With fqdn=True,
:func:socket.getfqdn, which may consult the resolver and so can
block; where no qualified name is known it returns the bare one.
Nothing is read at import: on Windows :func:platform.node is a WMI
query, which is too much to charge every import netimps.
Source code in src/netimps/_ip/_host.py
539 540 541 542 543 544 545 546 547 548 549 550 551 | |
is_link_scoped(ip)
True if ip is confined to link scope or narrower.
Covers loopback (127/8, ::1 -- host scope) and link-local
(169.254/16, fe80::/10 -- link scope), borrowing IPv6's scope
vocabulary for both families::
is_link_scoped(parse("127.0.0.1")) # True -- host scope
is_link_scoped(parse("169.254.1.1")) # True -- link scope
is_link_scoped(parse("10.0.0.5")) # False -- private, global scope
The shared practical property is that neither can usefully be routed off the local host or link, so proxying, forwarding or advertising such an address is always wrong. Keeping the definition in one place stops each caller from writing a subtly different version.
.. note::
This is not "is private". RFC 1918 ranges (10/8,
192.168/16) are globally scoped and routable within a site, so
they return False -- use ip.is_private for that question.
Source code in src/netimps/_ip/_classify.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
split_host(text, *, default_port=None)
Split "host:port" into (host, port), handling IPv6 brackets.
The parsing that looks trivial until IPv6 arrives, because a bare v6 address is full of colons::
split_host("example.com:8080") # ('example.com', 8080)
split_host("10.0.0.5") # ('10.0.0.5', None)
split_host("[::1]:8080") # ('::1', 8080)
split_host("::1") # ('::1', None) -- not port 1
split_host("example.com", 443) # ('example.com', 443)
The rule this implements: a bare IPv6 address must not be split on its
last colon, and only a bracketed one may carry a port. "::1" is the
address, never host "::" port 1 -- the mistake hand-rolled splitters
almost always make.
Brackets are stripped from the returned host, and a scope id is preserved
("[fe80::1%eth0]:80" -> ("fe80::1%eth0", 80)). A bracketed literal
with no port is the host alone: "[::1]" is ("::1", None).
default_port is used when no port is present.
A (host, port) pair is taken too, with port an integer or
None and default_port filling a None::
split_host(("example.com", None), default_port=69) # ('example.com', 69)
split_host(("[::1]", 80)) # ('::1', 80)
The host of a pair is split like any other, so a port written in both places is an error unless they agree.
text accepts the package's usual loose union, not only a str: an
address object, an :class:IPv4Interface/:class:IPv6Interface (its .ip
is used), a :class:Host or an :class:FQDN, as :func:join_host, the
inverse, does. A network, or a value that is not a host type at all
(None, an int, bytes), raises :class:TypeError.
Port text is ASCII digits, as RFC 3986 section 3.2.3 has it: "8_0",
"+80", a space and non-ASCII digits are refused. What sits inside
brackets has to be an IPv6 literal (section 3.2.2).
Raises :class:NetimpsValueError on empty input, an unclosed bracket,
brackets around anything but an IPv6 address, a port that is not ASCII digits
in 0-65535, or an unbracketed string with two or more colons that is not
a valid IPv6 address ("host:80:extra", a half-typed address).
Source code in src/netimps/_ip/_hosttext.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
split_zone(text)
Split an IPv6 %zone suffix off a host: (host, zone).
::
split_zone("fe80::1%eth0") # ('fe80::1', 'eth0')
split_zone("fe80::1%12") # ('fe80::1', '12')
split_zone("10.0.0.5") # ('10.0.0.5', None)
split_zone("example.com") # ('example.com', None)
Use it before :func:try_parse or an address comparison: ipaddress keeps
the zone as part of the address, so fe80::1%eth0 is not equal to
fe80::1 and matches nothing in :func:get_interfaces. The zone names an
interface, which is why it is returned rather than thrown away. Brackets are
:func:split_host's business, not this function's.
text takes the package's loose host union. Raises :class:TypeError
for a value that is not a host type, and :class:NetimpsValueError for a
% with nothing after it.
Source code in src/netimps/_ip/_hosttext.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
join_host(host, port=None)
Build "host:port" from its parts -- the inverse of :func:split_host.
The direction everyone writes by hand and gets wrong on IPv6, because a bare v6 address is full of colons and must be bracketed before a port can be appended::
join_host("example.com", 8080) # 'example.com:8080'
join_host("10.0.0.5", 8080) # '10.0.0.5:8080'
join_host("::1", 8080) # '[::1]:8080' -- not '::1:8080'
join_host("fe80::1%eth0", 80) # '[fe80::1%eth0]:80'
join_host("::1") # '::1' -- no port, no brackets
join_host("example.com") # 'example.com'
host accepts a string, an :class:IPv4Address/:class:IPv6Address, an
:class:IPv4Interface/:class:IPv6Interface (its .ip is used) or an
:class:FQDN. An already-bracketed string is accepted and not
double-bracketed.
Only an IPv6 literal is bracketed. A hostname never is, however many colons someone has put in it -- brackets in a URI authority mean "the thing inside is an address", so bracketing a name would produce something no resolver will accept.
Brackets are added when a port is present or absent, following the same
rule: a lone "::1" needs none, since there is no colon to disambiguate
from. That is what makes split_host(join_host(h, p)) == (h, p) hold.
:raises TypeError: for a network, a value that is not a host type, or a
port that is not an int (a bool is not one).
:raises NetimpsValueError: for an empty host, brackets around anything but
an IPv6 address, or a port outside 0-65535.
Source code in src/netimps/_ip/_hosttext.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
format_address(address)
The text of an address, the same on every supported Python.
str() of a v4-mapped IPv6 address is the hex form (::ffff:102:304)
before Python 3.13 and the mixed form (::ffff:1.2.3.4, RFC 5952 section
5) from it. This writes the mixed form on every interpreter and, for any
other address, what str() writes. A %zone is kept and the family
never changes (unmap is the function that changes it); an interface
keeps its /prefix.
:raises TypeError: for anything that is not an address or interface object: text is parsed first, and a network names no one address.
Source code in src/netimps/_ip/_types.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
unmap(value)
Collapse an IPv4-mapped IPv6 address to plain IPv4; pass anything else through.
::ffff:10.0.0.5 is how a dual-stack socket reports an IPv4 peer, and
almost nothing a caller does wants it in that form -- an ACL comparing against
10.0.0.0/8, a log line, a config lookup::
unmap("::ffff:10.0.0.5") # IPv4Address('10.0.0.5')
unmap("10.0.0.5") # IPv4Address('10.0.0.5') -- unchanged
unmap("2001:db8::1") # IPv6Address('2001:db8::1') -- unchanged
Built on :attr:ipaddress.IPv6Address.ipv4_mapped rather than stripping a
"::ffff:" prefix from the text. The string version looks equivalent and
is not -- measured, these are all genuinely mapped addresses that a
startswith("::ffff:") and "." in text test leaves untouched:
=========================== ========== ============================
input this gives the string test gives
=========================== ========== ============================
::FFFF:10.0.0.5 10.0.0.5 unchanged (wrong case)
::ffff:0:1 0.0.0.1 unchanged (no dot)
0:0:0:0:0:ffff:0a00:0005 10.0.0.5 unchanged (expanded form)
=========================== ========== ============================
One address has many spellings, and only the parsed form sees through them.
:raises NetimpsValueError: for text that is no address. :raises TypeError: for a value that is no address type.
Source code in src/netimps/_ip/_classify.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
is_wildcard(value)
Whether value means "every local address" -- the bind-anything form.
True for "", None, "0.0.0.0", "::" and any other spelling
whose address form is unspecified ("::0", "0000::0"), and for the
v4-mapped ::ffff:0.0.0.0::
is_wildcard("") # True -- what bind("") means
is_wildcard("0.0.0.0") # True
is_wildcard("::") # True
is_wildcard("127.0.0.1") # False
A %zone suffix does not change whether the address is unspecified.
Text that is no address is a host name, and a name is never the wildcard:
the answer is False. This is the one classifier that takes a name,
because it is asked of a listen host, and bind takes a name there. A
:class:Host or an :class:FQDN is read as the text it holds.
:raises TypeError: for a value that is neither text nor an address.
Source code in src/netimps/_ip/_classify.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
subtract(networks, remove)
Return networks minus every address in remove.
The set difference :mod:ipaddress leaves out -- it ships
collapse_addresses but nothing to punch holes::
subtract(["10.0.0.0/24"], ["10.0.0.64/26"])
# [IPv4Network('10.0.0.0/26'), IPv4Network('10.0.0.128/25')]
subtract(["0.0.0.0/0"], ["10.0.0.0/8", "192.168.0.0/16"]) # public v4
The result is collapsed, so it is the minimal set of networks covering
what is left. Removing something absent is a no-op, and removing a
superset yields []. Mixed families are handled independently: an IPv6
exclusion never affects IPv4 output.
Source code in src/netimps/_ip/_cidr.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
classify(text)
Read text as whichever of a MAC, a network, an interface or an
address it spells, and return that value.
The order, which decides text that could be read two ways:
- a :class:
MACAddress(any spelling it accepts); - text with a
/prefix length: an :data:IPNetworkwhen it has no host bits set ("10.0.0.0/24","10.0.0.5/32"), otherwise an :data:IPInterface("10.0.0.5/24"); - an :data:
IPAddress("10.0.0.5","fe80::1%eth0").
So a bare address is never read as a /32 network, and an address with a
prefix is never an address. A name is none of these: classify never
asks a resolver, so classify("example.com") raises. Resolve it with
:meth:Host.resolve first.
Raises :class:NetimpsValueError for text that is none of them, and
:class:TypeError for a non-str.
Source code in src/netimps/_parse.py
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 | |
is_valid(value, type=IPAddress, *, strict=None, **options)
is_valid(
value: object,
type: TypeForm[_T],
*,
strict: Optional[bool] = ...,
**options: object
) -> bool
is_valid(
value: object,
type: Callable[..., _T],
*,
strict: Optional[bool] = ...,
**options: object
) -> bool
is_valid(value: object) -> bool
Return True if value parses as type.
A rejected value is False, never an exception; an unusable type or
option raises TypeError, as in :func:try_parse.
Accepts the same type forms as :func:try_parse -- a type, a union
alias, or any callable::
is_valid("10.0.0.5", IPAddress) # True (the type alias)
is_valid("10.0.0.0/24", IPNetwork) # True
is_valid("aa:bb:cc:dd:ee:ff", MACAddress)
is_valid("nonsense", IPAddress) # False
When you want the parsed value too, use :func:try_parse instead of
calling this first -- one call, no double work. Same exception policy: only
ValueError/TypeError count as "invalid".
.. note::
A parser that legitimately returns None for valid input still counts
as valid here -- the parse succeeded. That is why this delegates via a
sentinel rather than testing try_parse(...) is not None, which cannot
tell "returned None" from "rejected the input".
Source code in src/netimps/_parse.py
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | |
parse(value, type=IPAddress, *, strict=None, **options)
parse(
value: object,
type: TypeForm[_T],
*,
strict: Optional[bool] = ...,
**options: object
) -> _T
parse(
value: object,
type: Callable[..., _T],
*,
strict: Optional[bool] = ...,
**options: object
) -> _T
parse(value: object) -> IPAddress
Build type from value, raising on bad input.
The single parsing entry point. type is a result type -- one of the
:data:IPAddress/:data:IPInterface/:data:IPNetwork unions, a concrete
IPv4Address &co, or any callable::
parse("10.0.0.5") # IPv4Address (the default)
parse("10.0.0.5/24", IPInterface) # IPv4Interface
parse("10.0.0.5/24", IPNetwork) # IPv4Network('10.0.0.0/24')
parse("10.0.0.5/24", IPNetwork, strict=True) # raises: host bits set
parse("aa:bb:cc:dd:ee:ff", MACAddress) # MACAddress
Every type accepts the full range of stdlib inputs -- str, int,
packed bytes, or an existing object -- because the builders are the
ipaddress.ip_* functions rather than the concrete constructors.
A union accepts either family; a concrete type enforces its own, so
parse("::1", IPv4Address) raises rather than quietly returning an
IPv6Address.
Networks are parsed non-strict by default (unlike the stdlib), so a host
address with a prefix normalises to its network instead of raising.
strict is the network builders' option and is passed only when given;
any other keyword passes through to the underlying builder.
Raises :class:NetimpsValueError (a :class:ValueError) on malformed input
or a family mismatch -- including the ipaddress builders' own errors --
and :class:TypeError for an unusable type. A callable type that is
not one of the package's builders raises whatever it raises. Use
:func:try_parse for the non-raising form.
Source code in src/netimps/_parse.py
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | |
try_parse(value, type=IPAddress, *, default=None, strict=None, **options)
try_parse(
value: object,
type: TypeForm[_T],
*,
default: None = ...,
strict: Optional[bool] = ...,
**options: object
) -> Optional[_T]
try_parse(
value: object,
type: TypeForm[_T],
*,
default: _D,
strict: Optional[bool] = ...,
**options: object
) -> Union[_T, _D]
try_parse(
value: object,
type: Callable[..., _T],
*,
default: None = ...,
strict: Optional[bool] = ...,
**options: object
) -> Optional[_T]
try_parse(
value: object,
type: Callable[..., _T],
*,
default: _D,
strict: Optional[bool] = ...,
**options: object
) -> Union[_T, _D]
try_parse(
value: object, *, default: None = ...
) -> Optional[IPAddress]
try_parse(
value: object, *, default: _D
) -> Union[IPAddress, _D]
Return type(value), or default if it rejects the input.
A rejected value never raises; an unusable type or option does.
The one non-raising parse for the whole package. type is either a
type -- including the union aliases, which are not themselves callable
-- or any callable that signals bad input with ValueError/TypeError::
try_parse("10.0.0.5", IPAddress) # IPv4Address('10.0.0.5')
try_parse("10.0.0.5", IPv4Address) # concrete: v6 input rejected
try_parse("nonsense", IPAddress) # None
try_parse(user_input, MACAddress) or DEFAULT_MAC
try_parse(raw, IPAddress, default=LOCALHOST) # explicit fallback
The union aliases IPAddress/IPInterface/IPNetwork accept either
family. A concrete type stays strict, so asking for one family and
getting the other is impossible::
try_parse("::1", IPAddress) # IPv6Address('::1') -- either family
try_parse("::1", IPv4Address) # None -- v4 was asked for
try_parse("10.0.0.5", IPv4Address) # IPv4Address('10.0.0.5')
Prefer this to is_valid followed by a parse: that pattern does the work
twice and leaves a window where the two disagree.
Generic in the type: try_parse(x, MACAddress) is typed
Optional[MACAddress], so a checker knows the result without a cast.
Only ValueError and TypeError are swallowed -- the two exceptions
that mean "bad input". Anything else (an OSError from a builder that
touches the network, a bug in it) propagates, because turning it
into None would disguise a real failure as a rejected value. A
type that is neither callable nor a known type raises TypeError:
that is a caller bug, not a rejected value.
:param default: returned instead of None when the input is rejected.
Also the seam :func:is_valid uses -- passing a sentinel is the only
way to tell "the parse returned None" from "it rejected the input".
Source code in src/netimps/_parse.py
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | |
get_default_port(scheme)
Return the conventional port for a URL scheme, or None if unknown.
Checks the built-in/registered table first, then falls back to the system
services database via :func:socket.getservbyname::
get_default_port("https") # 443
get_default_port("socks5") # 1080 (absent from /etc/services)
get_default_port("443") # 443 (a port number as text)
get_default_port("nope") # None
The database is asked for TCP first, then UDP, so a name carrying both answers with its TCP port on every platform rather than with whichever entry that host's database lists first.
Text made of ASCII digits is a port, not a scheme: it comes back as an
int, so a command line or a config file can hand over either spelling.
A number outside 0-65535 raises :class:NetimpsValueError; it is
neither a port nor a scheme. Any other spelling of a number ("+80",
"8_0", surrounding spaces) is looked up as a name and is unknown.
:raises TypeError: for a scheme that is not text.
Case-insensitive. Extend the table with :func:register_port.
Source code in src/netimps/_scheme.py
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | |
get_default_scheme(port)
Return the conventional scheme for a port, or None if unknown.
The inverse of :func:get_default_port::
get_default_scheme(443) # 'https'
get_default_scheme(1080) # 'socks' (canonical, not an alias)
get_default_scheme(9999) # None
Falls back to the system services database via
:func:socket.getservbyport, asked for TCP first, then UDP: port 514
is shell/cmd over TCP and syslog over UDP, and a
protocol-less lookup picks between them per platform. Where several
schemes share a port, the canonical one is returned -- see
:func:register_port.
An out-of-range port is None rather than an error: this is a table
lookup, not a socket operation, so "no such entry" is the honest answer.
The port-taking network helpers validate instead, via
:func:coerce_port.
Source code in src/netimps/_scheme.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | |
register_port(scheme, port, *, canonical=False)
Register (or override) a scheme's conventional port.
The built-in table covers the common cases; a protocol of the caller's own is added here::
register_port("myproto", 9999)
get_default_port("myproto") # 9999
get_default_scheme(9999) # 'myproto'
Re-registering a scheme moves it: the port it occupied before the call stops mapping back to it, or the registry would contradict itself::
register_port("myproto", 8888)
get_default_port("myproto") # 8888
get_default_scheme(9999) # None, not 'myproto'
If another scheme is still registered on the vacated port, it inherits the slot (earliest registration first, the same rule the initial index uses).
:param scheme: scheme name; matched case-insensitively.
:param port: TCP/UDP port number, 0-65535.
:param canonical: make scheme the name :func:get_default_scheme returns for
port, displacing any existing one. By default the first registration
for a port keeps that slot, so adding an alias does not silently change
what an existing port maps back to.
Raises :class:ValueError on an out-of-range port or empty scheme, and
:class:TypeError on a non-int port.
Source code in src/netimps/_scheme.py
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
clear_interface_cache()
Drop any cached enumeration, so the next cached call re-enumerates.
For a caller that knows the adapter set changed -- it bound a socket,
watched netlink, or handled WM_NETWORKCHANGE -- and should not wait out
the TTL. Harmless when nothing is cached.
Source code in src/netimps/_ifaddrs/_cache.py
49 50 51 52 53 54 55 56 57 58 59 | |
interface_enumerations()
How many times this process has really enumerated its adapters.
Monotonic, and counts only the syscall, never a cached hit -- which is
the point: with cache= a lookup and an enumeration stop being the same
event, and the enumeration is the one a packet flood multiplies. A test
reads it either side of the code under test::
before = interface_enumerations()
for _ in range(20):
get_interface(address, cache=True)
assert interface_enumerations() - before == 1
It is also worth exporting as a metric: how often a long-running server re-reads its adapters answers whether its cache is sized right.
Both raw=True and raw=False count into this one total. The cache is
keyed by raw, so a process using both pays two enumerations to warm up
and will see this advance twice.
:func:clear_interface_cache does not advance it -- dropping a cache
enumerates nothing by itself; the next cached call is what pays.
Source code in src/netimps/_ifaddrs/_cache.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |
get_interface(query=_NO_QUERY, *, index=None, strict=True, cache=False)
Return the first local interface matching query, or None.
The reverse of interface enumeration -- "a socket is bound here, which adapter is that?"::
get_interface(sock.getsockname()[0])
Accepts the same query forms as :func:iter_interfaces, an adapter name and
index= among them (get_interface(iface.name),
get_interface(index=iface.index)); singular lookup is exactly the first
plural result. Since addresses can appear on more than one adapter
(especially unscoped IPv6 link-local addresses) and so can a MAC (a teamed
or virtual adapter), this returns the first in enumeration order: use the
plural form when every match matters.
:param strict: when True (the default), a miss returns None. When
False, an address or IPInterface miss produces a synthetic
single-address Interface so a caller can still attribute traffic.
Network and MAC misses cannot be synthesized honestly and remain
None.
:param cache: reuse a recent enumeration rather than making the syscall --
True for :data:netimps.INTERFACE_CACHE_TTL seconds, or a number
for that TTL. This is the argument a per-packet caller wants. Each
call otherwise enumerates every adapter, measured at 35-42 ms on a host
with many of them.
cache=0 is a TTL of zero, so it enumerates and reseeds -- which is
the whole of "force a refresh". :func:netimps.clear_interface_cache
invalidates without a lookup.
The synthetic interface is named "<unknown>" and carries a host route
(/32 or /128), matching how degraded enumeration reports itself.
Source code in src/netimps/_ifaddrs/_lookup.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
get_interfaces(*, raw=False, cache=False)
Return this host's network interfaces.
Uses getifaddrs(3) on POSIX and GetAdaptersAddresses on Windows via
:mod:ctypes -- no third-party dependency -- so adapter names, MACs and
real prefix lengths are all available::
for iface in get_interfaces():
print(iface.name, iface.mac, [str(ip) for ip in iface.ips])
:param raw: when True, populate :attr:Interface.raw with the untouched
platform data (Linux/BSD flags; Windows adapter guid,
if_type, ...). Not portable -- outside the stability guarantee.
:param cache: reuse a recent enumeration instead of making the syscall.
False (the default) never caches and never reads a cached value, so
every call enumerates afresh. True uses
:data:INTERFACE_CACHE_TTL seconds, and a number is that TTL in
seconds -- so cache=0 enumerates now and reseeds the cache, a
TTL of zero being always stale. That is the only "force a refresh"
anyone needs, which is why there is no second argument for it;
:func:clear_interface_cache covers invalidating without a lookup.
The cache is process-wide and shared with
:func:`netimps.get_interface`, :func:`netimps.iter_interfaces` and
:func:`netimps.is_local_address`, which all take the same argument.
Opt-in on purpose. Enumeration is a syscall, and on a host with many adapters a measured 35-42 ms of one, so a per-packet caller needs a cache; but an adapter set changes under you, and silently answering from a stale snapshot by default would turn a cheap call into a wrong one. The caller knows which it wants.
Prefer an event to a TTL when you have one. A TTL is a guess about how long the answer stays true; if your program already knows the moment it can change, say so instead::
get_interfaces(cache=math.inf) # never expires on its own
...
clear_interface_cache() # at the moment it can change
That is strictly better than any TTL: no window of wrong answers, and no re-enumeration while nothing has changed. A server binding its sockets has exactly such a moment, since binding is when the set of addresses it serves can change.
A TTL (cache=True, or a number) is for the caller with no such moment --
a loop making many calls in a row that just wants to stop paying for every
one, and can tolerate :data:INTERFACE_CACHE_TTL of staleness.
Enumerating per datagram is not merely slow: at 35-42 ms it is slow enough that a packet flood can deny service on its own.
A cached call returns the same immutable objects, in a fresh list.
Interface cannot change after construction, ips is a tuple and
raw a read-only mapping, so one caller cannot corrupt another's view.
:func:clear_interface_cache also discards the result of an enumeration
that was already running when it was called.
Does not raise when the platform will not answer (an OSError from the
native call): it degrades to a hostname-resolution fallback in which
prefixes are not real (every address becomes a /32//128 under an
interface named "<unknown>", with the reason in raw and logged once
at debug). A degraded result is cached like any other -- it is the honest
answer for as long as the native call keeps failing. Any other exception is
a defect and propagates.
Source code in src/netimps/_ifaddrs/_cache.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | |
is_broadcast(address, interface=None, *, cache=False)
Whether address is an IPv4 broadcast address, limited or subnet.
The question a wildcard-bound UDP server asks about
:attr:netimps.Datagram.destination before answering: RFC 1123 says a TFTP
server must ignore a broadcast request, and DHCP has to tell a broadcast
DISCOVER from a unicast RENEW.
Pass interface whenever the caller has it -- a
:attr:netimps.Datagram.interface, say. Without it this enumerates every
adapter, measured at 1.25 ms against 0.004 ms, and a server asking the
question of every request pays that per packet. cache= is the fallback
for when the interface is genuinely unknown; it means what it means on
:func:get_interfaces.
255.255.255.255 (limited broadcast) needs no context and short-circuits
before any of that. The subnet broadcast does: 10.0.0.255 is only a
broadcast if some interface carries 10.0.0.0/24, so this consults
interface prefixes -- which is why it lives
here and not in :mod:netimps._ip. Pass interface to check one adapter
(the arrival interface, from Datagram.interface); omit it to check every
local one, which costs an enumeration.
A v4-mapped address is unmapped first, because a dual-stack listener
reports an IPv4 arrival as ::ffff:a.b.c.d and the broadcast question is
about the v4 address inside.
IPv6 has no broadcast -- it uses multicast instead -- so a genuine v6
address is always False here. :func:netimps.is_multicast is the
companion predicate, kept separate on purpose: "do not answer this" is
usually is_broadcast(a, i) or is_multicast(a), and one name meaning both
would hide which of the two it matched.
:raises NetimpsValueError: for text that is no address. :raises TypeError: for a network or a value of another type.
Source code in src/netimps/_ifaddrs/_addresses.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
is_local_address(address, *, cache=False)
Return whether address is loopback or assigned on this host.
This is deliberately narrower than private, link-local, on-link, routable
or reachable: those properties do not mean an address belongs to this
machine. Text that is no address raises :class:NetimpsValueError, and a
network or a value of another type :class:TypeError.
A %zone suffix is honoured rather than rejected (see
:func:iter_interfaces), so the address getsockname() hands back can be
passed straight in.
:param cache: reuse a recent enumeration -- see :func:get_interface. A
loopback address short-circuits before any enumeration, so the cache
only matters for the addresses that actually reach the adapter scan.
Source code in src/netimps/_ifaddrs/_lookup.py
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | |
is_local_host(host, *, resolve=False, cache=False)
Whether host names this machine.
::
is_local_host("localhost") # True
is_local_host("127.0.0.1") # True
is_local_host("[::1]:22") # True -- a port is ignored
is_local_host("10.0.0.5") # True only if an interface holds it
is_local_host("example.com") # False, without asking the resolver
True for a literal that :func:is_local_address accepts (loopback, or
assigned to an interface; a %zone is ignored, and a v4-mapped address is
judged as the v4 address inside; the short and numeric IPv4 spellings that
inet_aton reads, such as 127.1 and 2130706433, are literals), for localhost and any *.localhost
(RFC 6761), and for this machine's own host name, compared without case and
without a trailing dot.
Any other name is not resolved unless resolve=True: the answer then
comes from the OS resolver (the hosts file included) and is true when any
address it returns is local, and the fully qualified name of this machine
counts as well. That can block on the network, which is why it is opt-in.
cache is :func:get_interfaces's, and matters only for the literals that
reach the adapter scan. Never raises: text that is not a host, an empty one,
or a name that does not resolve is simply not local.
Source code in src/netimps/_ifaddrs/_lookup.py
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | |
is_unicast(address, interface=None, *, cache=False)
Whether a datagram sent to address was meant for one host.
False for the wildcard, a multicast group, and a broadcast (limited or
subnet); true for everything else. This is the "answer it or ignore it"
question a DHCP or TFTP server asks of :attr:netimps.Datagram.destination,
which callers wrote as not (is_broadcast(...) or is_multicast(...)) plus
a wildcard test of their own.
interface and cache mean what they mean on :func:is_broadcast, the one
part that can enumerate. A v4-mapped address is judged as the v4 address
inside it.
:raises NetimpsValueError: for text that is no address. :raises TypeError: for a network or a value of another type.
Source code in src/netimps/_ifaddrs/_addresses.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
iter_addresses(interfaces=None, *, family=None, cache=False)
Yield (interface, address) once per address, not once per adapter.
:func:get_interfaces groups every address under its adapter, which is the
right shape for "describe this host". Callers that filter or act per
address -- picking a bind target, excluding link-local, matching a subnet
-- want the flattened view instead, and would otherwise write the same
nested loop each time::
for iface, addr in iter_addresses():
if addr.ip in some_network:
bind_to(addr.ip)
:param interfaces: reuse an existing enumeration instead of calling
:func:get_interfaces again. Worth passing in a loop, since
enumeration is a syscall.
:param family: 4 or AF_INET, 6 or AF_INET6 to yield only
that family; None for both. Anything else raises
:class:ValueError when this function is called, not on the first
next(): a generator that validates lazily reports a bad argument
from somewhere the traceback does not name the caller.
:param cache: when interfaces is not given, reuse a recent enumeration, as
:func:get_interfaces does. False (the default) enumerates when the
first address is asked for. Ignored when interfaces is given.
The interface is the full :class:Interface, so its name, MAC and MTU
stay reachable -- the flattening loses no information.
Source code in src/netimps/_ifaddrs/_addresses.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
iter_interfaces(query=_NO_QUERY, *, index=None, cache=False)
Yield every local interface matching query, in OS order.
query may be an :class:Interface (yielded directly), an address, an
IPInterface (matched by its exact .ip), an IPNetwork (matched
when it contains any assigned address), or a :class:MACAddress. Address-
like strings, integers and packed bytes are accepted too; a slash-bearing
string is interpreted as a network when it is not an address.
MAC text and 6-byte packed values are recognised after IP parsing.
Integer MACs must be wrapped in MACAddress because an integer is also
a valid IP-address representation. Text that is no address, network or MAC
is an adapter name; index= names an interface by its index instead
of a query (an int query stays an address). Invalid queries and
misses yield nothing. Each matching interface is yielded once even if
several of its assigned addresses fall within a requested network.
A %zone-qualified IPv6 address (fe80::1%15, fe80::1%eth0) is
matched on its bare address and filtered by the zone, which names the
adapter -- the index on Linux/Windows, the adapter name on BSD. That form
is what getsockname(), getaddrinfo and every OS tool emit, and
matching it on the bare address keeps an address the library just reported
from being denied as non-local. A zone naming an adapter that does not hold the
address yields nothing, which is the honest answer to a contradiction.
Source code in src/netimps/_ifaddrs/_lookup.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
clear_resolution_cache()
Drop every cached answer, so the next cache= lookup asks again.
For a caller that knows a record changed and should not wait out the TTL. Harmless when nothing is cached.
Source code in src/netimps/_dns/_cache.py
28 29 30 31 32 33 34 35 | |
has_dns()
Whether dnspython is installed, so :func:resolve_dnspython can run.
Without it :func:resolve still answers address and reverse records from
the other backends, and raises :class:ResolutionError naming the extra for
a record type only dnspython serves.
Source code in src/netimps/_dns/_dnspython.py
20 21 22 23 24 25 26 27 28 29 30 31 | |
resolve(query, rdtype=None, *, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None, strict=False, source=None, cache=False, deadline=None)
resolve(
query: "HostLike",
rdtype: "Literal['a', 'A']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
backends: "Optional[Union[str, List[str]]]" = None,
strict: bool = False,
source: Optional[Union[str, List[str]]] = None,
cache: "Union[bool, float]" = False,
deadline: Optional[float] = None
) -> "List[IPv4Address]"
resolve(
query: "HostLike",
rdtype: "Literal['aaaa', 'AAAA']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
backends: "Optional[Union[str, List[str]]]" = None,
strict: bool = False,
source: Optional[Union[str, List[str]]] = None,
cache: "Union[bool, float]" = False,
deadline: Optional[float] = None
) -> "List[IPv6Address]"
resolve(
query: "HostLike",
rdtype: "Literal['ptr', 'PTR']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
backends: "Optional[Union[str, List[str]]]" = None,
strict: bool = False,
source: Optional[Union[str, List[str]]] = None,
cache: "Union[bool, float]" = False,
deadline: Optional[float] = None
) -> "List[str]"
resolve(
query: "HostLike",
rdtype: "Optional[Union[str, Tuple[str, ...]]]" = None,
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
backends: "Optional[Union[str, List[str]]]" = None,
strict: bool = False,
source: Optional[Union[str, List[str]]] = None,
cache: "Union[bool, float]" = False,
deadline: Optional[float] = None
) -> "List[Any]"
Resolve query, trying each backend in backends until one gives
a definitive answer.
::
resolve("example.com") # ['93.184.216.34']
resolve("example.com", "aaaa")
resolve("example.com", "mx", ns="1.1.1.1")
resolve("host", backends="system") # OS resolver only
resolve("host", backends=["nslookup", "dnspython"]) # custom order
resolve("8.8.8.8") # rdtype=None -> ptr -> ['dns.google']
Default order is ["dnspython", "wire", "system", "nslookup"]:
dnspython first (structured records, full rdtype support, explicit
ns=/search= control), then the standard-library DNS client --
only for an explicit ns= or source=, so ns= works without
dnspython -- then the OS resolver (hosts file, NSS, OS cache -- address
and reverse records, but no ns= override), then nslookup as a last
resort if no Python-level path is usable.
A backend is skipped, not tried and failed, when it structurally
cannot serve the request: :func:resolve_system for a rdtype outside
"a"/"aaaa"/"ptr", or for an explicit ns=/port=/
tcp= (the OS resolver takes no per-call nameserver, port or transport,
so running it would silently ignore the caller's choice);
:func:resolve_dnspython if dnspython is not installed.
Only a non-empty answer stops the chain. An empty one does not, and
that is deliberate: the backends resolve by structurally different
mechanisms, and DNS's "no such name" says nothing about what the OS
resolver can still answer. dnspython gets NXDOMAIN for localhost
on Windows and macOS while :func:resolve_system answers it from the
hosts file; the same holds for .local/mDNS names and anything else
only an NSS source knows. A backend that could not even attempt the query
(missing binary, timeout, transport failure) falls through as well. If
every applicable backend answers empty, the result is [] -- and so it
is when every one of them fails to attempt, unless strict=True.
The cost is latency on a genuinely non-existent name: two or three backend
calls instead of one, the last of which may spawn nslookup. Narrow
backends to opt out -- backends="dnspython" keeps the single call.
:param query: the name (or address, for reverse types) to look up. Also
accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used), not just a string.
:param rdtype: DNS record type. None (default) auto-selects: "ptr"
when query is an address literal (an "a"/"aaaa" lookup
of an address makes no sense), "a" otherwise. Pass an explicit
rdtype
to opt out. "a"/"aaaa"/"ptr" reach every backend; other
types are dnspython-only (see :func:resolve_dnspython).
("a", "aaaa") asks for both families at once: the OS resolver
answers in one call, in the order it chose, and every other backend
is asked for "a" and then "aaaa" with the answers joined.
:param ns: nameserver(s) to query instead of the system resolver. Honoured
by dnspython and nslookup (a single nameserver for the
latter); excludes system from the chain, since it cannot honour a
per-call nameserver.
:param timeout: seconds for one backend attempt. What it bounds differs by
backend: for dnspython and wire the whole backend call, every
nameserver and retry included; for system and nslookup each
candidate name of a search list. A record-type pair is one attempt per
type.
:param deadline: seconds for the whole call: every backend, record type
and search candidate shares it, and each attempt gets timeout or
what is left, whichever is smaller. None (the default) sets no
overall limit. Once it has passed the remaining backends are not
started; the outcome is [], or with strict=True the
:class:ResolutionTimeoutError.
:param port: nameserver port; dnspython only, and excludes system
from the chain when it is not 53.
:param tcp: query over TCP; dnspython only, and excludes system
from the chain when true.
:param search: search-list behaviour, honoured by all three backends (see
:func:resolve_dnspython, :func:resolve_system and
:func:resolve_nslookup respectively). Only dnspython has this
built in; the other two try one candidate name per call instead.
:param backends: explicit backend order/subset, by name ("dnspython",
"system", "nslookup") or a single name as a plain string.
None (default) uses all three in the default order, each filtered
for applicability as described above.
:param source: the local address -- or one per address family, as a
list -- the queries go out from. Honoured by wire (each nameserver
gets the address of its family) and dnspython (one address only);
excludes system and nslookup, which cannot choose it.
:param cache: reuse a recent answer. False (the default) neither reads
nor writes the cache; True keeps an answer for
:data:RESOLUTION_CACHE_TTL seconds and a number is that many.
Keyed on the name and every option, so a different ns or
rdtype is a different entry. An empty answer is cached like any
other, so a name that does not resolve is asked once per TTL; an
outage (every backend failed to ask) and an exception are not cached.
Call :func:clear_resolution_cache when the answer is known to have
changed. Process-wide and thread-safe.
:param strict: raise instead of returning [] when no backend could
ask -- every applicable one failed with a :class:ResolutionError
(resolver unreachable, timeout, nslookup missing). Off by default,
so a resolver outage looks the same to the caller as a name that does
not exist. Pass strict=True when the two must be told apart: the
last backend's :class:ResolutionError is raised, with the reason in
its message. It does not turn an empty answer into an error --
a name that genuinely does not resolve still returns [].
Contract: always a list, empty when the backends answered that there
is no such name or record or, without strict, when none could be asked;
never None. strict=True is the only way this raises for a resolver
outage. A request only dnspython could serve raises
:class:ResolutionError naming the dns extra when it is missing, strict
or not. A malformed query or unknown record type raises
:class:ValueError immediately, without trying every backend, since that
is a caller bug rather than a resolution outcome.
Source code in src/netimps/_dns/_chain.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | |
resolve_dnspython(query, rdtype=None, *, ns=None, timeout=5.0, port=53, tcp=False, search=True, source=None)
resolve_dnspython(
query: "HostLike",
rdtype: "Literal['a', 'A']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[str] = None
) -> "List[IPv4Address]"
resolve_dnspython(
query: "HostLike",
rdtype: "Literal['aaaa', 'AAAA']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[str] = None
) -> "List[IPv6Address]"
resolve_dnspython(
query: "HostLike",
rdtype: "Literal['ptr', 'PTR']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[str] = None
) -> "List[str]"
resolve_dnspython(
query: "HostLike",
rdtype: Optional[str] = None,
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[str] = None
) -> "List[Any]"
Resolve query via dnspython and return the answers as a list.
::
resolve_dnspython("example.com") # ['93.184.216.34']
resolve_dnspython("example.com", "aaaa")
resolve_dnspython("example.com", "mx", ns="1.1.1.1")
resolve_dnspython("host") # tries the resolv.conf search list
resolve_dnspython("host", search=["eng.example.com", "example.com"])
resolve_dnspython("host", search=False) # look up "host" literally
resolve_dnspython("8.8.8.8") # rdtype=None -> ptr -> ['dns.google']
Contract: always a list, empty when the resolver answered that the
name or the record does not exist -- never None. Callers can therefore
write if result: and index result[0] safely. A resolver that could
not be asked is not an empty answer: see below.
Records come back as native types: address records (A/AAAA) are
:class:ipaddress objects, everything else is a str::
resolve_dnspython("example.com")[0].is_private # an IPv4Address, not "1.2.3.4"
resolve_dnspython("example.com", "mx") # ['10 mail.example.com']
resolve_dnspython("example.com", "txt") # ['v=spf1 -all'] -- unquoted
Names lose their trailing root dot and TXT strings lose their surrounding quotes, since neither is wanted in practice.
:param query: the name (or address, for a "ptr" lookup) to look up.
Also accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used), not just a string.
:param rdtype: DNS record type ("a", "aaaa", "mx" ...). Second
because it is the argument callers actually vary. None (default)
auto-selects: "ptr" when query is an address literal (an
"a"/"aaaa" lookup of an address makes no sense), "a"
otherwise. Pass an explicit rdtype to opt out of the
auto-selection.
:param ns: optional nameserver, or list of nameservers, to query instead of
the system resolver. When omitted, the system resolver configuration
(/etc/resolv.conf, or the Windows equivalent) supplies both the
nameservers and, if search is enabled, the search list.
:param timeout: seconds to spend on the whole resolution, retries included
(None for dnspython's default). Bounds total time, not each query
-- a list of unreachable nameservers cannot stretch past it.
:param port: nameserver port, for resolvers not on 53.
:param tcp: query over TCP instead of UDP. Useful for large responses that
would otherwise be truncated.
:param search: how to expand an unqualified query, the way ping or
a browser would. True (default) tries the system resolver's own
search list (the search/domain directive in resolv.conf,
or the Windows per-adapter DNS suffix list) -- the same source ns
draws its nameservers from when ns is omitted. False looks up
query as-is only, no expansion. A list of domain names tries
exactly those suffixes instead of the system list, regardless of
ns. Ignored for an already-qualified (trailing-dot) query.
:param source: the local address the queries are sent from (None:
whatever the OS picks).
NXDOMAIN and "no answer" yield []. A timeout raises
:class:ResolutionTimeoutError; every server failing, or no resolver
configuration to read, raises :class:ResolutionError. A malformed query
or unknown record type raises :class:ValueError, since that is a caller
bug rather than a DNS result.
Needs the dns extra (pip install "netimps[dns]"); without it this
raises :class:ResolutionError saying so. :func:has_dns tells which.
Source code in src/netimps/_dns/_dnspython.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |
resolve_system(query, rdtype=None, *, timeout=5.0, search=True)
resolve_system(
query: "HostLike",
rdtype: "Literal['a', 'A']",
*,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[IPv4Address]"
resolve_system(
query: "HostLike",
rdtype: "Literal['aaaa', 'AAAA']",
*,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[IPv6Address]"
resolve_system(
query: "HostLike",
rdtype: "Literal['ptr', 'PTR']",
*,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[str]"
resolve_system(
query: "HostLike",
rdtype: "Optional[Union[str, Tuple[str, ...]]]" = None,
*,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[Any]"
Resolve query via the OS resolver (:func:socket.getaddrinfo/
:func:socket.gethostbyaddr).
::
resolve_system("example.com") # ['93.184.216.34']
resolve_system("example.com", "aaaa")
resolve_system("localhost") # /etc/hosts, no DNS query
resolve_system("host", search=False) # "host" only, no suffix expansion
resolve_system("host", search=["eng.example.com", "example.com"])
resolve_system("8.8.8.8") # rdtype=None -> ptr -> ['dns.google']
Goes through hosts file, NSS (nsswitch.conf) and DNS, in the order
the OS resolver applies them -- the same path getaddrinfo(3)/
gethostbyaddr(3)-based tools use. Unlike :func:resolve_dnspython,
this sees /etc/hosts entries, nsswitch.conf sources (mDNS, LDAP,
whatever NSS is configured with) and any OS-level resolver cache.
The trade-off: address and reverse records only (rdtype must be
"a", "aaaa" or "ptr"; anything else raises
:class:ResolutionError immediately, no query attempted), no ns= (it
always asks whatever resolver the OS is configured with -- there is no
per-call override), and no priority/TTL/other record metadata, since
neither :func:socket.getaddrinfo nor :func:socket.gethostbyaddr
exposes any of that.
:param query: the hostname (or address, for "ptr") to look up. Also
accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used), not just a string.
:param rdtype: "a", "aaaa" or "ptr". None (default)
auto-selects: "ptr" when query is an address literal, "a"
otherwise. Anything else raises.
:param timeout: seconds to wait per candidate name tried (see
search). There is no native per-call timeout for
getaddrinfo/gethostbyaddr, so each attempt runs in a daemon
helper thread and is abandoned (without cancelling the underlying
blocking call) past the deadline. The deadline bounds wall time:
a resolver that hangs for a minute still raises
:class:ResolutionTimeoutError at timeout, and the abandoned thread
holds up neither the caller nor interpreter exit. None waits
indefinitely.
:param search: how to expand an unqualified query. Ignored for
rdtype="ptr" -- an address has no search-list suffix to try.
There is no per-call search-list override on
:func:socket.getaddrinfo itself -- unlike dnspython/
nslookup, the OS resolver takes no such parameter -- so
search=True (default) simply leaves query as given and lets
the OS resolver apply its own configured search list (glibc's
ndots/search, the Windows per-adapter DNS suffix).
search=False appends a trailing ., which every resolver reads
as "already fully qualified" and skips search-list expansion for --
the same trick a shell's own host/getent scripts use. A list
of domain names instead tries query qualified with each, in
order, one :func:socket.getaddrinfo call per candidate, stopping at
the first with actual results -- this package's own expansion,
independent of (and untouched by) the OS resolver's. Already-
qualified (trailing-dot) names are unaffected by any of this.
Every candidate tried and none resolving (EAI_NONAME, no data) yields
[], never None. A temporary failure (EAI_AGAIN, EAI_FAIL)
raises :class:ResolutionError, and a deadline
:class:ResolutionTimeoutError: the resolver could not be asked. An
unsupported rdtype raises :class:ResolutionError too, since that is
this backend's fixed limitation, not a DNS outcome to report as "no records".
Source code in src/netimps/_dns/_system.py
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | |
resolve_nslookup(query, rdtype=None, *, ns=None, timeout=5.0, search=True)
resolve_nslookup(
query: "HostLike",
rdtype: "Literal['a', 'A']",
*,
ns: Optional[str] = None,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[IPv4Address]"
resolve_nslookup(
query: "HostLike",
rdtype: "Literal['aaaa', 'AAAA']",
*,
ns: Optional[str] = None,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[IPv6Address]"
resolve_nslookup(
query: "HostLike",
rdtype: "Literal['ptr', 'PTR']",
*,
ns: Optional[str] = None,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[str]"
resolve_nslookup(
query: "HostLike",
rdtype: Optional[str] = None,
*,
ns: Optional[str] = None,
timeout: Optional[float] = 5.0,
search: Union[bool, List[str]] = True
) -> "List[Any]"
Resolve query by shelling out to the nslookup binary.
::
resolve_nslookup("example.com") # ['93.184.216.34']
resolve_nslookup("example.com", "aaaa")
resolve_nslookup("example.com", ns="1.1.1.1")
resolve_nslookup("host") # tries the system search list
resolve_nslookup("host", search=["eng.example.com", "example.com"])
resolve_nslookup("8.8.8.8") # rdtype=None -> ptr -> ['dns.google']
A fallback for when neither dnspython nor :func:resolve_system is
usable: goes through whatever resolver nslookup itself is configured
to use (the OS resolver's nameservers, unless ns= overrides it).
:param query: the name (or address, for ptr) to look up. Also accepts
an address object or an :class:IPv4Interface/:class:IPv6Interface
(its .ip is used), not just a string.
:param rdtype: "a", "aaaa" or "ptr". None (default)
auto-selects: "ptr" when query is an address literal, "a"
otherwise. Anything else raises :class:ResolutionError immediately,
no subprocess spawned -- nslookup's plain-text output is only
parsed reliably for these three.
:param ns: nameserver to query, passed as nslookup's trailing
server argument. None uses nslookup's own default.
:param timeout: seconds to allow each subprocess attempt to run --
one per candidate name tried under search. None allows 30
seconds: a program never runs unbounded.
:param search: how to expand an unqualified query. nslookup has
no built-in search-list handling (unlike dnspython), so this
tries one nslookup call per candidate name, in order, and returns
the first with actual records: True (default) tries query
qualified with each domain in the system resolver's search list (via
the same resolv.conf/registry parsing :func:resolve_dnspython
uses -- [] there, e.g. dnspython not installed, falls back to
the literal name only); False looks up query as-is only; a
list of domain names tries exactly those suffixes. Ignored for an
already-qualified (trailing-dot) query or a "ptr" lookup,
neither of which a search list applies to.
A server answering "no such name" or "no such record" (NXDOMAIN or
equivalent, for every candidate tried) yields []. A missing
nslookup binary, a timeout, a non-zero exit with no "can't find"
line (no reachable server, a refused connection), a "can't find" line
whose reason after the colon is not a "no such name" answer (SERVFAIL,
REFUSED, Windows' No response from server), or an unsupported
rdtype raises :class:ResolutionError -- there was no definitive DNS
answer to report, so :func:resolve's chain moves on rather than treating
it as NXDOMAIN.
A query that nslookup would read as an option (one starting with
-) or that cannot be a name at all (empty, whitespace, control
characters) raises :class:ValueError before any subprocess is
spawned: nslookup has no -- end-of-options separator, so such a
query cannot be escaped into position -- left to reach the binary it
becomes an option, and an nslookup with no name argument goes
interactive and reads query names from stdin. The subprocess is given
:data:subprocess.DEVNULL for stdin as well, so it can never read the
caller's.
Source code in src/netimps/_dns/_nslookup.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | |
resolve_wire(query, rdtype=None, *, ns=None, timeout=5.0, port=53, tcp=False, search=True, source=None)
resolve_wire(
query: "HostLike",
rdtype: "Literal['a', 'A']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[Union[str, List[str]]] = None
) -> "List[IPv4Address]"
resolve_wire(
query: "HostLike",
rdtype: "Literal['aaaa', 'AAAA']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[Union[str, List[str]]] = None
) -> "List[IPv6Address]"
resolve_wire(
query: "HostLike",
rdtype: "Literal['ptr', 'PTR']",
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[Union[str, List[str]]] = None
) -> "List[str]"
resolve_wire(
query: "HostLike",
rdtype: Optional[str] = None,
*,
ns: Optional[Union[str, List[str]]] = None,
timeout: Optional[float] = 5.0,
port: int = 53,
tcp: bool = False,
search: Union[bool, List[str]] = True,
source: Optional[Union[str, List[str]]] = None
) -> "List[Any]"
Resolve query by speaking DNS to ns directly -- no dnspython,
no OS resolver. UDP, retried over TCP when the reply is truncated (or TCP
throughout with tcp=True); the nameservers in turn until one answers.
::
resolve_wire("example.com", ns="1.1.1.1")
resolve_wire("example.com", "aaaa", ns=["10.0.0.53:5353", "[fd00::53]"])
resolve_wire("example.com", ns="10.0.0.53", source="10.0.0.7")
:param ns: nameserver(s): host, host:port, [v6]:port.
None: /etc/resolv.conf's (POSIX; elsewhere
:class:ResolutionError).
:param rdtype: a, aaaa, cname, ptr, mx, txt,
ns, srv; None auto-selects as :func:resolve does.
Another type is a :class:ResolutionError (the chain moves on).
:param timeout: seconds for the whole resolution, every server included.
:param port: the port of an ns entry that names none.
:param source: the address the queries leave from -- one, or one per
address family; a server whose family has none is skipped.
:param search: a list of domains tries query under each, then as
given; True/False ask for query as given (this backend
reads no system search list).
Contract as the other backends: native values, [] for NXDOMAIN or no
record of the type, :class:ResolutionError when no server answered
(:class:ResolutionTimeoutError when the deadline or a socket timeout
was the reason). A reply the codec cannot read, or a name it cannot encode,
is not raised as such: it counts as that server not answering, and the
:class:DNSDecodeError is the __cause__ of the final error.
A CNAME chain inside the reply is followed.
Source code in src/netimps/_dns/_wire.py
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
resolve_doh(query, url, *, rdtype=None, timeout=5.0, fetch=None, allow_http=False)
resolve_doh(
query: "HostLike",
url: str,
*,
rdtype: "Literal['a', 'A']",
timeout: Optional[float] = 5.0,
fetch: "Optional[Callable[[str, bytes, dict, Optional[float]], bytes]]" = None,
allow_http: bool = False
) -> "List[IPv4Address]"
resolve_doh(
query: "HostLike",
url: str,
*,
rdtype: "Literal['aaaa', 'AAAA']",
timeout: Optional[float] = 5.0,
fetch: "Optional[Callable[[str, bytes, dict, Optional[float]], bytes]]" = None,
allow_http: bool = False
) -> "List[IPv6Address]"
resolve_doh(
query: "HostLike",
url: str,
*,
rdtype: "Literal['ptr', 'PTR']",
timeout: Optional[float] = 5.0,
fetch: "Optional[Callable[[str, bytes, dict, Optional[float]], bytes]]" = None,
allow_http: bool = False
) -> "List[str]"
resolve_doh(
query: "HostLike",
url: str,
*,
rdtype: Optional[str] = None,
timeout: Optional[float] = 5.0,
fetch: "Optional[Callable[[str, bytes, dict, Optional[float]], bytes]]" = None,
allow_http: bool = False
) -> "List[Any]"
Resolve query with DNS over HTTPS (RFC 8484): the DNS message
POSTed to url as application/dns-message.
::
resolve_doh("example.com", "https://cloudflare-dns.com/dns-query")
:param fetch: fetch(url, body, headers, timeout) -> bytes sends the
request -- so a caller with its own HTTP stack (a proxy, a CA bundle)
uses it. None: :mod:urllib.request, which follows no redirect and
reads at most 65,536 bytes. An OSError or ValueError from it is
a :class:ResolutionError.
:param rdtype: as :func:resolve_wire.
:param allow_http: accept an http:// URL. Without it anything but
https is a :class:ValueError before a request is made: a DNS
answer over plain HTTP can be rewritten on the path.
Contract as the other backends: native values, [] for NXDOMAIN or no
record of the type, :class:ResolutionError when the endpoint could not
be asked or answered something else (:class:ResolutionTimeoutError on a
timeout), with an unreadable reply as the error's __cause__
(:class:DNSDecodeError). A query the codec cannot encode (an empty or
over-long label) raises :class:DNSDecodeError itself, before anything is
sent. Not part of :func:resolve's chain. A reply body over 65,536 bytes, a
redirect and an HTTP error status are :class:ResolutionError; messages
name the URL without its credentials or query string.
Source code in src/netimps/_dns/_doh.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
ping(dst, *, tries=1, timeout=1.0, ipv6=None, src=None, size=None, ttl=None, dont_fragment=False, method='icmp', port=None, cache=False)
Ping dst; the result is truthy if it answered.
Returns a :class:PingResult rather than a bare bool, so the reply details
are available without re-running and re-parsing ping::
ping("8.8.8.8") # ICMP echo
ping("8.8.8.8", method="tcp", port=53) # time a TCP handshake
ping("10.0.0.5", method="udp", port=53) # datagram + reply or ICMP
if ping("8.8.8.8"): # still reads as a boolean
...
result = ping("8.8.8.8")
result.rtt # 0.005 (seconds)
result.ttl # 119
Shells out to the platform ping binary, translating timeout into the
right per-platform flags (Windows -n/-w in milliseconds; POSIX
-c/-W in whole seconds), and returns on the first success::
ping("10.0.0.1") # one attempt, 1s timeout
ping("example.com", tries=3, timeout=2.5)
ping("2001:db8::1", ipv6=True) # force ping6 semantics
ping(get_interfaces()[0].ipv4[0]) # an IPv4Interface -- pings its .ip
ping(IPv4Address("10.0.0.1")) # an address object directly
:param dst: hostname, address string, :class:IPv4Address/
:class:IPv6Address, or :class:IPv4Interface/:class:IPv6Interface
(its .ip is pinged, not the /prefix). A network
(:class:IPv4Network/:class:IPv6Network) raises :class:TypeError
-- it has no single address to ping.
:param tries: attempts before giving up. Values below 1 are treated as 1.
:param timeout: seconds to wait per attempt. Linux ping -W takes whole
seconds, so sub-second values are rounded up to 1 -- never down to
0, which some implementations read as "wait forever". Windows -w
and BSD/macOS -W take milliseconds (rounded up to 1 ms); BSD
ping6 has no wait flag and the subprocess deadline bounds it. The
timeout also bounds the lookup of a hostname dst, which happens
once before the first attempt.
:param ipv6: force the IPv6 or IPv4 family. Applies to all three
method values: it selects the -6/-4 flag for ICMP and the
address family the tcp/udp probes resolve and connect with.
None (default) lets the resolver decide, and accepts a reply from
either family. An address-literal dst decides for itself.
:param src: send from this local address, choosing which interface the
echo leaves by. Accepts an :class:Interface, an address object, or a
string::
ping("8.8.8.8", src=get_source_ip()) # address object
ping("8.8.8.8", src=get_interfaces()[0]) # Interface
ping("8.8.8.8", src="192.0.2.10") # literal
ping("8.8.8.8", src=MACAddress("00:00:5e:00:53:01")) # by MAC
A **MAC address** (object or string) is resolved to the interface
holding it -- convenient when the adapter is known by hardware address
rather than by a possibly-changing IP. An unknown MAC yields a falsy
result rather than falling back.
An ``Interface`` contributes its first non-loopback IPv4 address (its
IPv6 address when ``ipv6=True``), because Windows ``-S`` requires an
*address* -- passing an adapter name there fails. POSIX ``-I`` would
accept a name, but resolving it here keeps behaviour identical on both.
An interface holding no usable address yields a falsy result rather
than falling back to the default route.
Likewise an address not held by any local interface makes ``ping``
fail, so the result is falsy -- this never silently reroutes.
:param cache: reuse a recent enumeration of the adapters while src is
resolved, as :func:get_interfaces does (True for
:data:INTERFACE_CACHE_TTL seconds, or a number for that TTL).
False (the default) enumerates on each call; ignored without src.
:param size: ICMP payload bytes -- Windows -l, POSIX -s. Both
flags mean the same thing: neither counts headers, so the wire packet is
28 bytes larger (20 IP + 8 ICMP). Payload 1472 is exactly 1500 on the
wire, which is why that is the number a 1500-MTU link tops out at.
Verified on Windows against the DF boundary (1472 passes, 1473 does
not); ping(8) documents -s as "data bytes" identically.
:param ttl: initial hop limit (-i on Windows, -t on Linux, -m on
macOS/BSD ping and -h on ping6 -- the letters differ between
platforms, a classic source of scripts that silently do the wrong
thing).
A ``ttl`` too small to reach the target yields ``False`` on every
platform. That takes explicit work on Windows, whose ``ping`` exits
``0`` for "TTL expired in transit" -- counting a router's error as a
received reply -- so the raw exit code would report success although
the target was never reached. The reply address is verified instead
(see below), which is locale-independent.
:param method: how to probe -- "icmp" (default), "tcp" or
"udp". ICMP is the classic echo; the other two reach a host through
firewalls that drop echo but permit ordinary traffic.
All three answer **"is the host up?"**. A TCP *refusal* therefore counts
as success -- the RST proves something answered -- and so does an ICMP
port-unreachable for UDP. Use :func:`netimps.tcp_check` when the
question is "is the *service* up?", where a refusal is a failure.
**On Windows a refusal takes about two seconds to arrive**: the SYN is
retried before the RST is reported (measured 2.02 s against a closed
loopback port; macOS answers in 0.9 ms). A ``timeout`` under two seconds
therefore reports a refusing Windows host as down, so give ``method="tcp"``
a ``timeout`` of at least 3 there.
The UDP probe connects its socket before sending, so the ICMP
port-unreachable is delivered on POSIX as well as Windows -- an
unconnected socket never receives one on Linux/BSD, which would make
this method under-report liveness there for exactly the case it exists
for. Silence still means "no answer": UDP cannot tell a filtered
port from an absent host.
:param port: destination port for tcp/udp. Required for those, and
ignored for ICMP.
:param dont_fragment: set the DF bit (Windows -f, Linux -M do,
macOS/BSD ping -D). Combined with size, the standard manual
MTU probe: the largest size that still succeeds is the path MTU
minus 28. BSD ping6 has no verified DF flag, so the combination
raises :class:ValueError instead of sending fragmentable probes.
An empty dst gives a falsy result. A missing ping binary or a
non-zero exit also yield a falsy :class:PingResult -- reachability
failures are never raised.
Caller mistakes are raised, as they are elsewhere in this package, so a
typo cannot masquerade as an unreachable host: :class:ValueError for an
unknown method, a negative size, a ttl outside 1-255, a
tcp/udp probe with no port, dont_fragment on a method or
platform that cannot set it, and a dst beginning with -. That last
one matters more than it looks: the binary reads such a destination as an
option, and Windows ping -? prints usage and exits 0, which would
be reported as a successful ping of a host that was never contacted.
.. note:: This measures whether ICMP echo is answered, which is not the same as whether a host is up -- plenty of hosts and most cloud firewalls drop echo requests while serving traffic normally. Prefer a TCP connect to the port you actually care about when you can.
Source code in src/netimps/_ping/_run.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | |
scan_hosts(network, port=None, *, ports=None, timeout=1.0, workers=_DEFAULT_WORKERS)
Find responsive hosts on network, with the ports each answers on.
::
scan_hosts("192.168.1.0/24", port=22) # who has SSH open
scan_hosts("10.0.0.0/28", ports=[80, 443])
scan_hosts("192.168.1.0/24") # the 'common' set each
scan_hosts("192.168.1.0/24", port="https") # scheme name works too
:param network: anything :func:netimps.parse accepts as a network. Only
usable host addresses are probed -- the network and broadcast addresses
are skipped.
:param port: shorthand for ports=[port]. Accepts a scheme name too, so
port="ssh" is port=22.
:param ports: ports to probe per host; defaults to the "common" set
when omitted. Same forms as :func:scan_ports. An explicitly
empty ports means "nothing to scan" and returns [] -- it does
not fall back to the default set, which would sweep 36 ports across
the network a caller just said to probe on none.
:param timeout: per-connection timeout, floored to 50 ms as in
:func:scan_ports; a negative value raises.
:param workers: total concurrent connections across all hosts.
Returns [(address, [open_ports]), ...] sorted by address, including
only hosts with at least one open port.
.. note::
This is a TCP sweep, so a host that is up but answers on none of the
probed ports does not appear. That is the honest result for the question
asked -- it is not an ARP or ICMP discovery scan, and a firewalled host
is indistinguishable from an absent one. Widen ports if you need
better coverage.
Refuses networks larger than /16 (or IPv6 /112), and any sweep of more than
4,194,304 probes (hosts times ports): a /8 sweep is 16 million hosts, and a
/16 over every port 4.3 billion probes, which is a mistake rather than an
intention. Both raise :class:ValueError before the first probe. The work is
fed to the pool as it runs, so memory does not grow with the sweep.
Source code in src/netimps/_scan.py
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 | |
scan_ports(host, ports='common', *, timeout=1.0, workers=_DEFAULT_WORKERS)
Return the sorted open TCP ports on host.
::
scan_ports("192.168.1.1") # the 'common' set
scan_ports("localhost", "well-known") # ports 1-1023
scan_ports("10.0.0.5", range(8000, 8100))
scan_ports("10.0.0.5", [22, 80, 443])
scan_ports("10.0.0.5", "https") # scheme name -> 443
scan_ports("10.0.0.5", ["ssh", "https"]) # -> 22, 443
scan_ports("10.0.0.5", "22,https,8000") # a comma list -> 22, 443, 8000
host also accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used), same as :func:tcp_check.
:param ports: a :data:PORT_RANGES name ("common", "well-known",
"all"), a scheme name resolved via :func:get_default_port, a port
number, a comma-separated string of those ("22,80,https"; spaces
around an item are ignored, an empty item raises), or any iterable
mixing them. A range name wins over a scheme name where the two
collide. Each port must be in 0-65535; an empty iterable means
"nothing to scan" and returns [].
:param timeout: per-port connect timeout. This bounds the whole scan
(timeout x rounds), so keep it small on a large range -- but not so
small that a slow host reads as closed. Floored to 50 ms, as in
:func:tcp_check, since settimeout(0) means non-blocking and
would report every port closed; a negative value raises.
:param workers: concurrent connections. These tasks are I/O-bound, so the
useful number is far above the CPU count; very high values can exhaust
file descriptors or trip rate limiting.
host is resolved once for the whole scan rather than once per port
-- see :func:_probe_addresses for why that matters for correctness and
not only speed. A name that does not resolve returns [].
Open means "the TCP handshake completed" -- not that the service is healthy, and not that a filtered port is distinguishable from a closed one (both simply fail to connect).
Raises :class:NetimpsValueError for an unknown scheme or range name or an
empty item in a comma list, and :class:ValueError for a port outside
0-65535 or a negative timeout.
Source code in src/netimps/_scan.py
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 | |
is_multicast(address)
True if address is a multicast group (224.0.0.0/4 or ff00::/8).
::
is_multicast("224.0.0.251") # True -- mDNS
is_multicast("ff02::fb") # True
is_multicast("10.0.0.1") # False
Accepts an address as text, an int, packed bytes or an address
object, and an :class:IPv4Interface/:class:IPv6Interface, whose .ip
is tested::
is_multicast(IPv4Interface("239.1.2.3/32")) # True
That matters because this is the gatekeeper :func:join_group and
:func:leave_group use: testing the interface object directly asks whether
a network is multicast, which it never is.
A v4-mapped address is judged as the v4 address inside it, the same on every Python (the stdlib delegates only from 3.13). A mapped group is a real group -- it is how a dual-stack listener sees one.
:raises NetimpsValueError: for text that is no address. :raises TypeError: for a network or a value of another type.
Source code in src/netimps/_multicast.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
join_group(sock, group, *, interface=None, cache=False)
Join group on sock, optionally via a specific interface.
interface accepts an :class:Interface, a MAC, an adapter name or a
local address. Without one the kernel chooses by routing table, which on a
host with VMs or a VPN is often the wrong adapter -- and the failure is
silent: the socket simply never receives.
The spec is resolved per family: to a local address for an IPv4
group, to an interface index for an IPv6 one, since that is what each
mreq carries.
Raises :class:ValueError for a non-multicast group, or for an interface
that resolves to no usable address (IPv4) or no index (IPv6), and
:class:OSError if the kernel rejects the join.
cache is :func:get_interfaces's argument: with True or a TTL in
seconds, resolving interface reuses a recent enumeration instead of
making the syscall. False (the default) enumerates on each call.
Source code in src/netimps/_multicast.py
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
leave_group(sock, group, *, interface=None, cache=False)
Leave group on sock. The inverse of :func:join_group.
Closing the socket drops membership too, so this is only needed to leave a
group while keeping the socket open. cache is :func:join_group's.
Source code in src/netimps/_multicast.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
multicast_socket(group=None, port=0, *, interface=None, ttl=1, loop=True, bind=True, reuse=True, ipv6=None, cache=False)
Return a UDP socket configured for multicast, joined to group.
The whole setup in one call::
sock = multicast_socket("224.0.0.251", 5353) # mDNS listener
data, sender = sock.recvfrom(2048)
sender = multicast_socket(ttl=32, bind=False) # send-only, off-link
sender.sendto(b"hello", ("239.1.2.3", 9999))
:param group: group to join, or a list of them. None configures the
socket for sending without joining anything.
:param port: local port to bind. 0 picks a free one; a listener must
pass the port the senders use.
:param interface: :class:Interface, MAC, adapter name or local address to
join through. Strongly recommended on multi-homed hosts -- the
routing-table default is frequently the wrong adapter. Pins outgoing
traffic as well as the joins (IP_MULTICAST_IF for IPv4,
IPV6_MULTICAST_IF for IPv6), so sends and receives cannot end up
on different adapters.
:param ttl: hop limit for outgoing datagrams. The default of 1 keeps
traffic on the local link; raise it deliberately to cross routers.
:param loop: whether this host receives its own transmissions. True
(the default) is what you want when sender and listener share a host.
:param bind: bind to port. Binding to "" rather than the group
address, because binding to the group fails on Windows.
:param reuse: set SO_REUSEADDR (plus SO_REUSEPORT where it exists),
so several listeners can share the port. SO_REUSEPORT is absent on
Windows and is skipped there rather than raising.
:param ipv6: force the address family. None (the default) takes it from
group, which is what you want whenever there is one::
multicast_socket(ttl=32, bind=False) # IPv4 sender
multicast_socket(ttl=32, bind=False, ipv6=True) # IPv6 sender
It exists for the send-only case above, where there is no group to
infer from: without it that socket is IPv4, and an IPv6 sender would
be unreachable through this function. Passing a value that
contradicts ``group`` raises rather than quietly winning.
:param cache: :func:join_group's: reuse a recent enumeration while
interface is resolved, for every group and for the outgoing adapter.
The caller owns the socket and should close it; closing drops membership.
Raises :class:ValueError for a non-multicast group, for groups of mixed
address families (one socket has one family -- open two), for an ipv6
that contradicts group, and :class:OSError if binding or joining
fails.
Source code in src/netimps/_multicast.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | |
aretry(func, attempts=3, *, delay=0.5, multiplier=2.0, max_delay=30.0, jitter=0.1, retryable=DEFAULT_RETRYABLE, on_retry=None, jitter_seconds=None, symmetric=False)
async
:func:retry for a coroutine: await func(), waiting on the running loop.
::
result = await aretry(lambda: client.fetch(url))
result = await aretry(partial(fetch, url), attempts=5, delay=1.0)
func is called with no arguments and must return an awaitable (a
coroutine function, or a lambda returning one); anything else raises
:class:TypeError. Every other argument means what it means on
:func:retry, is checked by the same rules before the first call, and the
last exception is re-raised unwrapped. on_retry is a plain function.
The waits are asyncio.sleep on the running loop, so no thread is used
and the loop keeps running. Cancelling the task cancels the call or the wait
in progress and propagates CancelledError at once; retryable never
catches it, whatever it names, unless it names BaseException.
Source code in src/netimps/_retry.py
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 | |
backoff_delays(attempts=3, delay=0.5, *, multiplier=2.0, max_delay=30.0, jitter=0.1, jitter_seconds=None, symmetric=False)
Yield the delay before each retry -- attempts - 1 values.
Exposed separately so a caller driving its own loop (async, or with progress reporting) gets the same schedule without reimplementing it::
for wait in backoff_delays(attempts=5):
...
:param jitter: fraction of each delay to randomise, spreading retries so
that many clients failing together do not resynchronise into a
thundering herd. 0 disables it and makes the schedule exact.
:param jitter_seconds: randomise by an absolute, symmetric amount
instead -- uniform in [-jitter_seconds, +jitter_seconds], which is
what RFC 2131 §4.1 (DHCPv4) specifies. Overrides jitter when
set. The amplitude is capped at the current delay so the result cannot
go negative before clamping; for RFC 2131's own schedule (4 s upward
against a 1 s amplitude) that cap never engages.
:param symmetric: make the fractional jitter spread both ways,
delay * (1 ± jitter), which is what RFC 8415 §15 (DHCPv6)
specifies as RT = 2*RTprev + RAND*RTprev.
Every argument is checked when the function is called, not at the first
next(): attempts below 1, a negative delay, a multiplier
below 1, a max_delay below delay and a jitter outside 0 to 1
raise :class:ValueError. :func:retry and :class:Backoff apply the
same rules.
Delays are capped at max_delay. The default jitter is applied after
the cap and only ever reduces the wait, so max_delay is a genuine
ceiling -- the right default for generic retries, where overshooting a
ceiling is worse than a slightly short wait.
In the two symmetric modes max_delay caps the base, not the final
value, and a delay may exceed it by up to the jitter amplitude. That is
what the specifications require: RFC 8415 applies its jitter after the cap
(if RT > MRT: RT = MRT + RAND*MRT) and RFC 2131 randomises +/-1 s
around its 64 s maximum. Clamping instead would leave those modes one-sided
exactly at the cap, where a backed-off client spends nearly all of its
time, which would defeat the point of choosing them. No mode ever returns a
negative delay.
For a long-lived session that backs off on loss and resets on progress
-- a retransmission timer rather than a one-shot schedule -- use
:class:Backoff instead.
Source code in src/netimps/_retry.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
retry(func, attempts=3, *, delay=0.5, multiplier=2.0, max_delay=30.0, jitter=0.1, retryable=DEFAULT_RETRYABLE, on_retry=None, jitter_seconds=None, symmetric=False)
Call func(), retrying transient failures with exponential backoff.
::
result = retry(lambda: client.fetch(url))
result = retry(fetch, attempts=5, delay=1.0)
Returns whatever func returns. If every attempt fails, the last
exception is re-raised -- not wrapped -- so the traceback still points at
the real problem.
:param retryable: exception types worth another attempt. Defaults to
OSError, which covers the socket family and so also
:class:ResolutionError, an outage worth another attempt. Anything
else propagates immediately: a ValueError means the call is
malformed and will fail identically next time. :class:NoAnswerError
(a name or record that does not exist, from check=True) is an
OSError too and so is retried by default; a caller who does not want
that names the types to retry instead of OSError itself, for
example retryable=(ConnectionError, TimeoutError).
:param on_retry: called as (attempt, exception, next_delay) before each
wait -- the hook for logging, since this deliberately does no logging
of its own.
attempts counts total calls, not retries: attempts=1 calls once
and never sleeps. Delay grows by multiplier each round, capped at
max_delay, with jitter applied to avoid a thundering herd.
The schedule arguments are checked before the first call, by the rules of
:func:backoff_delays; attempts below 1 raises :class:ValueError.
Synchronous by design -- it blocks. For async, drive :func:backoff_delays
from your own loop.
Source code in src/netimps/_retry.py
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | |
CMSG_LEN(length)
Bytes one cmsg of length payload occupies, header included.
Native where CPython provides it; computed from WSACMSGHDR and pointer
alignment on Windows.
Source code in src/netimps/_msg/_dispatch.py
101 102 103 104 105 106 107 108 109 110 111 112 | |
CMSG_SPACE(length)
Buffer space one cmsg of length payload needs, padding included.
This is the one to size an ancbufsize with: the difference from
:func:CMSG_LEN is the trailing pad that lets a following header start
aligned, and leaving it out is how a second cmsg gets quietly truncated.
Source code in src/netimps/_msg/_dispatch.py
115 116 117 118 119 120 121 122 123 124 125 126 127 | |
has_recvmsg()
Whether :func:recvmsg and :func:sendmsg can actually run here.
True on POSIX, and on Windows when the Winsock bindings loaded. Prefer
this to probing :mod:socket for a method name, which answers a different
question once the patch is installed.
Source code in src/netimps/_msg/_dispatch.py
91 92 93 94 95 96 97 98 | |
recvmsg(sock, bufsize, ancbufsize=0, flags=0)
Receive a datagram with its ancillary data, on any platform.
Same signature and same 4-tuple as :meth:socket.socket.recvmsg:
(data, ancdata, msg_flags, address), where ancdata is a list of
(cmsg_level, cmsg_type, cmsg_data).
address is (host, port) for AF_INET and
(host, port, flowinfo, scope_id) for AF_INET6.
:raises OSError: with ENOTSUP where neither CPython nor Winsock can
serve it. Receiving errors propagate as the usual OSError
subclasses -- :class:BlockingIOError on an empty non-blocking socket,
on Windows as well as POSIX.
Source code in src/netimps/_msg/_dispatch.py
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
sendmsg(sock, buffers, ancdata=(), flags=0, address=None)
Send a datagram with ancillary data, on any platform.
Same signature as :meth:socket.socket.sendmsg: buffers is a sequence
of bytes-like objects (not a bare bytes), ancdata a sequence of
(cmsg_level, cmsg_type, cmsg_data), and the return is the byte count
sent.
A v6 address accepts a 2-, 3- or 4-tuple, and a %zone suffix on the
host is honoured when scope_id is absent.
:raises OSError: with ENOTSUP where neither CPython nor Winsock can
serve it.
Source code in src/netimps/_msg/_dispatch.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
is_socket_patched()
Whether netimps has added anything to :mod:socket right now.
A function rather than a constant because the answer changes when
:func:patch_socket_module is called, and a module-level flag copied by
from netimps import ... would go stale at the first toggle.
Source code in src/netimps/_msg/_patch.py
37 38 39 40 41 42 43 44 | |
patch_socket_module(enable=True, *, iov_max=None)
Add recvmsg/sendmsg/CMSG_LEN/CMSG_SPACE where missing.
netimps calls this on import, so sock.recvmsg(...) works on Windows
without the caller changing anything. Returns the names it changed, newest
call only -- an empty list means there was nothing to do.
Idempotent in both directions, and strictly additive: a name the platform
already provides is never replaced, so on Linux and macOS this is a
verified no-op. A name another copy of this package installed is taken
over, so the second import of the package in a process owns the patch.
enable=False removes exactly what was installed, leaving a
natively-provided name alone.
On a platform with no os.sysconf this also installs one -- see
:func:_shim_sysconf for why that is part of the same patch rather than a
separate feature.
:param enable: install when true, restore the original state when false.
:param iov_max: what the installed os.sysconf reports for
SC_IOV_MAX. Default :data:_IOV_MAX (1024, matching Linux).
**This is a batch size, not a ceiling, and it is a choice rather than a
measurement** -- which is why it is a parameter. Windows reports no
buffer-count limit anywhere (no ``IOV``-like socket name;
``WSASend``'s ``dwBufferCount`` is a bare ``DWORD``, and 1048576
buffers in one call were accepted on build 28000), and the system limit
is not settable on POSIX either: Linux's is
``#define UIO_MAXIOV 1024`` in ``linux/uio.h``, with no sysctl and no
``/proc`` entry. So there is nothing to query and nothing to set; this
is the only knob that exists. Raise it if you gather more than 1024
buffers per call and want them in one syscall.
:returns: the names added or removed by this call.
:raises ValueError: for an iov_max below 1.
Source code in src/netimps/_msg/_patch.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
has_pktinfo(family=_socket.AF_INET)
Whether a UDP socket of family can report each datagram's arrival interface on this host.
The question a server asks before deciding how to bind: with packet info, one wildcard socket serves every address and still knows which one a datagram reached; without it the wildcard has to be expanded into a socket per address, which on Linux then receives no broadcasts at all.
::
if has_pktinfo():
socks = [bind("", 67)]
else:
socks = [bind(str(a), 67) for a in addresses]
Decided by asking a socket, not by testing a name, which is the only
reliable way: getattr(socket, "IP_PKTINFO", None) is None on CPython
3.9-3.11 on every platform -- the constant arrived in 3.12 -- while the
kernel supported it throughout. A name test therefore reports "no" on a
platform that works, which silently pushes a server onto the per-address
path it did not need. :class:UDPEndpoint already used the documented
per-platform values rather than the constants for exactly this reason, and
this asks it the same way the endpoint does, on a throwaway socket.
The answer is cached per family for the life of the process, since it is a property of the platform and the interpreter rather than of any socket.
family is 4 or AF_INET, 6 or AF_INET6; anything else raises
:class:ValueError, since a silent False for a misspelt family is a
wrong answer.
Returns False rather than raising if a socket of that family cannot even
be created -- a v6 answer on a host with IPv6 disabled is "no", not an
error.
Source code in src/netimps/_udp/_support.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
bind_listen(listen=None, default_ports=0, *, family=None, per_address=None, device_binding=True, allow_address_takeover=False, broadcast=False)
Bind the sockets listen names and return an endpoint for each, in order.
listen, default_ports and family are parse_listen's. An address
is one socket; a wildcard is one socket that reports the arrival interface,
or one for each address of the host when per_address is true (or None
and the host reports no packet info); a wildcard limited to interfaces is one
wildcard socket whose endpoint .interfaces are the adapters, bound to the
device when device_binding allows and the platform can. Every socket is
exclusive, and an IPv6 socket is IPv6 only.
A selector that matches no adapter, and a limited binding with
per_address=True or on a host without packet info, raise
NetimpsValueError before a socket is opened. A failure closes every
socket this call opened and raises the error unchanged. What each kind of
entry opens is set out in netimps/_listen/AGENTS.md.
Source code in src/netimps/_listen/_binding.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
parse_listen(listen=None, default_ports=0, *, family=None)
The sockets listen names, each address and port once, in the order first written.
listen is None (the wildcard), text ("host", "host:port",
"[::1]:69", "*", ":67", "eth1:67", a MAC, several joined by
commas), an address, an Interface, a MACAddress, a ListenAddress,
a (host, ports) pair, or a sequence of those. Text that is no address
names an interface; a host name is never resolved. default_ports is an
int or a sequence, given to each binding that names no port. family
(AF_INET or AF_INET6) makes the wildcard forms and an interface
binding that family's; None takes the wildcard forms as IPv4's.
Raises TypeError for a value of a type the grammar does not take and
NetimpsValueError for anything else it refuses. The grammar in full is in
netimps/_listen/AGENTS.md.
Source code in src/netimps/_listen/_grammar.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
bind(address='', port=0, *, family=None, kind=_socket.SOCK_DGRAM, reuse_address=True, allow_address_takeover=False, reuse_port=False, broadcast=False, connreset=None, interface=None, device=None, cache=False, options=(), listen=None)
Create, configure and bind a socket in one call.
The setup every server repeats, with the options that are easy to get wrong handled once::
sock = bind("", 67, broadcast=True) # DHCP-style listener
sock = bind("127.0.0.1", 0, kind=SOCK_STREAM, listen=5)
sock = bind(port=5353, interface="eth0") # pin to one adapter
:param address: local address to bind. "" (the default) is the
wildcard, which is what a server almost always wants.
Accepts the same loose union as the rest of the package, not just a
``str``: an :class:`IPv4Address`/:class:`IPv6Address`, an
:class:`IPv4Interface`/:class:`IPv6Interface` (its ``.ip`` is used, since
the ``/prefix`` means nothing to ``bind``), a :class:`netimps.Host` or a
:class:`netimps.FQDN`: the value every other entry point in the package
takes, and not the raw ``TypeError`` the socket layer raises for it
("str, bytes or bytearray expected, not IPv4Address"). A *network*
raises :class:`TypeError`, because it has no single
address and guessing one (the network address? the first host?) would be
worse than refusing.
:param port: local port; 0 lets the OS choose.
:param family: 4 or AF_INET, 6 or AF_INET6; anything else
raises :class:ValueError. None (the default) takes the
family from what was given: an IPv6 literal, or an interface whose
address is IPv6, gives AF_INET6; an IPv4 literal gives AF_INET;
a name gives AF_INET when it has an IPv4 address and AF_INET6
when it has only IPv6. The wildcard "" is IPv4: a wildcard says
nothing about the family, and an IPv6 wildcard ("::") must be asked
for. A family that contradicts the address fails in the socket layer,
as it always has.
:param interface: bind to this adapter's address instead of address.
Accepts an :class:Interface, a MAC, an adapter name or an address --
the same union as ping(src=). Raises :class:ValueError if it
cannot be resolved, rather than silently binding the wildcard.
:param device: restrict the socket to this one device, so it receives only
what arrives on it. Takes the same union as interface. The address
stays what address says: with no address the wildcard is bound
(IPv4 unless family says otherwise), which is what a broadcast
listener on one adapter of a multi-homed host wants, and a socket bound
to an adapter's address hears no broadcast on Linux. With an address
too, the kernel applies both: the socket receives what is addressed to
that address and arrived on the device. device and interface
together raise :class:ValueError, since interface already stands
for an address on an adapter. A device naming no local interface raises
:class:ValueError and a platform with no device binding raises
:class:DeviceBindingUnsupportedError, both before a socket is
opened; :func:has_device_binding says which platforms can. Measured
on Linux only: see the platform notes in the header.
:param reuse_address: ask for the conventional "restart without waiting
out TIME_WAIT" behaviour. What that takes differs by platform,
so the flag is not a single socket option: POSIX gets
SO_REUSEADDR, Windows gets SO_EXCLUSIVEADDRUSE. See below.
:param allow_address_takeover: set SO_REUSEADDR on Windows too, where
it means something else entirely. Default False; on POSIX it adds
nothing, since reuse_address already sets exactly that option.
:param reuse_port: share the port with other sockets that ask the same.
POSIX sets SO_REUSEPORT. Windows has no such option: for a
datagram socket the flag takes the address-sharing path
(SO_REUSEADDR, as allow_address_takeover=True does), so two
sockets that both pass it bind one port; there, sharing also lets
another process take the port over. A stream socket on Windows is
unchanged and stays exclusive.
:param connreset: Windows only, and only for UDP. False turns
SIO_UDP_CONNRESET off, so that an ICMP port-unreachable provoked by
an earlier send stops being reported as
:class:ConnectionResetError on a later, unrelated receive; True
leaves the platform default (reporting on). None (the default) is
False for a datagram socket and leaves any other socket alone. A
no-op everywhere but Windows. See :func:disable_connreset.
:param cache: reuse a recent enumeration of the adapters when
interface or device has to be resolved, as
:func:get_interfaces does: True for
:data:INTERFACE_CACHE_TTL seconds or a number for that TTL. False
(the default) enumerates on each call. Ignored when neither is given.
:param options: extra (level, name, value) triples -- or
:class:SocketOption values -- for anything not covered by the named
arguments.
:param listen: call listen(backlog) after binding. Ignored for
datagram sockets, where it is meaningless.
.. warning::
SO_REUSEADDR is not the same option on Windows. On POSIX it
only permits binding an address still in TIME_WAIT -- and
only for a stream socket. TIME_WAIT is a TCP concept, so on a
UDP socket the option's one remaining effect on Linux is to permit
duplicate bindings of live sockets, which is traffic theft rather
than a restart convenience. So this does not set it for
SOCK_DGRAM; use reuse_port=True or
allow_address_takeover=True to share a UDP port deliberately. On
Windows it lets any process bind an addr:port another socket is
already listening on, and the later binder can win subsequent
connections -- reproduced on
Windows 11, where a plain second bind was refused with EACCES while
one through this function succeeded. So reuse_address=True sets
SO_EXCLUSIVEADDRUSE there instead, which is the safe request with
the same intent, and the literal option is available only by asking for
it by its consequence: allow_address_takeover=True.
Raises :class:OSError if the bind fails, with the text of
:func:bind_error_hint in its message when that function recognises the
failure: str(exc) is already something a user can act on. An address
that is taken is always :class:AddressInUseError; any other failure keeps
its own :class:OSError subclass and errno. The socket is closed before
the exception propagates, so a failed call leaks nothing.
Source code in src/netimps/_sockets/_bind.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
has_device_binding()
Whether bind(device=...) can restrict a socket to one device here.
Decided by asking the kernel on a throwaway socket, naming the loopback
device, so it also reflects what this process may do (SO_BINDTODEVICE
needs CAP_NET_RAW before Linux 5.7). Where the platform has no
measured option it is False without a socket being opened.
Source code in src/netimps/_sockets/_device.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
max_udp_payload(mtu, *, ipv6=False)
The largest UDP payload that fits mtu without fragmenting.
mtu - ip_header - 8, where the IP header is 20 for v4 and 40 for v6::
max_udp_payload(1500) # 1472
max_udp_payload(1500, ipv6=True) # 1452
Pair it with :attr:netimps.Interface.mtu to size a datagram to the
interface it will leave by. Three things make this worth a function rather
than arithmetic at the call site:
- the v4 figure uses the minimum 20-byte header, so a packet carrying IP options can still fragment. Subtract more if you set any.
- IPv6 counts its extension headers as payload, so 40 is exact only without them.
Interface.mtuisOptional[int], so a caller must handleNonerather than assume. That is why this takes anintand does not accept anInterface: theNonedecision belongs to the caller, who knows whether to fall back to 1500 or to refuse.
None means the platform genuinely could not read an MTU, and never
"unbounded": the Windows loopback adapter reports ULONG max, which
Interface.mtu clamps to 65535, and this returns the 65507 that was
measured to actually arrive. A None there would send a caller falling
back to 1500 to cap loopback at 1472.
Returns 0 rather than a negative number for an MTU too small to carry any payload.
:raises ValueError: for a negative mtu.
Source code in src/netimps/_sockets/_mtu.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
disable_connreset(sock)
Stop Windows reporting an ICMP port-unreachable on a later receive.
Returns whether anything was changed -- False off Windows, where there
is nothing to change.
The behaviour this turns off. On Windows an unconnected UDP socket that
provoked an ICMP port-unreachable gets it delivered as
:class:ConnectionResetError from a subsequent recvfrom -- one that may
have nothing to do with the peer that refused. A receive loop then dies on a
packet some unrelated host did not want. POSIX reports asynchronous ICMP
errors only on connected sockets, so this whole class of surprise does not
arise there.
This is the inverse face of a rule this package documents from the other
side: an unconnected POSIX probe never sees a port-unreachable, which is
why :class:netimps.UDPEndpoint's MTU probing connects.
Not applied to a socket you did not ask for. The report is occasionally
what a caller wants -- a client talking to one peer learns the peer is gone
-- so this is a call, not a hidden side effect. :func:bind makes it for a
datagram socket by default, which is what a server loop wants; pass
bind(..., connreset=True) to keep the report, or call this on a socket
from elsewhere.
There is no stdlib route to this. Measured on 3.14: CPython exports no
socket.SIO_UDP_CONNRESET on any version, and even given the documented
value (0x9800000C) socket.ioctl refuses it -- that method whitelists
a handful of commands and answers ValueError: invalid ioctl command for
the rest. So this goes through WSAIoctl by ctypes. The getattr
route is the trap: it compiles, runs, and is a silent no-op on every
platform, which is why this lives here once rather than being rewritten
per caller.
Best effort: a kernel or socket type that refuses the request leaves the socket as it was rather than raising, since this is an adjustment to behaviour and never a correctness requirement.
Source code in src/netimps/_sockets/_options.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
set_buffer_size(sock, *, receive=None, send=None)
Grow SO_RCVBUF/SO_SNDBUF, and report what was actually granted.
Returns (receive, send) as read back from the socket, never as
requested::
got_rx, got_tx = set_buffer_size(sock, receive=4 << 20)
if got_rx < 4 << 20:
log.warning("kernel granted %d of 4 MiB", got_rx)
The default UDP buffers are small -- 64 KiB on Windows -- so a burst of large datagrams overruns them and the tail is dropped, which at the protocol level looks like loss and costs a timeout per window. Raising them is the fix; knowing whether the raise took is the part that gets skipped.
The kernel is not obliged to agree, and does not say so. setsockopt
succeeds and then grants less, capped by net.core.rmem_max on Linux, and
Linux also doubles what is asked for its own bookkeeping, so a read-back
larger than the request is normal there and not a bug. Returning the
read-back is the whole point: a silent partial grant is the failure mode.
Only grows, never shrinks: a value already at or above the request is left
alone, so this cannot undo a caller's earlier tuning. None skips a
direction. A refused option is skipped rather than raised, and the
corresponding return value is whatever the socket reports.
A shortfall is logged once per process for each distinct request and
grant, at WARNING on this module's logger (netimps._sockets); the
package installs no handler. The return value reports it on every call.
Source code in src/netimps/_sockets/_options.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | |
discover_mtu(dst, *, low=576, high=9000, timeout=1.0, src=None, port=80, probe=True, method='icmp', tries=1, ipv6=None, ttl=None)
Measure the path MTU to dst in bytes, or None if undiscoverable.
Sends DF-flagged pings of growing size, binary-searching for the largest packet that survives the whole path unfragmented::
discover_mtu("example.com") # 1500, or 1420 through a VPN
This actually traverses the path, which is the difference from
:func:get_pmtu: that reports what the kernel already knows (often
nothing), while this goes and finds out. Packets really reach dst and
come back, so the answer reflects every hop in between -- including a
router that silently drops oversized DF packets without sending
"fragmentation needed", which nothing else will reveal.
Measured on one host: the local link was 9000 and get_pmtu returned
None, while this reported the true 1500.
The cost is a dozen or so probes and a destination willing to answer ICMP.
:param low: smallest MTU to consider. 576 is the IPv4 minimum every host
must accept, so anything smaller means the host is simply unreachable.
:param high: the size the search tries first as its ceiling. The path
cannot be wider than the link it leaves by, so a probe at high
that is answered does not end the search: it goes on up to that link's
MTU, and the answer is a measurement. Only when the link's MTU cannot
be read is high the ceiling, and a result equal to it then means
"at least high". A high above the link's MTU is lowered to it.
:param src: send from this interface -- same union as ping(src=).
:param port: destination port passed through to :func:get_pmtu.
:param method: how to probe. "icmp" (default) uses DF-flagged echo;
"udp" sends datagrams of growing size to port and needs
something there that replies. Use "udp" when ICMP is filtered but a
UDP service answers, or to measure what a UDP application can
actually push -- a middlebox may cap that below the ICMP-derived MTU.
``"tcp"`` **does not probe** -- it cannot: TCP is a stream and the
kernel segments it transparently, so a large ``send()`` silently
becomes many packets. It instead reads the negotiated MSS
(:func:`get_tcp_mss`) and adds the header of the family that
connected back (40 bytes for IPv4, 60 for IPv6), which is the closest
true equivalent. That is what the two *kernels agreed*,
not necessarily what a middlebox further along will pass -- use
``"icmp"`` or ``"udp"`` when the answer must be measured.
:param probe: set False to skip probing entirely and just return
:func:get_pmtu -- the kernel's cached answer, usually None.
:param tries: probes per size, passed to :func:ping for method="icmp";
tries=3 tolerates a lossy path.
:param ipv6: force the family; None takes whichever dst resolves to.
Applies to every method; the name is resolved once and the header
overhead is that of the address probed.
:param ttl: initial hop limit of the ICMP probes.
size and dont_fragment are what the search varies, so they are not
parameters.
Returns the MTU including headers (payload + 28 for IPv4 + ICMP), so it
is directly comparable with :attr:Interface.mtu. The platform ping
has a largest probe of its own (a 65500-byte payload on Windows, the
net.inet.raw.maxdgram sysctl on macOS and the BSDs: 8192 on macOS 15.7),
and so has a UDP socket there (net.inet.udp.maxdgram: a 9216-byte payload).
A destination on this host that the search takes that far is reported at
the loopback interface's MTU, since no hop narrows the path; any other
path that reaches it is retried with "udp" and is otherwise reported at
that limit, meaning "at least". Returns None when
the destination never answers -- common, since many hosts and most cloud
firewalls drop echo entirely, and that is indistinguishable from "every
size was too big" -- and also when the don't-fragment bit cannot be set
for this destination on this platform (BSD's ping6 has no DF flag).
That second None is deliberate: without DF the probe is fragmented and
reassembled, every size survives, and the search would return high as
though it had measured something.
.. note::
The result can be lower than any local Interface.mtu, and that
is the useful case: the bottleneck is somewhere along the path, not on
this host.
Source code in src/netimps/_sockets/_mtu.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | |
get_pmtu(dst, port=80, *, ipv6=None)
Return the path MTU the kernel has already learned, or None.
A lookup, not a measurement -- it reads IP_MTU (or IPV6_PATHMTU) on
a connected socket and sends nothing::
get_pmtu("example.com") # 1420, or None if nothing is cached
dst also accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used); ipv6= picks which family
a hostname is resolved to.
Instant and silent, but it answers a weaker question than
:func:discover_mtu:
Noneis a common answer. The kernel only knows a path MTU once its own discovery has learned one, which needs prior traffic that actually hit the limit. A fresh destination may report nothing.- Windows has no
IP_MTU(norIP_MTU_DISCOVER/IPV6_PATHMTU), so this always returnsNonethere. Nor is there another route: thedwForwardMtufield ofMIB_IPFORWARDROWreads 0 (verified viaGetBestRoute; Microsoft lists it as unsupported), and the newerMIB_IPFORWARD_ROW2dropped the field entirely. Route MTU lives at the interface level on Windows, which is :attr:Interface.mtu. Probing with :func:discover_mtuis the only way to learn a path MTU there. - macOS/BSD expose no IPv4 equivalent either, so v4 there is
Nonetoo. - When the kernel has an answer it can still be the local link MTU rather than the path minimum, if nothing has yet forced it lower.
.. note::
IP_MTU, IP_MTU_DISCOVER and IP_PMTUDISC_DO are not
exported by CPython on any platform, Linux included (measured on 3.13
and 3.14). Guarding on getattr(socket, "IP_MTU", None) therefore
made this function unreachable everywhere rather than on Windows only,
which is what the "usually None" story was hiding. The Linux
numbers are named from <linux/in.h> instead.
Use it as a free first guess; use :func:discover_mtu when the answer has
to be right.
Source code in src/netimps/_sockets/_pmtu.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |
get_tcp_mss(dst, port, *, timeout=3.0, ipv6=None)
Return the TCP maximum segment size negotiated with dst, or None.
The TCP counterpart to an MTU: the largest payload a single segment may carry, agreed during the handshake::
get_tcp_mss("example.com", 443) # 1460 on a 1500-MTU path
dst also accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used). ipv6= picks which family
a hostname is connected over; a destination of the other family gives
None.
This opens a real connection to read the value, then closes it.
MSS is normally the path MTU minus 40 (20 IPv4 + 20 TCP), so a reduced value is a useful signal: a VPN or tunnel is shrinking the path. Measured on one host: 1412 over a VPN where the link MTU was 1500, and 32741 on loopback.
Returns None where the platform does not expose TCP_MAXSEG or the
connection fails. Note this is what the kernels agreed, not what a
middlebox further along will actually pass -- for that, measure with
:func:discover_mtu.
Source code in src/netimps/_sockets/_pmtu.py
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
bind_error_hint(exc, port=None)
Turn a bind failure into a sentence a user can act on, or None.
The raw OSError from a failed bind is famously unhelpful, and the errno
differs per platform -- Windows reports WinError 10013/10048 where
POSIX reports EACCES/EADDRINUSE. :func:bind already puts this text
in the exception it raises; call this for an OSError that came from
somewhere else, such as a stdlib socket.bind::
try:
sock.bind(("", 67))
except OSError as exc:
raise OSError(bind_error_hint(exc, 67) or str(exc)) from exc
Returns None for anything unrecognised, so the caller keeps the
original error rather than a worse paraphrase. This does not raise --
deciding what to do with a failure belongs to the caller.
Source code in src/netimps/_sockets/_hint.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | |
get_free_port(src='127.0.0.1', *, family=None)
Return a port number that was free a moment ago.
Binds port 0, reads back whatever the OS assigned, and closes::
port = get_free_port()
server = start_my_server(port=port)
A getter, despite "free" in the name -- it acquires a number, it does not release anything. The port is not held open for you.
.. warning::
Inherently racy. The port is released the instant this returns, so
another process can take it before you bind. There is no way around that
with a returned port number -- if you can, bind port 0 in the server
itself and read back getsockname() instead of calling this.
SO_REUSEADDR is deliberately not set: it would let the OS hand back
a port still in TIME_WAIT, which then fails or steals traffic when the
caller binds it for real.
src is any host, as for :func:bind, and family follows it the way
it does there: an IPv6 address gives AF_INET6, an IPv4 one or the
default AF_INET.
Source code in src/netimps/_sockets/_bind.py
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 | |
get_source_ip(dst=_DEFAULT_PROBE, port=80, *, ipv6=None)
Return the local address the kernel would use to reach dst.
Answers "which of my addresses is the real one for this destination?" -- the question a hostname lookup gets wrong on any host with VMs, containers or a VPN::
get_source_ip() # IPv4Address('192.0.2.10')
get_source_ip("192.168.1.1") # the LAN-facing address
get_source_ip("2001:4860::8888") # an IPv6 src address
No packets are sent. connect() on a UDP socket only fixes the
socket's local endpoint by consulting the routing table, so this is
immediate and invisible to dst.
The answer depends on dst: with a VPN up, a public probe returns the
tunnel address while a LAN probe returns the physical one. Pass the address
you actually intend to talk to rather than trusting the default.
:param ipv6: which family to probe -- True for IPv6, False for
IPv4, None (the default) for whatever dst resolves to. The
family is not guessed from ":" in dst: a hostname never contains
a colon, so that test would probe every name as IPv4 and a v6-only
one would answer None.
Returns None if no route exists (e.g. IPv6 probe on an IPv4-only host).
The returned address carries no %zone: the zone identifies the adapter
rather than the address, and :func:get_interface is the way back to it.
Source code in src/netimps/_sockets/_connect.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
count_hops(dst, *, max_hops=30, timeout=1.0, allow_traceroute=True, ipv6=None)
Return the number of hops to dst, or None if it never answers.
Sends TTL-limited probes and counts the routers that reply, the same
technique traceroute uses::
count_hops("8.8.8.8") # 12
dst also accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used).
Uses raw-socket probes when available (root/Administrator), and otherwise
falls back to driving the system traceroute/tracert, so this works
unprivileged on a normal desktop. Pass allow_traceroute=False to require
the in-process path, which then raises :class:PermissionError instead of
shelling out.
The fallback is slower (seconds) because it runs a whole trace. Only the hop number and the destination address are read from its output, never the localised prose, so it is not locale-dependent.
:param ipv6: which family to resolve dst to -- True v6, False
v4, None (the default) whichever the resolver answers with. The
probes follow: ICMPv6 with IPV6_UNICAST_HOPS for a v6 target, and
the platform's v6 traceroute. gethostbyname is IPv4-only: through
it a v6 destination would return None, read as "never answered"
rather than "never asked".
Returns None when the destination never responds within max_hops.
That is common and usually not a missing route: host firewalls (Windows
Firewall in particular) routinely drop inbound ICMP even for an elevated
process, so None here means "no answer", never "unreachable". Treat it
as unknown rather than as a negative result.
Source code in src/netimps/_sockets/_hops.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | |
get_route(dst=_DEFAULT_PROBE, *, ipv6=None)
Return how traffic to dst leaves this host.
Reports the src address and the first hop -- the gateway a packet is
handed to, or None when the destination is on-link::
r = get_route("8.8.8.8")
r.src # IPv4Address('192.0.2.10')
r.gateway # IPv4Address('192.0.2.1')
r.on_link # False
get_route("127.0.0.1").on_link # True -- no router involved
dst also accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used). A hostname is resolved
through getaddrinfo, so ipv6= selects which of its records the
route is computed for; the IPv4-only gethostbyname used here before
meant an AAAA-only name reached no lookup at all.
First hop only, deliberately: it is available unprivileged on every
supported platform, whereas the full path requires raw sockets. See
:func:count_hops for distance, which does not.
Both address families are looked up, through GetBestRoute2 on Windows,
/proc/net/route and /proc/net/ipv6_route on Linux, and
route -n get on macOS/BSD -- the one platform where this spawns a
short-lived process, because there is no /proc to read and the
PF_ROUTE socket is the only other option. Where the lookup cannot be
made -- no route, no source to read, an unresolvable name -- gateway
is None and so is on_link, which is how "we did not find out"
is spelled. on_link is True means a real on-link determination.
Never raises for an unknown route: unknown pieces come back as
None/0 rather than an error. A network passed as dst still
raises :class:TypeError.
Source code in src/netimps/_sockets/_route.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | |
tcp_check(dst, port, *, timeout=3.0)
Return True if a TCP connection to dst:port is accepted.
The honest reachability test, and what you almost always want instead of
:func:netimps.ping: it proves the service answers, not merely that the
host replies to ICMP echo (which most cloud firewalls drop anyway)::
tcp_check("example.com", 443)
tcp_check("db.internal", 5432, timeout=1.0)
dst also accepts an address object or an :class:IPv4Interface/
:class:IPv6Interface (its .ip is used).
Never raises for a reachability outcome: refused, timed out, unresolvable
and unreachable all yield False. Only TCP handshake completion is
checked -- not that the service behind the port is healthy. Two argument
bugs are still raised rather than answered, because answering them would
be answering a different question: a network
(:class:IPv4Network/:class:IPv6Network) as dst raises
:class:TypeError, and a port outside 0-65535 raises
:class:ValueError instead of being masked to 16 bits by the socket layer
(port + 65536 silently answered about port).
:param timeout: bounds the whole call, across every address dst
resolves to. socket.create_connection would apply it once per
resolved address after an unbounded getaddrinfo, so a name with N
addresses could take N x timeout; here resolution happens once and
the connects share one monotonic deadline. 0 is floored to a small
positive value rather than taken
literally -- settimeout(0) means non-blocking, which reported every
open port as closed. None means no timeout at all.
.. note::
Not the same question as ping(dst, method="tcp", port=...). This
asks "is the service up?", so a refused connection is False. That
asks "is the host up?", and counts a refusal as success -- the RST
proves something answered. Same distinction as a service check versus an
ICMP echo. :func:wait_for_port and the scanners build on this one,
because they care about the service.
Source code in src/netimps/_sockets/_connect.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
wait_for_port(dst, port, *, deadline=30.0, interval=0.1, timeout=None)
Poll until dst:port accepts a connection, or deadline elapses.
The "wait for the service to come up" loop every deploy and container script contains::
if not wait_for_port("localhost", 5432, deadline=60):
raise RuntimeError("database never started")
dst accepts the same forms as :func:tcp_check (address objects,
IPv4Interface/IPv6Interface).
:param interval: delay between attempts. Backs off, growing by half each
round, up to the larger of 1s and interval so a long wait does not
spin and an interval above a second is never shortened.
:param deadline: seconds the whole wait may take.
:param timeout: per-attempt connect timeout; defaults to interval
bounded to at least 1s.
Returns True as soon as the port answers, False once deadline
has passed. The deadline is honoured overall, so this cannot overrun by
more than one attempt regardless of how long individual connects block:
:func:tcp_check bounds itself overall rather than per resolved address,
so a dst resolving to N addresses does not stretch an attempt to N.
An out-of-range port raises :class:ValueError (from
:func:tcp_check) rather than being masked to 16 bits.
Source code in src/netimps/_sockets/_connect.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
await_for_port(dst, port, *, deadline=30.0, interval=0.1, timeout=None)
async
:func:wait_for_port as a coroutine: poll until dst:port accepts.
::
if not await await_for_port("localhost", 5432, deadline=60):
raise RuntimeError("database never started")
Every argument means what it means on :func:wait_for_port, and so do the
True and False it returns and the :class:ValueError for a port
out of range. Each attempt is a non-blocking connect on the running loop and
each wait is asyncio.sleep, so the loop keeps running and no thread is
used for an address literal; a name is looked up with loop.getaddrinfo,
which uses the loop's executor. Cancelling the task closes the socket of the
attempt in progress and leaves nothing behind.
The deadline is a ceiling on the whole wait: an attempt is given no more
than what is left of it, so the call returns within a small fraction of a
second after deadline whatever the connect is doing.
Source code in src/netimps/_sockets/_aconnect.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |