p2p: resolve DNS-named addnodes before advertising them (#910) - #911
Merged
Conversation
NetAddress::Write_IPV4 split the stored address string on '.', demanded exactly four parts, and substituted 127.0.0.1 otherwise. A hostname seed such as --addnode rov.p2p-spb.xyz:8999 keeps the HOSTNAME as its stored address, so every addrs/getaddrs reply carrying that seed put loopback on the wire - an address that points every peer learning it back at itself. The outbound connect path already resolved these names correctly (core::Factory's Client::resolve). Memoise that resolver's A record and have the serializer render a name through the memo instead of inventing loopback. One resolver, no blocking DNS on the serialize/IO path, and the hostname stays the stored and dialled identity so a DNS seed still survives an IP change. On an unresolved name the ten per-coin getaddrs handlers now skip the entry rather than advertise it, and the serializer's last-resort path is a loud LOG_ERROR plus 0.0.0.0 - unroutable and rejected by is_connectable() at the far end - never loopback. Wire format unchanged: still 16 bytes, and the byte-producing loop is untouched, so a dotted quad (including a genuine --addnode 127.0.0.1:18999) serializes to the identical bytes. Pinned by KAT in the existing core_test target. Shared core: affects all five lanes (ltc/btc/bch/dgb/dash).
This was referenced Jul 27, 2026
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.
Fixes #910.
A DNS-named
--addnodewas advertised to peers as127.0.0.1.Verified root cause
src/core/netaddress.cpp—NetAddress::Write_IPV4opens at line 11; the defect is the fallback block at lines 23-28 (pre-fix):NetAddressstores the address as a string, and for a hostname seed that string is the hostname — deliberately, so every reconnect re-resolves it. The serializer split it on., saw 3 parts forrov.p2p-spb.xyz, and wrote loopback.Write_IPV4is what we put on the wire, so any peer that learned that seed from us got an address pointing back at itself.Confirmed on this tree:
src/core/factory.hpp:157,Client::resolve()issuesasync_resolve(addr.address(), addr.port_str())per dial, andpool::BaseNode(src/pool/node.hpp:37) inheritscore::Factory<Server, Client>, so this is the one dialler for all five lanes. That matches the field observation that the hotel is connected to66.151.242.154while logging the substitution warning forusa.p2p-spb.xyz.core::AddrStore::load),get_good_peers()(src/pool/node.hpp:169) hands them back verbatim, and the ten per-coinHANDLER(getaddrs)sites serialize them into anaddrsreply.Two corrections to the reported details, neither of which changes the fix:
netaddress.cpp:11is the function signature, not the substitution — that is lines 23-28.seed.node.example.com) passed thesize() != 4check and fell through tolexical_cast, which threw per label and produced0.0.0.0plus fourLOG_ERRORlines. So the pre-fix behaviour was "3-label name -> loopback, 4-label name -> garbage". Both are covered here.Which resolver was reused, and why
The existing one —
core::Factory'sClient::resolve()insrc/core/factory.hpp. Its completion handler already holds both the configured host string and the resolved endpoints, so the fix memoises the A record there (core::record_resolved_host). No second lookup is introduced anywhere, which is the point: two independent resolution paths that can disagree would be its own defect. It also means the fix is cross-coin by construction — every lane dials through this oneClient.Where resolution now happens
Not at serialization, and not blocking anywhere.
Write_IPV4does ashared_lockmap lookup —NetAddress::wire_ipv4_literal(). Never DNS. A dotted quad short-circuits before the lookup is even reached.address(),to_string(), the addr store key and the JSON persistence are all untouched, so a DNS seed still survives an IP change — that is the entire reason to use a hostname. A re-resolution that returns a different address simply overwrites the memo (logged at INFO)."localhost"is handled explicitly as a genuine resolution to127.0.0.1, not as a fallback. That also keeps the default-constructedNetAddress(whosem_ipis"localhost") serializing to exactly the bytes it always has.On resolution failure
Never loopback. Two layers:
NetAddress::is_wire_advertisable()is false for a name with no memoised A record, and all tenHANDLER(getaddrs)sites nowcontinuepast such entries instead of putting them in theaddrsreply. The seed is still dialled and still re-offered on the next sweep once a dial has resolved it — only the advertisement is withheld.Write_IPV4cannot skip — the message has a count prefix and the field is fixed-width — so an unrenderable address becomes0.0.0.0with aLOG_ERRORnaming the address.0.0.0.0is classifiedAddrClass::unspecifiedand rejected by the existingis_connectable()at the receiving end, so it misdirects nobody. Loopback is worse than no address precisely because it is valid-looking.A memoised AAAA answer is also treated as "unknown" rather than being fed into the four-byte IPv4 encoder.
Wire format
Unchanged.
Write_IPV4still emits exactly 16 bytes (12-byte IPv4-in-IPv6 prefix + 4 address bytes), and the byte-producing code — the split/lexical_cast/append loop — is untouched. The only change is what string it is handed: for a numeric literal, itself, byte-for-byte as before.The one behavioural difference for non-hostname input is that a malformed 4-part string (
1.2.3.999,a.b.c.d) now takes the loud0.0.0.0path instead of silently emitting1.2.3.231/0.0.0.0. That input was already producing garbage.What the tests prove
src/core/test/netaddress_resolution_test.cpp, folded into the existing allowlistedcore_testtarget — no newadd_executable, so it cannot land as a "Not Run" (the #769 trap).Wire pins (must not move a byte):
DottedQuadWireBytesAreUnchanged— seven KATs, including both hotel seed IPs,0.0.0.0and255.255.255.255, pinned to their exact 4 bytes inside the exact 16-byte envelope.GenuineLoopbackStillSerializesAndRoundTrips—127.0.0.1still gives7f 00 00 01, andNetService("127.0.0.1", 18999)still round-trips throughSerialize/Unserializeat 18 bytes. The hotel legitimately runs--addnode 127.0.0.1:18999.LocalhostStillMapsToLoopback—"localhost"and the default-constructedNetAddressstill serialize to loopback.DottedQuadRoundTripsThroughUnserialize.The defect (these fail on the pre-fix serializer):
UnresolvedHostnameDoesNotSerializeToLoopback—rov.p2p-spb.xyzmust not produce7f 00 00 01. Pre-fix it produces exactly that.ResolvedHostnameSerializesToItsARecord— after the connect path records the A record, both hotel hostnames serialize byte-identically to their literals.HostnameWithFourLabelsDoesNotSerializeAsGarbage— covers the second pre-fix path described above.ReResolutionTracksAnIpChange,ResolvedHostnameKeepsItsNameForReconnect— the seed keeps its name and follows its IP.MemoRefusesNonNumericAnswersAndLiteralKeys— the memo can never become a second source of names, and an AAAA answer does not corrupt the IPv4 envelope.AdvertisabilityGate— the skip predicate, including that genuine loopback stays advertisable.Regression fence:
EveryLaneFiltersUnrenderableAddrsFromGetaddrs— a source KAT asserting theis_wire_advertisable()guard exists and precedes theaddrs.push_back(...)in all tensrc/impl/{btc,ltc,bch,dgb,dash}/protocol_{actual,legacy}.cppgetaddrs handlers, so no lane can quietly regress.Blast radius
src/core/netaddress.cppandsrc/core/factory.hppare shared core: all five lanes (ltc / btc / bch / dgb / dash), bothActualandLegacyprotocols. This is not DASH-only —src/impl/btc/config_pool.hpp:212-222,src/impl/ltc/config_pool.hpp:216-228andsrc/impl/bch/config_pool.hpp:251-259ship hardcoded DNS-name bootstrap seeds (p2p-spb.xyz,ekb.p2p-spb.xyz,rov.p2p-spb.xyz,usa.p2p-spb.xyz,ml.toom.im,btc.p2pool.leblancnet.us,bch.p2pool.leblancnet.us,siberia.mine.nu), so a default-configured BTC/LTC/BCH node was advertising those as loopback with no operator action at all.Not reward-affecting: no share, block, payout or consensus path is touched. Peer-discovery quality only.
Not changed here
version'saddr_fromis still the hardcodedNetService{"0.0.0.0", <compile-time default port>}in every lane, and the coin-facingversionstill sends192.168.0.1/0.0.0.0. Both are pre-existing and separate from this issue.127.0.0.0) is untouched — that is the receive end of the same loopback mishandling.