diff --git a/cmd/devp2p/nodesetcmd.go b/cmd/devp2p/nodesetcmd.go index d7725fadb1..4b01a21c07 100644 --- a/cmd/devp2p/nodesetcmd.go +++ b/cmd/devp2p/nodesetcmd.go @@ -141,6 +141,7 @@ var filterFlags = map[string]nodeFilterC{ "-eth-network": {1, ethFilter}, "-les-server": {0, lesFilter}, "-snap": {0, snapFilter}, + "-dialable": {0, dialableFilter}, } // parseFilters parses nodeFilters from args. @@ -272,3 +273,15 @@ func snapFilter(args []string) (nodeFilter, error) { } return f, nil } + +func dialableFilter(args []string) (nodeFilter, error) { + f := func(n nodeJSON) bool { + var tcp, tcp6, quic, quic6 uint16 + n.N.Load((*enr.TCP)(&tcp)) + n.N.Load((*enr.TCP6)(&tcp6)) + n.N.Load((*enr.QUIC)(&quic)) + n.N.Load((*enr.QUIC6)(&quic6)) + return tcp != 0 || tcp6 != 0 || quic != 0 || quic6 != 0 + } + return f, nil +} diff --git a/cmd/devp2p/nodesetcmd_test.go b/cmd/devp2p/nodesetcmd_test.go new file mode 100644 index 0000000000..68f3dfb053 --- /dev/null +++ b/cmd/devp2p/nodesetcmd_test.go @@ -0,0 +1,70 @@ +// 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 . + +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) + } + } +} diff --git a/p2p/dnsdisc/tree.go b/p2p/dnsdisc/tree.go index a629d6291f..55ef9b19cc 100644 --- a/p2p/dnsdisc/tree.go +++ b/p2p/dnsdisc/tree.go @@ -162,6 +162,9 @@ func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) { if len(n.Record().Signature()) == 0 { return nil, fmt.Errorf("can't add node %v: unsigned node record", n.ID()) } + if err := checkRecordPorts(n.Record()); err != nil { + return nil, fmt.Errorf("can't add node %v: %v", n.ID(), err) + } } // Create the leaf list. @@ -393,6 +396,30 @@ func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) { return &enrEntry{n}, nil } +var portKeys = []string{ + enr.TCP(0).ENRKey(), enr.TCP6(0).ENRKey(), + enr.UDP(0).ENRKey(), enr.UDP6(0).ENRKey(), + enr.QUIC(0).ENRKey(), enr.QUIC6(0).ENRKey(), +} + +// checkRecordPorts verifies that port entries, if present, decode as a uint16. +// enr.Record keeps undecodable pairs as raw RLP, so an out-of-range port in a record +// from an external source would otherwise round-trip into a signed tree and fail to +// decode for consumers that read the port strictly. +func checkRecordPorts(r *enr.Record) error { + for _, key := range portKeys { + var port uint16 + err := r.Load(enr.WithEntry(key, &port)) + if enr.IsNotFound(err) { + continue + } + if err != nil { + return err + } + } + return nil +} + func isValidHash(s string) bool { dlen := b32format.DecodedLen(len(s)) if dlen < minHashLength || dlen > 32 || strings.ContainsAny(s, "\n\r") { diff --git a/p2p/dnsdisc/tree_test.go b/p2p/dnsdisc/tree_test.go index 9ed17aa4b3..9d012cf011 100644 --- a/p2p/dnsdisc/tree_test.go +++ b/p2p/dnsdisc/tree_test.go @@ -23,6 +23,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" ) func TestParseRoot(t *testing.T) { @@ -149,3 +150,20 @@ func TestMakeTree(t *testing.T) { t.Fatal("too few TXT records in output") } } + +func TestMakeTreeRejectsUndecodablePort(t *testing.T) { + keys := testKeys(1) + + oversized := new(enr.Record) + oversized.Set(enr.WithEntry("udp", uint32(70000))) + if err := enode.SignV4(oversized, keys[0]); err != nil { + t.Fatal(err) + } + n, err := enode.New(enode.ValidSchemes, oversized) + if err != nil { + t.Fatal(err) + } + if _, err := MakeTree(1, []*enode.Node{n}, nil); err == nil { + t.Errorf("MakeTree accepted record with out-of-range port: %s", n.String()) + } +}