Skip to content

Bump hickory-resolver from 0.25.2 to 0.26.2 - #175

Merged
liquidsec merged 1 commit into
devfrom
deps/hickory-resolver-0.26
Nov 10, 2026
Merged

liquidsec merged 1 commit into
devfrom
deps/hickory-resolver-0.26

Conversation

@liquidsec

Copy link
Copy Markdown
Contributor

Replaces #172, which failed to build.

Problem: hickory-resolver 0.26 removed NameServerConfig::socket_addr in favor of an ip field, so get_system_resolvers() stopped compiling.

Fix: Bump hickory-resolver to 0.26.2 and read ns.ip instead.

Note: hickory-proto and hickory-client stay on 0.25.2, so the lockfile now carries both 0.25 and 0.26 of hickory-proto.

Tests: fmt and clippy pass. Locally, get_system_resolvers() returns the nameserver listed in resolv.conf.

hickory-resolver 0.26 replaced NameServerConfig::socket_addr with an
`ip` field, so get_system_resolvers() reads that instead.
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

DNS Resolver Benchmark

this PR @ 9e8119c against dev @ 00fb035, base → head, median of 3 interleaved rounds. Bold is significant across all 56 comparisons (Benjamini-Hochberg at 1%); the rest is noise.

regime qps unanswered p99 attempts/query
ceiling/1-resolver 16,568 → 16,790 (+1.3%) 0.00% → 0.00% 1.204s → 1.188s (-1.3%) 1.00 → 1.00
pool/32-resolvers 16,469 → 16,649 (+1.1%) 0.00% → 0.00% 1.211s → 1.198s (-1.1%) 1.00 → 1.00
pool/32-persistent 18,473 → 18,474 (+0.0%) 0.00% → 0.00% 1.080s → 1.080s (-0.0%) 1.00 → 1.00
impaired/10%-loss 219 → 219 (-0.0%) 0.00% → 0.00% 7.309s → 7.314s (+0.1%) 1.11 → 1.11
impaired/20%-REFUSED 2,171 → 2,178 (+0.3%) 0.00% → 0.00% 0.921s → 0.918s (-0.3%) 1.24 → 1.24
impaired/50ms-latency 616 → 615 (-0.1%) 0.00% → 0.00% 3.247s → 3.249s (+0.1%) 1.00 → 1.00
impaired/20%-then-2.5%-dead-names 256 → 235 (-8.3%) 5.43% → 5.43% 11.682s → 11.017s (-5.7%) 1.05 → 1.05
All metrics
regime p50 p100 sockets conntrack
ceiling/1-resolver 0.658s → 0.644s (-2.1%) 1.207s → 1.191s (-1.3%) 42 → 42 (+0.0%) 13,778 → 12,601 (-8.5%)
pool/32-resolvers 0.652s → 0.647s (-0.8%) 1.214s → 1.201s (-1.1%) 74 → 71 (-4.1%) 17,896 → 17,920 (+0.1%)
pool/32-persistent 0.589s → 0.580s (-1.5%) 1.082s → 1.082s (-0.0%) 42 → 42 (+0.0%) 29 → 33 (+14%)
impaired/10%-loss 3.315s → 3.316s (+0.0%) 9.125s → 9.127s (+0.0%) 46 → 46 (+0.0%) 2,122 → 2,102 (-0.9%)
impaired/20%-REFUSED 0.621s → 0.621s (+0.0%) 0.921s → 0.918s (-0.3%) 46 → 46 (+0.0%) 2,284 → 2,313 (+1.3%)
impaired/50ms-latency 1.851s → 1.856s (+0.3%) 3.248s → 3.250s (+0.1%) 46 → 46 (+0.0%) 373 → 1,248 (+235%)
impaired/20%-then-2.5%-dead-names 4.113s → 3.849s (-6.4%) 11.714s → 12.768s (+9.0%) 39 → 39 (+0.0%) 61 → 3,015 (+4843%)

@en0f en0f left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 9e8119c.

🔴 Validation is stated, not shown, and misses the platforms that changed

The Tests paragraph is two claims with no command or output ("fmt and clippy pass" is lint, not tests), and the org git standard (§ Pull requests) asks for the commands run and their real output. CI covers Linux only: every test job runs on ubuntu-24.04. hickory-resolver 0.26 replaced the resolv.conf reader on macOS (SCDynamicStore) and Android (JNI), so get_system_resolvers() now reads from a different source on platforms its docstring promises.

Paste the commands you ran and their output, say which OS they ran on, and link the green CI run. Then either add python -c 'import blastdns; print(blastdns.get_system_resolvers())' on macOS next to scutil --dns, or state plainly that macOS and Android are not validated.

🟢 The one-line migration reads the same addresses on Linux and Windows

