cmd/devp2p, p2p: fix discovery RPC listener issues

Serve the RPC handler directly instead of via http.DefaultServeMux, which
also exposed /debug/pprof on the RPC port. lookupRandom now closes its
iterator and stops when the client disconnects, instead of hanging forever
on nodes with an empty table and leaking a context per call. Results go
through enode.ReadNodes: deduplicated (may return fewer than n) and empty
responses marshal as [] instead of null. The discv4 issues predate this
branch; fixed here because the combined listener exposes them.

Also export discover.SharedUDPConn and use it from p2p.Server and the
combined listener instead of keeping two diverging copies.
This commit is contained in:
Chase Wright 2026-07-10 10:22:41 -05:00
parent 95a3b35e97
commit 9b5737150a
No known key found for this signature in database
GPG key ID: 7EA41EDCB1BE04A7
6 changed files with 70 additions and 113 deletions

View file

@ -17,16 +17,10 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"net"
"net/http"
"net/netip"
"slices" "slices"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -55,13 +49,13 @@ func discoveryListen(ctx *cli.Context) error {
// v4 is the primary listener on the real socket and forwards packets it // v4 is the primary listener on the real socket and forwards packets it
// can't parse to v5 via the unhandled channel. // can't parse to v5 via the unhandled channel.
unhandled := make(chan discover.ReadPacket, 100) unhandled := make(chan discover.ReadPacket, 100)
config.Unhandled = unhandled v4cfg := config
v4, err := discover.ListenV4(socket, ln, config) v4cfg.Unhandled = unhandled
v4, err := discover.ListenV4(socket, ln, v4cfg)
if err != nil { if err != nil {
exit(err) exit(err)
} }
config.Unhandled = nil v5, err := discover.ListenV5(&discover.SharedUDPConn{UDPConn: socket, Unhandled: unhandled}, ln, config)
v5, err := discover.ListenV5(&sharedUDPConn{socket, unhandled}, ln, config)
if err != nil { if err != nil {
exit(err) exit(err)
} }
@ -75,38 +69,8 @@ func discoveryListen(ctx *cli.Context) error {
// v4 and v5 share the same local node, so this is the single record both announce. // v4 and v5 share the same local node, so this is the single record both announce.
fmt.Println(ln.Node()) fmt.Println(ln.Node())
httpAddr := ctx.String(httpAddrFlag.Name) return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{
if httpAddr == "" { "discv4": &discv4API{v4},
select {} "discv5": &discv5API{v5},
} })
srv := rpc.NewServer()
srv.RegisterName("discv4", &discv4API{v4})
srv.RegisterName("discv5", &discv5API{v5})
log.Info("Starting RPC API server", "addr", httpAddr)
http.DefaultServeMux.Handle("/", srv)
httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux}
return httpsrv.ListenAndServe()
}
// sharedUDPConn implements a shared connection, identical to the one used by
// p2p.Server: reads return packets the primary v4 listener found unprocessable
// and forwarded on the unhandled channel.
type sharedUDPConn struct {
*net.UDPConn
unhandled chan discover.ReadPacket
}
func (s *sharedUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
packet, ok := <-s.unhandled
if !ok {
return 0, netip.AddrPort{}, errors.New("connection was closed")
}
l := min(len(packet.Data), len(b))
copy(b[:l], packet.Data[:l])
return l, packet.Addr, nil
}
func (s *sharedUDPConn) Close() error {
return nil
} }

View file

