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