0.25.2 always built socket_addr with port 53, so ns.socket_addr.ip() only dropped a fixed port, and 0.26.2 exposes the same address as pub ip: IpAddr. Reading ns.ip keeps the deduplicated bare IPs that client.rs formats, and the pin bump lands in the same commit as the code it breaks, as toolchain.md asks.

Reproduce this review
# save the script below as verify-175.sh, then:
bash verify-175.sh blacklanternsecurity/blastdns 9e8119c8b9d752ba89fc942041024ff262f6956d
# expected: SUMMARY 9 PASS, 5 FAIL, 0 SKIP, 0 BROKEN
# control:  bash verify-175.sh blacklanternsecurity/blastdns 00fb035eda8809aaecf790858b29f6237f7c71b7   -> V01=FAIL V02=FAIL V03=FAIL
#!/usr/bin/env bash
# verify-175.sh: reproducible checks for blacklanternsecurity/blastdns#175
# Usage: bash verify-175.sh <owner/repo> <sha> [<control-patch>]
# Changes nothing outside a mktemp directory and nothing on GitHub (PR-VF-02).
# Uses only git, grep, awk and sed from the host; no fetched tools to pin.
#
# CONTROL: sha 00fb035eda8809aaecf790858b29f6237f7c71b7
# CONTROL-EXPECT: V01=FAIL V02=FAIL V03=FAIL
set -uo pipefail

REPO="${1:?owner/repo}"; SHA="${2:?sha}"; PATCH="${3:-}"
PASS=0; FAIL=0; SKIP=0; BROKEN=0
pass()   { echo "PASS $1 $2"; PASS=$((PASS+1)); }
fail()   { echo "FAIL $1 $2 -- $3"; FAIL=$((FAIL+1)); }
skip()   { echo "SKIP $1 $2 -- $3"; SKIP=$((SKIP+1)); }
broken() { echo "BROKEN $1 $2 -- $3"; BROKEN=$((BROKEN+1)); }
summary() { echo "SUMMARY $PASS PASS, $FAIL FAIL, $SKIP SKIP, $BROKEN BROKEN"; [ "$FAIL" -eq 0 ] && [ "$BROKEN" -eq 0 ]; }

echo "script_sha256 $(shasum -a 256 "$0" | cut -d' ' -f1)"
echo "git $(git --version | cut -d' ' -f3)"

WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0
GIT=(git -c credential.helper= -c 'credential.helper=!gh auth git-credential')
"${GIT[@]}" clone --quiet --filter=blob:none --no-checkout "https://github.com/$REPO.git" "$WORK/src" || { broken V00 "clone" "clone failed"; summary; exit 1; }
cd "$WORK/src"
"${GIT[@]}" cat-file -e "$SHA^{commit}" 2>/dev/null || "${GIT[@]}" fetch --quiet origin "+refs/pull/*/head:refs/remotes/pr/*" || true
"${GIT[@]}" checkout --quiet --detach "$SHA" || { broken V00 "checkout $SHA" "checkout failed"; summary; exit 1; }
[ "$(git rev-parse HEAD)" = "$SHA" ] || { broken V00 "HEAD is $SHA" "HEAD mismatch"; summary; exit 1; }
if [ -n "$PATCH" ]; then git apply "$PATCH" || { broken V00 "apply control patch" "git apply failed"; summary; exit 1; }; fi

# ---- checks: each line states the desired property --------------------------
# V01 Cargo.toml pins hickory-resolver 0.26.2
line=$(grep -E '^hickory-resolver *=' Cargo.toml || true)
if echo "$line" | grep -q 'version = "0.26.2"'; then pass V01 "Cargo.toml pins hickory-resolver 0.26.2"
else fail V01 "Cargo.toml pins hickory-resolver 0.26.2" "${line:-no hickory-resolver line}"; fi

# V02 Cargo.lock resolves hickory-resolver 0.26.2
lockver() { awk -v n="$1" '$0=="[[package]]"{p=0} $0=="name = \""n"\""{p=1} p&&/^version = /{gsub(/"/,"",$3);print $3;p=0}' Cargo.lock | sort -u | tr '\n' ' ' | sed 's/ $//'; }
rv=$(lockver hickory-resolver)
if [ "$rv" = "0.26.2" ]; then pass V02 "Cargo.lock resolves hickory-resolver 0.26.2"
else fail V02 "Cargo.lock resolves hickory-resolver 0.26.2" "resolved: ${rv:-none}"; fi

# V03 get_system_resolvers() reads the 0.26 NameServerConfig.ip field, not the removed socket_addr
body=$(awk '/^pub fn get_system_resolvers/{p=1} p{print} p&&/^}/{exit}' src/utils.rs)
if echo "$body" | grep -q '\.map(|ns| ns\.ip)' && ! echo "$body" | grep -q 'socket_addr'; then pass V03 "get_system_resolvers reads ns.ip (0.26 API)"
else fail V03 "get_system_resolvers reads ns.ip (0.26 API)" "$(echo "$body" | grep -n 'map(|ns|' | tr -s ' ')"; fi

