github.com/ethereum/go-ethereum@v1.16.1/cmd/devp2p/discv4cmd.go (about) 1 // Copyright 2019 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "errors" 21 "fmt" 22 "net" 23 "net/http" 24 "slices" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/ethereum/go-ethereum/cmd/devp2p/internal/v4test" 30 "github.com/ethereum/go-ethereum/common" 31 "github.com/ethereum/go-ethereum/crypto" 32 "github.com/ethereum/go-ethereum/log" 33 "github.com/ethereum/go-ethereum/p2p/discover" 34 "github.com/ethereum/go-ethereum/p2p/enode" 35 "github.com/ethereum/go-ethereum/params" 36 "github.com/ethereum/go-ethereum/rpc" 37 "github.com/urfave/cli/v2" 38 ) 39 40 var ( 41 discv4Command = &cli.Command{ 42 Name: "discv4", 43 Usage: "Node Discovery v4 tools", 44 Subcommands: []*cli.Command{ 45 discv4PingCommand, 46 discv4RequestRecordCommand, 47 discv4ResolveCommand, 48 discv4ResolveJSONCommand, 49 discv4CrawlCommand, 50 discv4TestCommand, 51 discv4ListenCommand, 52 }, 53 } 54 discv4PingCommand = &cli.Command{ 55 Name: "ping", 56 Usage: "Sends ping to a node", 57 Action: discv4Ping, 58 ArgsUsage: "<node>", 59 Flags: discoveryNodeFlags, 60 } 61 discv4RequestRecordCommand = &cli.Command{ 62 Name: "requestenr", 63 Usage: "Requests a node record using EIP-868 enrRequest", 64 Action: discv4RequestRecord, 65 ArgsUsage: "<node>", 66 Flags: discoveryNodeFlags, 67 } 68 discv4ResolveCommand = &cli.Command{ 69 Name: "resolve", 70 Usage: "Finds a node in the DHT", 71 Action: discv4Resolve, 72 ArgsUsage: "<node>", 73 Flags: discoveryNodeFlags, 74 } 75 discv4ResolveJSONCommand = &cli.Command{ 76 Name: "resolve-json", 77 Usage: "Re-resolves nodes in a nodes.json file", 78 Action: discv4ResolveJSON, 79 Flags: discoveryNodeFlags, 80 ArgsUsage: "<nodes.json file>", 81 } 82 discv4ListenCommand = &cli.Command{ 83 Name: "listen", 84 Usage: "Runs a discovery node", 85 Action: discv4Listen, 86 Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{ 87 httpAddrFlag, 88 }), 89 } 90 discv4CrawlCommand = &cli.Command{ 91 Name: "crawl", 92 Usage: "Updates a nodes.json file with random nodes found in the DHT", 93 Action: discv4Crawl, 94 Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{crawlTimeoutFlag, crawlParallelismFlag}), 95 } 96 discv4TestCommand = &cli.Command{ 97 Name: "test", 98 Usage: "Runs tests against a node", 99 Action: discv4Test, 100 Flags: []cli.Flag{ 101 remoteEnodeFlag, 102 testPatternFlag, 103 testTAPFlag, 104 testListen1Flag, 105 testListen2Flag, 106 }, 107 } 108 ) 109 110 var ( 111 bootnodesFlag = &cli.StringFlag{ 112 Name: "bootnodes", 113 Usage: "Comma separated nodes used for bootstrapping", 114 } 115 nodekeyFlag = &cli.StringFlag{ 116 Name: "nodekey", 117 Usage: "Hex-encoded node key", 118 } 119 nodedbFlag = &cli.StringFlag{ 120 Name: "nodedb", 121 Usage: "Nodes database location", 122 } 123 listenAddrFlag = &cli.StringFlag{ 124 Name: "addr", 125 Usage: "Listening address", 126 } 127 extAddrFlag = &cli.StringFlag{ 128 Name: "extaddr", 129 Usage: "UDP endpoint announced in ENR. You can provide a bare IP address or IP:port as the value of this flag.", 130 } 131 crawlTimeoutFlag = &cli.DurationFlag{ 132 Name: "timeout", 133 Usage: "Time limit for the crawl.", 134 Value: 30 * time.Minute, 135 } 136 crawlParallelismFlag = &cli.IntFlag{ 137 Name: "parallel", 138 Usage: "How many parallel discoveries to attempt.", 139 Value: 16, 140 } 141 remoteEnodeFlag = &cli.StringFlag{ 142 Name: "remote", 143 Usage: "Enode of the remote node under test", 144 EnvVars: []string{"REMOTE_ENODE"}, 145 } 146 httpAddrFlag = &cli.StringFlag{ 147 Name: "rpc", 148 Usage: "HTTP server listening address", 149 } 150 ) 151 152 var discoveryNodeFlags = []cli.Flag{ 153 bootnodesFlag, 154 nodekeyFlag, 155 nodedbFlag, 156 listenAddrFlag, 157 extAddrFlag, 158 } 159 160 func discv4Ping(ctx *cli.Context) error { 161 n := getNodeArg(ctx) 162 disc, _ := startV4(ctx) 163 defer disc.Close() 164 165 start := time.Now() 166 if _, err := disc.Ping(n); err != nil { 167 return fmt.Errorf("node didn't respond: %v", err) 168 } 169 fmt.Printf("node responded to ping (RTT %v).\n", time.Since(start)) 170 return nil 171 } 172 173 func discv4Listen(ctx *cli.Context) error { 174 disc, _ := startV4(ctx) 175 defer disc.Close() 176 177 fmt.Println(disc.Self()) 178 179 httpAddr := ctx.String(httpAddrFlag.Name) 180 if httpAddr == "" { 181 // Non-HTTP mode. 182 select {} 183 } 184 185 api := &discv4API{disc} 186 log.Info("Starting RPC API server", "addr", httpAddr) 187 srv := rpc.NewServer() 188 srv.RegisterName("discv4", api) 189 http.DefaultServeMux.Handle("/", srv) 190 httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux} 191 return httpsrv.ListenAndServe() 192 } 193 194 func discv4RequestRecord(ctx *cli.Context) error { 195 n := getNodeArg(ctx) 196 disc, _ := startV4(ctx) 197 defer disc.Close() 198 199 respN, err := disc.RequestENR(n) 200 if err != nil { 201 return fmt.Errorf("can't retrieve record: %v", err) 202 } 203 fmt.Println(respN.String()) 204 return nil 205 } 206 207 func discv4Resolve(ctx *cli.Context) error { 208 n := getNodeArg(ctx) 209 disc, _ := startV4(ctx) 210 defer disc.Close() 211 212 fmt.Println(disc.Resolve(n).String()) 213 return nil 214 } 215 216 func discv4ResolveJSON(ctx *cli.Context) error { 217 if ctx.NArg() < 1 { 218 return errors.New("need nodes file as argument") 219 } 220 nodesFile := ctx.Args().Get(0) 221 inputSet := make(nodeSet) 222 if common.FileExist(nodesFile) { 223 inputSet = loadNodesJSON(nodesFile) 224 } 225 226 // Add extra nodes from command line arguments. 227 var nodeargs []*enode.Node 228 for i := 1; i < ctx.NArg(); i++ { 229 n, err := parseNode(ctx.Args().Get(i)) 230 if err != nil { 231 exit(err) 232 } 233 nodeargs = append(nodeargs, n) 234 } 235 236 disc, config := startV4(ctx) 237 defer disc.Close() 238 239 c, err := newCrawler(inputSet, config.Bootnodes, disc, enode.IterNodes(nodeargs)) 240 if err != nil { 241 return err 242 } 243 c.revalidateInterval = 0 244 output := c.run(0, 1) 245 writeNodesJSON(nodesFile, output) 246 return nil 247 } 248 249 func discv4Crawl(ctx *cli.Context) error { 250 if ctx.NArg() < 1 { 251 return errors.New("need nodes file as argument") 252 } 253 nodesFile := ctx.Args().First() 254 inputSet := make(nodeSet) 255 if common.FileExist(nodesFile) { 256 inputSet = loadNodesJSON(nodesFile) 257 } 258 259 disc, config := startV4(ctx) 260 defer disc.Close() 261 262 c, err := newCrawler(inputSet, config.Bootnodes, disc, disc.RandomNodes()) 263 if err != nil { 264 return err 265 } 266 c.revalidateInterval = 10 * time.Minute 267 output := c.run(ctx.Duration(crawlTimeoutFlag.Name), ctx.Int(crawlParallelismFlag.Name)) 268 writeNodesJSON(nodesFile, output) 269 return nil 270 } 271 272 // discv4Test runs the protocol test suite. 273 func discv4Test(ctx *cli.Context) error { 274 // Configure test package globals. 275 if !ctx.IsSet(remoteEnodeFlag.Name) { 276 return fmt.Errorf("missing -%v", remoteEnodeFlag.Name) 277 } 278 v4test.Remote = ctx.String(remoteEnodeFlag.Name) 279 v4test.Listen1 = ctx.String(testListen1Flag.Name) 280 v4test.Listen2 = ctx.String(testListen2Flag.Name) 281 return runTests(ctx, v4test.AllTests) 282 } 283 284 // startV4 starts an ephemeral discovery V4 node. 285 func startV4(ctx *cli.Context) (*discover.UDPv4, discover.Config) { 286 ln, config := makeDiscoveryConfig(ctx) 287 socket := listen(ctx, ln) 288 disc, err := discover.ListenV4(socket, ln, config) 289 if err != nil { 290 exit(err) 291 } 292 return disc, config 293 } 294 295 func makeDiscoveryConfig(ctx *cli.Context) (*enode.LocalNode, discover.Config) { 296 var cfg discover.Config 297 298 if ctx.IsSet(nodekeyFlag.Name) { 299 key, err := crypto.HexToECDSA(ctx.String(nodekeyFlag.Name)) 300 if err != nil { 301 exit(fmt.Errorf("-%s: %v", nodekeyFlag.Name, err)) 302 } 303 cfg.PrivateKey = key 304 } else { 305 cfg.PrivateKey, _ = crypto.GenerateKey() 306 } 307 308 if commandHasFlag(ctx, bootnodesFlag) { 309 bn, err := parseBootnodes(ctx) 310 if err != nil { 311 exit(err) 312 } 313 cfg.Bootnodes = bn 314 } 315 316 dbpath := ctx.String(nodedbFlag.Name) 317 db, err := enode.OpenDB(dbpath) 318 if err != nil { 319 exit(err) 320 } 321 ln := enode.NewLocalNode(db, cfg.PrivateKey) 322 return ln, cfg 323 } 324 325 func parseExtAddr(spec string) (ip net.IP, port int, ok bool) { 326 ip = net.ParseIP(spec) 327 if ip != nil { 328 return ip, 0, true 329 } 330 host, portstr, err := net.SplitHostPort(spec) 331 if err != nil { 332 return nil, 0, false 333 } 334 ip = net.ParseIP(host) 335 if ip == nil { 336 return nil, 0, false 337 } 338 port, err = strconv.Atoi(portstr) 339 if err != nil { 340 return nil, 0, false 341 } 342 return ip, port, true 343 } 344 345 func listen(ctx *cli.Context, ln *enode.LocalNode) *net.UDPConn { 346 addr := ctx.String(listenAddrFlag.Name) 347 if addr == "" { 348 addr = "0.0.0.0:0" 349 } 350 socket, err := net.ListenPacket("udp4", addr) 351 if err != nil { 352 exit(err) 353 } 354 355 // Configure UDP endpoint in ENR from listener address. 356 usocket := socket.(*net.UDPConn) 357 uaddr := socket.LocalAddr().(*net.UDPAddr) 358 if uaddr.IP.IsUnspecified() { 359 ln.SetFallbackIP(net.IP{127, 0, 0, 1}) 360 } else { 361 ln.SetFallbackIP(uaddr.IP) 362 } 363 ln.SetFallbackUDP(uaddr.Port) 364 365 // If an ENR endpoint is set explicitly on the command-line, override 366 // the information from the listening address. Note this is careful not 367 // to set the UDP port if the external address doesn't have it. 368 extAddr := ctx.String(extAddrFlag.Name) 369 if extAddr != "" { 370 ip, port, ok := parseExtAddr(extAddr) 371 if !ok { 372 exit(fmt.Errorf("-%s: invalid external address %q", extAddrFlag.Name, extAddr)) 373 } 374 ln.SetStaticIP(ip) 375 if port != 0 { 376 ln.SetFallbackUDP(port) 377 } 378 } 379 380 return usocket 381 } 382 383 func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) { 384 s := params.MainnetBootnodes 385 if ctx.IsSet(bootnodesFlag.Name) { 386 input := ctx.String(bootnodesFlag.Name) 387 if input == "" { 388 return nil, nil 389 } 390 s = strings.Split(input, ",") 391 } 392 nodes := make([]*enode.Node, len(s)) 393 var err error 394 for i, record := range s { 395 nodes[i], err = parseNode(record) 396 if err != nil { 397 return nil, fmt.Errorf("invalid bootstrap node: %v", err) 398 } 399 } 400 return nodes, nil 401 } 402 403 type discv4API struct { 404 host *discover.UDPv4 405 } 406 407 func (api *discv4API) LookupRandom(n int) (ns []*enode.Node) { 408 it := api.host.RandomNodes() 409 for len(ns) < n && it.Next() { 410 ns = append(ns, it.Node()) 411 } 412 return ns 413 } 414 415 func (api *discv4API) Buckets() [][]discover.BucketNode { 416 return api.host.TableBuckets() 417 } 418 419 func (api *discv4API) Self() *enode.Node { 420 return api.host.Self() 421 }