From e89cce63fd1428c40031edb30b407cbb468abb08 Mon Sep 17 00:00:00 2001 From: Arthur Gavazza <32915690+arthurgavazza@users.noreply.github.com> Date: Thu, 30 Mar 2023 01:34:57 -0300 Subject: [PATCH] feat: add identify option to swarm peers command Fixes #9578 --- core/commands/swarm.go | 71 ++++++++++++++++++++++++--- test/cli/harness/peering.go | 37 ++++++++++++++ test/cli/peering_test.go | 55 +++++---------------- test/cli/swarm_test.go | 97 +++++++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 50 deletions(-) create mode 100644 test/cli/harness/peering.go create mode 100644 test/cli/swarm_test.go diff --git a/core/commands/swarm.go b/core/commands/swarm.go index 7d5ae96af..c9259bef0 100644 --- a/core/commands/swarm.go +++ b/core/commands/swarm.go @@ -2,6 +2,7 @@ package commands import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -21,8 +22,10 @@ import ( "github.com/ipfs/kubo/repo/fsrepo" cmds "github.com/ipfs/go-ipfs-cmds" + ic "github.com/libp2p/go-libp2p/core/crypto" inet "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + pstore "github.com/libp2p/go-libp2p/core/peerstore" rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager" ma "github.com/multiformats/go-multiaddr" madns "github.com/multiformats/go-multiaddr-dns" @@ -69,6 +72,7 @@ const ( swarmDirectionOptionName = "direction" swarmResetLimitsOptionName = "reset" swarmUsedResourcesPercentageName = "min-used-limit-perc" + swarmIdentifyOptionName = "identify" ) type peeringResult struct { @@ -236,17 +240,18 @@ var swarmPeersCmd = &cmds.Command{ cmds.BoolOption(swarmStreamsOptionName, "Also list information about open streams for each peer"), cmds.BoolOption(swarmLatencyOptionName, "Also list information about latency to each peer"), cmds.BoolOption(swarmDirectionOptionName, "Also list information about the direction of connection"), + cmds.BoolOption(swarmIdentifyOptionName, "Also list information about peers identify"), }, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { api, err := cmdenv.GetApi(env, req) if err != nil { return err } - verbose, _ := req.Options[swarmVerboseOptionName].(bool) latency, _ := req.Options[swarmLatencyOptionName].(bool) streams, _ := req.Options[swarmStreamsOptionName].(bool) direction, _ := req.Options[swarmDirectionOptionName].(bool) + identify, _ := req.Options[swarmIdentifyOptionName].(bool) conns, err := api.Swarm().Peers(req.Context) if err != nil { @@ -287,6 +292,15 @@ var swarmPeersCmd = &cmds.Command{ ci.Streams = append(ci.Streams, streamInfo{Protocol: string(s)}) } } + + if verbose || identify { + n, err := cmdenv.GetNode(env) + if err != nil { + return err + } + identifyResult, _ := ci.identifyPeer(n.Peerstore, c.ID()) + ci.Identify = identifyResult + } sort.Sort(&ci) out.Peers = append(out.Peers, ci) } @@ -411,12 +425,13 @@ type streamInfo struct { } type connInfo struct { - Addr string - Peer string - Latency string - Muxer string - Direction inet.Direction - Streams []streamInfo + Addr string `json:",omitempty"` + Peer string `json:",omitempty"` + Latency string `json:",omitempty"` + Muxer string `json:",omitempty"` + Direction inet.Direction `json:",omitempty"` + Streams []streamInfo `json:",omitempty"` + Identify IdOutput `json:",omitempty"` } func (ci *connInfo) Less(i, j int) bool { @@ -447,6 +462,48 @@ func (ci connInfos) Swap(i, j int) { ci.Peers[i], ci.Peers[j] = ci.Peers[j], ci.Peers[i] } +func (ci *connInfo) identifyPeer(ps pstore.Peerstore, p peer.ID) (IdOutput, error) { + var info IdOutput + info.ID = p.String() + + if pk := ps.PubKey(p); pk != nil { + pkb, err := ic.MarshalPublicKey(pk) + if err != nil { + return IdOutput{}, err + } + info.PublicKey = base64.StdEncoding.EncodeToString(pkb) + } + + addrInfo := ps.PeerInfo(p) + addrs, err := peer.AddrInfoToP2pAddrs(&addrInfo) + if err != nil { + return IdOutput{}, err + } + + for _, a := range addrs { + info.Addresses = append(info.Addresses, a.String()) + } + sort.Strings(info.Addresses) + + if protocols, err := ps.GetProtocols(p); err == nil { + info.Protocols = append(info.Protocols, protocols...) + sort.Slice(info.Protocols, func(i, j int) bool { return info.Protocols[i] < info.Protocols[j] }) + } + + if v, err := ps.Get(p, "ProtocolVersion"); err == nil { + if vs, ok := v.(string); ok { + info.ProtocolVersion = vs + } + } + if v, err := ps.Get(p, "AgentVersion"); err == nil { + if vs, ok := v.(string); ok { + info.AgentVersion = vs + } + } + + return info, nil +} + // directionString transfers to string func directionString(d inet.Direction) string { switch d { diff --git a/test/cli/harness/peering.go b/test/cli/harness/peering.go new file mode 100644 index 000000000..3fcd8bb6b --- /dev/null +++ b/test/cli/harness/peering.go @@ -0,0 +1,37 @@ +package harness + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/ipfs/kubo/config" +) + +type Peering struct { + From int + To int +} + +func newRandPort() int { + n := rand.Int() + return 3000 + (n % 1000) +} + +func CreatePeerNodes(t *testing.T, n int, peerings []Peering) (*Harness, Nodes) { + h := NewT(t) + nodes := h.NewNodes(n).Init() + nodes.ForEachPar(func(node *Node) { + node.UpdateConfig(func(cfg *config.Config) { + cfg.Routing.Type = config.NewOptionalString("none") + cfg.Addresses.Swarm = []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", newRandPort())} + }) + + }) + + for _, peering := range peerings { + nodes[peering.From].PeerWith(nodes[peering.To]) + } + + return h, nodes +} diff --git a/test/cli/peering_test.go b/test/cli/peering_test.go index ade52ec3c..9c6ab975d 100644 --- a/test/cli/peering_test.go +++ b/test/cli/peering_test.go @@ -1,12 +1,9 @@ package cli import ( - "fmt" - "math/rand" "testing" "time" - "github.com/ipfs/kubo/config" "github.com/ipfs/kubo/test/cli/harness" . "github.com/ipfs/kubo/test/cli/testutils" "github.com/libp2p/go-libp2p/core/peer" @@ -16,16 +13,6 @@ import ( func TestPeering(t *testing.T) { t.Parallel() - type peering struct { - from int - to int - } - - newRandPort := func() int { - n := rand.Int() - return 3000 + (n % 1000) - } - containsPeerID := func(p peer.ID, peers []peer.ID) bool { for _, peerID := range peers { if p == peerID { @@ -63,34 +50,16 @@ func TestPeering(t *testing.T) { }, 20*time.Second, 10*time.Millisecond, "%d -> %d peered", from.ID, to.ID) } - assertPeerings := func(h *harness.Harness, nodes []*harness.Node, peerings []peering) { - ForEachPar(peerings, func(peering peering) { - assertPeered(h, nodes[peering.from], nodes[peering.to]) + assertPeerings := func(h *harness.Harness, nodes []*harness.Node, peerings []harness.Peering) { + ForEachPar(peerings, func(peering harness.Peering) { + assertPeered(h, nodes[peering.From], nodes[peering.To]) }) } - createNodes := func(t *testing.T, n int, peerings []peering) (*harness.Harness, harness.Nodes) { - h := harness.NewT(t) - nodes := h.NewNodes(n).Init() - nodes.ForEachPar(func(node *harness.Node) { - node.UpdateConfig(func(cfg *config.Config) { - cfg.Routing.Type = config.NewOptionalString("none") - cfg.Addresses.Swarm = []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", newRandPort())} - }) - - }) - - for _, peering := range peerings { - nodes[peering.from].PeerWith(nodes[peering.to]) - } - - return h, nodes - } - t.Run("bidirectional peering should work (simultaneous connect)", func(t *testing.T) { t.Parallel() - peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}} - h, nodes := createNodes(t, 3, peerings) + peerings := []harness.Peering{{From: 0, To: 1}, {From: 1, To: 0}, {From: 1, To: 2}} + h, nodes := harness.CreatePeerNodes(t, 3, peerings) nodes.StartDaemons() assertPeerings(h, nodes, peerings) @@ -101,8 +70,8 @@ func TestPeering(t *testing.T) { t.Run("1 should reconnect to 2 when 2 disconnects from 1", func(t *testing.T) { t.Parallel() - peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}} - h, nodes := createNodes(t, 3, peerings) + peerings := []harness.Peering{{From: 0, To: 1}, {From: 1, To: 0}, {From: 1, To: 2}} + h, nodes := harness.CreatePeerNodes(t, 3, peerings) nodes.StartDaemons() assertPeerings(h, nodes, peerings) @@ -113,12 +82,12 @@ func TestPeering(t *testing.T) { t.Run("1 will peer with 2 when it comes online", func(t *testing.T) { t.Parallel() - peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}} - h, nodes := createNodes(t, 3, peerings) + peerings := []harness.Peering{{From: 0, To: 1}, {From: 1, To: 0}, {From: 1, To: 2}} + h, nodes := harness.CreatePeerNodes(t, 3, peerings) nodes[0].StartDaemon() nodes[1].StartDaemon() - assertPeerings(h, nodes, []peering{{from: 0, to: 1}, {from: 1, to: 0}}) + assertPeerings(h, nodes, []harness.Peering{{From: 0, To: 1}, {From: 1, To: 0}}) nodes[2].StartDaemon() assertPeerings(h, nodes, peerings) @@ -126,8 +95,8 @@ func TestPeering(t *testing.T) { t.Run("1 will re-peer with 2 when it disconnects and then comes back online", func(t *testing.T) { t.Parallel() - peerings := []peering{{from: 0, to: 1}, {from: 1, to: 0}, {from: 1, to: 2}} - h, nodes := createNodes(t, 3, peerings) + peerings := []harness.Peering{{From: 0, To: 1}, {From: 1, To: 0}, {From: 1, To: 2}} + h, nodes := harness.CreatePeerNodes(t, 3, peerings) nodes.StartDaemons() assertPeerings(h, nodes, peerings) diff --git a/test/cli/swarm_test.go b/test/cli/swarm_test.go new file mode 100644 index 000000000..d32208106 --- /dev/null +++ b/test/cli/swarm_test.go @@ -0,0 +1,97 @@ +package cli + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ipfs/kubo/test/cli/harness" + + "github.com/stretchr/testify/assert" +) + +// TODO: Migrate the rest of the sharness swarm test. +func TestSwarm(t *testing.T) { + type identifyType struct { + ID string + PublicKey string + Addresses []string + AgentVersion string + ProtocolVersion string + Protocols []string + } + type peer struct { + Identify identifyType + } + type expectedOutputType struct { + Peers []peer + } + + t.Parallel() + + t.Run("ipfs swarm peers returns empty peers when a node is not connected to any peers", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + res := node.RunIPFS("swarm", "peers", "--enc=json", "--identify") + var output expectedOutputType + err := json.Unmarshal(res.Stdout.Bytes(), &output) + assert.Nil(t, err) + assert.Equal(t, 0, len(output.Peers)) + + }) + t.Run("ipfs swarm peers with flag identify outputs expected identify information about connected peers", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + otherNode := harness.NewT(t).NewNode().Init().StartDaemon() + node.Connect(otherNode) + + res := node.RunIPFS("swarm", "peers", "--enc=json", "--identify") + var output expectedOutputType + err := json.Unmarshal(res.Stdout.Bytes(), &output) + assert.Nil(t, err) + actualID := output.Peers[0].Identify.ID + actualPublicKey := output.Peers[0].Identify.PublicKey + actualAgentVersion := output.Peers[0].Identify.AgentVersion + actualAdresses := output.Peers[0].Identify.Addresses + actualProtocolVersion := output.Peers[0].Identify.ProtocolVersion + actualProtocols := output.Peers[0].Identify.Protocols + + expectedID := otherNode.PeerID().String() + expectedAddresses := []string{fmt.Sprintf("%s/p2p/%s", otherNode.SwarmAddrs()[0], actualID)} + + assert.Equal(t, actualID, expectedID) + assert.NotNil(t, actualPublicKey) + assert.NotNil(t, actualAgentVersion) + assert.NotNil(t, actualProtocolVersion) + assert.Len(t, actualAdresses, 1) + assert.Equal(t, expectedAddresses[0], actualAdresses[0]) + assert.Greater(t, len(actualProtocols), 0) + + }) + + t.Run("ipfs swarm peers with flag identify outputs Identify field with data that matches calling ipfs id on a peer", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + otherNode := harness.NewT(t).NewNode().Init().StartDaemon() + node.Connect(otherNode) + + otherNodeIDResponse := otherNode.RunIPFS("id", "--enc=json") + var otherNodeIDOutput identifyType + err := json.Unmarshal(otherNodeIDResponse.Stdout.Bytes(), &otherNodeIDOutput) + assert.Nil(t, err) + res := node.RunIPFS("swarm", "peers", "--enc=json", "--identify") + + var output expectedOutputType + err = json.Unmarshal(res.Stdout.Bytes(), &output) + assert.Nil(t, err) + outputIdentify := output.Peers[0].Identify + + assert.Equal(t, outputIdentify.ID, otherNodeIDOutput.ID) + assert.Equal(t, outputIdentify.PublicKey, otherNodeIDOutput.PublicKey) + assert.Equal(t, outputIdentify.AgentVersion, otherNodeIDOutput.AgentVersion) + assert.Equal(t, outputIdentify.ProtocolVersion, otherNodeIDOutput.ProtocolVersion) + assert.ElementsMatch(t, outputIdentify.Addresses, otherNodeIDOutput.Addresses) + assert.ElementsMatch(t, outputIdentify.Protocols, otherNodeIDOutput.Protocols) + + }) +}