fix(config): build socket authorities from typed parts, never text concatenation - #117
Merged
Merged
Conversation
…ncatenation
`Config::bind_addr` rendered `DIG_NODE_HOST` and the port with
`format!("{host}:{port}")`, so an IPv6 host produced the unbracketed `::1:9778`.
That is not a socket address in the grammar, the bind failed, and a bind failure
on that listener is documented FATAL - so configuring the family 5.2 makes
preferred took the node down.
Fixed as a class rather than a site: every address accessor now returns a
`SocketAddr` built with `SocketAddr::new`, so the authority can only be rendered
by `SocketAddr`'s own `Display`, which brackets IPv6. An unbracketed rendering is
unrepresentable rather than merely avoided. The same typing fixes two further
live IPv6 defects the ticket did not name: the control client's `http://{addr}/`
JSON-RPC URL and the `extension_host` / bound-address operator log line, both
unparseable for a v6 host.
`open.rs` gains a typed `UrlAuthority` for the same reason. Its `serve_url`
defect was NOT live - `browser_host` already bracketed, so no caller could reach
it - but a `&str` signature cannot keep the next caller out.
Both `KNOWN_VIOLATIONS` entries are deleted and the #1593 scanner is green with
an empty list. Emptying it cost two guard properties, both replaced rather than
dropped: the matcher now takes its list as a parameter so the
excuses-only-its-own-call rule stays provable at zero entries, and the scan
asserts it read files from the sibling crate `dig-node-service` - the cross-crate
reach the waived count used to prove and, at `0 == 0`, no longer can.
Also enforces the advertised-TTL coupling to dig-dht instead of describing it in
a comment. Two relations are asserted against `DhtConfig::default()` itself: the
truncation bound (advertised <= provider_ttl) and the refresh bound
(republish_interval <= advertised). #1722 described "the margin" as zero, which
is true of the refresh bound (1h vs 1h) and not of the truncation bound (1h vs a
2h default); pinning only the named relation would have left the other
unguarded. Each is pinned from both sides via a pure predicate exercised at the
bound and one second past it, so the check cannot degrade into something that
only ever passes. The zero refresh margin is deliberately not closed: every way
to close it changes on-wire behaviour and trades clamp-safety against
refresh-margin in opposite directions, with no value satisfying both.
Bumps the workspace release version to 0.65.1 - PATCH under 2.4. A compatible
fix; no wire format, CLI surface, or `DIG_NODE_*` env contract changes, and
dig-node-service is the shipped binary rather than a published library, so the
return-type change reaches no external consumer.
Closes #1682
Closes #1722
Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
force-pushed
the
harden/1682-ipv6-authority
branch
from
July 28, 2026 21:55
43a76df to
23d45d2
Compare
MichaelTaylor3d
marked this pull request as ready for review
July 28, 2026 21:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes DIG-Network/dig_ecosystem#1682 (P1) and DIG-Network/dig_ecosystem#1722.
Scope correction — this batch was dispatched as five tickets; only two are dig-node
The other three live in different submodule repos, so folding them in here would break
single-writer. Reported rather than split on instinct:
holdings_announce.rs:575src/service/holdings_announce.rs)Vec::with_capacityis in the gossip crateDhtError::transport#1722 straddled dig-node-core and dig-dht, but needs no dig-dht change: dig-node-core already
depends on dig-dht and
DhtConfig::default()is public, so the relation is assertable from this side.No release-first ordering required.
#1682 — the P1
Config::bind_addrrendered host and port withformat!("{host}:{port}"), soDIG_NODE_HOST=::1produced the unbracketed
::1:9778. That is not a socket address in the grammar, the bind failed, anda bind failure on that listener is documented FATAL — so configuring the family §5.2 makes
preferred took the node down.
Fixed as a class, not a site: every address accessor now returns
SocketAddrbuilt viaSocketAddr::new, so the authority can only be rendered bySocketAddr's ownDisplay, whichbrackets. An unbracketed rendering is unrepresentable rather than avoided.
The typing found two further live IPv6 defects the ticket did not name, both consuming the same
broken string:
control_client::call_asyncbuilthttp://{addr}/— an unparseable JSON-RPC URL for a v6 host, soevery
digncontrol call would have failed.serverloggedextension_hostand the bound-address list ashttp://::1:9778, i.e. it told theoperator to configure a URL that cannot be parsed.
The second defect the ticket named is NOT live — premise correction
#1682 says
open.rs::serve_url"produces an unparseable URL for a v6 host". Onorigin/mainbrowser_hostalready brackets (Some(ip) if ip.is_ipv6() => format!("[{ip}]")), and it is theonly production caller, so no input could reach the defect. It was a latent trap in a
&strsignature, not a live bug. Fixed anyway with a typed
UrlAuthority— but the accompanying test is anhonest regression pin, not a red-first test, and this PR says so rather than claiming a red it
could not produce.
The allowlist was load-bearing for the guard itself
Both
KNOWN_VIOLATIONSentries are deleted and the #1593 scanner is green with an empty list.Emptying it silently cost two guard properties; both are replaced rather than dropped:
assert_eq!(waived, KNOWN_VIOLATIONS.len())—both tracked sites lived in the sibling crate. With the list empty that became
0 == 0, i.e. thedetector evaporated at the exact moment it succeeded. Replaced with a direct assertion that the
scan read at least 5 files from
dig-node-service(measured 20).is_known_violationnow takes its list as a parameter, soa_tracked_violation_excuses_only_its_own_call_not_its_whole_filestill proves theexcuses-only-its-own-call rule at zero entries (it previously indexed
[0]and would havepanicked). It additionally asserts that an empty list excuses nothing.
#1722 — the TTL coupling, and a premise correction
The ticket reports the margin between
ADVERTISED_TTL_SECSand dig-dht'sprovider_ttlas"provably zero". It is two different relations, and the zero one is not the one named:
advertised <= provider_ttlrepublish_interval <= advertisedPinning only the named relation would have left the real one unguarded. Both are now asserted against
DhtConfig::default()itself, each from both sides via a pure predicate exercised at the boundand one second past it — so the check cannot degrade into something that only ever passes.
Deliberately not closed: the zero refresh margin. Advertising 2h buys an hour of margin but is
clamped for any receiver configured below 2h (
provider_ttlis a config field, not a constant);dig-dht republishing sooner is another repo, release-first. The two directions trade clamp-safety
against refresh-margin and no value satisfies both. That is a wire-behaviour decision, not a test
fix — flagged for a call rather than guessed at.
Blast radius checked
gitnexus is disabled per the §2.0 override, so this was done with ripgrep plus a direct read of every
call site.
bind_addr— 5 production callers, all inspected and migrated:control_client.rs:45,server.rs:405(the/healthaddr field, now.to_string()),server.rs:1489(the FATAL bind),service.rs:786(install summary),service.rs:914(status).bind_addr_v6,dig_local_addr,dig_local_https_addr,dig_local_https_addr_v6—server.rsonly.serve_url/browser_host—open.rsonly, zero test callers. Helper signatures updated:warn_ipv6_bind_failed,warn_dig_local_bind_failed,spawn_https_listener,probe_health,call_async.Risk: LOW. No wire format, no
DIG_NODE_*env contract change, no CLI surface change. The oneobservable output change is a v6 authority now rendering bracketed in
/health/status/logs, whichwas previously unparseable anyway.
dig-node-serviceis the shipped binary, not a published library,so the return-type change reaches no external consumer.
Guard delete-probes — each guard reverted, each test confirmed RED
bind_addrback toformat!("{}:{}")an_ipv6_host_override_binds_a_parseable_addressandevery_bind_accessor_renders_a_parseable_authority_for_an_ipv6_hostboth FAIL (AddrParseError(Socket)); no other test affectedUrlAuthority::Socketrenders{ip}:{port}the_localhost_tier_url_brackets_an_ipv6_hostFAILS, printing the actual bad URLhttp://2001:db8::1:9778/s/...ADVERTISED_TTL_SECSto10_800(overprovider_ttl)ADVERTISED_TTL_SECSto1_800(underrepublish_interval)Each probe ran against a committed tree and was restored from a file copy, never
git checkout --.The #1682 test was genuinely red first, before any production change:
It compiles against both the old
Stringand the newSocketAddrreturn (it goes throughto_string()), so it is the same test in both states rather than a rewrite.Fixtures chosen against the real constraint
2001:db8::1over::1for the URL test: its own interior::is what makes a missing bracketambiguous rather than merely unusual, and a full-form address also exercises
SocketAddr'szero-compression rendering.
<=, so equality must passand the next representable value must fail. An hour-sized overshoot would still pass a predicate
with an off-by-one in it.
Verification
cargo test --workspace --all-features— 31 suites, 0 failurescargo clippy --all-features --all-targets— clean (--all-features, matching CI)cargo fmt --all -- --check— cleanbanned_address_patterns— 3/3 green with an empty allowlistVersion
0.65.0to0.65.1, PATCH per §2.4: a compatible fix (an IPv6 host that killed the node nowbinds), no removed/renamed external API, no wire/format/schema change, no changed default.
Interaction with the #1062 live-seed findings — read, NOT absorbed
Comment
5108796519reports nine defects; none are in this diff. One interacts and is worthrecording: finding 6, "opcode-222 announce is not ingested" (seed2 never logged
applied a verified announcement) means the #1722 TTL relation has never been exercised end-to-end on a livenetwork — the announce path it governs does not currently complete. This PR makes the constants
mechanically consistent; it does not and cannot prove the coupling correct in production, and #1722
must not be read as evidence that announce ingestion works. Findings 3 (cold-start HTTP with no P2P
engine) and 1/2 (relay-circuit mTLS with both ends acting as server) are independent of this diff.