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.
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; the only runtime dependency is dnspython, used solely
inside :func:resolve.
::
import netimps
netimps.parse("10.0.0.5") # -> IPv4Address
netimps.MACAddress("AA:BB:CC:DD:EE:FF").as_str("-")
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.
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 -- which is the case a bare
get_ip() handles badly, since it returns None and loses the name.
Source code in src/netimps/_ip.py
367 368 369 370 371 372 | |
is_address
property
True if the value is already an IP literal -- no DNS needed.
ip(refresh=False)
Resolve to an address, or None.
A literal is parsed directly; a hostname goes to DNS. The result is
cached, including a failure, because the common use is several
lookups in a row on the same object. Pass refresh=True to retry --
a name that failed once may resolve later.
Source code in src/netimps/_ip.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 | |
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/Cisco (aabb.ccdd.eeff) or bare
(AABBCCDDEEFF) -- as well as an int or another MACAddress. The
value is normalised to lowercase and compared/hashed by its canonical bytes,
so instances are usable as dict keys and set members.
as_str(sep) renders the address with an arbitrary separator between
octets; sep="" produces the bare form.
Source code in src/netimps/_mac.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
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 3-byte Organisationally Unique Identifier (vendor prefix).
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. Declared as a :data:typing.TypeGuard, so a checker
narrows value in the True branch.
Source code in src/netimps/_mac.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
try_parse(value)
classmethod
Return a MACAddress, or None if value is not one.
The type-local spelling of netimps.try_parse(value, MACAddress).
Prefer it to :meth:is_valid followed by construction -- one call, and
no window in which the two disagree.
Source code in src/netimps/_mac.py
97 98 99 100 101 102 103 104 105 106 107 108 | |
as_str(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.as_str("-") # 'aa-bb-cc-dd-ee-ff'
mac.as_str("-", upper=True) # 'AA-BB-CC-DD-EE-FF'
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
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
Interface(name, index=0, mac=None, ips=None, mtu=None, raw=None)
One network interface, normalised to be identical across platforms.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
Human-usable adapter name ( |
|
index |
:func: |
|
mac |
The hardware address, or |
|
ips |
Every address bound to the interface, each as an
|
|
mtu |
Link MTU in bytes, or |
|
raw |
|
Source code in src/netimps/_ifaddrs.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
is_loopback
property
True when this is the loopback interface.
Derived from the addresses rather than the name: lo (Linux),
lo0 (macOS) and Loopback Pseudo-Interface 1 (Windows) share no
common spelling, but 127.0.0.0/8 and ::1 do.
Requires a loopback address and no routable one. Not "every
address is loopback": macOS's lo0 also carries fe80::1/64, so
an all-must-match test reports it as non-loopback. Link-local addresses
are ignored here for that reason -- they are not routable, so they do
not make an interface non-loopback.
ipv4
property
Just the IPv4 addresses.
ipv6
property
Just the IPv6 addresses.
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. A
non-loopback entry wins; a loopback one is returned only when that
is genuinely all the interface has::
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
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.py
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 | |
PingResult(ok, host, rtt_ms=None, ttl=None, src=None, attempts=1)
Outcome of a :func:ping, usable directly as a boolean.
ping() has always answered "did it reply?", so this stays truthy on
success and falsy on failure -- if ping(host): keeps working -- while
carrying the details a caller would otherwise re-run ping to scrape.
Attributes:
| Name | Type | Description |
|---|---|---|
ok |
whether the destination replied. |
|
host |
the destination as given. |
|
rtt_ms |
round-trip time in milliseconds, or |
|
ttl |
TTL/hop-limit of the reply, or |
|
src |
address that answered, which on success is the destination. |
|
attempts |
how many probes were sent before this outcome. |
Source code in src/netimps/_ping.py
47 48 49 50 51 52 53 | |
Datagram
Bases: NamedTuple
One received datagram and where it came from.
Attributes:
| Name | Type | Description |
|---|---|---|
data |
bytes
|
the payload. |
sender |
Any
|
|
local_address |
Optional[Any]
|
the address the datagram was sent to, or |
interface_index |
int
|
receiving interface index, or |
interface |
Optional[Any]
|
the resolved :class: |
UdpEndpoint(sock, pktinfo=True)
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.
:param pktinfo: request arrival-interface data. True (the default)
enables it where supported and is a no-op elsewhere.
Source code in src/netimps/_udp.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
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 IP_PKTINFO is unavailable this still works; the interface
fields are simply empty.
Source code in src/netimps/_udp.py
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 | |
send(data, address, port, src=None)
Send a datagram, optionally forcing the src interface.
src accepts the usual union (an :class:Interface, a MAC, an
adapter name or an address). Where IP_PKTINFO is available this
pins the outgoing interface via sendmsg -- which matters when
replying to a broadcast on a multi-homed host, since the routing table
would otherwise pick for you.
Falls back to plain sendto when unsupported, so the call works
everywhere; the src is then whatever the kernel chooses.
Source code in src/netimps/_udp.py
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 | |
Route(dst, src=None, gateway=None, interface_index=0)
How traffic to a destination leaves this host.
Attributes:
| Name | Type | Description |
|---|---|---|
dst |
the destination this route was computed for. |
|
src |
local address the kernel would use (see :func: |
|
gateway |
next-hop router, or |
|
interface_index |
index of the outgoing interface, |
|
on_link |
bool
|
True when no gateway is needed. |
Source code in src/netimps/_sockets.py
475 476 477 478 479 | |
on_link
property
True when the destination is reachable without a router.
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.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
get_ip(address)
Resolve a hostname or literal address to an address object, or None.
Tries to parse address as a literal first and falls back to a DNS
lookup, returning None if both fail::
get_ip("10.0.0.5") # IPv4Address('10.0.0.5') -- no DNS traffic
get_ip("example.com") # IPv4Address('93.184.216.34')
get_ip("nonexistent.") # None
.. note::
The difference from try_parse(address) matters: that never
touches the network, while this may block on DNS. Use try_parse
to validate user input; use get_ip when you genuinely want a name
resolved.
Source code in src/netimps/_ip.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
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.py
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
normalize_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::
normalize_host("example.com:8080") # ('example.com', 8080)
normalize_host("10.0.0.5") # ('10.0.0.5', None)
normalize_host("[::1]:8080") # ('::1', 8080)
normalize_host("::1") # ('::1', None) -- not port 1
normalize_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)). default_port is
used when no port is present.
Raises :class:ValueError on empty input, an unclosed bracket, or a port
that is not an integer in 0-65535.
Source code in src/netimps/_ip.py
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 | |
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.py
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 | |
parse(value, type=IPAddress, **kwargs)
parse(
value: object, type: TypeForm[_T], **kwargs: Any
) -> _T
parse(
value: object, type: Callable[..., _T], **kwargs: Any
) -> _T
parse(value: object, **kwargs: Any) -> 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. Extra
kwargs pass through to the underlying builder.
Raises :class:ValueError on malformed input or a family mismatch, and
:class:TypeError for an unusable type. Use :func:try_parse for the
non-raising form.
Source code in src/netimps/__init__.py
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 | |
try_parse(value, type=IPAddress, default=None, **kwargs)
try_parse(
value: object,
type: TypeForm[_T],
default: None = ...,
**kwargs: Any
) -> Optional[_T]
try_parse(
value: object,
type: TypeForm[_T],
default: _D,
**kwargs: Any
) -> Union[_T, _D]
try_parse(
value: object,
type: Callable[..., _T],
default: None = ...,
**kwargs: Any
) -> Optional[_T]
try_parse(
value: object,
type: Callable[..., _T],
default: _D,
**kwargs: Any
) -> Union[_T, _D]
try_parse(
value: object, *, default: None = ..., **kwargs: Any
) -> Optional[IPAddress]
try_parse(
value: object, *, default: _D, **kwargs: Any
) -> Union[IPAddress, _D]
Return type(value), or default if it rejects the input. Never raises.
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/__init__.py
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 | |
is_valid(value, type=IPAddress, **kwargs)
is_valid(
value: object, type: TypeForm[_T], **kwargs: Any
) -> bool
is_valid(
value: object, type: Callable[..., _T], **kwargs: Any
) -> bool
is_valid(value: object, **kwargs: Any) -> bool
Return True if value parses as type. Never raises.
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/__init__.py
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 | |
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("nope") # None
Case-insensitive. Extend the table with :func:register_port.
Source code in src/netimps/_scheme.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
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. Where several schemes share a port, the
canonical one is returned -- see :func:register_port.
Source code in src/netimps/_scheme.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
register_port(scheme, port, canonical=False)
Register (or override) a scheme's conventional port.
The built-in table covers the common cases, but every consumer eventually has a protocol of its own::
register_port("myproto", 9999)
get_default_port("myproto") # 9999
get_default_scheme(9999) # 'myproto'
: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.
Source code in src/netimps/_scheme.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 | |
get_interfaces(raw=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.
Never raises for enumeration failure: if the native call is unavailable it
degrades to a hostname-resolution fallback in which prefixes are not
real (every address becomes a /32//128 under an interface named
"<unknown>").
Source code in src/netimps/_ifaddrs.py
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 | |
iter_addresses(interfaces=None, family=None)
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". Consumers 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 6 to yield only that family; None for both.
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.py
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 | |
resolve(query, rdtype='a', ns=None, timeout=5.0, port=53, tcp=False)
Resolve query via DNS and return the answers as a list of strings.
::
resolve("example.com") # ['93.184.216.34']
resolve("example.com", "aaaa")
resolve("example.com", "mx", ns="1.1.1.1")
Contract: always a list, empty when the name does not resolve --
never None. Callers can therefore write if result: and index
result[0] safely.
Records come back as native types: address records (A/AAAA) are
:class:ipaddress objects, everything else is a str::
resolve("example.com")[0].is_private # an IPv4Address, not "1.2.3.4"
resolve("example.com", "mx") # ['10 mail.example.com']
resolve("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 reverse types) to look up.
:param rdtype: DNS record type ("a", "aaaa", "mx" ...). Second
because it is the argument callers actually vary.
:param ns: optional nameserver, or list of nameservers, to query instead of
the system resolver.
: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.
A genuine lookup failure (NXDOMAIN, no answer, timeout, all servers failed)
yields []; a malformed query or unknown record type raises
:class:ValueError, since that is a caller bug rather than a DNS result.
Requires the dnspython package (installed with netimps).
Source code in src/netimps/_dns.py
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 | |
ping(dst, tries=1, timeout=1.0, ipv6=None, src=None, size=None, ttl=None, dont_fragment=False, method='icmp', port=None)
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_ms # 5.0
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
:param tries: attempts before giving up. Values below 1 are treated as 1.
:param timeout: seconds to wait per attempt. POSIX ping only accepts a
whole number of seconds, so sub-second values are rounded up to 1 --
never down to 0, which some implementations read as "wait forever".
:param ipv6: force the IPv6 (-6) or IPv4 (-4) binary. None
(default) lets the system resolver decide.
: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 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 POSIX -- the
letters are swapped between platforms, a classic src 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.
: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).
Combined with size, the standard manual MTU probe: the largest
size that still succeeds is the path MTU minus 28. Unsupported on
macOS/BSD ping, where it is ignored.
An empty dst gives a falsy result. Never raises: a missing ping
binary or a non-zero exit both yield a falsy :class:PingResult.
.. 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.py
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 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 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 | |
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.
Same forms as :func:scan_ports.
:param timeout: per-connection timeout.
: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): a /8 sweep is 16 million hosts, which is a mistake rather than an intention.
Source code in src/netimps/_scan.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 | |
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
:param ports: a :data:PORT_RANGES name ("common", "well-known",
"all"), a scheme name resolved via :func:get_default_port, a port
number, or any iterable mixing those. A range name wins over a scheme
name where the two collide.
: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.
: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.
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).
Source code in src/netimps/_scan.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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
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
Never raises: anything unparseable is False.
Source code in src/netimps/_multicast.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
join_group(sock, group, interface=None)
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.
Raises :class:ValueError for a non-multicast group or an interface with no
usable address, and :class:OSError if the kernel rejects the join.
Source code in src/netimps/_multicast.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | |
leave_group(sock, group, interface=None)
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.
Source code in src/netimps/_multicast.py
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
multicast_socket(group=None, port=0, interface=None, ttl=1, loop=True, bind=True, reuse=True)
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.
: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.
The caller owns the socket and should close it; closing drops membership.
Raises :class:ValueError for a non-multicast group, :class:OSError if
binding or joining fails.
Source code in src/netimps/_multicast.py
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 | |
backoff_delays(attempts=3, delay=0.5, multiplier=2.0, max_delay=30.0, jitter=0.1, _random=None)
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.
Delays are capped at max_delay. Jitter is applied after the cap and
only ever reduces the wait, so max_delay is a genuine ceiling.
Source code in src/netimps/_retry.py
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 | |
retry(func, attempts=3, delay=0.5, multiplier=2.0, max_delay=30.0, jitter=0.1, retryable=DEFAULT_RETRYABLE, on_retry=None, _sleep=_time.sleep, _random=None)
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. Anything else
propagates immediately: a ValueError means the call is malformed
and will fail identically next time.
: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.
Synchronous by design -- it blocks. For async, drive :func:backoff_delays
from your own loop.
Source code in src/netimps/_retry.py
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 | |
bind(address='', port=0, *, family=_socket.AF_INET, kind=_socket.SOCK_DGRAM, reuse_address=True, reuse_port=False, broadcast=False, interface=None, 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.
:param port: local port; 0 lets the OS choose.
: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 reuse_port: sets SO_REUSEPORT. A no-op where the option does
not exist (Windows) rather than an error, so the same call works
everywhere.
:param options: extra (level, name, value) triples for anything not
covered by the named arguments.
:param listen: call listen(backlog) after binding. Ignored for
datagram sockets, where it is meaningless.
Raises :class:OSError if the bind fails -- see :func:bind_error_hint
for turning that into something a user can act on. The socket is closed
before the exception propagates, so a failed call leaks nothing.
Source code in src/netimps/_sockets.py
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 | |
discover_mtu(dst, low=576, high=9000, timeout=1.0, src=None, port=80, probe=True, method='icmp', **ping_kwargs)
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: largest to consider. 9000 covers jumbo frames; the search
confirms the ceiling first, so a generous value costs one probe.
: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 40-byte IPv4+TCP header back, 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 ping_kwargs: passed straight to :func:ping for method="icmp",
so anything it accepts works here -- ipv6=True to force the family,
tries=3 to tolerate a lossy path. size and dont_fragment
are set by the search itself and cannot be overridden.
Returns the MTU including headers (payload + 28 for IPv4 + ICMP), so it
is directly comparable with :attr:Interface.mtu. 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".
.. 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.py
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 | |
get_pmtu(dst, port=80)
Return the path MTU the kernel has already learned, or None.
A lookup, not a measurement -- it reads IP_MTU on a connected socket
and sends nothing::
get_pmtu("example.com") # 1420, or None if nothing is cached
Instant and silent, but it answers a weaker question than
:func:discover_mtu:
Noneis the 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 reports nothing.- Windows has no
IP_MTU(norIP_MTU_DISCOVER/IP_DONTFRAG), 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. - 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.
Use it as a free first guess; use :func:discover_mtu when the answer has
to be right.
Source code in src/netimps/_sockets.py
825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 | |
get_tcp_mss(dst, port, timeout=3.0)
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
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.py
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 | |
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::
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.py
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 | |
interface_for(query, strict=True)
Return the first local interface matching query, or None.
The reverse of interface enumeration -- "a socket is bound here, which adapter is that?"::
interface_for(sock.getsockname()[0])
Accepts the same query forms as :func:interfaces_for; singular lookup is
exactly the first plural result. Since addresses can appear on more than
one adapter (especially unscoped IPv6 link-local addresses), 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 legacy callers can still attribute
traffic. Network and MAC misses cannot be synthesized honestly and
remain None.
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/_sockets.py
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 | |
interfaces_for(query)
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. Invalid queries and misses yield
nothing. Each matching interface is yielded once even if several of its
assigned addresses fall within a requested network.
Source code in src/netimps/_sockets.py
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
is_local_address(address)
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. Malformed input raises exactly as :func:parse does.
Source code in src/netimps/_sockets.py
300 301 302 303 304 305 306 307 308 309 310 311 312 | |
get_free_port(src='127.0.0.1', family=_socket.AF_INET)
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.
Source code in src/netimps/_sockets.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 | |
get_source_ip(dst=_DEFAULT_PROBE, port=80)
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.
Returns None if no route exists (e.g. IPv6 probe on an IPv4-only host).
Source code in src/netimps/_sockets.py
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 | |
hop_count(dst, max_hops=30, timeout=1.0, allow_traceroute=True)
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::
hop_count("8.8.8.8") # 12
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.
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.py
724 725 726 727 728 729 730 731 732 733 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 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 | |
get_route(dst=_DEFAULT_PROBE)
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
First hop only, deliberately: it is available unprivileged on every
supported platform, whereas the full path requires raw sockets. See
:func:hop_count for distance, which does not.
Never raises: unknown pieces come back as None/0 rather than an
error. The gateway is only resolvable on Windows and Linux; elsewhere it
stays None, so use .gateway is None to mean "on-link" only when
.src is also set.
Source code in src/netimps/_sockets.py
600 601 602 603 604 605 606 607 608 609 610 611 612 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 638 639 640 641 642 643 644 645 646 647 648 649 | |
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)
Never raises: refused, timed out, unresolvable and unreachable all yield
False. Only TCP handshake completion is checked -- not that the service
behind the port is healthy.
.. 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.py
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 | |
wait_for_port(dst, port, timeout=30.0, interval=0.1, connect_timeout=None)
Poll until dst:port accepts a connection, or timeout elapses.
The "wait for the service to come up" loop every deploy and container script contains::
if not wait_for_port("localhost", 5432, timeout=60):
raise RuntimeError("database never started")
:param interval: delay between attempts. Backs off up to 1s so a long wait
does not spin.
:param connect_timeout: per-attempt connect timeout; defaults to
interval bounded to at least 1s.
Returns True as soon as the port answers, False on timeout. The
deadline is honoured overall, so this cannot overrun by more than one
attempt regardless of how long individual connects block.
Source code in src/netimps/_sockets.py
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 | |