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