lanname 0.1.0__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.
- lanname-0.1.0/LICENSE +21 -0
- lanname-0.1.0/PKG-INFO +333 -0
- lanname-0.1.0/README.md +303 -0
- lanname-0.1.0/lanname/__init__.py +56 -0
- lanname-0.1.0/lanname/addrs.py +52 -0
- lanname-0.1.0/lanname/py.typed +0 -0
- lanname-0.1.0/lanname/resolver.py +497 -0
- lanname-0.1.0/lanname.egg-info/PKG-INFO +333 -0
- lanname-0.1.0/lanname.egg-info/SOURCES.txt +12 -0
- lanname-0.1.0/lanname.egg-info/dependency_links.txt +1 -0
- lanname-0.1.0/lanname.egg-info/top_level.txt +1 -0
- lanname-0.1.0/pyproject.toml +69 -0
- lanname-0.1.0/setup.cfg +4 -0
- lanname-0.1.0/tests/test_resolver.py +225 -0
lanname-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mjaksn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lanname-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lanname
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Address to hostname lookup on a local network: reverse DNS, mDNS and NetBIOS, cached and non-blocking, standard library only
|
|
5
|
+
Author: mjaksn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mjaksn/lanname
|
|
8
|
+
Project-URL: Source, https://github.com/mjaksn/lanname
|
|
9
|
+
Project-URL: Issues, https://github.com/mjaksn/lanname/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/mjaksn/lanname/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: dns,mdns,netbios,hostname,resolver,lan,reverse-dns
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: Name Service (DNS)
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# lanname
|
|
32
|
+
|
|
33
|
+
[](https://github.com/mjaksn/lanname/actions/workflows/ci.yml)
|
|
34
|
+
[](https://github.com/mjaksn/lanname/actions/workflows/release.yml)
|
|
35
|
+
[](https://github.com/mjaksn/lanname/blob/main/LICENSE)
|
|
36
|
+
|
|
37
|
+
Address to hostname lookup on a local network: reverse DNS, mDNS and NetBIOS,
|
|
38
|
+
cached and never blocking the caller. Standard library only, no dependencies,
|
|
39
|
+
Python 3.9 and up.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from lanname import Resolver
|
|
43
|
+
|
|
44
|
+
with Resolver(mode="dns", workers=4) as resolver:
|
|
45
|
+
name = resolver.lookup("192.168.1.10") # None until it is known
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
An IP address in a log line, a database row or a dashboard is much less useful
|
|
49
|
+
a year later than the name of the thing it was. The three ways to find that
|
|
50
|
+
name on a local network are all short, all standard library, and all annoying
|
|
51
|
+
enough to get right that nobody wants to write them twice.
|
|
52
|
+
|
|
53
|
+
Two properties are worth stating before anything else, because they are the
|
|
54
|
+
reasons to reach for this rather than call `socket.gethostbyaddr` yourself.
|
|
55
|
+
|
|
56
|
+
**It never blocks.** `lookup()` reads a cache and returns, always. Misses are
|
|
57
|
+
queued for background workers. A caller draining a socket cannot afford to
|
|
58
|
+
wait on a DNS round trip, and UDP in particular has no backpressure, so
|
|
59
|
+
anything that stalls the read loop loses packets silently.
|
|
60
|
+
|
|
61
|
+
**It is off until you ask.** The widest mode sends probes onto the LAN. A
|
|
62
|
+
library that did that unasked, inside a daemon somebody installed for an
|
|
63
|
+
unrelated reason, would be doing something the caller never sanctioned on a
|
|
64
|
+
network they may not own.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Contents
|
|
69
|
+
|
|
70
|
+
- [Installing](#installing)
|
|
71
|
+
- [Modes](#modes)
|
|
72
|
+
- [Resolver](#resolver)
|
|
73
|
+
- [What gets looked up](#what-gets-looked-up)
|
|
74
|
+
- [Static hosts files](#static-hosts-files)
|
|
75
|
+
- [What answered, over a session](#what-answered-over-a-session)
|
|
76
|
+
- [Counters](#counters)
|
|
77
|
+
- [Ceilings](#ceilings)
|
|
78
|
+
- [Logging](#logging)
|
|
79
|
+
- [The three methods on their own](#the-three-methods-on-their-own)
|
|
80
|
+
- [Limitations](#limitations)
|
|
81
|
+
- [Licence](#licence)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Installing
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
pip install lanname
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or from a checkout with `pip install .`, or by copying the `lanname/`
|
|
92
|
+
directory somewhere on the path. There is nothing to build and nothing to
|
|
93
|
+
install alongside it.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import lanname
|
|
97
|
+
lanname.__version__ # "0.1.0"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Modes
|
|
103
|
+
|
|
104
|
+
| mode | |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `"off"` | the default. No lookups, no threads, no traffic. |
|
|
107
|
+
| `"dns"` | reverse DNS only. Passive in the sense that it asks the resolver the machine already uses, but it is still a query per address. |
|
|
108
|
+
| `"all"` | reverse DNS, then mDNS to 224.0.0.251, then a NetBIOS status query to the host itself. **This sends probes onto the LAN.** |
|
|
109
|
+
|
|
110
|
+
The order matters and is not configurable: reverse DNS answers for anything
|
|
111
|
+
with a real record, mDNS catches the Apple and Linux hosts that publish over
|
|
112
|
+
multicast, and NetBIOS catches the Windows machines that answer to nothing
|
|
113
|
+
else. Each step only runs because the one before it came back empty.
|
|
114
|
+
|
|
115
|
+
`mode` can be changed on a running resolver with `set_mode()`, which starts the
|
|
116
|
+
worker threads if the resolver was constructed `"off"` and never had any.
|
|
117
|
+
Going back to `"off"` stops new work being queued but leaves the threads
|
|
118
|
+
parked on an empty queue; `shutdown()` is what retires them.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Resolver
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
Resolver(mode="off", hosts_files=(), workers=4, resolve_public=False,
|
|
126
|
+
fqdn=False, positive_ttl=3600, negative_ttl=300, timeout=1.0)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| argument | |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `mode` | one of the three above. `ValueError` for anything else. |
|
|
132
|
+
| `hosts_files` | paths to hosts-format files, read once at construction. See [below](#static-hosts-files). |
|
|
133
|
+
| `workers` | background lookup threads. They are daemons, and none are started at all while the mode is `"off"`. |
|
|
134
|
+
| `resolve_public` | look up public addresses too, default `False`. See [what gets looked up](#what-gets-looked-up). |
|
|
135
|
+
| `fqdn` | keep the full name rather than the first label. `False` gives `nas`, `True` gives `nas.local`. |
|
|
136
|
+
| `positive_ttl` | seconds a found name is cached, default 3600. |
|
|
137
|
+
| `negative_ttl` | seconds a failure is cached, default 300, so a host that does not answer is not asked again on every sighting. |
|
|
138
|
+
| `timeout` | per probe, in seconds, for mDNS and NetBIOS. Reverse DNS uses the system resolver's own timeout. |
|
|
139
|
+
|
|
140
|
+
| method | |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `lookup(addr)` | the name, or `None`. Never blocks. |
|
|
143
|
+
| `set_mode(mode)` | change mode while running, starting workers if needed. |
|
|
144
|
+
| `set_fqdn(fqdn)` | change the name form. Empties the cache, since every entry in it was shortened on the way in. |
|
|
145
|
+
| `local_hosts()` | every private address seen with a name, and the names it answered to. See [below](#what-answered-over-a-session). |
|
|
146
|
+
| `shutdown()` | ask the workers to stop. Also `__exit__`, so a `with` block does it. |
|
|
147
|
+
|
|
148
|
+
`lookup()` returning `None` means "not known yet", never "has no name". Ask
|
|
149
|
+
again the next time the address turns up; the answer appears once a worker has
|
|
150
|
+
been round. The first sighting of any address is always a miss, by design.
|
|
151
|
+
|
|
152
|
+
`shutdown()` is a courtesy rather than a requirement, since the workers are
|
|
153
|
+
daemon threads and the interpreter will not wait for them. What it buys is
|
|
154
|
+
that probes stop going out at the point the caller thinks it has stopped.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## What gets looked up
|
|
159
|
+
|
|
160
|
+
Not everything is worth a query, and two of the categories are worth refusing
|
|
161
|
+
outright.
|
|
162
|
+
|
|
163
|
+
| kind | looked up |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| private | yes, by every mode. This is what the package is for. |
|
|
166
|
+
| public | only with `resolve_public=True` |
|
|
167
|
+
| multicast | never |
|
|
168
|
+
| loopback, link-local, reserved, unspecified | never |
|
|
169
|
+
| not an address at all | never |
|
|
170
|
+
|
|
171
|
+
Public addresses are skipped by default because a busy link produces thousands
|
|
172
|
+
of them, most resolve to something uninformative like a cloud provider's
|
|
173
|
+
generic reverse record, and every one is a query somebody else can see. Turn
|
|
174
|
+
them on when the public side is the interesting half.
|
|
175
|
+
|
|
176
|
+
mDNS and NetBIOS are only ever tried for private addresses, whatever
|
|
177
|
+
`resolve_public` says. Both are link-local methods, and sending either to an
|
|
178
|
+
address off the local network is at best pointless and at worst rude.
|
|
179
|
+
|
|
180
|
+
`addr_kind(addr)` is exported if the same classification is useful elsewhere.
|
|
181
|
+
It returns one of `ADDR_KINDS`.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Static hosts files
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
Resolver(mode="off", hosts_files=["/etc/hosts", "static.hosts"])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Standard hosts format: an address, whitespace, a name, and `#` starts a
|
|
192
|
+
comment. The first entry for an address wins, and files are read in the order
|
|
193
|
+
given. Lines that do not parse are skipped rather than raising, and a file
|
|
194
|
+
that cannot be read at all is logged as a warning, on the grounds that a
|
|
195
|
+
missing optional file should not stop the program that asked for it.
|
|
196
|
+
|
|
197
|
+
Static entries answer **even in `"off"` mode**, and they answer without a
|
|
198
|
+
cache lookup or a queue round trip. A resolver constructed `"off"` with a
|
|
199
|
+
hosts file is a pure static mapping that starts no threads and sends no
|
|
200
|
+
traffic, which is a reasonable way to run this in an environment where active
|
|
201
|
+
lookups are not welcome.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## What answered, over a session
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
for addr, names in resolver.local_hosts():
|
|
209
|
+
print(addr, names[0], names[1:] or "")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Every private address that has ever resolved to a name, sorted by address,
|
|
213
|
+
each with its names most recent first. This is kept separately from the cache
|
|
214
|
+
and outlives it: the cache expires and evicts, and this is meant to answer
|
|
215
|
+
"what did you see all session" long after either has happened.
|
|
216
|
+
|
|
217
|
+
A host whose name changes keeps both, newest first, up to
|
|
218
|
+
`MAX_NAMES_PER_HOST`. That is usually a DHCP lease moving or a machine being
|
|
219
|
+
renamed, and the last few changes are the interesting part.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Counters
|
|
224
|
+
|
|
225
|
+
`resolver.stats` is a `collections.Counter`, safe to read at any time.
|
|
226
|
+
|
|
227
|
+
| key | |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| `hits` | answered from the cache |
|
|
230
|
+
| `resolved` | a worker found a name |
|
|
231
|
+
| `missed` | a worker found nothing, and the failure was cached for `negative_ttl` |
|
|
232
|
+
| `via_dns`, `via_mdns`, `via_netbios` | which method produced the name, for the ones that were found |
|
|
233
|
+
| `dropped` | lookups discarded because the work queue was full |
|
|
234
|
+
| `evicted` | cache entries dropped to stay under the ceiling |
|
|
235
|
+
|
|
236
|
+
`dropped` climbing means addresses are arriving faster than `workers` threads
|
|
237
|
+
can resolve them, and those addresses simply go unresolved for now. Raise
|
|
238
|
+
`workers`, or accept it: the queue is bounded on purpose, because the
|
|
239
|
+
alternative to dropping work is growing memory without limit.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Ceilings
|
|
244
|
+
|
|
245
|
+
Everything keyed by address has a bound. The addresses reaching this package
|
|
246
|
+
come off a network, so anything keyed by one and never evicted is a memory
|
|
247
|
+
leak that other hosts can pull on.
|
|
248
|
+
|
|
249
|
+
| constant | default | what it bounds | on overflow |
|
|
250
|
+
| --- | --- | --- | --- |
|
|
251
|
+
| `lanname.resolver.RESOLVER_CACHE_MAX` | 50,000 | cached names | least recently used evicted, counted in `stats["evicted"]` |
|
|
252
|
+
| `lanname.resolver.MAX_OBSERVED_HOSTS` | 5,000 | addresses remembered for `local_hosts()` | least recently seen evicted |
|
|
253
|
+
| `lanname.resolver.MAX_NAMES_PER_HOST` | 5 | names remembered for any one address | oldest forgotten |
|
|
254
|
+
| `lanname.addrs.MAX_ADDR_KIND_CACHE` | 100,000 | cached address classifications | stops caching |
|
|
255
|
+
|
|
256
|
+
The work queue holds 4,096 addresses and drops rather than blocking, counted
|
|
257
|
+
in `stats["dropped"]`.
|
|
258
|
+
|
|
259
|
+
The cache drops its oldest entry rather than emptying itself, and that
|
|
260
|
+
distinction is the whole reason it is an `OrderedDict`. Clearing it wholesale
|
|
261
|
+
would send every active host back through resolution at the same moment, which
|
|
262
|
+
in `"all"` mode is a burst of mDNS and NetBIOS probes onto the LAN, from a
|
|
263
|
+
program that was supposed to be quiet.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Logging
|
|
268
|
+
|
|
269
|
+
Everything goes to the `lanname` logger. The package installs a `NullHandler`
|
|
270
|
+
and nothing else, so records go nowhere until you configure a handler.
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
import logging
|
|
274
|
+
logging.basicConfig(level=logging.INFO)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
| logger | what |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| `lanname.resolver` | WARNING for a hosts file that could not be read, DEBUG for a lookup that raised |
|
|
280
|
+
|
|
281
|
+
Nothing here is logged per address at INFO or above. A resolver watching a
|
|
282
|
+
busy link would drown any log it shared.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## The three methods on their own
|
|
287
|
+
|
|
288
|
+
The scheduling and the asking are separable, and the asking is exported:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from lanname import mdns_reverse, netbios_name
|
|
292
|
+
|
|
293
|
+
mdns_reverse("192.168.1.10", timeout=1.0) # str or None
|
|
294
|
+
netbios_name("192.168.1.10", timeout=1.0) # str or None
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Both send one packet and wait for one answer, both block for up to `timeout`,
|
|
298
|
+
and both answer `None` rather than raising when a probe goes unanswered or a
|
|
299
|
+
reply does not parse. Reverse DNS has no wrapper here, because
|
|
300
|
+
`socket.gethostbyaddr` already is one.
|
|
301
|
+
|
|
302
|
+
`mdns_reverse` sends a PTR query to 224.0.0.251:5353 with the unicast-response
|
|
303
|
+
bit set and a multicast TTL of 1, so it stays on the link and there is no
|
|
304
|
+
group to join. `netbios_name` sends a NBSTAT query straight to the host's port
|
|
305
|
+
137 and prefers the unique workstation name out of the answer.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Limitations
|
|
310
|
+
|
|
311
|
+
- **The first answer is always `None`.** That is the design, not a bug. A
|
|
312
|
+
caller that genuinely needs the name before it can proceed wants
|
|
313
|
+
`socket.gethostbyaddr` and the blocking that comes with it.
|
|
314
|
+
- **mDNS and NetBIOS are IPv4 only.** Both open an `AF_INET` socket. Reverse
|
|
315
|
+
DNS works for either family, so an IPv6 address gets one method rather than
|
|
316
|
+
three.
|
|
317
|
+
- **NetBIOS is a Windows convention and a fading one.** Recent Windows can
|
|
318
|
+
have it disabled, and non-Windows hosts answer only if they run Samba.
|
|
319
|
+
It is the last method tried for exactly that reason.
|
|
320
|
+
- **Names are not verified.** A host answering NetBIOS or mDNS says what it
|
|
321
|
+
likes, and nothing here checks the claim against a forward lookup. Treat a
|
|
322
|
+
name from `"all"` mode as a label a host chose for itself, not as identity.
|
|
323
|
+
- **One resolver, one cache.** Two resolvers in a process do not share
|
|
324
|
+
anything, including the worker threads and the queue.
|
|
325
|
+
|
|
326
|
+
`lookup()` is safe to call from any thread; the cache and the observed-host
|
|
327
|
+
table are both behind a lock.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Licence
|
|
332
|
+
|
|
333
|
+
MIT. See [LICENSE](https://github.com/mjaksn/lanname/blob/main/LICENSE).
|
lanname-0.1.0/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# lanname
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mjaksn/lanname/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/mjaksn/lanname/actions/workflows/release.yml)
|
|
5
|
+
[](https://github.com/mjaksn/lanname/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Address to hostname lookup on a local network: reverse DNS, mDNS and NetBIOS,
|
|
8
|
+
cached and never blocking the caller. Standard library only, no dependencies,
|
|
9
|
+
Python 3.9 and up.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from lanname import Resolver
|
|
13
|
+
|
|
14
|
+
with Resolver(mode="dns", workers=4) as resolver:
|
|
15
|
+
name = resolver.lookup("192.168.1.10") # None until it is known
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
An IP address in a log line, a database row or a dashboard is much less useful
|
|
19
|
+
a year later than the name of the thing it was. The three ways to find that
|
|
20
|
+
name on a local network are all short, all standard library, and all annoying
|
|
21
|
+
enough to get right that nobody wants to write them twice.
|
|
22
|
+
|
|
23
|
+
Two properties are worth stating before anything else, because they are the
|
|
24
|
+
reasons to reach for this rather than call `socket.gethostbyaddr` yourself.
|
|
25
|
+
|
|
26
|
+
**It never blocks.** `lookup()` reads a cache and returns, always. Misses are
|
|
27
|
+
queued for background workers. A caller draining a socket cannot afford to
|
|
28
|
+
wait on a DNS round trip, and UDP in particular has no backpressure, so
|
|
29
|
+
anything that stalls the read loop loses packets silently.
|
|
30
|
+
|
|
31
|
+
**It is off until you ask.** The widest mode sends probes onto the LAN. A
|
|
32
|
+
library that did that unasked, inside a daemon somebody installed for an
|
|
33
|
+
unrelated reason, would be doing something the caller never sanctioned on a
|
|
34
|
+
network they may not own.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Contents
|
|
39
|
+
|
|
40
|
+
- [Installing](#installing)
|
|
41
|
+
- [Modes](#modes)
|
|
42
|
+
- [Resolver](#resolver)
|
|
43
|
+
- [What gets looked up](#what-gets-looked-up)
|
|
44
|
+
- [Static hosts files](#static-hosts-files)
|
|
45
|
+
- [What answered, over a session](#what-answered-over-a-session)
|
|
46
|
+
- [Counters](#counters)
|
|
47
|
+
- [Ceilings](#ceilings)
|
|
48
|
+
- [Logging](#logging)
|
|
49
|
+
- [The three methods on their own](#the-three-methods-on-their-own)
|
|
50
|
+
- [Limitations](#limitations)
|
|
51
|
+
- [Licence](#licence)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installing
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
pip install lanname
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or from a checkout with `pip install .`, or by copying the `lanname/`
|
|
62
|
+
directory somewhere on the path. There is nothing to build and nothing to
|
|
63
|
+
install alongside it.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import lanname
|
|
67
|
+
lanname.__version__ # "0.1.0"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Modes
|
|
73
|
+
|
|
74
|
+
| mode | |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `"off"` | the default. No lookups, no threads, no traffic. |
|
|
77
|
+
| `"dns"` | reverse DNS only. Passive in the sense that it asks the resolver the machine already uses, but it is still a query per address. |
|
|
78
|
+
| `"all"` | reverse DNS, then mDNS to 224.0.0.251, then a NetBIOS status query to the host itself. **This sends probes onto the LAN.** |
|
|
79
|
+
|
|
80
|
+
The order matters and is not configurable: reverse DNS answers for anything
|
|
81
|
+
with a real record, mDNS catches the Apple and Linux hosts that publish over
|
|
82
|
+
multicast, and NetBIOS catches the Windows machines that answer to nothing
|
|
83
|
+
else. Each step only runs because the one before it came back empty.
|
|
84
|
+
|
|
85
|
+
`mode` can be changed on a running resolver with `set_mode()`, which starts the
|
|
86
|
+
worker threads if the resolver was constructed `"off"` and never had any.
|
|
87
|
+
Going back to `"off"` stops new work being queued but leaves the threads
|
|
88
|
+
parked on an empty queue; `shutdown()` is what retires them.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Resolver
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
Resolver(mode="off", hosts_files=(), workers=4, resolve_public=False,
|
|
96
|
+
fqdn=False, positive_ttl=3600, negative_ttl=300, timeout=1.0)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| argument | |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `mode` | one of the three above. `ValueError` for anything else. |
|
|
102
|
+
| `hosts_files` | paths to hosts-format files, read once at construction. See [below](#static-hosts-files). |
|
|
103
|
+
| `workers` | background lookup threads. They are daemons, and none are started at all while the mode is `"off"`. |
|
|
104
|
+
| `resolve_public` | look up public addresses too, default `False`. See [what gets looked up](#what-gets-looked-up). |
|
|
105
|
+
| `fqdn` | keep the full name rather than the first label. `False` gives `nas`, `True` gives `nas.local`. |
|
|
106
|
+
| `positive_ttl` | seconds a found name is cached, default 3600. |
|
|
107
|
+
| `negative_ttl` | seconds a failure is cached, default 300, so a host that does not answer is not asked again on every sighting. |
|
|
108
|
+
| `timeout` | per probe, in seconds, for mDNS and NetBIOS. Reverse DNS uses the system resolver's own timeout. |
|
|
109
|
+
|
|
110
|
+
| method | |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `lookup(addr)` | the name, or `None`. Never blocks. |
|
|
113
|
+
| `set_mode(mode)` | change mode while running, starting workers if needed. |
|
|
114
|
+
| `set_fqdn(fqdn)` | change the name form. Empties the cache, since every entry in it was shortened on the way in. |
|
|
115
|
+
| `local_hosts()` | every private address seen with a name, and the names it answered to. See [below](#what-answered-over-a-session). |
|
|
116
|
+
| `shutdown()` | ask the workers to stop. Also `__exit__`, so a `with` block does it. |
|
|
117
|
+
|
|
118
|
+
`lookup()` returning `None` means "not known yet", never "has no name". Ask
|
|
119
|
+
again the next time the address turns up; the answer appears once a worker has
|
|
120
|
+
been round. The first sighting of any address is always a miss, by design.
|
|
121
|
+
|
|
122
|
+
`shutdown()` is a courtesy rather than a requirement, since the workers are
|
|
123
|
+
daemon threads and the interpreter will not wait for them. What it buys is
|
|
124
|
+
that probes stop going out at the point the caller thinks it has stopped.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## What gets looked up
|
|
129
|
+
|
|
130
|
+
Not everything is worth a query, and two of the categories are worth refusing
|
|
131
|
+
outright.
|
|
132
|
+
|
|
133
|
+
| kind | looked up |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| private | yes, by every mode. This is what the package is for. |
|
|
136
|
+
| public | only with `resolve_public=True` |
|
|
137
|
+
| multicast | never |
|
|
138
|
+
| loopback, link-local, reserved, unspecified | never |
|
|
139
|
+
| not an address at all | never |
|
|
140
|
+
|
|
141
|
+
Public addresses are skipped by default because a busy link produces thousands
|
|
142
|
+
of them, most resolve to something uninformative like a cloud provider's
|
|
143
|
+
generic reverse record, and every one is a query somebody else can see. Turn
|
|
144
|
+
them on when the public side is the interesting half.
|
|
145
|
+
|
|
146
|
+
mDNS and NetBIOS are only ever tried for private addresses, whatever
|
|
147
|
+
`resolve_public` says. Both are link-local methods, and sending either to an
|
|
148
|
+
address off the local network is at best pointless and at worst rude.
|
|
149
|
+
|
|
150
|
+
`addr_kind(addr)` is exported if the same classification is useful elsewhere.
|
|
151
|
+
It returns one of `ADDR_KINDS`.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Static hosts files
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
Resolver(mode="off", hosts_files=["/etc/hosts", "static.hosts"])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Standard hosts format: an address, whitespace, a name, and `#` starts a
|
|
162
|
+
comment. The first entry for an address wins, and files are read in the order
|
|
163
|
+
given. Lines that do not parse are skipped rather than raising, and a file
|
|
164
|
+
that cannot be read at all is logged as a warning, on the grounds that a
|
|
165
|
+
missing optional file should not stop the program that asked for it.
|
|
166
|
+
|
|
167
|
+
Static entries answer **even in `"off"` mode**, and they answer without a
|
|
168
|
+
cache lookup or a queue round trip. A resolver constructed `"off"` with a
|
|
169
|
+
hosts file is a pure static mapping that starts no threads and sends no
|
|
170
|
+
traffic, which is a reasonable way to run this in an environment where active
|
|
171
|
+
lookups are not welcome.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## What answered, over a session
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
for addr, names in resolver.local_hosts():
|
|
179
|
+
print(addr, names[0], names[1:] or "")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Every private address that has ever resolved to a name, sorted by address,
|
|
183
|
+
each with its names most recent first. This is kept separately from the cache
|
|
184
|
+
and outlives it: the cache expires and evicts, and this is meant to answer
|
|
185
|
+
"what did you see all session" long after either has happened.
|
|
186
|
+
|
|
187
|
+
A host whose name changes keeps both, newest first, up to
|
|
188
|
+
`MAX_NAMES_PER_HOST`. That is usually a DHCP lease moving or a machine being
|
|
189
|
+
renamed, and the last few changes are the interesting part.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Counters
|
|
194
|
+
|
|
195
|
+
`resolver.stats` is a `collections.Counter`, safe to read at any time.
|
|
196
|
+
|
|
197
|
+
| key | |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `hits` | answered from the cache |
|
|
200
|
+
| `resolved` | a worker found a name |
|
|
201
|
+
| `missed` | a worker found nothing, and the failure was cached for `negative_ttl` |
|
|
202
|
+
| `via_dns`, `via_mdns`, `via_netbios` | which method produced the name, for the ones that were found |
|
|
203
|
+
| `dropped` | lookups discarded because the work queue was full |
|
|
204
|
+
| `evicted` | cache entries dropped to stay under the ceiling |
|
|
205
|
+
|
|
206
|
+
`dropped` climbing means addresses are arriving faster than `workers` threads
|
|
207
|
+
can resolve them, and those addresses simply go unresolved for now. Raise
|
|
208
|
+
`workers`, or accept it: the queue is bounded on purpose, because the
|
|
209
|
+
alternative to dropping work is growing memory without limit.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Ceilings
|
|
214
|
+
|
|
215
|
+
Everything keyed by address has a bound. The addresses reaching this package
|
|
216
|
+
come off a network, so anything keyed by one and never evicted is a memory
|
|
217
|
+
leak that other hosts can pull on.
|
|
218
|
+
|
|
219
|
+
| constant | default | what it bounds | on overflow |
|
|
220
|
+
| --- | --- | --- | --- |
|
|
221
|
+
| `lanname.resolver.RESOLVER_CACHE_MAX` | 50,000 | cached names | least recently used evicted, counted in `stats["evicted"]` |
|
|
222
|
+
| `lanname.resolver.MAX_OBSERVED_HOSTS` | 5,000 | addresses remembered for `local_hosts()` | least recently seen evicted |
|
|
223
|
+
| `lanname.resolver.MAX_NAMES_PER_HOST` | 5 | names remembered for any one address | oldest forgotten |
|
|
224
|
+
| `lanname.addrs.MAX_ADDR_KIND_CACHE` | 100,000 | cached address classifications | stops caching |
|
|
225
|
+
|
|
226
|
+
The work queue holds 4,096 addresses and drops rather than blocking, counted
|
|
227
|
+
in `stats["dropped"]`.
|
|
228
|
+
|
|
229
|
+
The cache drops its oldest entry rather than emptying itself, and that
|
|
230
|
+
distinction is the whole reason it is an `OrderedDict`. Clearing it wholesale
|
|
231
|
+
would send every active host back through resolution at the same moment, which
|
|
232
|
+
in `"all"` mode is a burst of mDNS and NetBIOS probes onto the LAN, from a
|
|
233
|
+
program that was supposed to be quiet.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Logging
|
|
238
|
+
|
|
239
|
+
Everything goes to the `lanname` logger. The package installs a `NullHandler`
|
|
240
|
+
and nothing else, so records go nowhere until you configure a handler.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
import logging
|
|
244
|
+
logging.basicConfig(level=logging.INFO)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| logger | what |
|
|
248
|
+
| --- | --- |
|
|
249
|
+
| `lanname.resolver` | WARNING for a hosts file that could not be read, DEBUG for a lookup that raised |
|
|
250
|
+
|
|
251
|
+
Nothing here is logged per address at INFO or above. A resolver watching a
|
|
252
|
+
busy link would drown any log it shared.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## The three methods on their own
|
|
257
|
+
|
|
258
|
+
The scheduling and the asking are separable, and the asking is exported:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from lanname import mdns_reverse, netbios_name
|
|
262
|
+
|
|
263
|
+
mdns_reverse("192.168.1.10", timeout=1.0) # str or None
|
|
264
|
+
netbios_name("192.168.1.10", timeout=1.0) # str or None
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Both send one packet and wait for one answer, both block for up to `timeout`,
|
|
268
|
+
and both answer `None` rather than raising when a probe goes unanswered or a
|
|
269
|
+
reply does not parse. Reverse DNS has no wrapper here, because
|
|
270
|
+
`socket.gethostbyaddr` already is one.
|
|
271
|
+
|
|
272
|
+
`mdns_reverse` sends a PTR query to 224.0.0.251:5353 with the unicast-response
|
|
273
|
+
bit set and a multicast TTL of 1, so it stays on the link and there is no
|
|
274
|
+
group to join. `netbios_name` sends a NBSTAT query straight to the host's port
|
|
275
|
+
137 and prefers the unique workstation name out of the answer.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Limitations
|
|
280
|
+
|
|
281
|
+
- **The first answer is always `None`.** That is the design, not a bug. A
|
|
282
|
+
caller that genuinely needs the name before it can proceed wants
|
|
283
|
+
`socket.gethostbyaddr` and the blocking that comes with it.
|
|
284
|
+
- **mDNS and NetBIOS are IPv4 only.** Both open an `AF_INET` socket. Reverse
|
|
285
|
+
DNS works for either family, so an IPv6 address gets one method rather than
|
|
286
|
+
three.
|
|
287
|
+
- **NetBIOS is a Windows convention and a fading one.** Recent Windows can
|
|
288
|
+
have it disabled, and non-Windows hosts answer only if they run Samba.
|
|
289
|
+
It is the last method tried for exactly that reason.
|
|
290
|
+
- **Names are not verified.** A host answering NetBIOS or mDNS says what it
|
|
291
|
+
likes, and nothing here checks the claim against a forward lookup. Treat a
|
|
292
|
+
name from `"all"` mode as a label a host chose for itself, not as identity.
|
|
293
|
+
- **One resolver, one cache.** Two resolvers in a process do not share
|
|
294
|
+
anything, including the worker threads and the queue.
|
|
295
|
+
|
|
296
|
+
`lookup()` is safe to call from any thread; the cache and the observed-host
|
|
297
|
+
table are both behind a lock.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Licence
|
|
302
|
+
|
|
303
|
+
MIT. See [LICENSE](https://github.com/mjaksn/lanname/blob/main/LICENSE).
|