@ -17,10 +17,10 @@
package main package main
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/http"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -29,11 +29,9 @@ import (
"github.com/ethereum/go-ethereum/cmd/devp2p/internal/v4test" "github.com/ethereum/go-ethereum/cmd/devp2p/internal/v4test"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -176,19 +174,7 @@ func discv4Listen(ctx *cli.Context) error {
fmt.Println(disc.Self()) fmt.Println(disc.Self())
httpAddr := ctx.String(httpAddrFlag.Name) return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{"discv4": &discv4API{disc}})
if httpAddr == "" {
// Non-HTTP mode.
select {}
}
api := &discv4API{disc}
log.Info("Starting RPC API server", "addr", httpAddr)
srv := rpc.NewServer()
srv.RegisterName("discv4", api)
http.DefaultServeMux.Handle("/", srv)
httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux}
return httpsrv.ListenAndServe()
} }
func discv4RequestRecord(ctx *cli.Context) error { func discv4RequestRecord(ctx *cli.Context) error {
@ -428,13 +414,14 @@ type discv4API struct {
host *discover.UDPv4 host *discover.UDPv4
} }
func (api *discv4API) LookupRandom(n int) (ns []*enode.Node) { func (api *discv4API) LookupRandom(ctx context.Context, n int) []*enode.Node {
it := api.host.RandomNodes() it := api.host.RandomNodes()
defer it.Close() defer it.Close()
for len(ns) < n && it.Next() { go func() {
ns = append(ns, it.Node()) <-ctx.Done()
} it.Close()
return ns }()
return enode.ReadNodes(it, n)
} }
func (api *discv4API) Buckets() [][]discover.BucketNode { func (api *discv4API) Buckets() [][]discover.BucketNode {

View file

@ -17,18 +17,16 @@
package main package main
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"slices" "slices"
"time" "time"
"github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test" "github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -144,19 +142,7 @@ func discv5Listen(ctx *cli.Context) error {
fmt.Println(disc.Self()) fmt.Println(disc.Self())
httpAddr := ctx.String(httpAddrFlag.Name) return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{"discv5": &discv5API{disc}})
if httpAddr == "" {
// Non-HTTP mode.
select {}
}
api := &discv5API{disc}
log.Info("Starting RPC API server", "addr", httpAddr)
srv := rpc.NewServer()
srv.RegisterName("discv5", api)
http.DefaultServeMux.Handle("/", srv)
httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux}
return httpsrv.ListenAndServe()
} }
// startV5 starts an ephemeral discovery v5 node. // startV5 starts an ephemeral discovery v5 node.
@ -174,12 +160,14 @@ type discv5API struct {
host *discover.UDPv5 host *discover.UDPv5
} }
func (api *discv5API) LookupRandom(n int) (ns []*enode.Node) { func (api *discv5API) LookupRandom(ctx context.Context, n int) []*enode.Node {
it := api.host.RandomNodes() it := api.host.RandomNodes()
for len(ns) < n && it.Next() { defer it.Close()
ns = append(ns, it.Node()) go func() {
} <-ctx.Done()
return ns it.Close()
}()
return enode.ReadNodes(it, n)
} }
func (api *discv5API) Self() *enode.Node { func (api *discv5API) Self() *enode.Node {

View file

@ -18,11 +18,14 @@ package main
import ( import (
"fmt" "fmt"
"net/http"
"os" "os"
"github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags" "github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -93,6 +96,22 @@ func getNodeArg(ctx *cli.Context) *enode.Node {
return n return n
} }
// runRPCServer serves the given RPC APIs on httpAddr; if httpAddr is empty, it blocks forever without serving.
func runRPCServer(httpAddr string, apis map[string]any) error {
if httpAddr == "" {
select {}
}
srv := rpc.NewServer()
for name, api := range apis {
if err := srv.RegisterName(name, api); err != nil {
return err
}
}
log.Info("Starting RPC API server", "addr", httpAddr)
httpsrv := http.Server{Addr: httpAddr, Handler: srv}
return httpsrv.ListenAndServe()
}
func exit(err interface{}) { func exit(err interface{}) {
if err == nil { if err == nil {
os.Exit(0) os.Exit(0)

View file

@ -21,6 +21,7 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
crand "crypto/rand" crand "crypto/rand"
"encoding/binary" "encoding/binary"
"errors"
"iter" "iter"
"math/rand" "math/rand"
"net" "net"
@ -105,6 +106,30 @@ type ReadPacket struct {
Addr netip.AddrPort Addr netip.AddrPort
} }
// SharedUDPConn implements a shared connection. Write sends messages to the underlying
// connection while read returns messages that were found unprocessable and sent to the
// Unhandled channel by the primary listener.
type SharedUDPConn struct {
*net.UDPConn
Unhandled chan ReadPacket
}
// ReadFromUDPAddrPort implements UDPConn.
func (s *SharedUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
packet, ok := <-s.Unhandled
if !ok {
return 0, netip.AddrPort{}, errors.New("connection was closed")
}
l := min(len(packet.Data), len(b))
copy(b[:l], packet.Data[:l])
return l, packet.Addr, nil
}
// Close implements UDPConn. It does not close the underlying connection.
func (s *SharedUDPConn) Close() error {
return nil
}
type randomSource interface { type randomSource interface {
Intn(int) int Intn(int) int
Int63n(int64) int64 Int63n(int64) int64

View file

@ -330,32 +330,6 @@ func (srv *Server) Stop() {
srv.loopWG.Wait() srv.loopWG.Wait()
} }
// sharedUDPConn implements a shared connection. Write sends messages to the underlying connection while read returns
// messages that were found unprocessable and sent to the unhandled channel by the primary listener.
type sharedUDPConn struct {
*net.UDPConn
unhandled chan discover.ReadPacket
}
// ReadFromUDPAddrPort implements discover.UDPConn
func (s *sharedUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
packet, ok := <-s.unhandled
if !ok {
return 0, netip.AddrPort{}, errors.New("connection was closed")
}
l := len(packet.Data)
if l > len(b) {
l = len(b)
}
copy(b[:l], packet.Data[:l])
return l, packet.Addr, nil
}
// Close implements discover.UDPConn
func (s *sharedUDPConn) Close() error {
return nil
}
// Start starts running the server. // Start starts running the server.
// Servers can not be re-used after stopping. // Servers can not be re-used after stopping.
func (srv *Server) Start() (err error) { func (srv *Server) Start() (err error) {
@ -463,7 +437,7 @@ func (srv *Server) setupDiscovery() error {
// connection, so v5 can read unhandled messages from v4. // connection, so v5 can read unhandled messages from v4.
if srv.Config.DiscoveryV4 && srv.Config.DiscoveryV5 { if srv.Config.DiscoveryV4 && srv.Config.DiscoveryV5 {
unhandled = make(chan discover.ReadPacket, 100) unhandled = make(chan discover.ReadPacket, 100)
sconn = &sharedUDPConn{conn, unhandled} sconn = &discover.SharedUDPConn{UDPConn: conn, Unhandled: unhandled}
} }
// Start discovery services. // Start discovery services.