Skip to content

Commit

Permalink
p2p/enode: improve IPv6 support, add ENR text representation (#19663)
Browse files Browse the repository at this point in the history
* p2p/enr: add entries for for IPv4/IPv6 separation

This adds entry types for "ip6", "udp6", "tcp6" keys. The IP type stays
around because removing it would break a lot of code and force everyone
to care about the distinction.

* p2p/enode: track IPv4 and IPv6 address separately

LocalNode predicts the local node's UDP endpoint and updates the record.
This change makes it predict IPv4 and IPv6 endpoints separately since
they can now be in the record at the same time.

* p2p/enode: implement base64 text format
* all: switch to enode.Parse(...)

This allows passing base64-encoded node records to all the places that
previously accepted enode:// URLs. The URL format is still supported.

* cmd/bootnode, p2p: log node URL instead of ENR

...and return the base64 record in NodeInfo.
  • Loading branch information
fjl committed Jun 7, 2019
1 parent 896322b commit e83c3cc
Show file tree
Hide file tree
Showing 20 changed files with 463 additions and 219 deletions.
2 changes: 1 addition & 1 deletion cmd/bootnode/main.go
Expand Up @@ -143,7 +143,7 @@ func printNotice(nodeKey *ecdsa.PublicKey, addr net.UDPAddr) {
addr.IP = net.IP{127, 0, 0, 1}
}
n := enode.NewV4(nodeKey, addr.IP, 0, addr.Port)
fmt.Println(n.String())
fmt.Println(n.URLv4())
fmt.Println("Note: you're using cmd/bootnode, a developer tool.")
fmt.Println("We recommend using a regular node as bootstrap node for production deployments.")
}
2 changes: 1 addition & 1 deletion cmd/faucet/faucet.go
Expand Up @@ -260,7 +260,7 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network u
return nil, err
}
for _, boot := range enodes {
old, err := enode.ParseV4(boot.String())
old, err := enode.Parse(enode.ValidSchemes, boot.String())
if err == nil {
stack.Server().AddPeer(old)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/utils/flags.go
Expand Up @@ -794,7 +794,7 @@ func setBootstrapNodes(ctx *cli.Context, cfg *p2p.Config) {
cfg.BootstrapNodes = make([]*enode.Node, 0, len(urls))
for _, url := range urls {
if url != "" {
node, err := enode.ParseV4(url)
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
log.Crit("Bootstrap URL invalid", "enode", url, "err", err)
continue
Expand Down
6 changes: 3 additions & 3 deletions cmd/wnode/main.go
Expand Up @@ -203,7 +203,7 @@ func initialize() {
if len(*argEnode) == 0 {
argEnode = scanLineA("Please enter the peer's enode: ")
}
peer := enode.MustParseV4(*argEnode)
peer := enode.MustParse(*argEnode)
peers = append(peers, peer)
}

Expand Down Expand Up @@ -747,9 +747,9 @@ func requestExpiredMessagesLoop() {
}

func extractIDFromEnode(s string) []byte {
n, err := enode.ParseV4(s)
n, err := enode.Parse(enode.ValidSchemes, s)
if err != nil {
utils.Fatalf("Failed to parse enode: %s", err)
utils.Fatalf("Failed to parse node: %s", err)
}
return n.ID().Bytes()
}
Expand Down
2 changes: 1 addition & 1 deletion les/serverpool.go
Expand Up @@ -505,7 +505,7 @@ func parseTrustedNodes(trustedNodes []string) map[enode.ID]*enode.Node {
nodes := make(map[enode.ID]*enode.Node)

for _, node := range trustedNodes {
node, err := enode.ParseV4(node)
node, err := enode.Parse(enode.ValidSchemes, node)
if err != nil {
log.Warn("Trusted node URL invalid", "enode", node, "err", err)
continue
Expand Down
2 changes: 1 addition & 1 deletion les/ulc.go
Expand Up @@ -34,7 +34,7 @@ func newULC(ulcConfig *eth.ULCConfig) *ulc {
}
m := make(map[string]struct{}, len(ulcConfig.TrustedServers))
for _, id := range ulcConfig.TrustedServers {
node, err := enode.ParseV4(id)
node, err := enode.Parse(enode.ValidSchemes, id)
if err != nil {
log.Debug("Failed to parse trusted server", "id", id, "err", err)
continue
Expand Down
8 changes: 4 additions & 4 deletions node/api.go
Expand Up @@ -49,7 +49,7 @@ func (api *PrivateAdminAPI) AddPeer(url string) (bool, error) {
return false, ErrNodeStopped
}
// Try to add the url as a static peer and return
node, err := enode.ParseV4(url)
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
Expand All @@ -65,7 +65,7 @@ func (api *PrivateAdminAPI) RemovePeer(url string) (bool, error) {
return false, ErrNodeStopped
}
// Try to remove the url as a static peer and return
node, err := enode.ParseV4(url)
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
Expand All @@ -80,7 +80,7 @@ func (api *PrivateAdminAPI) AddTrustedPeer(url string) (bool, error) {
if server == nil {
return false, ErrNodeStopped
}
node, err := enode.ParseV4(url)
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
Expand All @@ -96,7 +96,7 @@ func (api *PrivateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
if server == nil {
return false, ErrNodeStopped
}
node, err := enode.ParseV4(url)
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion node/config.go
Expand Up @@ -425,7 +425,7 @@ func (c *Config) parsePersistentNodes(w *bool, path string) []*enode.Node {
if url == "" {
continue
}
node, err := enode.ParseV4(url)
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
log.Error(fmt.Sprintf("Node URL %s: %v\n", url, err))
continue
Expand Down
8 changes: 4 additions & 4 deletions p2p/discover/v4_udp_test.go
Expand Up @@ -342,10 +342,10 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {

// send the reply as two packets.
list := []*node{
wrapNode(enode.MustParseV4("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")),
wrapNode(enode.MustParseV4("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")),
wrapNode(enode.MustParseV4("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")),
wrapNode(enode.MustParseV4("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")),
wrapNode(enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")),
wrapNode(enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")),
wrapNode(enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")),
wrapNode(enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")),
}
rpclist := make([]rpcNode, len(list))
for i := range list {
Expand Down
120 changes: 82 additions & 38 deletions p2p/enode/localnode.go
Expand Up @@ -48,23 +48,32 @@ type LocalNode struct {
db *DB

// everything below is protected by a lock
mu sync.Mutex
seq uint64
entries map[string]enr.Entry
udpTrack *netutil.IPTracker // predicts external UDP endpoint
staticIP net.IP
fallbackIP net.IP
fallbackUDP int
mu sync.Mutex
seq uint64
entries map[string]enr.Entry
endpoint4 lnEndpoint
endpoint6 lnEndpoint
}

type lnEndpoint struct {
track *netutil.IPTracker
staticIP, fallbackIP net.IP
fallbackUDP int
}

// NewLocalNode creates a local node.
func NewLocalNode(db *DB, key *ecdsa.PrivateKey) *LocalNode {
ln := &LocalNode{
id: PubkeyToIDV4(&key.PublicKey),
db: db,
key: key,
udpTrack: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
entries: make(map[string]enr.Entry),
id: PubkeyToIDV4(&key.PublicKey),
db: db,
key: key,
entries: make(map[string]enr.Entry),
endpoint4: lnEndpoint{
track: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
},
endpoint6: lnEndpoint{
track: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
},
}
ln.seq = db.localSeq(ln.id)
ln.invalidate()
Expand All @@ -89,13 +98,22 @@ func (ln *LocalNode) Node() *Node {
return ln.cur.Load().(*Node)
}

// Seq returns the current sequence number of the local node record.
func (ln *LocalNode) Seq() uint64 {
ln.mu.Lock()
defer ln.mu.Unlock()

return ln.seq
}

// ID returns the local node ID.
func (ln *LocalNode) ID() ID {
return ln.id
}

// Set puts the given entry into the local record, overwriting
// any existing value.
// Set puts the given entry into the local record, overwriting any existing value.
// Use Set*IP and SetFallbackUDP to set IP addresses and UDP port, otherwise they'll
// be overwritten by the endpoint predictor.
func (ln *LocalNode) Set(e enr.Entry) {
ln.mu.Lock()
defer ln.mu.Unlock()
Expand Down Expand Up @@ -127,13 +145,20 @@ func (ln *LocalNode) delete(e enr.Entry) {
}
}

func (ln *LocalNode) endpointForIP(ip net.IP) *lnEndpoint {
if ip.To4() != nil {
return &ln.endpoint4
}
return &ln.endpoint6
}

// SetStaticIP sets the local IP to the given one unconditionally.
// This disables endpoint prediction.
func (ln *LocalNode) SetStaticIP(ip net.IP) {
ln.mu.Lock()
defer ln.mu.Unlock()

ln.staticIP = ip
ln.endpointForIP(ip).staticIP = ip
ln.updateEndpoints()
}

Expand All @@ -143,17 +168,18 @@ func (ln *LocalNode) SetFallbackIP(ip net.IP) {
ln.mu.Lock()
defer ln.mu.Unlock()

ln.fallbackIP = ip
ln.endpointForIP(ip).fallbackIP = ip
ln.updateEndpoints()
}

// SetFallbackUDP sets the last-resort UDP port. This port is used
// SetFallbackUDP sets the last-resort UDP-on-IPv4 port. This port is used
// if no endpoint prediction can be made.
func (ln *LocalNode) SetFallbackUDP(port int) {
ln.mu.Lock()
defer ln.mu.Unlock()

ln.fallbackUDP = port
ln.endpoint4.fallbackUDP = port
ln.endpoint6.fallbackUDP = port
ln.updateEndpoints()
}

Expand All @@ -163,7 +189,7 @@ func (ln *LocalNode) UDPEndpointStatement(fromaddr, endpoint *net.UDPAddr) {
ln.mu.Lock()
defer ln.mu.Unlock()

ln.udpTrack.AddStatement(fromaddr.String(), endpoint.String())
ln.endpointForIP(endpoint.IP).track.AddStatement(fromaddr.String(), endpoint.String())
ln.updateEndpoints()
}

Expand All @@ -173,32 +199,50 @@ func (ln *LocalNode) UDPContact(toaddr *net.UDPAddr) {
ln.mu.Lock()
defer ln.mu.Unlock()

ln.udpTrack.AddContact(toaddr.String())
ln.endpointForIP(toaddr.IP).track.AddContact(toaddr.String())
ln.updateEndpoints()
}

// updateEndpoints updates the record with predicted endpoints.
func (ln *LocalNode) updateEndpoints() {
// Determine the endpoints.
newIP := ln.fallbackIP
newUDP := ln.fallbackUDP
if ln.staticIP != nil {
newIP = ln.staticIP
} else if ip, port := predictAddr(ln.udpTrack); ip != nil {
newIP = ip
newUDP = port
}
ip4, udp4 := ln.endpoint4.get()
ip6, udp6 := ln.endpoint6.get()

// Update the record.
if newIP != nil && !newIP.IsUnspecified() {
ln.set(enr.IP(newIP))
if newUDP != 0 {
ln.set(enr.UDP(newUDP))
} else {
ln.delete(enr.UDP(0))
}
if ip4 != nil && !ip4.IsUnspecified() {
ln.set(enr.IPv4(ip4))
} else {
ln.delete(enr.IP{})
ln.delete(enr.IPv4{})
}
if ip6 != nil && !ip6.IsUnspecified() {
ln.set(enr.IPv6(ip6))
} else {
ln.delete(enr.IPv6{})
}
if udp4 != 0 {
ln.set(enr.UDP(udp4))
} else {
ln.delete(enr.UDP(0))
}
if udp6 != 0 && udp6 != udp4 {
ln.set(enr.UDP6(udp6))
} else {
ln.delete(enr.UDP6(0))
}
}

// get returns the endpoint with highest precedence.
func (e *lnEndpoint) get() (newIP net.IP, newPort int) {
newPort = e.fallbackUDP
if e.fallbackIP != nil {
newIP = e.fallbackIP
}
if e.staticIP != nil {
newIP = e.staticIP
} else if ip, port := predictAddr(e.track); ip != nil {
newIP = ip
newPort = port
}
return newIP, newPort
}

// predictAddr wraps IPTracker.PredictEndpoint, converting from its string-based
Expand Down
46 changes: 46 additions & 0 deletions p2p/enode/localnode_test.go
Expand Up @@ -17,10 +17,13 @@
package enode

import (
"math/rand"
"net"
"testing"

"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/stretchr/testify/assert"
)

func newLocalNodeForTesting() (*LocalNode, *DB) {
Expand Down Expand Up @@ -74,3 +77,46 @@ func TestLocalNodeSeqPersist(t *testing.T) {
t.Fatalf("wrong seq %d on instance with changed key, want 1", s)
}
}

// This test checks behavior of the endpoint predictor.
func TestLocalNodeEndpoint(t *testing.T) {
var (
fallback = &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 80}
predicted = &net.UDPAddr{IP: net.IP{127, 0, 1, 2}, Port: 81}
staticIP = net.IP{127, 0, 1, 2}
)
ln, db := newLocalNodeForTesting()
defer db.Close()

// Nothing is set initially.
assert.Equal(t, net.IP(nil), ln.Node().IP())
assert.Equal(t, 0, ln.Node().UDP())
assert.Equal(t, uint64(1), ln.Node().Seq())

// Set up fallback address.
ln.SetFallbackIP(fallback.IP)
ln.SetFallbackUDP(fallback.Port)
assert.Equal(t, fallback.IP, ln.Node().IP())
assert.Equal(t, fallback.Port, ln.Node().UDP())
assert.Equal(t, uint64(2), ln.Node().Seq())

// Add endpoint statements from random hosts.
for i := 0; i < iptrackMinStatements; i++ {
assert.Equal(t, fallback.IP, ln.Node().IP())
assert.Equal(t, fallback.Port, ln.Node().UDP())
assert.Equal(t, uint64(2), ln.Node().Seq())

from := &net.UDPAddr{IP: make(net.IP, 4), Port: 90}
rand.Read(from.IP)
ln.UDPEndpointStatement(from, predicted)
}
assert.Equal(t, predicted.IP, ln.Node().IP())
assert.Equal(t, predicted.Port, ln.Node().UDP())
assert.Equal(t, uint64(3), ln.Node().Seq())

// Static IP overrides prediction.
ln.SetStaticIP(staticIP)
assert.Equal(t, staticIP, ln.Node().IP())
assert.Equal(t, fallback.Port, ln.Node().UDP())
assert.Equal(t, uint64(4), ln.Node().Seq())
}

0 comments on commit e83c3cc

Please sign in to comment.