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