Skip to content

fix(config): build socket authorities from typed parts, never text concatenation - #117

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
harden/1682-ipv6-authority
Jul 28, 2026
Merged

fix(config): build socket authorities from typed parts, never text concatenation#117
MichaelTaylor3d merged 1 commit into
mainfrom
harden/1682-ipv6-authority

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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:

ticket actual home why not here
#1723 holdings_announce.rs:575 dig-gossip (src/service/holdings_announce.rs) dig-node has no such file; the pre-verification Vec::with_capacity is in the gossip crate
#1675 DhtError::transport dig-dht the error type is defined there
#1674 serde_json echo dig-dht / dig-gossip / dig-nat / dig-peer four crates, none of them dig-node

#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_addr rendered host and port with format!("{host}:{port}"), so DIG_NODE_HOST=::1
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, not a site: every address accessor now returns SocketAddr built via
SocketAddr::new, so the authority can only be rendered by SocketAddr's own Display, which
brackets. 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_async built http://{addr}/ — an unparseable JSON-RPC URL for a v6 host, so
    every dign control call would have failed.
  • server logged extension_host and the bound-address list as http://::1:9778, i.e. it told the
    operator 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". On origin/main
browser_host already brackets (Some(ip) if ip.is_ipv6() => format!("[{ip}]")), and it is the
only production caller, so no input could reach the defect. It was a latent trap in a &str
signature, not a live bug. Fixed anyway with a typed UrlAuthority — but the accompanying test is an
honest 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_VIOLATIONS entries 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:

  1. The scanner proved it escaped its own crate via assert_eq!(waived, KNOWN_VIOLATIONS.len())
    both tracked sites lived in the sibling crate. With the list empty that became 0 == 0, i.e. the
    detector 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).
  2. is_known_violation now takes its list as a parameter, so
    a_tracked_violation_excuses_only_its_own_call_not_its_whole_file still proves the
    excuses-only-its-own-call rule at zero entries (it previously indexed [0] and would have
    panicked). 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_SECS and dig-dht's provider_ttl as
"provably zero". It is two different relations, and the zero one is not the one named:

relation current failure mode
advertised <= provider_ttl 1h vs 2h — 1h margin the claim is silently clamped
republish_interval <= advertised 1h vs 1h — zero records expire before re-announcement

Pinning 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 bound
and 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_ttl is 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 /health addr 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_v6server.rs only. serve_url / browser_host
open.rs only, 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 one
observable output change is a v6 authority now rendering bracketed in /health/status/logs, which
was previously unparseable anyway. dig-node-service is 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

guard removed result
bind_addr back to format!("{}:{}") an_ipv6_host_override_binds_a_parseable_address and every_bind_accessor_renders_a_parseable_authority_for_an_ipv6_host both FAIL (AddrParseError(Socket)); no other test affected
UrlAuthority::Socket renders {ip}:{port} the_localhost_tier_url_brackets_an_ipv6_host FAILS, printing the actual bad URL http://2001:db8::1:9778/s/...
ADVERTISED_TTL_SECS to 10_800 (over provider_ttl) truncation relation FAILS: "advertised 10800s exceeds provider_ttl 7200s"
ADVERTISED_TTL_SECS to 1_800 (under republish_interval) refresh relation FAILS: "republish_interval 3600s exceeds advertised 1800s"

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:

panicked at config.rs:594: DIG_NODE_HOST=::1 renders "::1:9778", which does not parse:
invalid socket address syntax

It compiles against both the old String and the new SocketAddr return (it goes through
to_string()), so it is the same test in both states rather than a rewrite.

Fixtures chosen against the real constraint

  • 2001:db8::1 over ::1 for the URL test: its own interior :: is what makes a missing bracket
    ambiguous rather than merely unusual, and a full-form address also exercises SocketAddr's
    zero-compression rendering.
  • The TTL bounds step by one second, not a round number: the bound is <=, so equality must pass
    and the next representable value must fail. An hour-sized overshoot would still pass a predicate
    with an off-by-one in it.
  • The sibling-crate floor (5) is taken from the measured count (20), not picked round.

Verification

  • cargo test --workspace --all-features31 suites, 0 failures
  • cargo clippy --all-features --all-targets — clean (--all-features, matching CI)
  • cargo fmt --all -- --check — clean
  • banned_address_patterns — 3/3 green with an empty allowlist

Version

0.65.0 to 0.65.1, PATCH per §2.4: a compatible fix (an IPv6 host that killed the node now
binds), no removed/renamed external API, no wire/format/schema change, no changed default.

Interaction with the #1062 live-seed findings — read, NOT absorbed

Comment 5108796519 reports nine defects; none are in this diff. One interacts and is worth
recording: 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 live
network
— 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.

@MichaelTaylor3d MichaelTaylor3d changed the title fix(config): construct socket authorities from typed parts, never text concatenation fix(config): build socket authorities from typed parts, never text concatenation Jul 28, 2026
…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
MichaelTaylor3d force-pushed the harden/1682-ipv6-authority branch from 43a76df to 23d45d2 Compare July 28, 2026 21:55
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review July 28, 2026 21:56
@MichaelTaylor3d
MichaelTaylor3d merged commit fcd4ef1 into main Jul 28, 2026
16 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the harden/1682-ipv6-authority branch July 28, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant