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 }