@cryptotaxi247 / kubo / commits / 978091a62

feat: implement peering service

MVP for #6097 This feature will repeatedly reconnect (with a randomized exponential backoff) to peers in a set of "peered" peers. In the future, this should be extended to: 1. Include a CLI for modifying this list at runtime. 2. Include additional options for peers we want to _protect_ but not connect to. 3. Allow configuring timeouts, backoff, etc. 4. Allow groups? Possibly through textile threads. 5. Allow for runtime-only peering rules. 6. Different reconnect policies. But this MVP should be a significant step forward.

Steven Allen committed May 25, 2020 at 09:26 UTC 978091a626a0e1f00a797fc4e2de99f4bfee943b
9 files changed +457 -3
core/core.go
+2
@@ -48,6 +48,7 @@ import (
48 "github.com/ipfs/go-ipfs/namesys"
49 ipnsrp "github.com/ipfs/go-ipfs/namesys/republisher"
50 "github.com/ipfs/go-ipfs/p2p"
51 + "github.com/ipfs/go-ipfs/peering"
52 "github.com/ipfs/go-ipfs/repo"
53 )
54
@@ -83,6 +84,7 @@ type IpfsNode struct {
84
85 // Online
86 PeerHost p2phost.Host `optional:"true"` // the network host (server+client)
87 + Peering peering.PeeringService `optional:"true"`
88 Filters *ma.Filters `optional:"true"`
89 Bootstrapper io.Closer `optional:"true"` // the periodic bootstrapper
90 Routing routing.Routing `optional:"true"` // the routing system. recommend ipfs-dht
core/node/groups.go
+2
@@ -250,6 +250,8 @@ func Online(bcfg *BuildCfg, cfg *config.Config) fx.Option {
250 fx.Provide(OnlineExchange(shouldBitswapProvide)),
251 maybeProvide(Graphsync, cfg.Experimental.GraphsyncEnabled),
252 fx.Provide(Namesys(ipnsCacheSize)),
253 + fx.Provide(Peering),
254 + PeerWith(cfg.Peering.Peers...),
255
256 fx.Invoke(IpnsRepublisher(repubPeriod, recordLifetime)),
257
core/node/peering.go new
+34
@@ -0,0 +1,34 @@
1 +package node
2 +
3 +import (
4 + "context"
5 +
6 + "github.com/ipfs/go-ipfs/peering"
7 + "github.com/libp2p/go-libp2p-core/host"
8 + "github.com/libp2p/go-libp2p-core/peer"
9 + "go.uber.org/fx"
10 +)
11 +
12 +// Peering constructs the peering service and hooks it into fx's lifetime
13 +// management system.
14 +func Peering(lc fx.Lifecycle, host host.Host) *peering.PeeringService {
15 + ps := peering.NewPeeringService(host)
16 + lc.Append(fx.Hook{
17 + OnStart: func(context.Context) error {
18 + return ps.Start()
19 + },
20 + OnStop: func(context.Context) error {
21 + return ps.Stop()
22 + },
23 + })
24 + return ps
25 +}
26 +
27 +// PeerWith configures the peering service to peer with the specified peers.
28 +func PeerWith(peers ...peer.AddrInfo) fx.Option {
29 + return fx.Invoke(func(ps *peering.PeeringService) {
30 + for _, ai := range peers {
31 + ps.AddPeer(ai)
32 + }
33 + })
34 +}
docs/config.md
+23
@@ -139,6 +139,8 @@ documented in `ipfs config profile --help`.
139 - [`Pubsub`](#pubsub)
140 - [`Pubsub.Router`](#pubsubrouter)
141 - [`Pubsub.DisableSigning`](#pubsubdisablesigning)
142 + - [`Peering`](#peering)
143 + - [`Peering.Peers`](#peeringpeers)
144 - [`Reprovider`](#reprovider)
145 - [`Reprovider.Interval`](#reproviderinterval)
146 - [`Reprovider.Strategy`](#reproviderstrategy)
@@ -157,6 +159,7 @@ documented in `ipfs config profile --help`.
159 - [`Swarm.ConnMgr.HighWater`](#swarmconnmgrhighwater)
160 - [`Swarm.ConnMgr.GracePeriod`](#swarmconnmgrgraceperiod)
161
162 +
163 ## `Addresses`
164
165 Contains information about various listener addresses to be used by this node.
@@ -703,6 +706,26 @@ intentionally re-using the real message's message ID.
706
707 Default: `false`
708
709 +### `Peering`
710 +
711 +Configures the peering subsystem. The peering subsystem configures go-ipfs to
712 +connect to, remain connected to, and reconnect to a set of peers. Peers should
713 +use this subsystem to create "sticky" links between frequently used peers for
714 +improved reliability.
715 +
716 +#### `Peering.Peers`
717 +
718 +The set of peers with which to peer. Each entry is of the form:
719 +
720 +```js
721 +{
722 + "ID": "QmSomePeerID", # The peers ID.
723 + "Addrs": ["/ip4/1.2.3.4/tcp/1234"] # Known addresses for the peer. If none are specified, the DHT will be queried.
724 +}
725 +```
726 +
727 +Additional fields may be added in the future.
728 +
729 ## `Reprovider`
730
731 ### `Reprovider.Interval`
go.mod
+2 -1
@@ -32,7 +32,7 @@ require (
32 github.com/ipfs/go-ipfs-blockstore v0.1.4
33 github.com/ipfs/go-ipfs-chunker v0.0.5
34 github.com/ipfs/go-ipfs-cmds v0.2.9
35 - github.com/ipfs/go-ipfs-config v0.6.1
35 + github.com/ipfs/go-ipfs-config v0.7.0
36 github.com/ipfs/go-ipfs-ds-help v0.1.1
37 github.com/ipfs/go-ipfs-exchange-interface v0.0.1
38 github.com/ipfs/go-ipfs-exchange-offline v0.0.1
@@ -94,6 +94,7 @@ require (
94 github.com/opentracing/opentracing-go v1.1.0
95 github.com/pkg/errors v0.9.1
96 github.com/prometheus/client_golang v1.6.0
97 + github.com/stretchr/testify v1.5.1
98 github.com/syndtr/goleveldb v1.0.0
99 github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc
100 github.com/whyrusleeping/go-sysinfo v0.0.0-20190219211824-4a357d4b90b1
go.sum
+2 -2
@@ -301,8 +301,8 @@ github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7Na
301 github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8=
302 github.com/ipfs/go-ipfs-cmds v0.2.9 h1:zQTENe9UJrtCb2bOtRoDGjtuo3rQjmuPdPnVlqoBV/M=
303 github.com/ipfs/go-ipfs-cmds v0.2.9/go.mod h1:ZgYiWVnCk43ChwoH8hAmI1IRbuVtq3GSTHwtRB/Kqhk=
304 -github.com/ipfs/go-ipfs-config v0.6.1 h1:d1f0fEEpUQ9R+6c0VZMNy2P+wCl4K4DO4VHJBvgWwFw=
305 -github.com/ipfs/go-ipfs-config v0.6.1/go.mod h1:GQUxqb0NfkZmEU92PxqqqLVVFTLpoGGUlBaTyDaAqrE=
304 +github.com/ipfs/go-ipfs-config v0.7.0 h1:cClINg8v28//KaYMwt1aSjbS8eGJjNKIEnahpT/2hYk=
305 +github.com/ipfs/go-ipfs-config v0.7.0/go.mod h1:GQUxqb0NfkZmEU92PxqqqLVVFTLpoGGUlBaTyDaAqrE=
306 github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
307 github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
308 github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
peering/peering.go new
+259
@@ -0,0 +1,259 @@
1 +package peering
2 +
3 +import (
4 + "context"
5 + "errors"
6 + "math/rand"
7 + "sync"
8 + "time"
9 +
10 + "github.com/ipfs/go-log"
11 + "github.com/libp2p/go-libp2p-core/host"
12 + "github.com/libp2p/go-libp2p-core/network"
13 + "github.com/libp2p/go-libp2p-core/peer"
14 + "github.com/multiformats/go-multiaddr"
15 +)
16 +
17 +// maxBackoff is the maximum time between reconnect attempts.
18 +const (
19 + maxBackoff = 10 * time.Minute
20 + connmgrTag = "ipfs-peering"
21 + // This needs to be sufficient to prevent two sides from simultaneously
22 + // dialing.
23 + initialDelay = 5 * time.Second
24 +)
25 +
26 +var logger = log.Logger("peering")
27 +
28 +type state int
29 +
30 +const (
31 + stateInit state = iota
32 + stateRunning
33 + stateStopped
34 +)
35 +
36 +// peerHandler keeps track of all state related to a specific "peering" peer.
37 +type peerHandler struct {
38 + peer peer.ID
39 + host host.Host
40 + ctx context.Context
41 + cancel context.CancelFunc
42 +
43 + mu sync.Mutex
44 + addrs []multiaddr.Multiaddr
45 + timer *time.Timer
46 +
47 + nextDelay time.Duration
48 +}
49 +
50 +func (ph *peerHandler) stop() {
51 + ph.mu.Lock()
52 + defer ph.mu.Unlock()
53 +
54 + if ph.timer != nil {
55 + ph.timer.Stop()
56 + ph.timer = nil
57 + }
58 +}
59 +
60 +func (ph *peerHandler) nextBackoff() time.Duration {
61 + // calculate the timeout
62 + if ph.nextDelay < maxBackoff {
63 + ph.nextDelay += ph.nextDelay/2 + time.Duration(rand.Int63n(int64(ph.nextDelay)))
64 + }
65 + return ph.nextDelay
66 +}
67 +
68 +func (ph *peerHandler) reconnect() {
69 + // Try connecting
70 +
71 + ph.mu.Lock()
72 + addrs := append(([]multiaddr.Multiaddr)(nil), ph.addrs...)
73 + ph.mu.Unlock()
74 +
75 + logger.Debugw("reconnecting", "peer", ph.peer, "addrs", addrs)
76 +
77 + err := ph.host.Connect(ph.ctx, peer.AddrInfo{ID: ph.peer, Addrs: addrs})
78 + if err != nil {
79 + logger.Debugw("failed to reconnect", "peer", ph.peer, "error", err)
80 + // Ok, we failed. Extend the timeout.
81 + ph.mu.Lock()
82 + if ph.timer != nil {
83 + // Only counts if the timer still exists. If not, a
84 + // connection _was_ somehow established.
85 + ph.timer.Reset(ph.nextBackoff())
86 + }
87 + // Otherwise, someone else has stopped us so we can assume that
88 + // we're either connected or someone else will start us.
89 + ph.mu.Unlock()
90 + }
91 +
92 + // Always call this. We could have connected since we processed the
93 + // error.
94 + ph.stopIfConnected()
95 +}
96 +
97 +func (ph *peerHandler) stopIfConnected() {
98 + ph.mu.Lock()
99 + defer ph.mu.Unlock()
100 +
101 + if ph.timer != nil && ph.host.Network().Connectedness(ph.peer) == network.Connected {
102 + logger.Debugw("successfully reconnected", "peer", ph.peer)
103 + ph.timer.Stop()
104 + ph.timer = nil
105 + ph.nextDelay = initialDelay
106 + }
107 +}
108 +
109 +// startIfDisconnected is the inverse of stopIfConnected.
110 +func (ph *peerHandler) startIfDisconnected() {
111 + ph.mu.Lock()
112 + defer ph.mu.Unlock()
113 +
114 + if ph.timer == nil && ph.host.Network().Connectedness(ph.peer) != network.Connected {
115 + logger.Debugw("disconnected from peer", "peer", ph.peer)
116 + // Always start with a short timeout so we can stagger things a bit.
117 + ph.timer = time.AfterFunc(ph.nextBackoff(), ph.reconnect)
118 + }
119 +}
120 +
121 +// PeeringService maintains connections to specified peers, reconnecting on
122 +// disconnect with a back-off.
123 +type PeeringService struct {
124 + host host.Host
125 +
126 + mu sync.RWMutex
127 + peers map[peer.ID]*peerHandler
128 +
129 + ctx context.Context
130 + cancel context.CancelFunc
131 + state state
132 +}
133 +
134 +// NewPeeringService constructs a new peering service. Peers can be added and
135 +// removed immediately, but connections won't be formed until `Start` is called.
136 +func NewPeeringService(host host.Host) *PeeringService {
137 + ps := &PeeringService{host: host, peers: make(map[peer.ID]*peerHandler)}
138 + ps.ctx, ps.cancel = context.WithCancel(context.Background())
139 + return ps
140 +}
141 +
142 +// Start starts the peering service, connecting and maintaining connections to
143 +// all registered peers. It returns an error if the service has already been
144 +// stopped.
145 +func (ps *PeeringService) Start() error {
146 + ps.mu.Lock()
147 + defer ps.mu.Unlock()
148 +
149 + switch ps.state {
150 + case stateInit:
151 + logger.Infow("starting")
152 + case stateRunning:
153 + return nil
154 + case stateStopped:
155 + return errors.New("already stopped")
156 + }
157 + ps.host.Network().Notify((*netNotifee)(ps))
158 + ps.state = stateRunning
159 + for _, handler := range ps.peers {
160 + go handler.startIfDisconnected()
161 + }
162 + return nil
163 +}
164 +
165 +// Stop stops the peering service.
166 +func (ps *PeeringService) Stop() error {
167 + ps.cancel()
168 + ps.host.Network().StopNotify((*netNotifee)(ps))
169 +
170 + ps.mu.Lock()
171 + defer ps.mu.Unlock()
172 +
173 + if ps.state == stateRunning {
174 + logger.Infow("stopping")
175 + for _, handler := range ps.peers {
176 + handler.stop()
177 + }
178 + }
179 + return nil
180 +}
181 +
182 +// AddPeer adds a peer to the peering service. This function may be safely
183 +// called at any time: before the service is started, while running, or after it
184 +// stops.
185 +//
186 +// Add peer may also be called multiple times for the same peer. The new
187 +// addresses will replace the old.
188 +func (ps *PeeringService) AddPeer(info peer.AddrInfo) {
189 + ps.mu.Lock()
190 + defer ps.mu.Unlock()
191 +
192 + if handler, ok := ps.peers[info.ID]; ok {
193 + logger.Infow("updating addresses", "peer", info.ID, "addrs", info.Addrs)
194 + handler.addrs = info.Addrs
195 + } else {
196 + logger.Infow("peer added", "peer", info.ID, "addrs", info.Addrs)
197 + ps.host.ConnManager().Protect(info.ID, connmgrTag)
198 +
199 + handler = &peerHandler{
200 + host: ps.host,
201 + peer: info.ID,
202 + addrs: info.Addrs,
203 + nextDelay: initialDelay,
204 + }
205 + handler.ctx, handler.cancel = context.WithCancel(ps.ctx)
206 + ps.peers[info.ID] = handler
207 + if ps.state == stateRunning {
208 + go handler.startIfDisconnected()
209 + }
210 + }
211 +}
212 +
213 +// RemovePeer removes a peer from the peering service. This function may be
214 +// safely called at any time: before the service is started, while running, or
215 +// after it stops.
216 +func (ps *PeeringService) RemovePeer(id peer.ID) {
217 + ps.mu.Lock()
218 + defer ps.mu.Unlock()
219 +
220 + if handler, ok := ps.peers[id]; ok {
221 + logger.Infow("peer removed", "peer", id)
222 + ps.host.ConnManager().Unprotect(id, connmgrTag)
223 +
224 + handler.stop()
225 + handler.cancel()
226 + delete(ps.peers, id)
227 + }
228 +}
229 +
230 +type netNotifee PeeringService
231 +
232 +func (nn *netNotifee) Connected(_ network.Network, c network.Conn) {
233 + ps := (*PeeringService)(nn)
234 +
235 + p := c.RemotePeer()
236 + ps.mu.RLock()
237 + defer ps.mu.RUnlock()
238 +
239 + if handler, ok := ps.peers[p]; ok {
240 + // use a goroutine to avoid blocking events.
241 + go handler.stopIfConnected()
242 + }
243 +}
244 +func (nn *netNotifee) Disconnected(_ network.Network, c network.Conn) {
245 + ps := (*PeeringService)(nn)
246 +
247 + p := c.RemotePeer()
248 + ps.mu.RLock()
249 + defer ps.mu.RUnlock()
250 +
251 + if handler, ok := ps.peers[p]; ok {
252 + // use a goroutine to avoid blocking events.
253 + go handler.startIfDisconnected()
254 + }
255 +}
256 +func (nn *netNotifee) OpenedStream(network.Network, network.Stream) {}
257 +func (nn *netNotifee) ClosedStream(network.Network, network.Stream) {}
258 +func (nn *netNotifee) Listen(network.Network, multiaddr.Multiaddr) {}
259 +func (nn *netNotifee) ListenClose(network.Network, multiaddr.Multiaddr) {}
peering/peering_test.go new
+6
@@ -0,0 +1,6 @@
1 +package peering
2 +
3 +import "testing"
4 +
5 +func TestPeeringService(t *testing.T) {
6 +}
test/sharness/t0171-peering.sh new
+127
@@ -0,0 +1,127 @@
1 +#!/usr/bin/env bash
2 +
3 +test_description="Test peering service"
4 +
5 +. lib/test-lib.sh
6 +
7 +NUM_NODES=3
8 +
9 +test_expect_success 'init iptb' '
10 + rm -rf .iptb/ &&
11 + iptb testbed create -type localipfs -count $NUM_NODES -init
12 +'
13 +
14 +test_expect_success 'disabling routing' '
15 + iptb run -- ipfs config Routing.Type none
16 +'
17 +
18 +for i in $(seq 0 2); do
19 + ADDR="$(printf '["/ip4/127.0.0.1/tcp/%s"]' "$(( 3000 + ( RANDOM % 1000 ) ))")"
20 + test_expect_success "configuring node $i to listen on $ADDR" '
21 + ipfsi "$i" config --json Addresses.Swarm "$ADDR"
22 + '
23 +done
24 +
25 +peer_id() {
26 + ipfsi "$1" config Identity.PeerID
27 +}
28 +
29 +peer_addrs() {
30 + ipfsi "$1" config Addresses.Swarm
31 +}
32 +
33 +peer() {
34 + PEER1="$1" &&
35 + PEER2="$2" &&
36 + PEER_LIST="$(ipfsi "$PEER1" config Peering.Peers)" &&
37 + { [[ "$PEER_LIST" == "null" ]] || PEER_LIST_INNER="${PEER_LIST:1:-1}"; } &&
38 + ADDR_INFO="$(printf '[%s{"ID": "%s", "Addrs": %s}]' \
39 + "${PEER_LIST_INNER:+${PEER_LIST_INNER},}" \
40 + "$(peer_id "$PEER2")" \
41 + "$(peer_addrs "$PEER2")")" &&
42 + ipfsi "$PEER1" config --json Peering.Peers "${ADDR_INFO}"
43 +}
44 +
45 +# Peer:
46 +# - 0 <-> 1
47 +# - 1 -> 2
48 +test_expect_success 'configure peering' '
49 + peer 0 1 &&
50 + peer 1 0 &&
51 + peer 1 2
52 +'
53 +
54 +list_peers() {
55 + ipfsi "$1" swarm peers | sed 's|.*/p2p/\([^/]*\)$|\1|' | sort -u
56 +}
57 +
58 +check_peers() {
59 + sleep 20 # give it some time to settle.
60 + test_expect_success 'verifying peering for peer 0' '
61 + list_peers 0 > peers_0_actual &&
62 + peer_id 1 > peers_0_expected &&
63 + test_cmp peers_0_expected peers_0_actual
64 + '
65 +
66 + test_expect_success 'verifying peering for peer 1' '
67 + list_peers 1 > peers_1_actual &&
68 + { peer_id 0 && peer_id 2 ; } | sort -u > peers_1_expected &&
69 + test_cmp peers_1_expected peers_1_actual
70 + '
71 +
72 + test_expect_success 'verifying peering for peer 2' '
73 + list_peers 2 > peers_2_actual &&
74 + peer_id 1 > peers_2_expected &&
75 + test_cmp peers_2_expected peers_2_actual
76 + '
77 +}
78 +
79 +test_expect_success 'startup cluster' '
80 + iptb start -wait &&
81 + iptb run -- ipfs log level peering debug
82 +'
83 +
84 +check_peers
85 +
86 +disconnect() {
87 + ipfsi "$1" swarm disconnect "/p2p/$(peer_id "$2")"
88 +}
89 +
90 +# Bidiractional peering shouldn't cause problems (e.g., simultaneous connect
91 +# issues).
92 +test_expect_success 'disconnecting 0->1' '
93 + disconnect 0 1
94 +'
95 +
96 +check_peers
97 +
98 +# 1 should reconnect to 2 when 2 disconnects from 1.
99 +test_expect_success 'disconnecting 2->1' '
100 + disconnect 2 1
101 +'
102 +
103 +check_peers
104 +
105 +# 2 isn't peering. This test ensures that 1 will re-peer with 2 when it comes
106 +# back online.
107 +test_expect_success 'stopping 2' '
108 + iptb stop 2
109 +'
110 +
111 +# Wait to disconnect
112 +sleep 30
113 +
114 +test_expect_success 'starting 2' '
115 + iptb start 2
116 +'
117 +
118 +# Wait for backoff
119 +sleep 30
120 +
121 +check_peers
122 +
123 +test_expect_success "stop testbed" '
124 + iptb stop
125 +'
126 +
127 +test_done