# V04 the Python boundary test still asserts bare IPs with no port
if awk '/^def test_get_system_resolvers/{p=1} p&&/^def /&&!/test_get_system_resolvers/{exit} p{print}' blastdns/tests/test_client.py | grep -q 'assert ":" not in resolver'; then
  pass V04 "test_get_system_resolvers asserts no port at the Python boundary"
else fail V04 "test_get_system_resolvers asserts no port at the Python boundary" "assertion not found"; fi

# V05 hickory_resolver is used only for system_conf in src/utils.rs (no 0.26 type crosses into 0.25 code)
uses=$(grep -rln 'hickory_resolver' --include='*.rs' src | sort | tr '\n' ' ' | sed 's/ $//')
if [ "$uses" = "src/utils.rs" ] && [ "$(grep -c 'hickory_resolver' src/utils.rs)" = "1" ] && grep -q '^use hickory_resolver::system_conf;' src/utils.rs; then
  pass V05 "hickory_resolver used only as system_conf in src/utils.rs"
else fail V05 "hickory_resolver used only as system_conf in src/utils.rs" "used in: ${uses:-none}"; fi

# V06 the commit subject is a Conventional Commit (git's own merge commits exempt)
subj=$(git log -1 --format=%s)
if [ "$(git rev-list --parents -n1 HEAD | wc -w | tr -d ' ')" -gt 2 ]; then skip V06 "commit subject is a Conventional Commit" "HEAD is a merge commit: $subj"
elif echo "$subj" | grep -Eq '^(feat|fix|docs|test|refactor|perf|build|ci|chore)(\([^)]+\))?!?: .+'; then pass V06 "commit subject is a Conventional Commit"
else fail V06 "commit subject is a Conventional Commit" "subject: $subj"; fi

# V07 Cargo.lock carries a single hickory-proto version
pv=$(lockver hickory-proto)
if [ "$(echo "$pv" | wc -w | tr -d ' ')" = "1" ]; then pass V07 "Cargo.lock carries one hickory-proto version"
else fail V07 "Cargo.lock carries one hickory-proto version" "versions: $pv"; fi

# ---- added after the round-1 challenge ---------------------------------------
# Upstream crate sources, verified against the checksums in Cargo.lock (base lock for 0.25.2,
# this tree's lock for 0.26.2), with a wrong-hash control.
BASE_SHA=00fb035eda8809aaecf790858b29f6237f7c71b7
lockck() { awk -v n="$1" -v v="$2" '$0=="[[package]]"{p=0;q=0} $0=="name = \""n"\""{p=1} p&&$0=="version = \""v"\""{q=1} p&&q&&/^checksum = /{gsub(/"/,"",$3);print $3;exit}'; }
CR="$WORK/crates"; mkdir -p "$CR"
getcrate() { # name version checksum -> extracts into $CR, returns 0 when the checksum matched
  curl -fsSLo "$CR/$1-$2.crate" "https://static.crates.io/crates/$1/$1-$2.crate" || return 2
  echo "$3  $CR/$1-$2.crate" | shasum -a 256 -c - >/dev/null 2>&1 || return 3
  tar xzf "$CR/$1-$2.crate" -C "$CR" || return 2
}
if [ "$rv" != "0.26.2" ]; then
  for id in V08 V09 V10 V11 V13 V14; do skip $id "upstream hickory-resolver comparison" "this tree does not resolve 0.26.2"; done
