From e83c3ccc47b2001b7871b60084d10c5f861c9c93 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 7 Jun 2019 15:31:00 +0200 Subject: [PATCH] p2p/enode: improve IPv6 support, add ENR text representation (#19663) * 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. --- cmd/bootnode/main.go | 2 +- cmd/faucet/faucet.go | 2 +- cmd/utils/flags.go | 2 +- cmd/wnode/main.go | 6 +- les/serverpool.go | 2 +- les/ulc.go | 2 +- node/api.go | 8 +- node/config.go | 2 +- p2p/discover/v4_udp_test.go | 8 +- p2p/enode/localnode.go | 120 +++++++++++++++-------- p2p/enode/localnode_test.go | 46 +++++++++ p2p/enode/node.go | 67 ++++++++++--- p2p/enode/node_test.go | 87 ++++++++++++++++- p2p/enode/urlv4.go | 18 ++-- p2p/enode/urlv4_test.go | 185 +++++++++++++----------------------- p2p/enr/enr.go | 12 ++- p2p/enr/enr_test.go | 24 ++--- p2p/enr/entries.go | 76 ++++++++++++++- p2p/server.go | 9 +- whisper/whisperv6/api.go | 4 +- 20 files changed, 463 insertions(+), 219 deletions(-) diff --git a/cmd/bootnode/main.go b/cmd/bootnode/main.go index a40b32b600..2f9bba1117 100644 --- a/cmd/bootnode/main.go +++ b/cmd/bootnode/main.go @@ -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.") } diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 08b23d46cd..f8092084ad 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -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) } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 002d37f164..973e47ea0c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -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 diff --git a/cmd/wnode/main.go b/cmd/wnode/main.go index 97e5852013..99cf84ec1b 100644 --- a/cmd/wnode/main.go +++ b/cmd/wnode/main.go @@ -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) } @@ -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() } diff --git a/les/serverpool.go b/les/serverpool.go index 668f39c562..3e8cdee410 100644 --- a/les/serverpool.go +++ b/les/serverpool.go @@ -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 diff --git a/les/ulc.go b/les/ulc.go index c6d4155529..8792f60d3c 100644 --- a/les/ulc.go +++ b/les/ulc.go @@ -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 diff --git a/node/api.go b/node/api.go index cc80efabec..66cd1dde33 100644 --- a/node/api.go +++ b/node/api.go @@ -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) } @@ -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) } @@ -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) } @@ -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) } diff --git a/node/config.go b/node/config.go index 31d36f203f..1905ac7fa5 100644 --- a/node/config.go +++ b/node/config.go @@ -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 diff --git a/p2p/discover/v4_udp_test.go b/p2p/discover/v4_udp_test.go index 9d7badea19..c4f5b5de0f 100644 --- a/p2p/discover/v4_udp_test.go +++ b/p2p/discover/v4_udp_test.go @@ -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 { diff --git a/p2p/enode/localnode.go b/p2p/enode/localnode.go index 623f8eae18..d8aa02a77e 100644 --- a/p2p/enode/localnode.go +++ b/p2p/enode/localnode.go @@ -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() @@ -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() @@ -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() } @@ -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() } @@ -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() } @@ -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 diff --git a/p2p/enode/localnode_test.go b/p2p/enode/localnode_test.go index f5e3496d63..00746a8d27 100644 --- a/p2p/enode/localnode_test.go +++ b/p2p/enode/localnode_test.go @@ -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) { @@ -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()) +} diff --git a/p2p/enode/node.go b/p2p/enode/node.go index b454ab2554..9eb2544ffe 100644 --- a/p2p/enode/node.go +++ b/p2p/enode/node.go @@ -18,6 +18,7 @@ package enode import ( "crypto/ecdsa" + "encoding/base64" "encoding/hex" "errors" "fmt" @@ -27,8 +28,11 @@ import ( "strings" "github.com/ethereum/go-ethereum/p2p/enr" + "github.com/ethereum/go-ethereum/rlp" ) +var errMissingPrefix = errors.New("missing 'enr:' prefix for base64-encoded record") + // Node represents a host on the network. type Node struct { r enr.Record @@ -48,6 +52,34 @@ func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) { return node, nil } +// MustParse parses a node record or enode:// URL. It panics if the input is invalid. +func MustParse(rawurl string) *Node { + n, err := Parse(ValidSchemes, rawurl) + if err != nil { + panic("invalid node: " + err.Error()) + } + return n +} + +// Parse decodes and verifies a base64-encoded node record. +func Parse(validSchemes enr.IdentityScheme, input string) (*Node, error) { + if strings.HasPrefix(input, "enode://") { + return ParseV4(input) + } + if !strings.HasPrefix(input, "enr:") { + return nil, errMissingPrefix + } + bin, err := base64.RawURLEncoding.DecodeString(input[4:]) + if err != nil { + return nil, err + } + var r enr.Record + if err := rlp.DecodeBytes(bin, &r); err != nil { + return nil, err + } + return New(validSchemes, &r) +} + // ID returns the node identifier. func (n *Node) ID() ID { return n.id @@ -68,11 +100,19 @@ func (n *Node) Load(k enr.Entry) error { return n.r.Load(k) } -// IP returns the IP address of the node. +// IP returns the IP address of the node. This prefers IPv4 addresses. func (n *Node) IP() net.IP { - var ip net.IP - n.Load((*enr.IP)(&ip)) - return ip + var ( + ip4 enr.IPv4 + ip6 enr.IPv6 + ) + if n.Load(&ip4) == nil { + return net.IP(ip4) + } + if n.Load(&ip6) == nil { + return net.IP(ip6) + } + return nil } // UDP returns the UDP port of the node. @@ -105,10 +145,11 @@ func (n *Node) Record() *enr.Record { return &cpy } -// checks whether n is a valid complete node. +// ValidateComplete checks whether n has a valid IP and UDP port. +// Deprecated: don't use this method. func (n *Node) ValidateComplete() error { if n.Incomplete() { - return errors.New("incomplete node") + return errors.New("missing IP address") } if n.UDP() == 0 { return errors.New("missing UDP port") @@ -122,20 +163,24 @@ func (n *Node) ValidateComplete() error { return n.Load(&key) } -// The string representation of a Node is a URL. -// Please see ParseNode for a description of the format. +// String returns the text representation of the record. func (n *Node) String() string { - return n.v4URL() + if isNewV4(n) { + return n.URLv4() // backwards-compatibility glue for NewV4 nodes + } + enc, _ := rlp.EncodeToBytes(&n.r) // always succeeds because record is valid + b64 := base64.RawURLEncoding.EncodeToString(enc) + return "enr:" + b64 } // MarshalText implements encoding.TextMarshaler. func (n *Node) MarshalText() ([]byte, error) { - return []byte(n.v4URL()), nil + return []byte(n.String()), nil } // UnmarshalText implements encoding.TextUnmarshaler. func (n *Node) UnmarshalText(text []byte) error { - dec, err := ParseV4(string(text)) + dec, err := Parse(ValidSchemes, string(text)) if err == nil { *n = *dec } diff --git a/p2p/enode/node_test.go b/p2p/enode/node_test.go index 861a70bd64..d15859c477 100644 --- a/p2p/enode/node_test.go +++ b/p2p/enode/node_test.go @@ -17,9 +17,12 @@ package enode import ( + "bytes" "encoding/hex" "fmt" + "math/big" "testing" + "testing/quick" "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/rlp" @@ -43,7 +46,7 @@ func TestPythonInterop(t *testing.T) { var ( wantID = HexID("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") wantSeq = uint64(1) - wantIP = enr.IP{127, 0, 0, 1} + wantIP = enr.IPv4{127, 0, 0, 1} wantUDP = enr.UDP(30303) ) if n.Seq() != wantSeq { @@ -52,7 +55,7 @@ func TestPythonInterop(t *testing.T) { if n.ID() != wantID { t.Errorf("wrong id: got %x, want %x", n.ID(), wantID) } - want := map[enr.Entry]interface{}{new(enr.IP): &wantIP, new(enr.UDP): &wantUDP} + want := map[enr.Entry]interface{}{new(enr.IPv4): &wantIP, new(enr.UDP): &wantUDP} for k, v := range want { desc := fmt.Sprintf("loading key %q", k.ENRKey()) if assert.NoError(t, n.Load(k), desc) { @@ -60,3 +63,83 @@ func TestPythonInterop(t *testing.T) { } } } + +func TestHexID(t *testing.T) { + ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} + id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + id2 := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + + if id1 != ref { + t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) + } + if id2 != ref { + t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:]) + } +} + +func TestID_textEncoding(t *testing.T) { + ref := ID{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30, + 0x31, 0x32, + } + hex := "0102030405060708091011121314151617181920212223242526272829303132" + + text, err := ref.MarshalText() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(text, []byte(hex)) { + t.Fatalf("text encoding did not match\nexpected: %s\ngot: %s", hex, text) + } + + id := new(ID) + if err := id.UnmarshalText(text); err != nil { + t.Fatal(err) + } + if *id != ref { + t.Fatalf("text decoding did not match\nexpected: %s\ngot: %s", ref, id) + } +} + +func TestID_distcmp(t *testing.T) { + distcmpBig := func(target, a, b ID) int { + tbig := new(big.Int).SetBytes(target[:]) + abig := new(big.Int).SetBytes(a[:]) + bbig := new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig)) + } + if err := quick.CheckEqual(DistCmp, distcmpBig, nil); err != nil { + t.Error(err) + } +} + +// The random tests is likely to miss the case where a and b are equal, +// this test checks it explicitly. +func TestID_distcmpEqual(t *testing.T) { + base := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + x := ID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} + if DistCmp(base, x, x) != 0 { + t.Errorf("DistCmp(base, x, x) != 0") + } +} + +func TestID_logdist(t *testing.T) { + logdistBig := func(a, b ID) int { + abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(abig, bbig).BitLen() + } + if err := quick.CheckEqual(LogDist, logdistBig, nil); err != nil { + t.Error(err) + } +} + +// The random tests is likely to miss the case where a and b are equal, +// this test checks it explicitly. +func TestID_logdistEqual(t *testing.T) { + x := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + if LogDist(x, x) != 0 { + t.Errorf("LogDist(x, x) != 0") + } +} diff --git a/p2p/enode/urlv4.go b/p2p/enode/urlv4.go index 50e9485d04..2372d4820b 100644 --- a/p2p/enode/urlv4.go +++ b/p2p/enode/urlv4.go @@ -70,7 +70,7 @@ func ParseV4(rawurl string) (*Node, error) { if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil { id, err := parsePubkey(m[1]) if err != nil { - return nil, fmt.Errorf("invalid node ID (%v)", err) + return nil, fmt.Errorf("invalid public key (%v)", err) } return NewV4(id, nil, 0, 0), nil } @@ -81,7 +81,7 @@ func ParseV4(rawurl string) (*Node, error) { // contained in the node has a zero-length signature. func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node { var r enr.Record - if ip != nil { + if len(ip) > 0 { r.Set(enr.IP(ip)) } if udp != 0 { @@ -98,6 +98,12 @@ func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node { return n } +// isNewV4 returns true for nodes created by NewV4. +func isNewV4(n *Node) bool { + var k s256raw + return n.r.IdentityScheme() == "" && n.r.Load(&k) == nil && len(n.r.Signature()) == 0 +} + func parseComplete(rawurl string) (*Node, error) { var ( id *ecdsa.PublicKey @@ -116,7 +122,7 @@ func parseComplete(rawurl string) (*Node, error) { return nil, errors.New("does not contain node ID") } if id, err = parsePubkey(u.User.String()); err != nil { - return nil, fmt.Errorf("invalid node ID (%v)", err) + return nil, fmt.Errorf("invalid public key (%v)", err) } // Parse the IP address. host, port, err := net.SplitHostPort(u.Host) @@ -126,10 +132,6 @@ func parseComplete(rawurl string) (*Node, error) { if ip = net.ParseIP(host); ip == nil { return nil, errors.New("invalid IP address") } - // Ensure the IP is 4 bytes long for IPv4 addresses. - if ipv4 := ip.To4(); ipv4 != nil { - ip = ipv4 - } // Parse the port numbers. if tcpPort, err = strconv.ParseUint(port, 10, 16); err != nil { return nil, errors.New("invalid port") @@ -157,7 +159,7 @@ func parsePubkey(in string) (*ecdsa.PublicKey, error) { return crypto.UnmarshalPubkey(b) } -func (n *Node) v4URL() string { +func (n *Node) URLv4() string { var ( scheme enr.ID nodeid string diff --git a/p2p/enode/urlv4_test.go b/p2p/enode/urlv4_test.go index 3680ab6b78..69ed111021 100644 --- a/p2p/enode/urlv4_test.go +++ b/p2p/enode/urlv4_test.go @@ -17,44 +17,63 @@ package enode import ( - "bytes" "crypto/ecdsa" - "math/big" "net" "reflect" "strings" "testing" - "testing/quick" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/enr" ) var parseNodeTests = []struct { - rawurl string + input string wantError string wantResult *Node }{ + // Records + { + input: "enr:-IS4QGrdq0ugARp5T2BZ41TrZOqLc_oKvZoPuZP5--anqWE_J-Tucc1xgkOL7qXl0puJgT7qc2KSvcupc4NCb0nr4tdjgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQM6UUF2Rm-oFe1IH_rQkRCi00T2ybeMHRSvw1HDpRvjPYN1ZHCCdl8", + wantResult: func() *Node { + testKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8") + var r enr.Record + r.Set(enr.IP{127, 0, 0, 1}) + r.Set(enr.UDP(30303)) + r.SetSeq(99) + SignV4(&r, testKey) + n, _ := New(ValidSchemes, &r) + return n + }(), + }, + // Invalid Records { - rawurl: "http://foobar", - wantError: `invalid URL scheme, want "enode"`, + input: "enr:", + wantError: "EOF", // could be nicer }, { - rawurl: "enode://01010101@123.124.125.126:3", - wantError: `invalid node ID (wrong length, want 128 hex chars)`, + input: "enr:x", + wantError: "illegal base64 data at input byte 0", }, - // Complete nodes with IP address. { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3", + input: "enr:-EmGZm9vYmFyY4JpZIJ2NIJpcIR_AAABiXNlY3AyNTZrMaEDOlFBdkZvqBXtSB_60JEQotNE9sm3jB0Ur8NRw6Ub4z2DdWRwgnZf", + wantError: enr.ErrInvalidSig.Error(), + }, + // Complete node URLs with IP address and ports + { + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3", wantError: `invalid IP address`, }, { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo", + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo", wantError: `invalid port`, }, { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo", + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo", wantError: `invalid discport in query`, }, { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", wantResult: NewV4( hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), net.IP{0x7f, 0x0, 0x0, 0x1}, @@ -63,7 +82,7 @@ var parseNodeTests = []struct { ), }, { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", wantResult: NewV4( hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), net.ParseIP("::"), @@ -72,7 +91,7 @@ var parseNodeTests = []struct { ), }, { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150", + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150", wantResult: NewV4( hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), @@ -81,7 +100,7 @@ var parseNodeTests = []struct { ), }, { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334", + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334", wantResult: NewV4( hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), net.IP{0x7f, 0x0, 0x0, 0x1}, @@ -89,34 +108,42 @@ var parseNodeTests = []struct { 22334, ), }, - // Incomplete nodes with no address. + // Incomplete node URLs with no address { - rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", + input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", wantResult: NewV4( hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), nil, 0, 0, ), }, + // Invalid URLs { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", - wantResult: NewV4( - hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - nil, 0, 0, - ), + input: "", + wantError: errMissingPrefix.Error(), }, - // Invalid URLs { - rawurl: "01010101", - wantError: `invalid node ID (wrong length, want 128 hex chars)`, + input: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", + wantError: errMissingPrefix.Error(), }, { - rawurl: "enode://01010101", - wantError: `invalid node ID (wrong length, want 128 hex chars)`, + input: "01010101", + wantError: errMissingPrefix.Error(), }, { - // This test checks that errors from url.Parse are handled. - rawurl: "://foo", - wantError: `parse ://foo: missing protocol scheme`, + input: "enode://01010101@123.124.125.126:3", + wantError: `invalid public key (wrong length, want 128 hex chars)`, + }, + { + input: "enode://01010101", + wantError: `invalid public key (wrong length, want 128 hex chars)`, + }, + { + input: "http://foobar", + wantError: errMissingPrefix.Error(), + }, + { + input: "://foo", + wantError: errMissingPrefix.Error(), }, } @@ -130,22 +157,22 @@ func hexPubkey(h string) *ecdsa.PublicKey { func TestParseNode(t *testing.T) { for _, test := range parseNodeTests { - n, err := ParseV4(test.rawurl) + n, err := Parse(ValidSchemes, test.input) if test.wantError != "" { if err == nil { - t.Errorf("test %q:\n got nil error, expected %#q", test.rawurl, test.wantError) + t.Errorf("test %q:\n got nil error, expected %#q", test.input, test.wantError) continue } else if err.Error() != test.wantError { - t.Errorf("test %q:\n got error %#q, expected %#q", test.rawurl, err.Error(), test.wantError) + t.Errorf("test %q:\n got error %#q, expected %#q", test.input, err.Error(), test.wantError) continue } } else { if err != nil { - t.Errorf("test %q:\n unexpected error: %v", test.rawurl, err) + t.Errorf("test %q:\n unexpected error: %v", test.input, err) continue } if !reflect.DeepEqual(n, test.wantResult) { - t.Errorf("test %q:\n result mismatch:\ngot: %#v\nwant: %#v", test.rawurl, n, test.wantResult) + t.Errorf("test %q:\n result mismatch:\ngot: %#v\nwant: %#v", test.input, n, test.wantResult) } } } @@ -153,91 +180,11 @@ func TestParseNode(t *testing.T) { func TestNodeString(t *testing.T) { for i, test := range parseNodeTests { - if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") { + if test.wantError == "" && strings.HasPrefix(test.input, "enode://") { str := test.wantResult.String() - if str != test.rawurl { - t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl) + if str != test.input { + t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.input) } } } } - -func TestHexID(t *testing.T) { - ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} - id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") - id2 := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") - - if id1 != ref { - t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) - } - if id2 != ref { - t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:]) - } -} - -func TestID_textEncoding(t *testing.T) { - ref := ID{ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, - 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30, - 0x31, 0x32, - } - hex := "0102030405060708091011121314151617181920212223242526272829303132" - - text, err := ref.MarshalText() - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(text, []byte(hex)) { - t.Fatalf("text encoding did not match\nexpected: %s\ngot: %s", hex, text) - } - - id := new(ID) - if err := id.UnmarshalText(text); err != nil { - t.Fatal(err) - } - if *id != ref { - t.Fatalf("text decoding did not match\nexpected: %s\ngot: %s", ref, id) - } -} - -func TestNodeID_distcmp(t *testing.T) { - distcmpBig := func(target, a, b ID) int { - tbig := new(big.Int).SetBytes(target[:]) - abig := new(big.Int).SetBytes(a[:]) - bbig := new(big.Int).SetBytes(b[:]) - return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig)) - } - if err := quick.CheckEqual(DistCmp, distcmpBig, nil); err != nil { - t.Error(err) - } -} - -// The random tests is likely to miss the case where a and b are equal, -// this test checks it explicitly. -func TestNodeID_distcmpEqual(t *testing.T) { - base := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - x := ID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} - if DistCmp(base, x, x) != 0 { - t.Errorf("DistCmp(base, x, x) != 0") - } -} - -func TestNodeID_logdist(t *testing.T) { - logdistBig := func(a, b ID) int { - abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) - return new(big.Int).Xor(abig, bbig).BitLen() - } - if err := quick.CheckEqual(LogDist, logdistBig, nil); err != nil { - t.Error(err) - } -} - -// The random tests is likely to miss the case where a and b are equal, -// this test checks it explicitly. -func TestNodeID_logdistEqual(t *testing.T) { - x := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - if LogDist(x, x) != 0 { - t.Errorf("LogDist(x, x) != 0") - } -} diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go index 444820c158..c36ae9e3ed 100644 --- a/p2p/enr/enr.go +++ b/p2p/enr/enr.go @@ -163,6 +163,16 @@ func (r *Record) invalidate() { r.raw = nil } +// Signature returns the signature of the record. +func (r *Record) Signature() []byte { + if r.signature == nil { + return nil + } + cpy := make([]byte, len(r.signature)) + copy(cpy, r.signature) + return cpy +} + // EncodeRLP implements rlp.Encoder. Encoding fails if // the record is unsigned. func (r Record) EncodeRLP(w io.Writer) error { @@ -173,7 +183,7 @@ func (r Record) EncodeRLP(w io.Writer) error { return err } -// DecodeRLP implements rlp.Decoder. Decoding verifies the signature. +// DecodeRLP implements rlp.Decoder. Decoding doesn't verify the signature. func (r *Record) DecodeRLP(s *rlp.Stream) error { dec, raw, err := decodeRecord(s) if err != nil { diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go index 449c898a84..434685e0b6 100644 --- a/p2p/enr/enr_test.go +++ b/p2p/enr/enr_test.go @@ -49,23 +49,23 @@ func TestGetSetID(t *testing.T) { } // TestGetSetIP4 tests encoding/decoding and setting/getting of the IP key. -func TestGetSetIP4(t *testing.T) { - ip := IP{192, 168, 0, 3} +func TestGetSetIPv4(t *testing.T) { + ip := IPv4{192, 168, 0, 3} var r Record r.Set(ip) - var ip2 IP + var ip2 IPv4 require.NoError(t, r.Load(&ip2)) assert.Equal(t, ip, ip2) } -// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP key. -func TestGetSetIP6(t *testing.T) { - ip := IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68} +// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP6 key. +func TestGetSetIPv6(t *testing.T) { + ip := IPv6{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68} var r Record r.Set(ip) - var ip2 IP + var ip2 IPv6 require.NoError(t, r.Load(&ip2)) assert.Equal(t, ip, ip2) } @@ -83,7 +83,7 @@ func TestGetSetUDP(t *testing.T) { func TestLoadErrors(t *testing.T) { var r Record - ip4 := IP{127, 0, 0, 1} + ip4 := IPv4{127, 0, 0, 1} r.Set(ip4) // Check error for missing keys. @@ -185,13 +185,13 @@ func TestSeq(t *testing.T) { func TestGetSetOverwrite(t *testing.T) { var r Record - ip := IP{192, 168, 0, 3} + ip := IPv4{192, 168, 0, 3} r.Set(ip) - ip2 := IP{192, 168, 0, 4} + ip2 := IPv4{192, 168, 0, 4} r.Set(ip2) - var ip3 IP + var ip3 IPv4 require.NoError(t, r.Load(&ip3)) assert.Equal(t, ip2, ip3) } @@ -200,7 +200,7 @@ func TestGetSetOverwrite(t *testing.T) { func TestSignEncodeAndDecode(t *testing.T) { var r Record r.Set(UDP(30303)) - r.Set(IP{127, 0, 0, 1}) + r.Set(IPv4{127, 0, 0, 1}) require.NoError(t, signTest([]byte{5}, &r)) blob, err := rlp.EncodeToBytes(r) diff --git a/p2p/enr/entries.go b/p2p/enr/entries.go index 347990ab64..f2118401af 100644 --- a/p2p/enr/entries.go +++ b/p2p/enr/entries.go @@ -60,11 +60,21 @@ type TCP uint16 func (v TCP) ENRKey() string { return "tcp" } +// UDP is the "udp" key, which holds the IPv6-specific UDP port of the node. +type TCP6 uint16 + +func (v TCP6) ENRKey() string { return "tcp6" } + // UDP is the "udp" key, which holds the UDP port of the node. type UDP uint16 func (v UDP) ENRKey() string { return "udp" } +// UDP is the "udp" key, which holds the IPv6-specific UDP port of the node. +type UDP6 uint16 + +func (v UDP6) ENRKey() string { return "udp6" } + // ID is the "id" key, which holds the name of the identity scheme. type ID string @@ -72,17 +82,27 @@ const IDv4 = ID("v4") // the default identity scheme func (v ID) ENRKey() string { return "id" } -// IP is the "ip" key, which holds the IP address of the node. +// IP is either the "ip" or "ip6" key, depending on the value. +// Use this value to encode IP addresses that can be either v4 or v6. +// To load an address from a record use the IPv4 or IPv6 types. type IP net.IP -func (v IP) ENRKey() string { return "ip" } +func (v IP) ENRKey() string { + if net.IP(v).To4() == nil { + return "ip6" + } + return "ip" +} // EncodeRLP implements rlp.Encoder. func (v IP) EncodeRLP(w io.Writer) error { if ip4 := net.IP(v).To4(); ip4 != nil { return rlp.Encode(w, ip4) } - return rlp.Encode(w, net.IP(v)) + if ip6 := net.IP(v).To16(); ip6 != nil { + return rlp.Encode(w, ip6) + } + return fmt.Errorf("invalid IP address: %v", net.IP(v)) } // DecodeRLP implements rlp.Decoder. @@ -96,6 +116,56 @@ func (v *IP) DecodeRLP(s *rlp.Stream) error { return nil } +// IPv4 is the "ip" key, which holds the IP address of the node. +type IPv4 net.IP + +func (v IPv4) ENRKey() string { return "ip" } + +// EncodeRLP implements rlp.Encoder. +func (v IPv4) EncodeRLP(w io.Writer) error { + ip4 := net.IP(v).To4() + if ip4 == nil { + return fmt.Errorf("invalid IPv4 address: %v", net.IP(v)) + } + return rlp.Encode(w, ip4) +} + +// DecodeRLP implements rlp.Decoder. +func (v *IPv4) DecodeRLP(s *rlp.Stream) error { + if err := s.Decode((*net.IP)(v)); err != nil { + return err + } + if len(*v) != 4 { + return fmt.Errorf("invalid IPv4 address, want 4 bytes: %v", *v) + } + return nil +} + +// IPv6 is the "ip6" key, which holds the IP address of the node. +type IPv6 net.IP + +func (v IPv6) ENRKey() string { return "ip6" } + +// EncodeRLP implements rlp.Encoder. +func (v IPv6) EncodeRLP(w io.Writer) error { + ip6 := net.IP(v).To16() + if ip6 == nil { + return fmt.Errorf("invalid IPv6 address: %v", net.IP(v)) + } + return rlp.Encode(w, ip6) +} + +// DecodeRLP implements rlp.Decoder. +func (v *IPv6) DecodeRLP(s *rlp.Stream) error { + if err := s.Decode((*net.IP)(v)); err != nil { + return err + } + if len(*v) != 16 { + return fmt.Errorf("invalid IPv6 address, want 16 bytes: %v", *v) + } + return nil +} + // KeyError is an error related to a key. type KeyError struct { Key string diff --git a/p2p/server.go b/p2p/server.go index 566f01ffc5..f17ef2c2bf 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -39,7 +39,6 @@ import ( "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/netutil" - "github.com/ethereum/go-ethereum/rlp" ) const ( @@ -602,7 +601,7 @@ type dialer interface { } func (srv *Server) run(dialstate dialer) { - srv.log.Info("Started P2P networking", "self", srv.localnode.Node()) + srv.log.Info("Started P2P networking", "self", srv.localnode.Node().URLv4()) defer srv.loopWG.Done() defer srv.nodedb.Close() @@ -1034,7 +1033,7 @@ func (srv *Server) NodeInfo() *NodeInfo { node := srv.Self() info := &NodeInfo{ Name: srv.Name, - Enode: node.String(), + Enode: node.URLv4(), ID: node.ID().String(), IP: node.IP().String(), ListenAddr: srv.ListenAddr, @@ -1042,9 +1041,7 @@ func (srv *Server) NodeInfo() *NodeInfo { } info.Ports.Discovery = node.UDP() info.Ports.Listener = node.TCP() - if enc, err := rlp.EncodeToBytes(node.Record()); err == nil { - info.ENR = "0x" + hex.EncodeToString(enc) - } + info.ENR = node.String() // Gather all the running protocol infos (only once per protocol type) for _, proto := range srv.Protocols { diff --git a/whisper/whisperv6/api.go b/whisper/whisperv6/api.go index 7609a03c28..d6d4c8d3de 100644 --- a/whisper/whisperv6/api.go +++ b/whisper/whisperv6/api.go @@ -103,7 +103,7 @@ func (api *PublicWhisperAPI) SetBloomFilter(ctx context.Context, bloom hexutil.B // MarkTrustedPeer marks a peer trusted, which will allow it to send historic (expired) messages. // Note: This function is not adding new nodes, the node needs to exists as a peer. func (api *PublicWhisperAPI) MarkTrustedPeer(ctx context.Context, url string) (bool, error) { - n, err := enode.ParseV4(url) + n, err := enode.Parse(enode.ValidSchemes, url) if err != nil { return false, err } @@ -291,7 +291,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (hexutil. // send to specific node (skip PoW check) if len(req.TargetPeer) > 0 { - n, err := enode.ParseV4(req.TargetPeer) + n, err := enode.Parse(enode.ValidSchemes, req.TargetPeer) if err != nil { return nil, fmt.Errorf("failed to parse target peer: %s", err) }