diff --git a/docs/cli/README.md b/docs/cli/README.md index c10c8bf9c2..d5648dca7d 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -12,6 +12,8 @@ - [```attach```](./attach.md) +- [```bootnode```](./bootnode.md) + - [```chain```](./chain.md) - [```chain sethead```](./chain_sethead.md) diff --git a/docs/cli/bootnode.md b/docs/cli/bootnode.md new file mode 100644 index 0000000000..3e60252341 --- /dev/null +++ b/docs/cli/bootnode.md @@ -0,0 +1,17 @@ +# Bootnode + +## Options + +- ```listen-addr```: listening address of bootnode (:) + +- ```v5```: Enable UDP v5 + +- ```log-level```: Log level (trace|debug|info|warn|error|crit) + +- ```nat```: port mapping mechanism (any|none|upnp|pmp|extip:) + +- ```node-key```: file or hex node key + +- ```save-key```: path to save the ecdsa private key + +- ```dry-run``` \ No newline at end of file diff --git a/internal/cli/bootnode.go b/internal/cli/bootnode.go new file mode 100644 index 0000000000..f9127494a6 --- /dev/null +++ b/internal/cli/bootnode.go @@ -0,0 +1,220 @@ +package cli + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/internal/cli/flagset" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/nat" + "github.com/mitchellh/cli" +) + +type BootnodeCommand struct { + UI cli.Ui + + listenAddr string + v5 bool + logLevel string + nat string + nodeKey string + saveKey string + dryRun bool +} + +// Help implements the cli.Command interface +func (b *BootnodeCommand) Help() string { + return `Usage: bor bootnode` +} + +// MarkDown implements cli.MarkDown interface +func (c *BootnodeCommand) MarkDown() string { + items := []string{ + "# Bootnode", + c.Flags().MarkDown(), + } + return strings.Join(items, "\n\n") +} + +func (b *BootnodeCommand) Flags() *flagset.Flagset { + flags := flagset.NewFlagSet("bootnode") + + flags.StringFlag(&flagset.StringFlag{ + Name: "listen-addr", + Default: "0.0.0.0:30303", + Usage: "listening address of bootnode (:)", + Value: &b.listenAddr, + }) + flags.BoolFlag(&flagset.BoolFlag{ + Name: "v5", + Default: false, + Usage: "Enable UDP v5", + Value: &b.v5, + }) + flags.StringFlag(&flagset.StringFlag{ + Name: "log-level", + Default: "info", + Usage: "Log level (trace|debug|info|warn|error|crit)", + Value: &b.logLevel, + }) + flags.StringFlag(&flagset.StringFlag{ + Name: "nat", + Default: "none", + Usage: "port mapping mechanism (any|none|upnp|pmp|extip:)", + Value: &b.nat, + }) + flags.StringFlag(&flagset.StringFlag{ + Name: "node-key", + Default: "", + Usage: "file or hex node key", + Value: &b.nodeKey, + }) + flags.StringFlag(&flagset.StringFlag{ + Name: "save-key", + Default: "", + Usage: "path to save the ecdsa private key", + Value: &b.saveKey, + }) + flags.BoolFlag(&flagset.BoolFlag{ + Name: "dry-run", + Default: false, + Usage: "validates parameters and prints bootnode configurations, but does not start bootnode", + Value: &b.dryRun, + }) + + return flags +} + +// Synopsis implements the cli.Command interface +func (b *BootnodeCommand) Synopsis() string { + return "Start a bootnode" +} + +// Run implements the cli.Command interface +func (b *BootnodeCommand) Run(args []string) int { + flags := b.Flags() + if err := flags.Parse(args); err != nil { + b.UI.Error(err.Error()) + return 1 + } + + glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false))) + + lvl, err := log.LvlFromString(strings.ToLower(b.logLevel)) + if err == nil { + glogger.Verbosity(lvl) + } else { + glogger.Verbosity(log.LvlInfo) + } + log.Root().SetHandler(glogger) + + natm, err := nat.Parse(b.nat) + if err != nil { + b.UI.Error(fmt.Sprintf("failed to parse nat: %v", err)) + return 1 + } + + // create a one time key + var nodeKey *ecdsa.PrivateKey + if b.nodeKey != "" { + // try to read the key either from file or command line + if _, err := os.Stat(b.nodeKey); errors.Is(err, os.ErrNotExist) { + if nodeKey, err = crypto.HexToECDSA(b.nodeKey); err != nil { + b.UI.Error(fmt.Sprintf("failed to parse hex address: %v", err)) + return 1 + } + } else { + if nodeKey, err = crypto.LoadECDSA(b.nodeKey); err != nil { + b.UI.Error(fmt.Sprintf("failed to load node key: %v", err)) + return 1 + } + } + } else { + // generate a new temporal key + if nodeKey, err = crypto.GenerateKey(); err != nil { + b.UI.Error(fmt.Sprintf("could not generate key: %v", err)) + return 1 + } + if b.saveKey != "" { + path := b.saveKey + + // save the private key + if err = crypto.SaveECDSA(filepath.Join(path, "priv.key"), nodeKey); err != nil { + b.UI.Error(fmt.Sprintf("failed to write node priv key: %v", err)) + return 1 + } + // save the public key + pubRaw := fmt.Sprintf("%x", crypto.FromECDSAPub(&nodeKey.PublicKey)[1:]) + if err := ioutil.WriteFile(filepath.Join(path, "pub.key"), []byte(pubRaw), 0755); err != nil { + b.UI.Error(fmt.Sprintf("failed to write node pub key: %v", err)) + return 1 + } + } + } + + addr, err := net.ResolveUDPAddr("udp", b.listenAddr) + if err != nil { + b.UI.Error(fmt.Sprintf("could not resolve udp addr '%s': %v", b.listenAddr, err)) + return 1 + } + conn, err := net.ListenUDP("udp", addr) + if err != nil { + b.UI.Error(fmt.Sprintf("failed to listen udp addr '%s': %v", b.listenAddr, err)) + return 1 + } + + realaddr := conn.LocalAddr().(*net.UDPAddr) + if natm != nil { + if !realaddr.IP.IsLoopback() { + go nat.Map(natm, nil, "udp", realaddr.Port, realaddr.Port, "ethereum discovery") + } + if ext, err := natm.ExternalIP(); err == nil { + realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port} + } + } + + n := enode.NewV4(&nodeKey.PublicKey, addr.IP, addr.Port, addr.Port) + b.UI.Info(n.String()) + + if b.dryRun { + return 0 + } + + db, _ := enode.OpenDB("") + ln := enode.NewLocalNode(db, nodeKey) + cfg := discover.Config{ + PrivateKey: nodeKey, + Log: log.Root(), + } + if b.v5 { + if _, err := discover.ListenV5(conn, ln, cfg); err != nil { + utils.Fatalf("%v", err) + } + } else { + if _, err := discover.ListenUDP(conn, ln, cfg); err != nil { + utils.Fatalf("%v", err) + } + } + + signalCh := make(chan os.Signal, 4) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + + sig := <-signalCh + + b.UI.Output(fmt.Sprintf("Caught signal: %v", sig)) + b.UI.Output("Gracefully shutting down agent...") + + return 0 +} diff --git a/internal/cli/command.go b/internal/cli/command.go index 4cb089b9ff..d1851594a7 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -27,6 +27,10 @@ func Run(args []string) int { mappedCommands := make(map[string]cli.CommandFactory) for k, v := range commands { + // Declare a new v to limit the scope of v to inside the block, so the anonymous function below + // can get the "current" value of v, instead of the value of last v in the loop. + // See this post: https://stackoverflow.com/questions/10116507/go-transfer-var-into-anonymous-function for more explanation + v := v mappedCommands[k] = func() (cli.Command, error) { cmd, err := v() return cmd.(cli.Command), err @@ -153,6 +157,11 @@ func Commands() map[string]MarkDownCommandFactory { Meta2: meta2, }, nil }, + "bootnode": func() (MarkDownCommand, error) { + return &BootnodeCommand{ + UI: ui, + }, nil + }, } }