github.com/theQRL/go-zond@v0.1.1/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  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/aws/aws-sdk-go-v2/aws"
    28  	"github.com/aws/aws-sdk-go-v2/config"
    29  	"github.com/aws/aws-sdk-go-v2/credentials"
    30  	"github.com/aws/aws-sdk-go-v2/service/route53"
    31  	"github.com/aws/aws-sdk-go-v2/service/route53/types"
    32  	"github.com/theQRL/go-zond/log"
    33  	"github.com/theQRL/go-zond/p2p/dnsdisc"
    34  	"github.com/urfave/cli/v2"
    35  	"golang.org/x/exp/slices"
    36  )
    37  
    38  const (
    39  	// Route53 limits change sets to 32k of 'RDATA size'. Change sets are also limited to
    40  	// 1000 items. UPSERTs count double.
    41  	// https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests-changeresourcerecordsets
    42  	route53ChangeSizeLimit  = 32000
    43  	route53ChangeCountLimit = 1000
    44  	maxRetryLimit           = 60
    45  )
    46  
    47  var (
    48  	route53AccessKeyFlag = &cli.StringFlag{
    49  		Name:    "access-key-id",
    50  		Usage:   "AWS Access Key ID",
    51  		EnvVars: []string{"AWS_ACCESS_KEY_ID"},
    52  	}
    53  	route53AccessSecretFlag = &cli.StringFlag{
    54  		Name:    "access-key-secret",
    55  		Usage:   "AWS Access Key Secret",
    56  		EnvVars: []string{"AWS_SECRET_ACCESS_KEY"},
    57  	}
    58  	route53ZoneIDFlag = &cli.StringFlag{
    59  		Name:  "zone-id",
    60  		Usage: "Route53 Zone ID",
    61  	}
    62  	route53RegionFlag = &cli.StringFlag{
    63  		Name:  "aws-region",
    64  		Usage: "AWS Region",
    65  		Value: "eu-central-1",
    66  	}
    67  )
    68  
    69  type route53Client struct {
    70  	api    *route53.Client
    71  	zoneID string
    72  }
    73  
    74  type recordSet struct {
    75  	values []string
    76  	ttl    int64
    77  }
    78  
    79  // newRoute53Client sets up a Route53 API client from command line flags.
    80  func newRoute53Client(ctx *cli.Context) *route53Client {
    81  	akey := ctx.String(route53AccessKeyFlag.Name)
    82  	asec := ctx.String(route53AccessSecretFlag.Name)
    83  	if akey == "" || asec == "" {
    84  		exit(errors.New("need Route53 Access Key ID and secret to proceed"))
    85  	}
    86  	creds := aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(akey, asec, ""))
    87  	cfg, err := config.LoadDefaultConfig(context.Background(), config.WithCredentialsProvider(creds))
    88  	if err != nil {
    89  		exit(fmt.Errorf("can't initialize AWS configuration: %v", err))
    90  	}
    91  	cfg.Region = ctx.String(route53RegionFlag.Name)
    92  	return &route53Client{
    93  		api:    route53.NewFromConfig(cfg),
    94  		zoneID: ctx.String(route53ZoneIDFlag.Name),
    95  	}
    96  }
    97  
    98  // deploy uploads the given tree to Route53.
    99  func (c *route53Client) deploy(name string, t *dnsdisc.Tree) error {
   100  	if err := c.checkZone(name); err != nil {
   101  		return err
   102  	}
   103  
   104  	// Compute DNS changes.
   105  	existing, err := c.collectRecords(name)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	log.Info(fmt.Sprintf("Found %d TXT records", len(existing)))
   110  	records := t.ToTXT(name)
   111  	changes := c.computeChanges(name, records, existing)
   112  
   113  	// Submit to API.
   114  	comment := fmt.Sprintf("enrtree update of %s at seq %d", name, t.Seq())
   115  	return c.submitChanges(changes, comment)
   116  }
   117  
   118  // deleteDomain removes all TXT records of the given domain.
   119  func (c *route53Client) deleteDomain(name string) error {
   120  	if err := c.checkZone(name); err != nil {
   121  		return err
   122  	}
   123  
   124  	// Compute DNS changes.
   125  	existing, err := c.collectRecords(name)
   126  	if err != nil {
   127  		return err
   128  	}
   129  	log.Info(fmt.Sprintf("Found %d TXT records", len(existing)))
   130  	changes := makeDeletionChanges(existing, nil)
   131  
   132  	// Submit to API.
   133  	comment := "enrtree delete of " + name
   134  	return c.submitChanges(changes, comment)
   135  }
   136  
   137  // submitChanges submits the given DNS changes to Route53.
   138  func (c *route53Client) submitChanges(changes []types.Change, comment string) error {
   139  	if len(changes) == 0 {
   140  		log.Info("No DNS changes needed")
   141  		return nil
   142  	}
   143  
   144  	var err error
   145  	batches := splitChanges(changes, route53ChangeSizeLimit, route53ChangeCountLimit)
   146  	changesToCheck := make([]*route53.ChangeResourceRecordSetsOutput, len(batches))
   147  	for i, changes := range batches {
   148  		log.Info(fmt.Sprintf("Submitting %d changes to Route53", len(changes)))
   149  		batch := &types.ChangeBatch{
   150  			Changes: changes,
   151  			Comment: aws.String(fmt.Sprintf("%s (%d/%d)", comment, i+1, len(batches))),
   152  		}
   153  		req := &route53.ChangeResourceRecordSetsInput{HostedZoneId: &c.zoneID, ChangeBatch: batch}
   154  		changesToCheck[i], err = c.api.ChangeResourceRecordSets(context.TODO(), req)
   155  		if err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	// Wait for all change batches to propagate.
   161  	for _, change := range changesToCheck {
   162  		log.Info(fmt.Sprintf("Waiting for change request %s", *change.ChangeInfo.Id))
   163  		wreq := &route53.GetChangeInput{Id: change.ChangeInfo.Id}
   164  		var count int
   165  		for {
   166  			wresp, err := c.api.GetChange(context.TODO(), wreq)
   167  			if err != nil {
   168  				return err
   169  			}
   170  
   171  			count++
   172  
   173  			if wresp.ChangeInfo.Status == types.ChangeStatusInsync || count >= maxRetryLimit {
   174  				break
   175  			}
   176  
   177  			time.Sleep(30 * time.Second)
   178  		}
   179  	}
   180  	return nil
   181  }
   182  
   183  // checkZone verifies zone information for the given domain.
   184  func (c *route53Client) checkZone(name string) (err error) {
   185  	if c.zoneID == "" {
   186  		c.zoneID, err = c.findZoneID(name)
   187  	}
   188  	return err
   189  }
   190  
   191  // findZoneID searches for the Zone ID containing the given domain.
   192  func (c *route53Client) findZoneID(name string) (string, error) {
   193  	log.Info(fmt.Sprintf("Finding Route53 Zone ID for %s", name))
   194  	var req route53.ListHostedZonesByNameInput
   195  	for {
   196  		resp, err := c.api.ListHostedZonesByName(context.TODO(), &req)
   197  		if err != nil {
   198  			return "", err
   199  		}
   200  		for _, zone := range resp.HostedZones {
   201  			if isSubdomain(name, *zone.Name) {
   202  				return *zone.Id, nil
   203  			}
   204  		}
   205  		if !resp.IsTruncated {
   206  			break
   207  		}
   208  		req.DNSName = resp.NextDNSName
   209  		req.HostedZoneId = resp.NextHostedZoneId
   210  	}
   211  	return "", errors.New("can't find zone ID for " + name)
   212  }
   213  
   214  // computeChanges creates DNS changes for the given set of DNS discovery records.
   215  // The 'existing' arg is the set of records that already exist on Route53.
   216  func (c *route53Client) computeChanges(name string, records map[string]string, existing map[string]recordSet) []types.Change {
   217  	// Convert all names to lowercase.
   218  	lrecords := make(map[string]string, len(records))
   219  	for name, r := range records {
   220  		lrecords[strings.ToLower(name)] = r
   221  	}
   222  	records = lrecords
   223  
   224  	var (
   225  		changes []types.Change
   226  		inserts int
   227  		upserts int
   228  		skips   int
   229  	)
   230  
   231  	for path, newValue := range records {
   232  		prevRecords, exists := existing[path]
   233  		prevValue := strings.Join(prevRecords.values, "")
   234  
   235  		// prevValue contains quoted strings, encode newValue to compare.
   236  		newValue = splitTXT(newValue)
   237  
   238  		// Assign TTL.
   239  		ttl := int64(rootTTL)
   240  		if path != name {
   241  			ttl = int64(treeNodeTTL)
   242  		}
   243  
   244  		if !exists {
   245  			// Entry is unknown, push a new one
   246  			log.Debug(fmt.Sprintf("Creating %s = %s", path, newValue))
   247  			changes = append(changes, newTXTChange("CREATE", path, ttl, newValue))
   248  			inserts++
   249  		} else if prevValue != newValue || prevRecords.ttl != ttl {
   250  			// Entry already exists, only change its content.
   251  			log.Info(fmt.Sprintf("Updating %s from %s to %s", path, prevValue, newValue))
   252  			changes = append(changes, newTXTChange("UPSERT", path, ttl, newValue))
   253  			upserts++
   254  		} else {
   255  			log.Debug(fmt.Sprintf("Skipping %s = %s", path, newValue))
   256  			skips++
   257  		}
   258  	}
   259  
   260  	// Iterate over the old records and delete anything stale.
   261  	deletions := makeDeletionChanges(existing, records)
   262  	changes = append(changes, deletions...)
   263  
   264  	log.Info("Computed DNS changes",
   265  		"changes", len(changes),
   266  		"inserts", inserts,
   267  		"skips", skips,
   268  		"deleted", len(deletions),
   269  		"upserts", upserts)
   270  	// Ensure changes are in the correct order.
   271  	sortChanges(changes)
   272  	return changes
   273  }
   274  
   275  // makeDeletionChanges creates record changes which delete all records not contained in 'keep'.
   276  func makeDeletionChanges(records map[string]recordSet, keep map[string]string) []types.Change {
   277  	var changes []types.Change
   278  	for path, set := range records {
   279  		if _, ok := keep[path]; ok {
   280  			continue
   281  		}
   282  		log.Debug(fmt.Sprintf("Deleting %s = %s", path, strings.Join(set.values, "")))
   283  		changes = append(changes, newTXTChange("DELETE", path, set.ttl, set.values...))
   284  	}
   285  	return changes
   286  }
   287  
   288  // sortChanges ensures DNS changes are in leaf-added -> root-changed -> leaf-deleted order.
   289  func sortChanges(changes []types.Change) {
   290  	score := map[string]int{"CREATE": 1, "UPSERT": 2, "DELETE": 3}
   291  	slices.SortFunc(changes, func(a, b types.Change) int {
   292  		if a.Action == b.Action {
   293  			return strings.Compare(*a.ResourceRecordSet.Name, *b.ResourceRecordSet.Name)
   294  		}
   295  		if score[string(a.Action)] < score[string(b.Action)] {
   296  			return -1
   297  		}
   298  		if score[string(a.Action)] > score[string(b.Action)] {
   299  			return 1
   300  		}
   301  		return 0
   302  	})
   303  }
   304  
   305  // splitChanges splits up DNS changes such that each change batch
   306  // is smaller than the given RDATA limit.
   307  func splitChanges(changes []types.Change, sizeLimit, countLimit int) [][]types.Change {
   308  	var (
   309  		batches    [][]types.Change
   310  		batchSize  int
   311  		batchCount int
   312  	)
   313  	for _, ch := range changes {
   314  		// Start new batch if this change pushes the current one over the limit.
   315  		count := changeCount(ch)
   316  		size := changeSize(ch) * count
   317  		overSize := batchSize+size > sizeLimit
   318  		overCount := batchCount+count > countLimit
   319  		if len(batches) == 0 || overSize || overCount {
   320  			batches = append(batches, nil)
   321  			batchSize = 0
   322  			batchCount = 0
   323  		}
   324  		batches[len(batches)-1] = append(batches[len(batches)-1], ch)
   325  		batchSize += size
   326  		batchCount += count
   327  	}
   328  	return batches
   329  }
   330  
   331  // changeSize returns the RDATA size of a DNS change.
   332  func changeSize(ch types.Change) int {
   333  	size := 0
   334  	for _, rr := range ch.ResourceRecordSet.ResourceRecords {
   335  		if rr.Value != nil {
   336  			size += len(*rr.Value)
   337  		}
   338  	}
   339  	return size
   340  }
   341  
   342  func changeCount(ch types.Change) int {
   343  	if ch.Action == types.ChangeActionUpsert {
   344  		return 2
   345  	}
   346  	return 1
   347  }
   348  
   349  // collectRecords collects all TXT records below the given name.
   350  func (c *route53Client) collectRecords(name string) (map[string]recordSet, error) {
   351  	var req route53.ListResourceRecordSetsInput
   352  	req.HostedZoneId = &c.zoneID
   353  	existing := make(map[string]recordSet)
   354  	log.Info("Loading existing TXT records", "name", name, "zone", c.zoneID)
   355  	for page := 0; ; page++ {
   356  		log.Debug("Loading existing TXT records", "name", name, "zone", c.zoneID, "page", page)
   357  		resp, err := c.api.ListResourceRecordSets(context.TODO(), &req)
   358  		if err != nil {
   359  			return existing, err
   360  		}
   361  		for _, set := range resp.ResourceRecordSets {
   362  			if !isSubdomain(*set.Name, name) || set.Type != types.RRTypeTxt {
   363  				continue
   364  			}
   365  			s := recordSet{ttl: *set.TTL}
   366  			for _, rec := range set.ResourceRecords {
   367  				s.values = append(s.values, *rec.Value)
   368  			}
   369  			name := strings.TrimSuffix(*set.Name, ".")
   370  			existing[name] = s
   371  		}
   372  
   373  		if !resp.IsTruncated {
   374  			break
   375  		}
   376  		// Set the cursor to the next batch. From the AWS docs:
   377  		//
   378  		// To display the next page of results, get the values of NextRecordName,
   379  		// NextRecordType, and NextRecordIdentifier (if any) from the response. Then submit
   380  		// another ListResourceRecordSets request, and specify those values for
   381  		// StartRecordName, StartRecordType, and StartRecordIdentifier.
   382  		req.StartRecordIdentifier = resp.NextRecordIdentifier
   383  		req.StartRecordName = resp.NextRecordName
   384  		req.StartRecordType = resp.NextRecordType
   385  	}
   386  	log.Info("Loaded existing TXT records", "name", name, "zone", c.zoneID, "records", len(existing))
   387  	return existing, nil
   388  }
   389  
   390  // newTXTChange creates a change to a TXT record.
   391  func newTXTChange(action, name string, ttl int64, values ...string) types.Change {
   392  	r := types.ResourceRecordSet{
   393  		Type: types.RRTypeTxt,
   394  		Name: &name,
   395  		TTL:  &ttl,
   396  	}
   397  	var rrs []types.ResourceRecord
   398  	for _, val := range values {
   399  		var rr types.ResourceRecord
   400  		rr.Value = aws.String(val)
   401  		rrs = append(rrs, rr)
   402  	}
   403  
   404  	r.ResourceRecords = rrs
   405  
   406  	return types.Change{
   407  		Action:            types.ChangeAction(action),
   408  		ResourceRecordSet: &r,
   409  	}
   410  }
   411  
   412  // isSubdomain returns true if name is a subdomain of domain.
   413  func isSubdomain(name, domain string) bool {
   414  	domain = strings.TrimSuffix(domain, ".")
   415  	name = strings.TrimSuffix(name, ".")
   416  	return strings.HasSuffix("."+name, "."+domain)
   417  }
   418  
   419  // splitTXT splits value into a list of quoted 255-character strings.
   420  func splitTXT(value string) string {
   421  	var result strings.Builder
   422  	for len(value) > 0 {
   423  		rlen := len(value)
   424  		if rlen > 253 {
   425  			rlen = 253
   426  		}
   427  		result.WriteString(strconv.Quote(value[:rlen]))
   428  		value = value[rlen:]
   429  	}
   430  	return result.String()
   431  }