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 }