github.com/ethereum-optimism/optimism/l2geth@v0.0.0-20230612200230-50b04ade19e3/cmd/devp2p/dns_route53.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  	"sort"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/aws/aws-sdk-go/aws"
    27  	"github.com/aws/aws-sdk-go/aws/credentials"
    28  	"github.com/aws/aws-sdk-go/aws/session"
    29  	"github.com/aws/aws-sdk-go/service/route53"
    30  	"github.com/ethereum-optimism/optimism/l2geth/log"
    31  	"github.com/ethereum-optimism/optimism/l2geth/p2p/dnsdisc"
    32  	"gopkg.in/urfave/cli.v1"
    33  )
    34  
    35  // The Route53 limits change sets to this size. DNS changes need to be split
    36  // up into multiple batches to work around the limit.
    37  const route53ChangeLimit = 30000
    38  
    39  var (
    40  	route53AccessKeyFlag = cli.StringFlag{
    41  		Name:   "access-key-id",
    42  		Usage:  "AWS Access Key ID",
    43  		EnvVar: "AWS_ACCESS_KEY_ID",
    44  	}
    45  	route53AccessSecretFlag = cli.StringFlag{
    46  		Name:   "access-key-secret",
    47  		Usage:  "AWS Access Key Secret",
    48  		EnvVar: "AWS_SECRET_ACCESS_KEY",
    49  	}
    50  	route53ZoneIDFlag = cli.StringFlag{
    51  		Name:  "zone-id",
    52  		Usage: "Route53 Zone ID",
    53  	}
    54  )
    55  
    56  type route53Client struct {
    57  	api    *route53.Route53
    58  	zoneID string
    59  }
    60  
    61  type recordSet struct {
    62  	values []string
    63  	ttl    int64
    64  }
    65  
    66  // newRoute53Client sets up a Route53 API client from command line flags.
    67  func newRoute53Client(ctx *cli.Context) *route53Client {
    68  	akey := ctx.String(route53AccessKeyFlag.Name)
    69  	asec := ctx.String(route53AccessSecretFlag.Name)
    70  	if akey == "" || asec == "" {
    71  		exit(fmt.Errorf("need Route53 Access Key ID and secret proceed"))
    72  	}
    73  	config := &aws.Config{Credentials: credentials.NewStaticCredentials(akey, asec, "")}
    74  	session, err := session.NewSession(config)
    75  	if err != nil {
    76  		exit(fmt.Errorf("can't create AWS session: %v", err))
    77  	}
    78  	return &route53Client{
    79  		api:    route53.New(session),
    80  		zoneID: ctx.String(route53ZoneIDFlag.Name),
    81  	}
    82  }
    83  
    84  // deploy uploads the given tree to Route53.
    85  func (c *route53Client) deploy(name string, t *dnsdisc.Tree) error {
    86  	if err := c.checkZone(name); err != nil {
    87  		return err
    88  	}
    89  
    90  	// Compute DNS changes.
    91  	existing, err := c.collectRecords(name)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	log.Info(fmt.Sprintf("Found %d TXT records", len(existing)))
    96  
    97  	records := t.ToTXT(name)
    98  	changes := c.computeChanges(name, records, existing)
    99  	if len(changes) == 0 {
   100  		log.Info("No DNS changes needed")
   101  		return nil
   102  	}
   103  
   104  	// Submit change batches.
   105  	batches := splitChanges(changes, route53ChangeLimit)
   106  	for i, changes := range batches {
   107  		log.Info(fmt.Sprintf("Submitting %d changes to Route53", len(changes)))
   108  		batch := new(route53.ChangeBatch)
   109  		batch.SetChanges(changes)
   110  		batch.SetComment(fmt.Sprintf("enrtree update %d/%d of %s at seq %d", i+1, len(batches), name, t.Seq()))
   111  		req := &route53.ChangeResourceRecordSetsInput{HostedZoneId: &c.zoneID, ChangeBatch: batch}
   112  		resp, err := c.api.ChangeResourceRecordSets(req)
   113  		if err != nil {
   114  			return err
   115  		}
   116  
   117  		log.Info(fmt.Sprintf("Waiting for change request %s", *resp.ChangeInfo.Id))
   118  		wreq := &route53.GetChangeInput{Id: resp.ChangeInfo.Id}
   119  		if err := c.api.WaitUntilResourceRecordSetsChanged(wreq); err != nil {
   120  			return err
   121  		}
   122  	}
   123  	return nil
   124  }
   125  
   126  // checkZone verifies zone information for the given domain.
   127  func (c *route53Client) checkZone(name string) (err error) {
   128  	if c.zoneID == "" {
   129  		c.zoneID, err = c.findZoneID(name)
   130  	}
   131  	return err
   132  }
   133  
   134  // findZoneID searches for the Zone ID containing the given domain.
   135  func (c *route53Client) findZoneID(name string) (string, error) {
   136  	log.Info(fmt.Sprintf("Finding Route53 Zone ID for %s", name))
   137  	var req route53.ListHostedZonesByNameInput
   138  	for {
   139  		resp, err := c.api.ListHostedZonesByName(&req)
   140  		if err != nil {
   141  			return "", err
   142  		}
   143  		for _, zone := range resp.HostedZones {
   144  			if isSubdomain(name, *zone.Name) {
   145  				return *zone.Id, nil
   146  			}
   147  		}
   148  		if !*resp.IsTruncated {
   149  			break
   150  		}
   151  		req.DNSName = resp.NextDNSName
   152  		req.HostedZoneId = resp.NextHostedZoneId
   153  	}
   154  	return "", errors.New("can't find zone ID for " + name)
   155  }
   156  
   157  // computeChanges creates DNS changes for the given record.
   158  func (c *route53Client) computeChanges(name string, records map[string]string, existing map[string]recordSet) []*route53.Change {
   159  	// Convert all names to lowercase.
   160  	lrecords := make(map[string]string, len(records))
   161  	for name, r := range records {
   162  		lrecords[strings.ToLower(name)] = r
   163  	}
   164  	records = lrecords
   165  
   166  	var changes []*route53.Change
   167  	for path, val := range records {
   168  		ttl := int64(rootTTL)
   169  		if path != name {
   170  			ttl = int64(treeNodeTTL)
   171  		}
   172  
   173  		prevRecords, exists := existing[path]
   174  		prevValue := combineTXT(prevRecords.values)
   175  		if !exists {
   176  			// Entry is unknown, push a new one
   177  			log.Info(fmt.Sprintf("Creating %s = %q", path, val))
   178  			changes = append(changes, newTXTChange("CREATE", path, ttl, splitTXT(val)))
   179  		} else if prevValue != val {
   180  			// Entry already exists, only change its content.
   181  			log.Info(fmt.Sprintf("Updating %s from %q to %q", path, prevValue, val))
   182  			changes = append(changes, newTXTChange("UPSERT", path, ttl, splitTXT(val)))
   183  		} else {
   184  			log.Info(fmt.Sprintf("Skipping %s = %q", path, val))
   185  		}
   186  	}
   187  
   188  	// Iterate over the old records and delete anything stale.
   189  	for path, set := range existing {
   190  		if _, ok := records[path]; ok {
   191  			continue
   192  		}
   193  		// Stale entry, nuke it.
   194  		log.Info(fmt.Sprintf("Deleting %s = %q", path, combineTXT(set.values)))
   195  		changes = append(changes, newTXTChange("DELETE", path, set.ttl, set.values))
   196  	}
   197  
   198  	sortChanges(changes)
   199  	return changes
   200  }
   201  
   202  // sortChanges ensures DNS changes are in leaf-added -> root-changed -> leaf-deleted order.
   203  func sortChanges(changes []*route53.Change) {
   204  	score := map[string]int{"CREATE": 1, "UPSERT": 2, "DELETE": 3}
   205  	sort.Slice(changes, func(i, j int) bool {
   206  		if *changes[i].Action == *changes[j].Action {
   207  			return *changes[i].ResourceRecordSet.Name < *changes[j].ResourceRecordSet.Name
   208  		}
   209  		return score[*changes[i].Action] < score[*changes[j].Action]
   210  	})
   211  }
   212  
   213  // splitChanges splits up DNS changes such that each change batch
   214  // is smaller than the given RDATA limit.
   215  func splitChanges(changes []*route53.Change, limit int) [][]*route53.Change {
   216  	var batches [][]*route53.Change
   217  	var batchSize int
   218  	for _, ch := range changes {
   219  		// Start new batch if this change pushes the current one over the limit.
   220  		size := changeSize(ch)
   221  		if len(batches) == 0 || batchSize+size > limit {
   222  			batches = append(batches, nil)
   223  			batchSize = 0
   224  		}
   225  		batches[len(batches)-1] = append(batches[len(batches)-1], ch)
   226  		batchSize += size
   227  	}
   228  	return batches
   229  }
   230  
   231  // changeSize returns the RDATA size of a DNS change.
   232  func changeSize(ch *route53.Change) int {
   233  	size := 0
   234  	for _, rr := range ch.ResourceRecordSet.ResourceRecords {
   235  		if rr.Value != nil {
   236  			size += len(*rr.Value)
   237  		}
   238  	}
   239  	return size
   240  }
   241  
   242  // collectRecords collects all TXT records below the given name.
   243  func (c *route53Client) collectRecords(name string) (map[string]recordSet, error) {
   244  	log.Info(fmt.Sprintf("Retrieving existing TXT records on %s (%s)", name, c.zoneID))
   245  	var req route53.ListResourceRecordSetsInput
   246  	req.SetHostedZoneId(c.zoneID)
   247  	existing := make(map[string]recordSet)
   248  	err := c.api.ListResourceRecordSetsPages(&req, func(resp *route53.ListResourceRecordSetsOutput, last bool) bool {
   249  		for _, set := range resp.ResourceRecordSets {
   250  			if !isSubdomain(*set.Name, name) || *set.Type != "TXT" {
   251  				continue
   252  			}
   253  			s := recordSet{ttl: *set.TTL}
   254  			for _, rec := range set.ResourceRecords {
   255  				s.values = append(s.values, *rec.Value)
   256  			}
   257  			name := strings.TrimSuffix(*set.Name, ".")
   258  			existing[name] = s
   259  		}
   260  		return true
   261  	})
   262  	return existing, err
   263  }
   264  
   265  // newTXTChange creates a change to a TXT record.
   266  func newTXTChange(action, name string, ttl int64, values []string) *route53.Change {
   267  	var c route53.Change
   268  	var r route53.ResourceRecordSet
   269  	var rrs []*route53.ResourceRecord
   270  	for _, val := range values {
   271  		rr := new(route53.ResourceRecord)
   272  		rr.SetValue(val)
   273  		rrs = append(rrs, rr)
   274  	}
   275  	r.SetType("TXT")
   276  	r.SetName(name)
   277  	r.SetTTL(ttl)
   278  	r.SetResourceRecords(rrs)
   279  	c.SetAction(action)
   280  	c.SetResourceRecordSet(&r)
   281  	return &c
   282  }
   283  
   284  // isSubdomain returns true if name is a subdomain of domain.
   285  func isSubdomain(name, domain string) bool {
   286  	domain = strings.TrimSuffix(domain, ".")
   287  	name = strings.TrimSuffix(name, ".")
   288  	return strings.HasSuffix("."+name, "."+domain)
   289  }
   290  
   291  // combineTXT concatenates the given quoted strings into a single unquoted string.
   292  func combineTXT(values []string) string {
   293  	result := ""
   294  	for _, v := range values {
   295  		if v[0] == '"' {
   296  			v = v[1 : len(v)-1]
   297  		}
   298  		result += v
   299  	}
   300  	return result
   301  }
   302  
   303  // splitTXT splits value into a list of quoted 255-character strings.
   304  func splitTXT(value string) []string {
   305  	var result []string
   306  	for len(value) > 0 {
   307  		rlen := len(value)
   308  		if rlen > 253 {
   309  			rlen = 253
   310  		}
   311  		result = append(result, strconv.Quote(value[:rlen]))
   312  		value = value[rlen:]
   313  	}
   314  	return result
   315  }