github.com/core-coin/go-core/v2@v2.1.9/cmd/devp2p/dns_route53.go (about)

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