dns_route53.go 9.1 KB

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