else
  ck25=$(git show "$BASE_SHA:Cargo.lock" | lockck hickory-resolver 0.25.2); ck26=$(lockck hickory-resolver 0.26.2 < Cargo.lock)
  getcrate hickory-resolver 0.25.2 "$ck25"; r25=$?; getcrate hickory-resolver 0.26.2 "$ck26"; r26=$?
  if echo "0000000000000000000000000000000000000000000000000000000000000000  $CR/hickory-resolver-0.26.2.crate" | shasum -a 256 -c - >/dev/null 2>&1; then
    broken V08 "wrong-hash control fails" "accepted a wrong hash"
  else pass V08 "wrong-hash control fails"; fi
  if [ $r25 -ne 0 ] || [ $r26 -ne 0 ]; then
    for id in V09 V10 V11 V13 V14; do skip $id "upstream hickory-resolver comparison" "crate fetch or checksum failed ($r25/$r26)"; done
  else
    # V09 (A2) 0.26.2 NameServerConfig exposes the address as `pub ip: IpAddr`
    if grep -q 'pub ip: IpAddr' "$CR/hickory-resolver-0.26.2/src/config.rs"; then pass V09 "0.26.2 NameServerConfig.ip is an IpAddr"
    else fail V09 "0.26.2 NameServerConfig.ip is an IpAddr" "field not found"; fi
    # V10 (A2) 0.25.2 always built socket_addr with port 53, so dropping the port loses nothing
    if grep -q 'socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT)' "$CR/hickory-resolver-0.25.2/src/system_conf/unix.rs" && grep -q 'const DEFAULT_PORT: u16 = 53;' "$CR/hickory-resolver-0.25.2/src/system_conf/unix.rs"; then
      pass V10 "0.25.2 system_conf always used port 53"
    else fail V10 "0.25.2 system_conf always used port 53" "pattern not found"; fi
    # V11 (C5/C6) the macOS and Android system_conf backends are unchanged between 0.25.2 and 0.26.2
    if grep -q 'target_vendor = "apple"' "$CR/hickory-resolver-0.26.2/src/system_conf/mod.rs" && ! grep -q 'target_vendor = "apple"' "$CR/hickory-resolver-0.25.2/src/system_conf/mod.rs"; then
      fail V11 "macOS/Android system_conf backends unchanged by the bump" "0.26.2 adds target_vendor=apple and target_os=android backends; 0.25.2 read resolv.conf on all unix"
    else pass V11 "macOS/Android system_conf backends unchanged by the bump"; fi
    # V13 (round-2 C18) Windows reads nameserver IPs the same way in both versions (port 53 in 0.25, bare ip in 0.26)
    if grep -q 'SocketAddr::new(\*dns_server, 53)' "$CR/hickory-resolver-0.25.2/src/system_conf/windows.rs" && grep -q 'NameServerConfig::udp_and_tcp(ip)' "$CR/hickory-resolver-0.26.2/src/system_conf/windows.rs"; then
      pass V13 "Windows nameserver addresses read the same way in 0.25.2 and 0.26.2"
    else fail V13 "Windows nameserver addresses read the same way in 0.25.2 and 0.26.2" "pattern not found"; fi
    # V14 (round-2 C20) the 0.26 macOS backend accepts a scoped IPv6 nameserver as 0.25's resolv-conf path did
    ckrc=$(lockck resolv-conf 0.7.6 < Cargo.lock)
    if [ -z "$ckrc" ]; then skip V14 "0.26 macOS backend accepts scoped IPv6 nameservers" "resolv-conf 0.7.6 not in Cargo.lock"
    elif ! getcrate resolv-conf 0.7.6 "$ckrc"; then skip V14 "0.26 macOS backend accepts scoped IPv6 nameservers" "resolv-conf fetch or checksum failed"
    elif grep -q 'IpAddr::from_str(&Cow::from(&\*n))' "$CR/hickory-resolver-0.26.2/src/system_conf/apple.rs" && grep -q 'failed to parse nameserver address: {e}"))?;' "$CR/hickory-resolver-0.26.2/src/system_conf/apple.rs" && grep -q "split('%')" "$CR/resolv-conf-0.7.6/src/ip.rs"; then
      fail V14 "0.26 macOS backend accepts scoped IPv6 nameservers" "apple.rs parses ServerAddresses with std IpAddr::from_str and '?', which fails the whole call on 'fe80::1%en0'; resolv-conf (0.25's macOS path) splits the scope"
    else pass V14 "0.26 macOS backend accepts scoped IPv6 nameservers"; fi
  fi
fi

# V12 (C13, pre-existing) an IPv6 system resolver keeps its address through client.rs's "{ip}:53" formatting
if grep -q 'format!("{}:53", ip)' src/client.rs; then
  if python3 -I -c 'import ipaddress,sys; s="2001:db8::1"+":53"; sys.exit(0 if ipaddress.ip_address(s)!=ipaddress.ip_address("2001:db8::1") else 1)' 2>/dev/null; then
    fail V12 "IPv6 system resolver keeps its address in client.rs" "src/client.rs formats '{}:53'; '2001:db8::1:53' is itself a valid, different IPv6 address that parse_resolver's fallback accepts"
  else pass V12 "IPv6 system resolver keeps its address in client.rs"; fi
else pass V12 "IPv6 system resolver keeps its address in client.rs"; fi
# ------------------------------------------------------------------------------
summary

@liquidsec

liquidsec commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

The script's V12 check caught a real IPv6 bug, opened #176 for it. It's been in client.rs since the system-resolver fallback was added and has nothing to do with this PR.

We won't be pasting command output or CI links here. CI runs on every PR, and its results are already on this one.

@en0f en0f left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved,. Minor IPv6 bug will be addressed in #176

@liquidsec
liquidsec merged commit 9ef0fa0 into dev Nov 10, 2026
23 checks passed
@liquidsec
liquidsec deleted the deps/hickory-resolver-0.26 branch November 10, 2026 14:16
@singlerider singlerider added the dependencies Pull requests that update a dependency file label Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants