github.com/cryptotooltop/go-ethereum@v0.0.0-20231103184714-151d1922f3e5/cmd/devp2p/dnscmd.go (about)

     1  // Copyright 2018 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  	"crypto/ecdsa"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"time"
    27  
    28  	"gopkg.in/urfave/cli.v1"
    29  
    30  	"github.com/scroll-tech/go-ethereum/accounts/keystore"
    31  	"github.com/scroll-tech/go-ethereum/common"
    32  	"github.com/scroll-tech/go-ethereum/console/prompt"
    33  	"github.com/scroll-tech/go-ethereum/p2p/dnsdisc"
    34  	"github.com/scroll-tech/go-ethereum/p2p/enode"
    35  )
    36  
    37  var (
    38  	dnsCommand = cli.Command{
    39  		Name:  "dns",
    40  		Usage: "DNS Discovery Commands",
    41  		Subcommands: []cli.Command{
    42  			dnsSyncCommand,
    43  			dnsSignCommand,
    44  			dnsTXTCommand,
    45  			dnsCloudflareCommand,
    46  			dnsRoute53Command,
    47  			dnsRoute53NukeCommand,
    48  		},
    49  	}
    50  	dnsSyncCommand = cli.Command{
    51  		Name:      "sync",
    52  		Usage:     "Download a DNS discovery tree",
    53  		ArgsUsage: "<url> [ <directory> ]",
    54  		Action:    dnsSync,
    55  		Flags:     []cli.Flag{dnsTimeoutFlag},
    56  	}
    57  	dnsSignCommand = cli.Command{
    58  		Name:      "sign",
    59  		Usage:     "Sign a DNS discovery tree",
    60  		ArgsUsage: "<tree-directory> <key-file>",
    61  		Action:    dnsSign,
    62  		Flags:     []cli.Flag{dnsDomainFlag, dnsSeqFlag},
    63  	}
    64  	dnsTXTCommand = cli.Command{
    65  		Name:      "to-txt",
    66  		Usage:     "Create a DNS TXT records for a discovery tree",
    67  		ArgsUsage: "<tree-directory> <output-file>",
    68  		Action:    dnsToTXT,
    69  	}
    70  	dnsCloudflareCommand = cli.Command{
    71  		Name:      "to-cloudflare",
    72  		Usage:     "Deploy DNS TXT records to CloudFlare",
    73  		ArgsUsage: "<tree-directory>",
    74  		Action:    dnsToCloudflare,
    75  		Flags:     []cli.Flag{cloudflareTokenFlag, cloudflareZoneIDFlag},
    76  	}
    77  	dnsRoute53Command = cli.Command{
    78  		Name:      "to-route53",
    79  		Usage:     "Deploy DNS TXT records to Amazon Route53",
    80  		ArgsUsage: "<tree-directory>",
    81  		Action:    dnsToRoute53,
    82  		Flags: []cli.Flag{
    83  			route53AccessKeyFlag,
    84  			route53AccessSecretFlag,
    85  			route53ZoneIDFlag,
    86  			route53RegionFlag,
    87  		},
    88  	}
    89  	dnsRoute53NukeCommand = cli.Command{
    90  		Name:      "nuke-route53",
    91  		Usage:     "Deletes DNS TXT records of a subdomain on Amazon Route53",
    92  		ArgsUsage: "<domain>",
    93  		Action:    dnsNukeRoute53,
    94  		Flags: []cli.Flag{
    95  			route53AccessKeyFlag,
    96  			route53AccessSecretFlag,
    97  			route53ZoneIDFlag,
    98  			route53RegionFlag,
    99  		},
   100  	}
   101  )
   102  
   103  var (
   104  	dnsTimeoutFlag = cli.DurationFlag{
   105  		Name:  "timeout",
   106  		Usage: "Timeout for DNS lookups",
   107  	}
   108  	dnsDomainFlag = cli.StringFlag{
   109  		Name:  "domain",
   110  		Usage: "Domain name of the tree",
   111  	}
   112  	dnsSeqFlag = cli.UintFlag{
   113  		Name:  "seq",
   114  		Usage: "New sequence number of the tree",
   115  	}
   116  )
   117  
   118  const (
   119  	rootTTL               = 30 * 60              // 30 min
   120  	treeNodeTTL           = 4 * 7 * 24 * 60 * 60 // 4 weeks
   121  	treeNodeTTLCloudflare = 24 * 60 * 60         // 1 day
   122  )
   123  
   124  // dnsSync performs dnsSyncCommand.
   125  func dnsSync(ctx *cli.Context) error {
   126  	var (
   127  		c      = dnsClient(ctx)
   128  		url    = ctx.Args().Get(0)
   129  		outdir = ctx.Args().Get(1)
   130  	)
   131  	domain, _, err := dnsdisc.ParseURL(url)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	if outdir == "" {
   136  		outdir = domain
   137  	}
   138  
   139  	t, err := c.SyncTree(url)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	def := treeToDefinition(url, t)
   144  	def.Meta.LastModified = time.Now()
   145  	writeTreeMetadata(outdir, def)
   146  	writeTreeNodes(outdir, def)
   147  	return nil
   148  }
   149  
   150  func dnsSign(ctx *cli.Context) error {
   151  	if ctx.NArg() < 2 {
   152  		return fmt.Errorf("need tree definition directory and key file as arguments")
   153  	}
   154  	var (
   155  		defdir  = ctx.Args().Get(0)
   156  		keyfile = ctx.Args().Get(1)
   157  		def     = loadTreeDefinition(defdir)
   158  		domain  = directoryName(defdir)
   159  	)
   160  	if def.Meta.URL != "" {
   161  		d, _, err := dnsdisc.ParseURL(def.Meta.URL)
   162  		if err != nil {
   163  			return fmt.Errorf("invalid 'url' field: %v", err)
   164  		}
   165  		domain = d
   166  	}
   167  	if ctx.IsSet(dnsDomainFlag.Name) {
   168  		domain = ctx.String(dnsDomainFlag.Name)
   169  	}
   170  	if ctx.IsSet(dnsSeqFlag.Name) {
   171  		def.Meta.Seq = ctx.Uint(dnsSeqFlag.Name)
   172  	} else {
   173  		def.Meta.Seq++ // Auto-bump sequence number if not supplied via flag.
   174  	}
   175  	t, err := dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links)
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	key := loadSigningKey(keyfile)
   181  	url, err := t.Sign(key, domain)
   182  	if err != nil {
   183  		return fmt.Errorf("can't sign: %v", err)
   184  	}
   185  
   186  	def = treeToDefinition(url, t)
   187  	def.Meta.LastModified = time.Now()
   188  	writeTreeMetadata(defdir, def)
   189  	return nil
   190  }
   191  
   192  // directoryName returns the directory name of the given path.
   193  // For example, when dir is "foo/bar", it returns "bar".
   194  // When dir is ".", and the working directory is "example/foo", it returns "foo".
   195  func directoryName(dir string) string {
   196  	abs, err := filepath.Abs(dir)
   197  	if err != nil {
   198  		exit(err)
   199  	}
   200  	return filepath.Base(abs)
   201  }
   202  
   203  // dnsToTXT performs dnsTXTCommand.
   204  func dnsToTXT(ctx *cli.Context) error {
   205  	if ctx.NArg() < 1 {
   206  		return fmt.Errorf("need tree definition directory as argument")
   207  	}
   208  	output := ctx.Args().Get(1)
   209  	if output == "" {
   210  		output = "-" // default to stdout
   211  	}
   212  	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
   213  	if err != nil {
   214  		return err
   215  	}
   216  	writeTXTJSON(output, t.ToTXT(domain))
   217  	return nil
   218  }
   219  
   220  // dnsToCloudflare performs dnsCloudflareCommand.
   221  func dnsToCloudflare(ctx *cli.Context) error {
   222  	if ctx.NArg() != 1 {
   223  		return fmt.Errorf("need tree definition directory as argument")
   224  	}
   225  	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
   226  	if err != nil {
   227  		return err
   228  	}
   229  	client := newCloudflareClient(ctx)
   230  	return client.deploy(domain, t)
   231  }
   232  
   233  // dnsToRoute53 performs dnsRoute53Command.
   234  func dnsToRoute53(ctx *cli.Context) error {
   235  	if ctx.NArg() != 1 {
   236  		return fmt.Errorf("need tree definition directory as argument")
   237  	}
   238  	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
   239  	if err != nil {
   240  		return err
   241  	}
   242  	client := newRoute53Client(ctx)
   243  	return client.deploy(domain, t)
   244  }
   245  
   246  // dnsNukeRoute53 performs dnsRoute53NukeCommand.
   247  func dnsNukeRoute53(ctx *cli.Context) error {
   248  	if ctx.NArg() != 1 {
   249  		return fmt.Errorf("need domain name as argument")
   250  	}
   251  	client := newRoute53Client(ctx)
   252  	return client.deleteDomain(ctx.Args().First())
   253  }
   254  
   255  // loadSigningKey loads a private key in Ethereum keystore format.
   256  func loadSigningKey(keyfile string) *ecdsa.PrivateKey {
   257  	keyjson, err := ioutil.ReadFile(keyfile)
   258  	if err != nil {
   259  		exit(fmt.Errorf("failed to read the keyfile at '%s': %v", keyfile, err))
   260  	}
   261  	password, _ := prompt.Stdin.PromptPassword("Please enter the password for '" + keyfile + "': ")
   262  	key, err := keystore.DecryptKey(keyjson, password)
   263  	if err != nil {
   264  		exit(fmt.Errorf("error decrypting key: %v", err))
   265  	}
   266  	return key.PrivateKey
   267  }
   268  
   269  // dnsClient configures the DNS discovery client from command line flags.
   270  func dnsClient(ctx *cli.Context) *dnsdisc.Client {
   271  	var cfg dnsdisc.Config
   272  	if commandHasFlag(ctx, dnsTimeoutFlag) {
   273  		cfg.Timeout = ctx.Duration(dnsTimeoutFlag.Name)
   274  	}
   275  	return dnsdisc.NewClient(cfg)
   276  }
   277  
   278  // There are two file formats for DNS node trees on disk:
   279  //
   280  // The 'TXT' format is a single JSON file containing DNS TXT records
   281  // as a JSON object where the keys are names and the values are objects
   282  // containing the value of the record.
   283  //
   284  // The 'definition' format is a directory containing two files:
   285  //
   286  //      enrtree-info.json    -- contains sequence number & links to other trees
   287  //      nodes.json           -- contains the nodes as a JSON array.
   288  //
   289  // This format exists because it's convenient to edit. nodes.json can be generated
   290  // in multiple ways: it may be written by a DHT crawler or compiled by a human.
   291  
   292  type dnsDefinition struct {
   293  	Meta  dnsMetaJSON
   294  	Nodes []*enode.Node
   295  }
   296  
   297  type dnsMetaJSON struct {
   298  	URL          string    `json:"url,omitempty"`
   299  	Seq          uint      `json:"seq"`
   300  	Sig          string    `json:"signature,omitempty"`
   301  	Links        []string  `json:"links"`
   302  	LastModified time.Time `json:"lastModified"`
   303  }
   304  
   305  func treeToDefinition(url string, t *dnsdisc.Tree) *dnsDefinition {
   306  	meta := dnsMetaJSON{
   307  		URL:   url,
   308  		Seq:   t.Seq(),
   309  		Sig:   t.Signature(),
   310  		Links: t.Links(),
   311  	}
   312  	if meta.Links == nil {
   313  		meta.Links = []string{}
   314  	}
   315  	return &dnsDefinition{Meta: meta, Nodes: t.Nodes()}
   316  }
   317  
   318  // loadTreeDefinition loads a directory in 'definition' format.
   319  func loadTreeDefinition(directory string) *dnsDefinition {
   320  	metaFile, nodesFile := treeDefinitionFiles(directory)
   321  	var def dnsDefinition
   322  	err := common.LoadJSON(metaFile, &def.Meta)
   323  	if err != nil && !os.IsNotExist(err) {
   324  		exit(err)
   325  	}
   326  	if def.Meta.Links == nil {
   327  		def.Meta.Links = []string{}
   328  	}
   329  	// Check link syntax.
   330  	for _, link := range def.Meta.Links {
   331  		if _, _, err := dnsdisc.ParseURL(link); err != nil {
   332  			exit(fmt.Errorf("invalid link %q: %v", link, err))
   333  		}
   334  	}
   335  	// Check/convert nodes.
   336  	nodes := loadNodesJSON(nodesFile)
   337  	if err := nodes.verify(); err != nil {
   338  		exit(err)
   339  	}
   340  	def.Nodes = nodes.nodes()
   341  	return &def
   342  }
   343  
   344  // loadTreeDefinitionForExport loads a DNS tree and ensures it is signed.
   345  func loadTreeDefinitionForExport(dir string) (domain string, t *dnsdisc.Tree, err error) {
   346  	metaFile, _ := treeDefinitionFiles(dir)
   347  	def := loadTreeDefinition(dir)
   348  	if def.Meta.URL == "" {
   349  		return "", nil, fmt.Errorf("missing 'url' field in %v", metaFile)
   350  	}
   351  	domain, pubkey, err := dnsdisc.ParseURL(def.Meta.URL)
   352  	if err != nil {
   353  		return "", nil, fmt.Errorf("invalid 'url' field in %v: %v", metaFile, err)
   354  	}
   355  	if t, err = dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links); err != nil {
   356  		return "", nil, err
   357  	}
   358  	if err := ensureValidTreeSignature(t, pubkey, def.Meta.Sig); err != nil {
   359  		return "", nil, err
   360  	}
   361  	return domain, t, nil
   362  }
   363  
   364  // ensureValidTreeSignature checks that sig is valid for tree and assigns it as the
   365  // tree's signature if valid.
   366  func ensureValidTreeSignature(t *dnsdisc.Tree, pubkey *ecdsa.PublicKey, sig string) error {
   367  	if sig == "" {
   368  		return fmt.Errorf("missing signature, run 'devp2p dns sign' first")
   369  	}
   370  	if err := t.SetSignature(pubkey, sig); err != nil {
   371  		return fmt.Errorf("invalid signature on tree, run 'devp2p dns sign' to update it")
   372  	}
   373  	return nil
   374  }
   375  
   376  // writeTreeMetadata writes a DNS node tree metadata file to the given directory.
   377  func writeTreeMetadata(directory string, def *dnsDefinition) {
   378  	metaJSON, err := json.MarshalIndent(&def.Meta, "", jsonIndent)
   379  	if err != nil {
   380  		exit(err)
   381  	}
   382  	if err := os.Mkdir(directory, 0744); err != nil && !os.IsExist(err) {
   383  		exit(err)
   384  	}
   385  	metaFile, _ := treeDefinitionFiles(directory)
   386  	if err := ioutil.WriteFile(metaFile, metaJSON, 0644); err != nil {
   387  		exit(err)
   388  	}
   389  }
   390  
   391  func writeTreeNodes(directory string, def *dnsDefinition) {
   392  	ns := make(nodeSet, len(def.Nodes))
   393  	ns.add(def.Nodes...)
   394  	_, nodesFile := treeDefinitionFiles(directory)
   395  	writeNodesJSON(nodesFile, ns)
   396  }
   397  
   398  func treeDefinitionFiles(directory string) (string, string) {
   399  	meta := filepath.Join(directory, "enrtree-info.json")
   400  	nodes := filepath.Join(directory, "nodes.json")
   401  	return meta, nodes
   402  }
   403  
   404  // writeTXTJSON writes TXT records in JSON format.
   405  func writeTXTJSON(file string, txt map[string]string) {
   406  	txtJSON, err := json.MarshalIndent(txt, "", jsonIndent)
   407  	if err != nil {
   408  		exit(err)
   409  	}
   410  	if file == "-" {
   411  		os.Stdout.Write(txtJSON)
   412  		fmt.Println()
   413  		return
   414  	}
   415  	if err := ioutil.WriteFile(file, txtJSON, 0644); err != nil {
   416  		exit(err)
   417  	}
   418  }