Skip to content

Commit 0568e81

Browse files
authored
p2p/dnsdisc: add implementation of EIP-1459 (#20094)
This adds an implementation of node discovery via DNS TXT records to the go-ethereum library. The implementation doesn't match EIP-1459 exactly, the main difference being that this implementation uses separate merkle trees for tree links and ENRs. The EIP will be updated to match p2p/dnsdisc. To maintain DNS trees, cmd/devp2p provides a frontend for the p2p/dnsdisc library. The new 'dns' subcommands can be used to create, sign and deploy DNS discovery trees.
1 parent 32b07e8 commit 0568e81

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+10698
-49
lines changed

cmd/devp2p/discv4cmd.go

Lines changed: 74 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ package main
1919
import (
2020
"fmt"
2121
"net"
22-
"sort"
2322
"strings"
2423
"time"
2524

25+
"github.com/ethereum/go-ethereum/common"
2626
"github.com/ethereum/go-ethereum/crypto"
2727
"github.com/ethereum/go-ethereum/p2p/discover"
2828
"github.com/ethereum/go-ethereum/p2p/enode"
@@ -38,23 +38,34 @@ var (
3838
discv4PingCommand,
3939
discv4RequestRecordCommand,
4040
discv4ResolveCommand,
41+
discv4ResolveJSONCommand,
4142
},
4243
}
4344
discv4PingCommand = cli.Command{
44-
Name: "ping",
45-
Usage: "Sends ping to a node",
46-
Action: discv4Ping,
45+
Name: "ping",
46+
Usage: "Sends ping to a node",
47+
Action: discv4Ping,
48+
ArgsUsage: "<node>",
4749
}
4850
discv4RequestRecordCommand = cli.Command{
49-
Name: "requestenr",
50-
Usage: "Requests a node record using EIP-868 enrRequest",
51-
Action: discv4RequestRecord,
51+
Name: "requestenr",
52+
Usage: "Requests a node record using EIP-868 enrRequest",
53+
Action: discv4RequestRecord,
54+
ArgsUsage: "<node>",
5255
}
5356
discv4ResolveCommand = cli.Command{
54-
Name: "resolve",
55-
Usage: "Finds a node in the DHT",
56-
Action: discv4Resolve,
57-
Flags: []cli.Flag{bootnodesFlag},
57+
Name: "resolve",
58+
Usage: "Finds a node in the DHT",
59+
Action: discv4Resolve,
60+
ArgsUsage: "<node>",
61+
Flags: []cli.Flag{bootnodesFlag},
62+
}
63+
discv4ResolveJSONCommand = cli.Command{
64+
Name: "resolve-json",
65+
Usage: "Re-resolves nodes in a nodes.json file",
66+
Action: discv4ResolveJSON,
67+
Flags: []cli.Flag{bootnodesFlag},
68+
ArgsUsage: "<nodes.json file>",
5869
}
5970
)
6071

@@ -64,10 +75,8 @@ var bootnodesFlag = cli.StringFlag{
6475
}
6576

