go-ethereum/cmd/devp2p/nodesetcmd_test.go
Chase Wright 8d84e5001a
p2p/dnsdisc, cmd/devp2p: keep invalid and unreachable nodes out of DNS trees (#35312)
The DNS discovery crawler (ethereum/discv4-crawl) builds signed enrtree
lists by crawling the network and running the collected records through
`devp2p nodeset filter` and `devp2p dns sign`. Those records come from
external nodes, and `enr.Record` keeps entries it can't decode as raw
RLP, so a self-signed record with an out-of-range port (EIP-778 defines
ports only as "big endian integer", not `uint16`) can round-trip
verbatim into a signed tree. Consumers that decode ports strictly then
fail to decode that record.

Two publish-side checks:

- `MakeTree` rejects a record if a present port entry
(`tcp`/`tcp6`/`udp`/`udp6`/`quic`/`quic6`) does not decode as a
`uint16`. This is an invariant at the signing boundary: geth won't sign
a tree containing a record with an undecodable port, regardless of how
the node list was produced. Absent and zero ports are unaffected.

- `devp2p nodeset filter -dialable` keeps only nodes advertising a
usable RLPx port (non-zero `tcp`/`tcp6`/`quic`/`quic6`), letting the
crawler drop discovery-only and unreachable nodes so consumers aren't
handed peers they can't connect to.

The two are deliberately separate: the `MakeTree` check is a correctness
guard that always applies, while `-dialable` is an opt-in selection
filter for the crawler pipeline. A follow-up will add `-dialable` to
discv4-crawl's `filter_list`.
2026-07-14 09:05:02 -05:00

70 lines
1.9 KiB
Go

// Copyright 2026 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
)
func TestDialableFilter(t *testing.T) {
filter, err := dialableFilter(nil)
if err != nil {
t.Fatal(err)
}
makeNode := func(entries ...enr.Entry) nodeJSON {
r := new(enr.Record)
for _, e := range entries {
r.Set(e)
}
key, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
if err := enode.SignV4(r, key); err != nil {
t.Fatal(err)
}
n, err := enode.New(enode.ValidSchemes, r)
if err != nil {
t.Fatal(err)
}
return nodeJSON{N: n}
}
tests := []struct {
name string
node nodeJSON
want bool
}{
{"tcp", makeNode(enr.TCP(30303)), true},
{"tcp6", makeNode(enr.TCP6(30303)), true},
{"quic", makeNode(enr.QUIC(30303)), true},
{"no ports", makeNode(), false},
{"udp only", makeNode(enr.UDP(30303)), false},
{"zero tcp", makeNode(enr.TCP(0)), false},
{"oversized tcp", makeNode(enr.WithEntry("tcp", uint32(70000))), false},
}
for _, tt := range tests {
if got := filter(tt.node); got != tt.want {
t.Errorf("%s: dialable = %v, want %v", tt.name, got, tt.want)
}
}
}