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