6677
func discv4Ping(ctx *cli.Context) error {
67-
n, disc, err := getNodeArgAndStartV4(ctx)
68-
if err != nil {
69-
return err
70-
}
78+
n := getNodeArg(ctx)
79+
disc := startV4(ctx)
7180
defer disc.Close()
7281

7382
start := time.Now()
@@ -79,10 +88,8 @@ func discv4Ping(ctx *cli.Context) error {
7988
}
8089

8190
func discv4RequestRecord(ctx *cli.Context) error {
82-
n, disc, err := getNodeArgAndStartV4(ctx)
83-
if err != nil {
84-
return err
85-
}
91+
n := getNodeArg(ctx)
92+
disc := startV4(ctx)
8693
defer disc.Close()
8794

8895
respN, err := disc.RequestENR(n)
@@ -94,33 +101,43 @@ func discv4RequestRecord(ctx *cli.Context) error {
94101
}
95102

96103
func discv4Resolve(ctx *cli.Context) error {
97-
n, disc, err := getNodeArgAndStartV4(ctx)
98-
if err != nil {
99-
return err
100-
}
104+
n := getNodeArg(ctx)
105+
disc := startV4(ctx)
101106
defer disc.Close()
102107

103108
fmt.Println(disc.Resolve(n).String())
104109
return nil
105110
}
106111

107-
func getNodeArgAndStartV4(ctx *cli.Context) (*enode.Node, *discover.UDPv4, error) {
108-
if ctx.NArg() != 1 {
109-
return nil, nil, fmt.Errorf("missing node as command-line argument")
112+
func discv4ResolveJSON(ctx *cli.Context) error {
113+
if ctx.NArg() < 1 {
114+
return fmt.Errorf("need nodes file as argument")
110115
}
111-
n, err := parseNode(ctx.Args()[0])
112-
if err != nil {
113-
return nil, nil, err
116+
disc := startV4(ctx)
117+
defer disc.Close()
118+
file := ctx.Args().Get(0)
119+
120+
// Load existing nodes in file.
121+
var nodes []*enode.Node
122+
if common.FileExist(file) {
123+
nodes = loadNodesJSON(file).nodes()
114124
}
115-
var bootnodes []*enode.Node
116-
if commandHasFlag(ctx, bootnodesFlag) {
117-
bootnodes, err = parseBootnodes(ctx)
125+
// Add nodes from command line arguments.
126+
for i := 1; i < ctx.NArg(); i++ {
127+
n, err := parseNode(ctx.Args().Get(i))
118128
if err != nil {
119-
return nil, nil, err
129+
exit(err)
120130
}
131+
nodes = append(nodes, n)
132+
}
133+
134+
result := make(nodeSet, len(nodes))
135+
for _, n := range nodes {
136+
n = disc.Resolve(n)
137+
result[n.ID()] = nodeJSON{Seq: n.Seq(), N: n}
121138
}
122-
disc, err := startV4(bootnodes)
123-
return n, disc, err
139+
writeNodesJSON(file, result)
140+
return nil
124141
}
125142

126143
func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
@@ -139,28 +156,39 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
139156
return nodes, nil
140157
}
141158

142-
// commandHasFlag returns true if the current command supports the given flag.
143-
func commandHasFlag(ctx *cli.Context, flag cli.Flag) bool {
144-
flags := ctx.FlagNames()
145-
sort.Strings(flags)
146-
i := sort.SearchStrings(flags, flag.GetName())
147-
return i != len(flags) && flags[i] == flag.GetName()
159+
// startV4 starts an ephemeral discovery V4 node.
160+
func startV4(ctx *cli.Context) *discover.UDPv4 {
161+
socket, ln, cfg, err := listen()
162+
if err != nil {
163+
exit(err)
164+
}
165+
if commandHasFlag(ctx, bootnodesFlag) {
166+
bn, err := parseBootnodes(ctx)
167+
if err != nil {
168+
exit(err)
169+
}
170+
cfg.Bootnodes = bn
171+
}
172+
disc, err := discover.ListenV4(socket, ln, cfg)
173+
if err != nil {
174+
exit(err)
175+
}
176+
return disc
148177
}
149178

150-
// startV4 starts an ephemeral discovery V4 node.
151-
func startV4(bootnodes []*enode.Node) (*discover.UDPv4, error) {
179+
func listen() (*net.UDPConn, *enode.LocalNode, discover.Config, error) {
152180
var cfg discover.Config
153-
cfg.Bootnodes = bootnodes
154181
cfg.PrivateKey, _ = crypto.GenerateKey()
155182
db, _ := enode.OpenDB("")
156183
ln := enode.NewLocalNode(db, cfg.PrivateKey)
157184

158185
socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{0, 0, 0, 0}})
159186
if err != nil {
160-
return nil, err
187+
db.Close()
188+
return nil, nil, cfg, err
161189
}
162190
addr := socket.LocalAddr().(*net.UDPAddr)
163191
ln.SetFallbackIP(net.IP{127, 0, 0, 1})
164192
ln.SetFallbackUDP(addr.Port)
165-
return discover.ListenUDP(socket, ln, cfg)
193+
return socket, ln, cfg, nil
166194
}

cmd/devp2p/dns_cloudflare.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
"fmt"
21+
"strings"
22+
23+
"github.com/cloudflare/cloudflare-go"
24+
"github.com/ethereum/go-ethereum/log"
25+
"github.com/ethereum/go-ethereum/p2p/dnsdisc"
26+
"gopkg.in/urfave/cli.v1"
27+
)
28+
29+
var (
30+
cloudflareTokenFlag = cli.StringFlag{
31+
Name: "token",
32+
Usage: "CloudFlare API token",
33+
EnvVar: "CLOUDFLARE_API_TOKEN",
34+
}
35+
cloudflareZoneIDFlag = cli.StringFlag{
36+
Name: "zoneid",
37+
Usage: "CloudFlare Zone ID (optional)",
38+
}
39+
)
40+
41+
type cloudflareClient struct {
42+
*cloudflare.API
43+
zoneID string
44+
}
45+
46+
// newCloudflareClient sets up a CloudFlare API client from command line flags.
47+
func newCloudflareClient(ctx *cli.Context) *cloudflareClient {
48+
token := ctx.String(cloudflareTokenFlag.Name)
49+
if token == "" {
50+
exit(fmt.Errorf("need cloudflare API token to proceed"))
51+
}
52+
api, err := cloudflare.NewWithAPIToken(token)
53+
if err != nil {
54+
exit(fmt.Errorf("can't create Cloudflare client: %v", err))
55+
}
56+
return &cloudflareClient{
57+
API: api,
58+
zoneID: ctx.String(cloudflareZoneIDFlag.Name),
59+
}
60+
}
61+
62+
// deploy uploads the given tree to CloudFlare DNS.
63+
func (c *cloudflareClient) deploy(name string, t *dnsdisc.Tree) error {
64+
if err := c.checkZone(name); err != nil {
65+
return err
66+
}
67+
records := t.ToTXT(name)
68+
return c.uploadRecords(name, records)
69+
}
70+
71+
// checkZone verifies permissions on the CloudFlare DNS Zone for name.
72+
func (c *cloudflareClient) checkZone(name string) error {
73+
if c.zoneID == "" {
74+
log.Info(fmt.Sprintf("Finding CloudFlare zone ID for %s", name))
75+
id, err := c.ZoneIDByName(name)
76+
if err != nil {
77+
return err
78+
}
79+
c.zoneID = id
80+
}
81+
log.Info(fmt.Sprintf("Checking Permissions on zone %s", c.zoneID))
82+
zone, err := c.ZoneDetails(c.zoneID)
83+
if err != nil {
84+
return err
85+
}
86+
if !strings.HasSuffix(name, "."+zone.Name) {
87+
return fmt.Errorf("CloudFlare zone name %q does not match name %q to be deployed", zone.Name, name)
88+
}
89+
needPerms := map[string]bool{"#zone:edit": false, "#zone:read": false}
90+
for _, perm := range zone.Permissions {
91+
if _, ok := needPerms[perm]; ok {
92+
needPerms[perm] = true
93+
}
94+
}
95+
for _, ok := range needPerms {
96+
if !ok {
97+
return fmt.Errorf("wrong permissions on zone %s: %v", c.zoneID, needPerms)
98+
}
99+
}
100+
return nil
101+
}
102+
103+
// uploadRecords updates the TXT records at a particular subdomain. All non-root records
104+
// will have a TTL of "infinity" and all existing records not in the new map will be
105+
// nuked!
106+
func (c *cloudflareClient) uploadRecords(name string, records map[string]string) error {
107+
// Convert all names to lowercase.
108+
lrecords := make(map[string]string, len(records))
109+
for name, r := range records {
110+
lrecords[strings.ToLower(name)] = r
111+
}
112+
records = lrecords
113+
114+
log.Info(fmt.Sprintf("Retrieving existing TXT records on %s", name))
115+
entries, err := c.DNSRecords(c.zoneID, cloudflare.DNSRecord{Type: "TXT"})
116+
if err != nil {
117+
return err
118+
}
119+
existing := make(map[string]cloudflare.DNSRecord)
120+
for _, entry := range entries {
121+
if !strings.HasSuffix(entry.Name, name) {
122+
continue
123+
}
124+
existing[strings.ToLower(entry.Name)] = entry
125+
}
126+
127+
// Iterate over the new records and inject anything missing.
128+
for path, val := range records {
129+
old, exists := existing[path]
130+
if !exists {
131+
// Entry is unknown, push a new one to Cloudflare.
132+
log.Info(fmt.Sprintf("Creating %s = %q", path, val))
133+
ttl := 1
134+
if path != name {
135+
ttl = 2147483647 // Max TTL permitted by Cloudflare
136+
}
137+
_, err = c.CreateDNSRecord(c.zoneID, cloudflare.DNSRecord{Type: "TXT", Name: path, Content: val, TTL: ttl})
138+
} else if old.Content != val {
139+
// Entry already exists, only change its content.
140+
log.Info(fmt.Sprintf("Updating %s from %q to %q", path, old.Content, val))
141+
old.Content = val
142+
err = c.UpdateDNSRecord(c.zoneID, old.ID, old)
143+
} else {
144+
log.Info(fmt.Sprintf("Skipping %s = %q", path, val))
145+
}
146+
if err != nil {
147+
return fmt.Errorf("failed to publish %s: %v", path, err)
148+
}
149+
}
150+
151+
// Iterate over the old records and delete anything stale.
152+
for path, entry := range existing {
153+
if _, ok := records[path]; ok {
154+
continue
155+
}
156+
// Stale entry, nuke it.
157+
log.Info(fmt.Sprintf("Deleting %s = %q", path, entry.Content))
158+
if err := c.DeleteDNSRecord(c.zoneID, entry.ID); err != nil {
159+
return fmt.Errorf("failed to delete %s: %v", path, err)
160+
}
161+
}
162+
return nil
163+
}

0 commit comments

Comments
 (0)