Skip to content

Commit f06c0aa

Browse files
Pin _addr_equiv IPv6 bracket-tolerance and ValueError fallback
``cluster._addr_equiv`` gates leader-redirect-trust: when ``_query_leader`` returns a redirect target, the helper decides whether the target is the same node as the one already in the store (skip authorization) or a real redirect (run ``_check_redirect``). The canonical ``(host, port)`` tuple comparison and the ``ValueError`` fallback (literal string equality on parse failure) had no direct test coverage. Pin both shapes so a future refactor that changes the fallback semantics — e.g. returning ``False`` on parse error — cannot silently flip the redirect-policy gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9beabe8 commit f06c0aa

2 files changed

Lines changed: 72 additions & 3 deletions

File tree

src/dqliteclient/cluster.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,17 @@
7878

7979

8080
def _addr_equiv(a: str, b: str) -> bool:
81-
"""Compare host:port strings tolerating IPv6 bracketing differences.
81+
"""Compare host:port strings via the canonical ``(host, port)``
82+
tuple shape produced by :func:`_parse_address`.
8283
8384
Falls back to literal equality for unparseable inputs so a
8485
malformed string never crashes the comparison. Hostname-vs-IP
8586
mismatch (``localhost:9001`` vs ``127.0.0.1:9001``) is not
86-
canonicalised — DNS resolution belongs elsewhere — but
87-
``[::1]:9001`` and ``::1:9001`` resolve to the same tuple.
87+
canonicalised — DNS resolution belongs elsewhere. Note that
88+
``_parse_address`` rejects unbracketed IPv6 (per the strict-
89+
validation hardening), so ``[::1]:9001`` and ``::1:9001`` do
90+
NOT compare equal — the unbracketed form raises ``ValueError``
91+
and the fallback compares literal strings.
8892
"""
8993
try:
9094
return _parse_address(a) == _parse_address(b)

tests/test_addr_equiv.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Pin: ``cluster._addr_equiv`` IPv6 bracket-tolerance and the
2+
ValueError fallback for malformed addresses.
3+
4+
The function gates leader-redirect-trust: when
5+
``_query_leader`` returns the redirect target, ``_addr_equiv``
6+
decides whether the target is the same node as the
7+
``node.address`` already in the store (skip authorization) or
8+
a real redirect (run ``_check_redirect``). The canonical
9+
``(host, port)`` tuple comparison must tolerate bracketed vs.
10+
unbracketed IPv6 forms; malformed-on-both-sides must compare
11+
equal via the literal-equality fallback so a hostile peer
12+
cannot defeat the redirect-policy check by sending a non-
13+
parseable address.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import pytest
19+
20+
from dqliteclient.cluster import _addr_equiv
21+
22+
23+
@pytest.mark.parametrize(
24+
("a", "b", "expected"),
25+
[
26+
# Identical canonical addresses.
27+
("localhost:9001", "localhost:9001", True),
28+
("[::1]:9001", "[::1]:9001", True),
29+
# Different hosts, same port.
30+
("node-a:9001", "node-b:9001", False),
31+
# Same host, different ports.
32+
("localhost:9001", "localhost:9002", False),
33+
],
34+
)
35+
def test_addr_equiv_well_formed_addresses(a: str, b: str, expected: bool) -> None:
36+
assert _addr_equiv(a, b) is expected
37+
38+
39+
def test_addr_equiv_unbracketed_ipv6_falls_back_to_literal_compare() -> None:
40+
"""``_parse_address`` rejects unbracketed IPv6 — so the
41+
fallback compares literal strings. The two forms are NOT
42+
equivalent under ``_addr_equiv``; the redirect-trust gate
43+
runs the policy check on the unbracketed form. Pin this
44+
behaviour so a future relaxation of the parser would also
45+
update the docstring's "resolve to same tuple" claim."""
46+
assert _addr_equiv("[::1]:9001", "::1:9001") is False
47+
48+
49+
def test_addr_equiv_falls_back_to_literal_equality_on_malformed() -> None:
50+
"""When both sides are syntactically invalid, the parser
51+
raises ``ValueError`` and the fallback compares literal
52+
strings. Pin so a future refactor that changes the
53+
ValueError-fallback shape (e.g. returning False on parse
54+
error) can't silently flip the redirect-policy gate."""
55+
assert _addr_equiv("not-an-address", "not-an-address") is True
56+
assert _addr_equiv("not-an-address", "different-malformed") is False
57+
58+
59+
def test_addr_equiv_one_side_malformed_returns_false() -> None:
60+
"""One well-formed, one malformed → fallback to literal
61+
string comparison; the strings differ so result is False.
62+
Pin against a refactor that crashes on parse failure of one
63+
side."""
64+
assert _addr_equiv("localhost:9001", "garbage") is False
65+
assert _addr_equiv("garbage", "localhost:9001") is False

0 commit comments

Comments
 (0)