netimps 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netimps-0.2.0 → netimps-0.2.2}/AGENTS.md +35 -14
- {netimps-0.2.0 → netimps-0.2.2}/CHANGELOG.md +88 -1
- {netimps-0.2.0 → netimps-0.2.2}/PKG-INFO +8 -4
- {netimps-0.2.0 → netimps-0.2.2}/README.md +5 -2
- {netimps-0.2.0 → netimps-0.2.2}/pyproject.toml +2 -1
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/AGENTS.md +108 -42
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/__init__.py +3 -1
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_dns.py +180 -70
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_iface_spec.py +85 -2
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_ip.py +81 -17
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_multicast.py +62 -34
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_ping.py +172 -57
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_scan.py +17 -8
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_sockets.py +66 -22
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_udp.py +12 -3
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/cli.py +2 -2
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_ip.py +66 -0
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_net.py +322 -8
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_scan.py +139 -1
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_sockets.py +242 -0
- {netimps-0.2.0 → netimps-0.2.2}/.gitignore +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/LICENSE +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/docs/api/reference.md +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/docs/changelog.md +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/docs/index.md +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/mkdocs.yml +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/__main__.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_ifaddrs.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_mac.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_retry.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_scheme.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/src/netimps/py.typed +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_centralized.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_cli.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_interfaces.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/tests/test_mac.py +0 -0
- {netimps-0.2.0 → netimps-0.2.2}/tests/typing/api.py +0 -0
|
@@ -62,19 +62,22 @@ Requires Python 3.9+. No hard runtime dependencies.
|
|
|
62
62
|
|
|
63
63
|
```
|
|
64
64
|
src/netimps/
|
|
65
|
-
├── __init__.py
|
|
66
|
-
├── _ip.py
|
|
67
|
-
├── _mac.py
|
|
68
|
-
├── _scheme.py
|
|
69
|
-
├── cli.py
|
|
70
|
-
├── __main__.py
|
|
71
|
-
├── _scan.py
|
|
72
|
-
├── _multicast.py
|
|
73
|
-
├── _ifaddrs.py
|
|
74
|
-
├── _sockets.py
|
|
75
|
-
├── _dns.py
|
|
76
|
-
├── _ping.py
|
|
77
|
-
|
|
65
|
+
├── __init__.py # the public surface: generic parse/try_parse/is_valid
|
|
66
|
+
├── _ip.py # private: IP type aliases, builder tables, IP helpers
|
|
67
|
+
├── _mac.py # private: MACAddress value type
|
|
68
|
+
├── _scheme.py # private: scheme <-> port registry
|
|
69
|
+
├── cli.py # public: duho-backed CLI (needs the `cli` extra)
|
|
70
|
+
├── __main__.py # `python -m netimps`
|
|
71
|
+
├── _scan.py # private: concurrent port/host scanning
|
|
72
|
+
├── _multicast.py # private: group membership and socket setup
|
|
73
|
+
├── _ifaddrs.py # private: ctypes getifaddrs/GetAdaptersAddresses bindings
|
|
74
|
+
├── _sockets.py # private: source IP, free port, tcp/wait, route, hops, MTU
|
|
75
|
+
├── _dns.py # private: resolve() chaining dnspython/system/nslookup backends
|
|
76
|
+
├── _ping.py # private: ping() over the platform binary
|
|
77
|
+
├── _retry.py # private: bounded retry with exponential backoff
|
|
78
|
+
├── _udp.py # private: UDP receive with arrival interface (IP_PKTINFO)
|
|
79
|
+
├── _iface_spec.py # private: shared InterfaceSpec coercion (MAC/name/Interface -> address)
|
|
80
|
+
└── py.typed # PEP 561 marker — the package ships inline type hints
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
**The import surface is still flat** — everything is re-exported from
|
|
@@ -96,6 +99,7 @@ map:
|
|
|
96
99
|
| --- | --- |
|
|
97
100
|
| `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
|
|
98
101
|
| `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
102
|
+
| `AddressLike` | accepted-input union for a single destination (hostname, address, or interface object) |
|
|
99
103
|
| `IPv4Address`, `IPv4Interface`, `IPv4Network`, `IPv6Address`, `IPv6Interface`, `IPv6Network` | stdlib concrete-type re-exports |
|
|
100
104
|
| `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
|
|
101
105
|
| `MACAddress` | parse / classify / render MAC addresses |
|
|
@@ -104,7 +108,7 @@ map:
|
|
|
104
108
|
| `collapse`, `subtract` | CIDR set maths |
|
|
105
109
|
| `normalize_host` | `host:port` splitting, IPv6-aware |
|
|
106
110
|
| `get_default_port`, `get_default_scheme`, `register_port` | scheme ↔ port registry |
|
|
107
|
-
| `resolve` | DNS lookup → native records (`[]` on failure) |
|
|
111
|
+
| `resolve`, `resolve_dnspython`, `resolve_system`, `resolve_nslookup` | DNS lookup → native records (`[]` on failure); `resolve` chains the three backends, each independently callable |
|
|
108
112
|
| `ping`, `PingResult` | reachability with RTT and TTL |
|
|
109
113
|
| `bind`, `bind_error_hint`, `interface_for`, `interfaces_for`, `is_local_address` | socket creation and local membership |
|
|
110
114
|
| `get_source_ip`, `get_free_port`, `tcp_check`, `wait_for_port` | socket helpers |
|
|
@@ -144,6 +148,23 @@ map:
|
|
|
144
148
|
- **Check for silent platform gaps before adding a socket option.** `IP_MTU`,
|
|
145
149
|
`IP_MTU_DISCOVER`, `IP_DONTFRAG` and `SO_REUSEPORT` do not exist on Windows;
|
|
146
150
|
binding a multicast socket to the group address fails there too.
|
|
151
|
+
- **IPv6 multicast names an adapter by *index*, IPv4 by *address*.** They are
|
|
152
|
+
not two spellings of one thing: feeding an address to the v6 side does not
|
|
153
|
+
raise, it lands as index `0`, which is "kernel's choice". Use
|
|
154
|
+
`_iface_spec.interface_index()` for anything v6, `interface_address()` for
|
|
155
|
+
v4.
|
|
156
|
+
- **POSIX delivers asynchronous ICMP errors only to *connected* UDP sockets.**
|
|
157
|
+
An unconnected probe never sees a port-unreachable and just times out, while
|
|
158
|
+
Windows reports it either way — so the unconnected version tests green here
|
|
159
|
+
and under-reports on Linux CI. Also note `_discover_mtu_udp` is still
|
|
160
|
+
unconnected by design; it treats such errors as failure anyway.
|
|
161
|
+
- **`ThreadPoolExecutor.__exit__` calls `shutdown(wait=True)`,** and its atexit
|
|
162
|
+
hook joins worker threads too. It is therefore the wrong tool for bounding a
|
|
163
|
+
blocking call: a daemon `threading.Thread` joined through a queue is what
|
|
164
|
+
`_dns._resolve_system_once` uses, and why.
|
|
165
|
+
- **Match a ping reply by address token, and remember hostnames are plural.**
|
|
166
|
+
`gethostbyname` is IPv4-only — use `getaddrinfo` with an explicit family, or
|
|
167
|
+
`ipv6=` silently does nothing.
|
|
147
168
|
- **Windows exposes no cached path MTU.** Already investigated, so do not
|
|
148
169
|
re-derive it: `MIB_IPFORWARDROW.dwForwardMtu` reads 0 (unsupported), and
|
|
149
170
|
`MIB_IPFORWARD_ROW2` has no MTU field. `Interface.mtu` is the link MTU;
|
|
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.2] - 2026-08-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`resolve_system(timeout=)` now bounds wall time.** The lookup ran inside a
|
|
15
|
+
`with ThreadPoolExecutor(...)` block, whose `__exit__` joins the worker
|
|
16
|
+
still blocked in `getaddrinfo` -- so the timeout changed *what* was raised
|
|
17
|
+
but not *when*, and a 30s resolver hang still cost the caller 30s despite
|
|
18
|
+
`timeout=5.0`. `resolve()`'s chain consequently never reached `nslookup` at
|
|
19
|
+
the promised deadline either.
|
|
20
|
+
- **`interface=` is honoured for IPv6 multicast.** The spec was reduced to an
|
|
21
|
+
address and then passed to `if_nametoindex()`, which always fails for an
|
|
22
|
+
address string, so the `IPV6_JOIN_GROUP` index silently stayed `0` --
|
|
23
|
+
"kernel's choice", the exact default `interface=` exists to override.
|
|
24
|
+
`IPV6_MULTICAST_IF` was never set at all, so sends left by the default
|
|
25
|
+
route while joins listened elsewhere. Both now resolve the adapter to its
|
|
26
|
+
interface index. IPv4 behaviour is unchanged.
|
|
27
|
+
- **`ping(hostname, ipv6=True)` is no longer always false.** The reply address
|
|
28
|
+
was resolved with the IPv4-only `gethostbyname`, so a v6 reply was checked
|
|
29
|
+
against a v4 expectation and never matched. The expectation now comes from
|
|
30
|
+
`getaddrinfo` honouring `ipv6=`, and a name resolving to several addresses
|
|
31
|
+
counts as answered if the reply came from any of them.
|
|
32
|
+
- **`ping(..., method="tcp"/"udp")` reaches IPv6 destinations.** Both probes
|
|
33
|
+
opened `AF_INET` sockets unconditionally, so a v6 destination failed inside
|
|
34
|
+
`connect`/`sendto` and was reported as unreachable -- a wrong falsy answer
|
|
35
|
+
rather than an error. `ipv6=` now applies to all three methods.
|
|
36
|
+
- **`ping(..., method="udp")` detects ICMP port-unreachable on POSIX.** The
|
|
37
|
+
probe socket was never connected, and POSIX delivers asynchronous ICMP
|
|
38
|
+
errors only to connected UDP sockets -- so the documented "host answered,
|
|
39
|
+
nothing listening" signal worked on Windows alone and the probe just timed
|
|
40
|
+
out on Linux/macOS. `ECONNREFUSED` and `ECONNRESET` both now count.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- The shipped API header named `PingResult.source` and `Route.source`; both
|
|
45
|
+
attributes are spelled `.src` (and `PingResult.host` was unlisted).
|
|
46
|
+
- Recorded the per-family multicast interface selection, `ping`'s `ipv6=`
|
|
47
|
+
reach across all three methods, and `resolve_system`'s real wall-time
|
|
48
|
+
deadline in the shipped header.
|
|
49
|
+
- Added the known, still-unverified macOS/BSD `ping6` reply-shape gap
|
|
50
|
+
(`from <addr>,` rather than `from <addr>:`) to the header rather than
|
|
51
|
+
guessing a parser change without a macOS runner.
|
|
52
|
+
|
|
53
|
+
## [0.2.1] - 2026-07-30
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **`AddressLike`**, a new type alias (`str | IPv4Address | IPv6Address |
|
|
58
|
+
IPv4Interface | IPv6Interface`) accepted by every `dst`-typed parameter:
|
|
59
|
+
`ping`, `tcp_check`, `wait_for_port`, `get_route`, `hop_count`, `get_pmtu`,
|
|
60
|
+
`discover_mtu`, `get_tcp_mss`, `scan_ports(host)`, `get_ip`,
|
|
61
|
+
`UdpEndpoint.send`, and `resolve`'s (and its backends') `query`. An
|
|
62
|
+
`IPv4Interface`/`IPv6Interface` unwraps to its `.ip` -- previously passing
|
|
63
|
+
one stringified with its `/prefix` intact, which every consumer
|
|
64
|
+
(subprocess argument, socket call, DNS query) read as garbage. A network
|
|
65
|
+
(`IPv4Network`/`IPv6Network`) raises `TypeError`, since it has no single
|
|
66
|
+
address to use.
|
|
67
|
+
- **`resolve()` (and all three backends) auto-select `rdtype`.** It now
|
|
68
|
+
defaults to `None`, which picks `"ptr"` when `query` is an address literal
|
|
69
|
+
and `"a"` otherwise -- `resolve("8.8.8.8")` now returns `['dns.google']`
|
|
70
|
+
instead of attempting a nonsensical A lookup on a literal address. Pass an
|
|
71
|
+
explicit `rdtype` to opt out. `resolve_system()` gains `"ptr"` support (via
|
|
72
|
+
`socket.gethostbyaddr()`) to make this work across every backend.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **`ping(src=...)` crashed with `NameError` instead of returning a falsy
|
|
77
|
+
result** when `src` named an interface with no usable address (e.g. an
|
|
78
|
+
unknown adapter name, or a MAC not currently present) -- a leftover
|
|
79
|
+
reference to an undefined `hostname` variable instead of `dst`. Found via
|
|
80
|
+
a `mypy` pass while auditing type annotations; a regression test now
|
|
81
|
+
covers the path.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- Public functions across the package now carry complete parameter and
|
|
86
|
+
return type annotations (previously missing on, among others, `collapse`,
|
|
87
|
+
`subtract`, `get_ip`, `PingResult`, `Route`, `scan_hosts`, `is_multicast`,
|
|
88
|
+
`join_group`/`leave_group`, `multicast_socket`, `UdpEndpoint`, and `bind`).
|
|
89
|
+
The recurring "loose interface spec" parameter (`ping(src=)`,
|
|
90
|
+
`bind(interface=)`, `discover_mtu(src=)`, `multicast_socket(interface=)`,
|
|
91
|
+
etc.) now shares one internal type alias instead of being unannotated at
|
|
92
|
+
each call site.
|
|
93
|
+
|
|
10
94
|
## [0.2.0] - 2026-07-29
|
|
11
95
|
|
|
12
96
|
### Added
|
|
@@ -155,7 +239,10 @@ below is simply what the package contains.
|
|
|
155
239
|
- **`Host`**, **`retry()`/`backoff_delays()`**, and the named networks `APIPA`,
|
|
156
240
|
`LOOPBACK_V4`, `LOOPBACK_V6`, `LINK_LOCAL_V6`.
|
|
157
241
|
|
|
158
|
-
[Unreleased]: https://github.com/jose-pr/netimps/compare/v0.
|
|
242
|
+
[Unreleased]: https://github.com/jose-pr/netimps/compare/v0.2.2...HEAD
|
|
243
|
+
[0.2.2]: https://github.com/jose-pr/netimps/compare/v0.2.1...v0.2.2
|
|
244
|
+
[0.2.1]: https://github.com/jose-pr/netimps/compare/v0.2.0...v0.2.1
|
|
245
|
+
[0.2.0]: https://github.com/jose-pr/netimps/compare/v0.1.0...v0.2.0
|
|
159
246
|
[0.1.0]: https://github.com/jose-pr/netimps/compare/v0.0.2...v0.1.0
|
|
160
247
|
[0.0.2]: https://github.com/jose-pr/netimps/releases/tag/v0.0.2
|
|
161
248
|
[0.0.1]: https://github.com/jose-pr/netimps/releases/tag/v0.0.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: netimps
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Small, self-contained network utilities: IP/MAC types, DNS lookup, ping, NIC discovery
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/netimps
|
|
6
6
|
Project-URL: Documentation, https://jose-pr.github.io/netimps/
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
20
|
Classifier: Topic :: System :: Networking
|
|
20
21
|
Classifier: Typing :: Typed
|
|
21
22
|
Requires-Python: >=3.9
|
|
@@ -86,8 +87,9 @@ nothing to compile and no wheel to miss for your platform.
|
|
|
86
87
|
- **Multicast** — `multicast_socket` handling the join dance whose failure
|
|
87
88
|
modes are otherwise silent.
|
|
88
89
|
- **DNS and ping** — `resolve()` chaining `dnspython`/OS resolver/`nslookup`
|
|
89
|
-
backends and returning native types
|
|
90
|
-
|
|
90
|
+
backends and returning native types, accepting an IP/interface object
|
|
91
|
+
directly and auto-selecting a reverse lookup for an address query; `ping()`
|
|
92
|
+
returning round-trip time and TTL, not just a boolean.
|
|
91
93
|
|
|
92
94
|
## Installation
|
|
93
95
|
|
|
@@ -106,6 +108,7 @@ library never requires either.
|
|
|
106
108
|
netimps interfaces # names, MACs, MTU, addresses
|
|
107
109
|
netimps ping 8.8.8.8 -m tcp -p 443 # icmp | tcp | udp
|
|
108
110
|
netimps resolve example.com aaaa
|
|
111
|
+
netimps resolve 8.8.8.8 # no rdtype -> auto ptr -> dns.google
|
|
109
112
|
netimps check example.com https # port number or scheme name
|
|
110
113
|
netimps mtu 8.8.8.8 # measured, not guessed
|
|
111
114
|
netimps scan 192.0.2.0/29 -p common
|
|
@@ -193,6 +196,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
|
|
|
193
196
|
| --- | --- |
|
|
194
197
|
| `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
|
|
195
198
|
| `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
199
|
+
| `AddressLike` | accepted-input union for a single destination (hostname, address, or `IPv4Interface`/`IPv6Interface` — its `.ip` is used) |
|
|
196
200
|
| `IPv4Address`, `IPv4Interface`, ... | stdlib concrete-type re-exports |
|
|
197
201
|
| `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
|
|
198
202
|
| `MACAddress` | parse / classify / render MAC addresses |
|
|
@@ -46,8 +46,9 @@ nothing to compile and no wheel to miss for your platform.
|
|
|
46
46
|
- **Multicast** — `multicast_socket` handling the join dance whose failure
|
|
47
47
|
modes are otherwise silent.
|
|
48
48
|
- **DNS and ping** — `resolve()` chaining `dnspython`/OS resolver/`nslookup`
|
|
49
|
-
backends and returning native types
|
|
50
|
-
|
|
49
|
+
backends and returning native types, accepting an IP/interface object
|
|
50
|
+
directly and auto-selecting a reverse lookup for an address query; `ping()`
|
|
51
|
+
returning round-trip time and TTL, not just a boolean.
|
|
51
52
|
|
|
52
53
|
## Installation
|
|
53
54
|
|
|
@@ -66,6 +67,7 @@ library never requires either.
|
|
|
66
67
|
netimps interfaces # names, MACs, MTU, addresses
|
|
67
68
|
netimps ping 8.8.8.8 -m tcp -p 443 # icmp | tcp | udp
|
|
68
69
|
netimps resolve example.com aaaa
|
|
70
|
+
netimps resolve 8.8.8.8 # no rdtype -> auto ptr -> dns.google
|
|
69
71
|
netimps check example.com https # port number or scheme name
|
|
70
72
|
netimps mtu 8.8.8.8 # measured, not guessed
|
|
71
73
|
netimps scan 192.0.2.0/29 -p common
|
|
@@ -153,6 +155,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
|
|
|
153
155
|
| --- | --- |
|
|
154
156
|
| `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
|
|
155
157
|
| `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
158
|
+
| `AddressLike` | accepted-input union for a single destination (hostname, address, or `IPv4Interface`/`IPv6Interface` — its `.ip` is used) |
|
|
156
159
|
| `IPv4Address`, `IPv4Interface`, ... | stdlib concrete-type re-exports |
|
|
157
160
|
| `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
|
|
158
161
|
| `MACAddress` | parse / classify / render MAC addresses |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netimps"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
authors = [{ name = "Jose A." }]
|
|
9
9
|
description = "Small, self-contained network utilities: IP/MAC types, DNS lookup, ping, NIC discovery"
|
|
10
10
|
readme = "README.md"
|
|
@@ -19,6 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python :: 3.11",
|
|
20
20
|
"Programming Language :: Python :: 3.12",
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
22
23
|
"Operating System :: OS Independent",
|
|
23
24
|
"Topic :: System :: Networking",
|
|
24
25
|
"Typing :: Typed",
|
|
@@ -16,7 +16,7 @@ is self-contained: it references nothing outside the installed distribution.
|
|
|
16
16
|
testing, releasing) is not shipped; it lives with the source at
|
|
17
17
|
<https://github.com/jose-pr/netimps>.
|
|
18
18
|
|
|
19
|
-
`netimps.__version__` — the package version string (currently `"0.2.
|
|
19
|
+
`netimps.__version__` — the package version string (currently `"0.2.2"`).
|
|
20
20
|
|
|
21
21
|
## Argument naming
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ The package is consistent about what the first argument means:
|
|
|
24
24
|
|
|
25
25
|
| Name | Meaning | Examples |
|
|
26
26
|
| --- | --- | --- |
|
|
27
|
-
| `dst` | where traffic is **sent** | `ping`, `tcp_check`, `wait_for_port`, `get_route`, `hop_count`, `discover_mtu` |
|
|
27
|
+
| `dst` | where traffic is **sent** | `ping`, `tcp_check`, `wait_for_port`, `get_route`, `hop_count`, `discover_mtu`, `get_tcp_mss`, `get_pmtu`, `scan_ports(host)` |
|
|
28
28
|
| `src` | where traffic is **sent from** | `ping(src=)`, `get_free_port(src=)`, `discover_mtu(src=)` |
|
|
29
29
|
| `host` / `network` | the thing being **examined** | `scan_ports(host)`, `scan_hosts(network)` |
|
|
30
30
|
| `address` / `ip` | an address being **classified** (no DNS) | `get_ip`, `interface_for`, `interfaces_for`, `is_local_address`, `is_multicast`, `is_link_scoped` |
|
|
@@ -32,6 +32,14 @@ The package is consistent about what the first argument means:
|
|
|
32
32
|
`dst`/`src` are abbreviated symmetrically, matching packet-header convention.
|
|
33
33
|
A `dst` accepts a hostname; an `address` does not.
|
|
34
34
|
|
|
35
|
+
**Every `dst`-typed parameter accepts `AddressLike`** — a hostname string, an
|
|
36
|
+
address string, an existing `IPv4Address`/`IPv6Address`, or an
|
|
37
|
+
`IPv4Interface`/`IPv6Interface` (its `.ip` is used, dropping the `/prefix`,
|
|
38
|
+
which every consumer of a destination -- a subprocess argument, a socket
|
|
39
|
+
call, a DNS query -- would otherwise read as garbage). A network
|
|
40
|
+
(`IPv4Network`/`IPv6Network`) raises `TypeError`, since it has no single
|
|
41
|
+
address to send to. `get_ip` and `resolve`'s `query` accept the same forms.
|
|
42
|
+
|
|
35
43
|
## Types vs parsing — read this first
|
|
36
44
|
|
|
37
45
|
The noun names are **types you annotate with**; you turn values into them with
|
|
@@ -57,6 +65,7 @@ The union aliases are **not callable** — `IPAddress("10.0.0.5")` is a
|
|
|
57
65
|
| `IPAddressLike` | anything accepted *as input* for an address |
|
|
58
66
|
| `IPInterfaceLike` | anything accepted *as input* for an address + prefix |
|
|
59
67
|
| `IPNetworkLike` | anything accepted *as input* for a network |
|
|
68
|
+
| `AddressLike` | `str \| IPv4Address \| IPv6Address \| IPv4Interface \| IPv6Interface` -- any `dst`-typed parameter |
|
|
60
69
|
| `MACLike` | `str \| int \| bytes \| MACAddress` |
|
|
61
70
|
|
|
62
71
|
Plus the stdlib concretes re-exported so callers need not import `ipaddress`:
|
|
@@ -212,7 +221,22 @@ failure** (NXDOMAIN, NODATA, timeout) — never `None` — with **native
|
|
|
212
221
|
types**: `A`/`AAAA` records are `ipaddress` objects, everything else is
|
|
213
222
|
`str` (trailing root dot stripped, TXT strings unquoted).
|
|
214
223
|
|
|
215
|
-
**`resolve(query, rdtype=
|
|
224
|
+
**`resolve(query, rdtype=None, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None)`**
|
|
225
|
+
|
|
226
|
+
`query` accepts `AddressLike` (a hostname string, an address string, an
|
|
227
|
+
`IPv4Address`/`IPv6Address`, or an `IPv4Interface`/`IPv6Interface` -- its
|
|
228
|
+
`.ip` is used), not just a plain string.
|
|
229
|
+
|
|
230
|
+
`rdtype=None` (default) **auto-selects**: `"ptr"` when `query` is an address
|
|
231
|
+
literal (an `"a"`/`"aaaa"` lookup *of* an address makes no sense), `"a"`
|
|
232
|
+
otherwise -- the same default as before this was configurable::
|
|
233
|
+
|
|
234
|
+
resolve("example.com") # rdtype auto -> "a" -> ['93.184.216.34']
|
|
235
|
+
resolve("8.8.8.8") # rdtype auto -> "ptr" -> ['dns.google']
|
|
236
|
+
|
|
237
|
+
Pass an explicit `rdtype` to opt out -- `rdtype="a"` on an address still
|
|
238
|
+
attempts a literal (and empty) A lookup rather than being silently
|
|
239
|
+
overridden.
|
|
216
240
|
|
|
217
241
|
Tries each backend in `backends` (default `["dnspython", "system",
|
|
218
242
|
"nslookup"]`) until one gives a **definitive** answer — records, or a real
|
|
@@ -224,16 +248,21 @@ the last such error is raised. `backends` also accepts a single name as a
|
|
|
224
248
|
plain string (`backends="system"`), or a custom order/subset
|
|
225
249
|
(`backends=["nslookup", "dnspython"]`).
|
|
226
250
|
|
|
227
|
-
- **`system`** is skipped automatically for a
|
|
228
|
-
explicit `ns=`/`port=` — it has no per-call
|
|
229
|
-
it anyway would silently ignore the
|
|
251
|
+
- **`system`** is skipped automatically for a `rdtype` outside
|
|
252
|
+
`"a"`/`"aaaa"`/`"ptr"`, or an explicit `ns=`/`port=` — it has no per-call
|
|
253
|
+
nameserver override, so running it anyway would silently ignore the
|
|
254
|
+
caller's choice.
|
|
230
255
|
- A malformed query or unknown record type raises `ValueError` immediately,
|
|
231
256
|
without trying every backend — that's a caller bug, not a resolution
|
|
232
257
|
outcome.
|
|
233
258
|
|
|
234
|
-
**`resolve_dnspython(query, rdtype=
|
|
259
|
+
**`resolve_dnspython(query, rdtype=None, ns=None, timeout=5.0, port=53, tcp=False, search=True)`**
|
|
235
260
|
|
|
236
|
-
The original backend: `dnspython`, structured records, every `rdtype`.
|
|
261
|
+
The original backend: `dnspython`, structured records, every `rdtype`. Same
|
|
262
|
+
`AddressLike` `query` and auto-`rdtype` behavior as `resolve()`. A `"ptr"`
|
|
263
|
+
lookup (explicit or auto-selected) uses dnspython's `resolve_address()`,
|
|
264
|
+
which builds the reverse (`in-addr.arpa`/`ip6.arpa`) name from the literal
|
|
265
|
+
address itself -- the caller never constructs that name by hand.
|
|
237
266
|
|
|
238
267
|
- **`ns=None` (default) uses the system resolver configuration** —
|
|
239
268
|
`/etc/resolv.conf` on POSIX, the registry on Windows. Pass `ns=` (a string
|
|
@@ -249,25 +278,30 @@ The original backend: `dnspython`, structured records, every `rdtype`.
|
|
|
249
278
|
already-qualified (trailing-dot) `query`.
|
|
250
279
|
- `timeout` bounds the **whole resolution including retries**, so a list of
|
|
251
280
|
dead nameservers cannot run past it.
|
|
252
|
-
-
|
|
253
|
-
`ResolutionError` (not `ValueError`) if it isn't installed, so
|
|
254
|
-
chain falls through to the next backend instead of erroring
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
- **
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
- `dnspython` is an **optional** dependency (`pip install netimps[dns]`).
|
|
282
|
+
Raises `ResolutionError` (not `ValueError`) if it isn't installed, so
|
|
283
|
+
`resolve()`'s chain falls through to the next backend instead of erroring
|
|
284
|
+
outright.
|
|
285
|
+
|
|
286
|
+
**`resolve_system(query, rdtype=None, timeout=5.0, search=True)`**
|
|
287
|
+
|
|
288
|
+
The OS resolver, via `socket.getaddrinfo()`/`socket.gethostbyaddr()` — **hosts
|
|
289
|
+
file, NSS (`nsswitch.conf`) and DNS, in the order the OS applies them**,
|
|
290
|
+
including any OS-level resolver cache. This is what `resolve_dnspython`
|
|
291
|
+
cannot see (its own DNS query bypasses all of that). Same `AddressLike`
|
|
292
|
+
`query` and auto-`rdtype` behavior as `resolve()`.
|
|
293
|
+
|
|
294
|
+
- **Address and reverse records only**: `rdtype` must be `"a"`, `"aaaa"` or
|
|
295
|
+
`"ptr"`; anything else raises `ResolutionError` immediately, no query
|
|
296
|
+
attempted. `"ptr"` goes through `gethostbyaddr()` rather than
|
|
297
|
+
`getaddrinfo()` and returns `[hostname]`.
|
|
298
|
+
- **No `ns=` override** — the OS resolver functions always ask whatever
|
|
299
|
+
nameserver the OS is configured with; there's no per-call parameter at that
|
|
300
|
+
layer (not even via `ctypes` — reaching a specific nameserver without
|
|
301
|
+
shelling out means speaking DNS wire protocol yourself, which is what
|
|
302
|
+
`dnspython` already does).
|
|
303
|
+
- **`search`** (ignored for `"ptr"`, which has no suffix to expand):
|
|
304
|
+
`getaddrinfo` itself takes no search-list parameter either, so
|
|
271
305
|
`search=True` (default) just leaves `query` as given and the OS resolver's
|
|
272
306
|
own configured search list (glibc `ndots`/`search`, Windows per-adapter DNS
|
|
273
307
|
suffix) applies as it normally would. `search=False` appends a trailing
|
|
@@ -275,12 +309,18 @@ DNS query bypasses all of that).
|
|
|
275
309
|
trick `host`/`getent` scripts use. A **list of domain names** tries `query`
|
|
276
310
|
qualified with each, in order, one `getaddrinfo` call per candidate,
|
|
277
311
|
independent of (and untouched by) the OS's own search list.
|
|
312
|
+
- **`timeout` bounds wall time, per candidate name tried.** `getaddrinfo` has
|
|
313
|
+
no timeout of its own, so each attempt runs in a daemon helper thread that
|
|
314
|
+
is abandoned at the deadline; the underlying call is not cancelled, but
|
|
315
|
+
neither the caller nor interpreter exit waits for it. A broken resolver
|
|
316
|
+
therefore costs `timeout`, not however long it takes to give up — which is
|
|
317
|
+
also what lets `resolve()`'s chain reach `nslookup` on schedule.
|
|
278
318
|
|
|
279
|
-
**`resolve_nslookup(query, rdtype=
|
|
319
|
+
**`resolve_nslookup(query, rdtype=None, ns=None, timeout=5.0, search=True)`**
|
|
280
320
|
|
|
281
321
|
Shells out to the `nslookup` binary — a fallback for when neither Python-level
|
|
282
322
|
path is usable. Address records only: `rdtype` must be `"a"`, `"aaaa"` or
|
|
283
|
-
`"ptr"`.
|
|
323
|
+
`"ptr"`. Same `AddressLike` `query` and auto-`rdtype` behavior as `resolve()`.
|
|
284
324
|
|
|
285
325
|
- Parses **both BIND-style** (`Address: 1.2.3.4`, one line per address) **and
|
|
286
326
|
Windows-style** (`Addresses:` with continuation lines, and NODATA as a bare
|
|
@@ -288,12 +328,13 @@ path is usable. Address records only: `rdtype` must be `"a"`, `"aaaa"` or
|
|
|
288
328
|
its NXDOMAIN message on **stderr**, not stdout — both streams are checked.
|
|
289
329
|
- `ns=` is passed as `nslookup`'s trailing `server` argument (a single
|
|
290
330
|
nameserver, not a list).
|
|
291
|
-
- **`search`** has the same three-way contract as the other backends
|
|
292
|
-
`nslookup` has no built-in search-list
|
|
293
|
-
`nslookup` call per candidate name, in order,
|
|
294
|
-
actual records. `search=True` (default) draws
|
|
295
|
-
system resolver's search config (reusing
|
|
296
|
-
registry parsing if it's installed; `[]` —
|
|
331
|
+
- **`search`** has the same three-way contract as the other backends
|
|
332
|
+
(ignored for `"ptr"`), but `nslookup` has no built-in search-list
|
|
333
|
+
handling — this issues one `nslookup` call per candidate name, in order,
|
|
334
|
+
stopping at the first with actual records. `search=True` (default) draws
|
|
335
|
+
the candidate list from the system resolver's search config (reusing
|
|
336
|
+
`dnspython`'s `resolv.conf`/registry parsing if it's installed; `[]` —
|
|
337
|
+
literal name only — if not).
|
|
297
338
|
- Raises `ResolutionError` (not `ValueError`) for a missing binary, a
|
|
298
339
|
timeout, or an unparseable output shape — a genuine "no such name" is
|
|
299
340
|
still `[]`.
|
|
@@ -303,11 +344,12 @@ path is usable. Address records only: `rdtype` must be `"a"`, `"aaaa"` or
|
|
|
303
344
|
**`ping(dst, tries=1, timeout=1.0, ipv6=None, src=None, size=None, ttl=None, dont_fragment=False, method="icmp", port=None) -> PingResult`**
|
|
304
345
|
|
|
305
346
|
`PingResult` is **truthy on success** and compares equal to `bool`, so
|
|
306
|
-
`if ping(host):` and `== True` keep working, while carrying `.ok`, `.
|
|
307
|
-
`.ttl`, `.
|
|
347
|
+
`if ping(host):` and `== True` keep working, while carrying `.ok`, `.host`,
|
|
348
|
+
`.rtt_ms`, `.ttl`, `.src`, `.attempts`.
|
|
308
349
|
|
|
309
350
|
| Argument | Notes |
|
|
310
351
|
| --- | --- |
|
|
352
|
+
| `dst` | `AddressLike` (hostname, address string, address object, or `IPv4Interface`/`IPv6Interface` -- its `.ip` is pinged). `ping(get_interfaces()[0].ipv4[0])` works directly. |
|
|
311
353
|
| `src` | `Interface`, address, **MAC**, adapter name or string. A MAC is resolved to the adapter holding it. |
|
|
312
354
|
| `size` | ICMP payload bytes. The wire packet is **28 bytes larger** (20 IP + 8 ICMP). |
|
|
313
355
|
| `ttl` | initial hop limit — `-i` on Windows, `-t` on POSIX (the letters are **swapped**). |
|
|
@@ -324,6 +366,19 @@ failure. `tcp` and `udp` also report `rtt_ms`; only ICMP reports `ttl`.
|
|
|
324
366
|
"TTL expired in transit", so the reply address is verified rather than
|
|
325
367
|
trusting the exit code. Locale-independent — it matches on addresses, never
|
|
326
368
|
prose.
|
|
369
|
+
- **`ipv6=` applies to all three methods**, not just the ICMP binary: it picks
|
|
370
|
+
the `-6`/`-4` flag, the family the reply address is resolved in, and the
|
|
371
|
+
family the `tcp`/`udp` probe sockets use. `ipv6=None` accepts either. A
|
|
372
|
+
hostname with several addresses counts as answered if the reply came from
|
|
373
|
+
any of them.
|
|
374
|
+
- **The `udp` probe connects its socket before sending**, which is why the ICMP
|
|
375
|
+
port-unreachable is seen on POSIX and not only on Windows — an unconnected
|
|
376
|
+
UDP socket is never delivered an asynchronous ICMP error on Linux/BSD.
|
|
377
|
+
`ECONNREFUSED` and `ECONNRESET` both count as "the host answered".
|
|
378
|
+
- **Known gap: macOS/BSD `ping6` reply lines.** Verification looks for
|
|
379
|
+
`from <addr>:`; BSD `ping6` is documented as printing `from <addr>,`. If
|
|
380
|
+
that holds, a v6 *literal* ping on macOS verifies as falsy despite a healthy
|
|
381
|
+
reply. Unverified on real hardware, so deliberately not "fixed" by guess.
|
|
327
382
|
- An unusable `src` (unknown MAC, adapter with no address, foreign address)
|
|
328
383
|
gives a falsy result — it **never silently falls back** to the default route.
|
|
329
384
|
- Never raises: missing binary, hung subprocess and non-zero exit are all falsy.
|
|
@@ -363,9 +418,11 @@ failure. `tcp` and `udp` also report `rtt_ms`; only ICMP reports `ttl`.
|
|
|
363
418
|
or reachable alone do not count. Malformed input raises like `parse`;
|
|
364
419
|
loopback answers before interface discovery.
|
|
365
420
|
- **`get_source_ip(dst="8.8.8.8", port=80)`** — which local address the kernel
|
|
366
|
-
would use to reach `dst`.
|
|
367
|
-
`
|
|
368
|
-
|
|
421
|
+
would use to reach `dst`. `dst` accepts `AddressLike` (an address object or
|
|
422
|
+
`IPv4Interface`/`IPv6Interface`, not just a string). **Sends no packets.**
|
|
423
|
+
The answer depends on `dst`: with a VPN up, a public probe returns the
|
|
424
|
+
tunnel address and a LAN probe the physical one. Correct where hostname
|
|
425
|
+
resolution picks a VM adapter.
|
|
369
426
|
- **`get_free_port(src="127.0.0.1", family=AF_INET) -> int`** — bind port 0 and
|
|
370
427
|
read it back. **Inherently racy** — the port frees the instant it returns; if
|
|
371
428
|
you can, bind port 0 in the server itself instead. `SO_REUSEADDR` is
|
|
@@ -379,7 +436,7 @@ failure. `tcp` and `udp` also report `rtt_ms`; only ICMP reports `ttl`.
|
|
|
379
436
|
|
|
380
437
|
## Routing, hops and MTU
|
|
381
438
|
|
|
382
|
-
- **`get_route(dst="8.8.8.8") -> Route`** — `.
|
|
439
|
+
- **`get_route(dst="8.8.8.8") -> Route`** — `.dst`, `.src`, `.gateway`,
|
|
383
440
|
`.interface_index`, `.on_link`. **First hop only, deliberately** — that is
|
|
384
441
|
available unprivileged everywhere, unlike the full path. Never raises;
|
|
385
442
|
unknown pieces are `None`/`0`. The gateway resolves on Windows and Linux only.
|
|
@@ -463,6 +520,12 @@ receives nothing, and looks fine:
|
|
|
463
520
|
*both* send and receive. Without it the kernel picks by routing table, which
|
|
464
521
|
on a multi-homed host is regularly the wrong adapter. An unknown interface
|
|
465
522
|
**raises** rather than falling back.
|
|
523
|
+
- **The two families identify an adapter differently**, and the spec is
|
|
524
|
+
resolved accordingly: IPv4 by local *address* (`IP_ADD_MEMBERSHIP`,
|
|
525
|
+
`IP_MULTICAST_IF`), IPv6 by interface *index* (`IPV6_JOIN_GROUP`,
|
|
526
|
+
`IPV6_MULTICAST_IF`). An adapter the platform reports **no index** for
|
|
527
|
+
raises for an IPv6 group, because index `0` means "kernel's choice" — the
|
|
528
|
+
default `interface=` was passed to override.
|
|
466
529
|
|
|
467
530
|
## UDP with arrival interface
|
|
468
531
|
|
|
@@ -473,7 +536,9 @@ network a request came from.
|
|
|
473
536
|
|
|
474
537
|
`recv(bufsize, resolve_interface=True) -> Datagram`, with `.data`, `.sender`,
|
|
475
538
|
`.local_address`, `.interface_index` and `.interface`.
|
|
476
|
-
`send(data, address, port,
|
|
539
|
+
`send(data, address, port, src=None)` pins the outgoing interface. `address`
|
|
540
|
+
accepts `AddressLike`; `src` the usual loose interface spec (`Interface`,
|
|
541
|
+
MAC, adapter name or address).
|
|
477
542
|
|
|
478
543
|
- **Degrades rather than failing.** `recvmsg` does not exist on Windows and
|
|
479
544
|
`IP_PKTINFO` is not universal; there the interface fields are simply empty.
|
|
@@ -526,6 +591,7 @@ dependency on the CLI half.
|
|
|
526
591
|
netimps interfaces # names, MACs, MTU, addresses
|
|
527
592
|
netimps ping 8.8.8.8 -m tcp -p 443 # icmp | tcp | udp
|
|
528
593
|
netimps resolve example.com aaaa
|
|
594
|
+
netimps resolve 8.8.8.8 # no rdtype -> auto ptr -> dns.google
|
|
529
595
|
netimps check example.com https # port or scheme name
|
|
530
596
|
netimps route 8.8.8.8 --hops
|
|
531
597
|
netimps mtu 8.8.8.8 -m udp -p 9999
|
|
@@ -74,6 +74,7 @@ from ._ip import (
|
|
|
74
74
|
_BUILDERS,
|
|
75
75
|
_BUILDER_DEFAULTS,
|
|
76
76
|
_CONCRETE,
|
|
77
|
+
AddressLike,
|
|
77
78
|
IPAddress,
|
|
78
79
|
IPAddressLike,
|
|
79
80
|
IPInterface,
|
|
@@ -102,6 +103,7 @@ __all__ = [
|
|
|
102
103
|
"IPAddressLike",
|
|
103
104
|
"IPInterfaceLike",
|
|
104
105
|
"IPNetworkLike",
|
|
106
|
+
"AddressLike",
|
|
105
107
|
"MACLike",
|
|
106
108
|
# Parsing.
|
|
107
109
|
"parse",
|
|
@@ -161,7 +163,7 @@ __all__ = [
|
|
|
161
163
|
"HOST_DN",
|
|
162
164
|
]
|
|
163
165
|
|
|
164
|
-
__version__ = "0.2.
|
|
166
|
+
__version__ = "0.2.2"
|
|
165
167
|
|
|
166
168
|
#: Fully-qualified (or short) name of the host running this process.
|
|
167
169
|
HOST_DN = _platform.node()
|