dns-shield 0.1.1__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.
@@ -0,0 +1,84 @@
1
+ # Secrets and local configuration -- never commit these.
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ *.env
6
+ secrets
7
+ secrets.*
8
+ secrets/
9
+ secret
10
+ *.pem
11
+ *.key
12
+ *.p12
13
+ *.pfx
14
+ credentials.json
15
+ credentials*.yml
16
+ service-account*.json
17
+ .netrc
18
+
19
+ # Python
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+ *.so
24
+ .Python
25
+ build/
26
+ develop-eggs/
27
+ dist/
28
+ downloads/
29
+ eggs/
30
+ .eggs/
31
+ lib64/
32
+ parts/
33
+ sdist/
34
+ var/
35
+ wheels/
36
+ share/python-wheels/
37
+ *.egg-info/
38
+ .installed.cfg
39
+ *.egg
40
+ MANIFEST
41
+
42
+ # Virtual environments
43
+ .venv/
44
+ venv/
45
+ ENV/
46
+ env/
47
+ .python-version
48
+
49
+ # Testing and coverage
50
+ .pytest_cache/
51
+ .tox/
52
+ .nox/
53
+ .coverage
54
+ .coverage.*
55
+ coverage.xml
56
+ htmlcov/
57
+ .hypothesis/
58
+ .mypy_cache/
59
+ .dmypy.json
60
+ ruff_cache/
61
+ .benchmarks/
62
+
63
+ # Build tooling
64
+ *.manifest
65
+ *.spec
66
+ pip-log.txt
67
+ pip-delete-this-directory.txt
68
+ site/
69
+
70
+ # Editors and OS
71
+ .vscode/
72
+ .idea/
73
+ *.swp
74
+ *.swo
75
+ *~
76
+ .DS_Store
77
+ Thumbs.db
78
+
79
+ # Jupyter
80
+ .ipynb_checkpoints/
81
+
82
+ # Local scratch -- diagnostic output may contain personal network details.
83
+ *.local
84
+ scratch/
@@ -0,0 +1,119 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.1] - 2026-10-04
11
+
12
+ ### Changed
13
+
14
+ - Packaging and release metadata only; no code or behaviour changes from the
15
+ entries below. Added complete PyPI metadata (PEP 639 `license` expression,
16
+ author, project URLs and classifiers) and a Trusted Publishing (OIDC) release
17
+ workflow that publishes on `v*` tags.
18
+
19
+ ### Fixed
20
+
21
+ - **A false `POISONED` verdict when both resolvers returned the same address.**
22
+ The poisoning rule's guard was `answers_disagree or not system_alive`, and by
23
+ the time that rule was reached `not system_alive` was always true — so the
24
+ disagreement condition was never actually consulted. A host whose system probe
25
+ transiently failed while the DoH probe to the *same* address succeeded was
26
+ reported as poisoning, with a summary claiming the address "does not serve the
27
+ host" about an address the tool had just fetched HTTP 200 from. Two probes to
28
+ one literal IP, seconds apart, is a transient failure, not a resolver lie. The
29
+ rule now requires genuine disagreement, with an explicit branch for the
30
+ agreed-address case. Guarded by `TestSameAddressIsNeverPoisoning`, including an
31
+ exhaustive test over every probe-outcome combination asserting that no
32
+ non-disagreeing input can ever yield `POISONED`.
33
+ - `ConnectionResetError` was classified as `other-error` rather than `refused`.
34
+ It is not a subclass of `ConnectionRefusedError`, so it fell through to the
35
+ generic handler — an active refusal was reported as an unknown error. Both are
36
+ now classified by one shared `classify_connection_error`, so the mapping cannot
37
+ drift between probe paths.
38
+ - `read_head` accepted a header block truncated by a peer close, returning a
39
+ status with no headers and no body — indistinguishable from a legitimate empty
40
+ `200`, with the failure surfacing later as a confusing JSON decode error. It
41
+ now raises `connection closed before headers were complete`.
42
+ - `dechunk` silently dropped data on a malformed chunk terminator. It now
43
+ validates the CRLF and raises rather than presenting a short body as complete.
44
+ - Status-line parsing used `split(" ", 2)`, rejecting legal responses such as
45
+ `HTTP/1.1 204` (no reason phrase) and doubled spaces. Now parsed tolerantly.
46
+ - `backoff_delay` used pure full jitter with no floor, admitting a zero-length
47
+ sleep and a near-instant retry loop against a fast-refusing endpoint. A
48
+ `base_s` floor is now applied, including to honoured `retryAfter` hints.
49
+ - Resolution failures were labelled `kind="connect"`, misdirecting diagnosis of
50
+ a DoH outage as a connection problem. Now `kind="resolution"`.
51
+
52
+ ### Changed
53
+
54
+ - `ProbeResult` gains `slow_refusal` and `refusal_mode`, so an active refusal is
55
+ distinguished from a blackhole timeout in the evidence output.
56
+ - README: the poisoning fingerprint now documents **both** observed failure
57
+ modes (active refusal and blackhole timeout) with the ten-trial measurement
58
+ that shows the mode varies run to run, rather than asserting only the ~10 ms
59
+ RST from the original report. Exit-code and `--sibling` descriptions corrected
60
+ to match what the code actually does.
61
+
62
+ ## [0.1.0] - 2026-09-28
63
+
64
+ Initial release.
65
+
66
+ ### Added
67
+
68
+ - **`dns_shield.detect`** — diagnoses ISP DNS poisoning by comparing the system
69
+ resolver against DNS-over-HTTPS, then probing both answers.
70
+ - `Verdict` enum: `HEALTHY`, `POISONED`, `SUSPICIOUS`, `UNREACHABLE`, `UNKNOWN`.
71
+ - `FailureKind` classification: `SUCCESS`, `REFUSED`, `TIMEOUT`, `DNS_FAILURE`,
72
+ `TLS_FAILURE`, `HTTP_ERROR`, `OTHER_ERROR`.
73
+ - `ProbeResult` records connect latency, HTTP status, and the peer TLS
74
+ certificate subject/issuer, so a verdict can be audited.
75
+ - Deliberately distinguishes DNS poisoning from a genuine outage, a real
76
+ HTTP 451/403 geo-restriction, and TLS interception. The classifier reports
77
+ `UNREACHABLE` rather than accusing the resolver when the true address fails
78
+ as well.
79
+ - **`dns_shield.resolve`** — DNS-over-HTTPS resolver.
80
+ - Pluggable providers: Cloudflare, Google, Quad9, any `application/dns-json`
81
+ endpoint, or a chain of them with per-provider failover.
82
+ - TTL-aware cache; answers kept in the resolver's order and consumed
83
+ round-robin across the address pool.
84
+ - Returns the CNAME chain alongside A/AAAA records.
85
+ - **`dns_shield.transport`** — SNI-preserving HTTP/1.1 client.
86
+ - Resolves over DoH, then dials the resulting address with SNI and `Host` set
87
+ to the hostname. Never calls `socket.getaddrinfo` on the request path.
88
+ - Rotates across the resolved address set before failing; re-resolves only
89
+ after every known address has failed.
90
+ - Retries only genuinely retryable statuses (408, 425, 429, 500, 502, 503,
91
+ 504) with exponential backoff and jitter, honouring a `retryAfter` hint.
92
+ - Optional `SuccessContract` for APIs that signal failure inside an
93
+ HTTP 200 body, with a Binance BAPI preset.
94
+ - **`dns_shield.patch`** — targeted override helpers.
95
+ - `ShieldSession` and `resolve_and_call()` for one-line use.
96
+ - `RequestsShieldAdapter` for opt-in, per-prefix `requests` integration.
97
+ - Explicitly scoped: nothing here patches global state or touches `/etc/hosts`.
98
+ - **`dns_shield.cli`** — `check`, `fetch` and `hosts` subcommands.
99
+ - Meaningful exit codes: 0 healthy, 1 poisoned/suspicious, 2
100
+ unreachable/unknown, 3 usage error.
101
+ - `--json` output on every subcommand.
102
+ - Typed throughout (`py.typed`), Python 3.10+, zero runtime dependencies.
103
+ - Offline test suite: 246 tests, with an autouse fixture that blocks real
104
+ sockets so no test can silently acquire a network dependency. Opt-in `live`
105
+ marker for integration tests.
106
+
107
+ ### Notes
108
+
109
+ - Measured against the motivating case: an Indonesian ISP (Biznet) returning a
110
+ single bogus address for every `*.binance.com` hostname, while the real hosts
111
+ behind CloudFront answered HTTP 200.
112
+ - The bogus address was observed to fail non-deterministically — sometimes an
113
+ instant RST, sometimes a 2-second timeout. The classifier treats both as
114
+ consistent with poisoning and records the latency as evidence rather than
115
+ making it the verdict.
116
+
117
+ [Unreleased]: https://github.com/beduldul/dns-shield/compare/v0.1.1...HEAD
118
+ [0.1.1]: https://github.com/beduldul/dns-shield/compare/v0.1.0...v0.1.1
119
+ [0.1.0]: https://github.com/beduldul/dns-shield/releases/tag/v0.1.0
@@ -0,0 +1,77 @@
1
+ # Contributing to dns-shield
2
+
3
+ Thanks for looking. Short version: keep it honest and keep the tests offline.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/beduldul/dns-shield
9
+ cd dns-shield
10
+ python -m venv .venv && . .venv/bin/activate
11
+ pip install -e ".[dev]"
12
+ python -m pytest -q
13
+ ```
14
+
15
+ ## The two rules that matter
16
+
17
+ **1. Tests must not touch the network.** An autouse fixture in
18
+ `tests/conftest.py` blocks real sockets, so this is enforced rather than
19
+ requested. If you genuinely need the real network, mark the test:
20
+
21
+ ```python
22
+ @pytest.mark.live
23
+ def test_something_live() -> None:
24
+ ...
25
+ ```
26
+
27
+ Live tests are deselected by default (`-m 'not live'` in `addopts`). Run them
28
+ with `python -m pytest -m live`. Keep them few and stable.
29
+
30
+ **2. Do not call anything "poisoned" without evidence.** The classifier in
31
+ `detect.py` is the part of this project most likely to do harm by being
32
+ confidently wrong. Before adding a rule, ask: *could this fire on a genuinely
33
+ down host, a genuine HTTP 451, or a corporate TLS proxy?* If yes, it needs
34
+ another condition. Every verdict branch needs a test proving it, including the
35
+ negative cases. `tests/test_detect.py::TestNotPoisoned` is the file to extend.
36
+
37
+ There is also a hard rule enforced in `tests/test_patch.py`: **never disable TLS
38
+ verification.** No `verify=False`, no `CERT_NONE`, no
39
+ `_create_unverified_context`. The technique works *because* certificates are
40
+ valid; accepting a bad one would be a silent security downgrade.
41
+
42
+ ## Style
43
+
44
+ - Python 3.10+, full type hints (the package ships `py.typed`). Avoid `Any`
45
+ where a real type exists; the exceptions are the `urllib`/`requests` interop
46
+ boundaries.
47
+ - Immutability: return new values, don't mutate arguments.
48
+ - Small modules, small functions. `dns_shield/` is deliberately split so each
49
+ file has one job.
50
+ - Backticks in docstrings for `` `code` ``, and comments that explain *why*.
51
+ This codebase's comments carry measured facts and traps that are not
52
+ re-derivable from the code — please keep that standard.
53
+ - Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`,
54
+ `chore:`.
55
+
56
+ ## Reporting a poisoning case
57
+
58
+ Genuinely useful bug reports include:
59
+
60
+ - the hostname and the country/ISP,
61
+ - `dns-shield check <host> --json` output, and
62
+ - `dig +short <host>` output from the same machine.
63
+
64
+ That lets the fingerprint be checked against a real case. Please redact anything
65
+ personal.
66
+
67
+ ## Pull requests
68
+
69
+ - One logical change per PR.
70
+ - Tests for the change; the suite must pass offline.
71
+ - Note any user-visible change in `CHANGELOG.md` under *Unreleased*.
72
+ - If you add a verdict branch, add the negative test that proves it does not
73
+ misfire.
74
+
75
+ ## Licence
76
+
77
+ Contributions are accepted under the MIT licence (see `LICENSE`).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 beduldul
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.