github.com/avahowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/gateway/peers.go (about) 1 package gateway 2 3 import ( 4 "errors" 5 "net" 6 "time" 7 8 "github.com/NebulousLabs/Sia/build" 9 "github.com/NebulousLabs/Sia/crypto" 10 "github.com/NebulousLabs/Sia/encoding" 11 "github.com/NebulousLabs/Sia/modules" 12 "github.com/NebulousLabs/muxado" 13 ) 14 15 const ( 16 // the gateway will abort a connection attempt after this long 17 dialTimeout = 2 * time.Minute 18 // the gateway will not accept inbound connections above this threshold 19 fullyConnectedThreshold = 128 20 // the gateway will ask for more addresses below this threshold 21 minNodeListLen = 100 22 ) 23 24 var ( 25 // The gateway will sleep this long between incoming connections. 26 acceptInterval = func() time.Duration { 27 switch build.Release { 28 case "dev": 29 return 3 * time.Second 30 case "standard": 31 return 3 * time.Second 32 case "testing": 33 return 10 * time.Millisecond 34 default: 35 panic("unrecognized build.Release") 36 } 37 }() 38 39 errPeerRejectedConn = errors.New("peer rejected connection") 40 ) 41 42 // insufficientVersionError indicates a peer's version is insufficient. 43 type insufficientVersionError string 44 45 // Error implements the error interface for insufficientVersionError. 46 func (s insufficientVersionError) Error() string { 47 return "unacceptable version: " + string(s) 48 } 49 50 type peer struct { 51 modules.Peer 52 sess muxado.Session 53 } 54 55 func (p *peer) open() (modules.PeerConn, error) { 56 conn, err := p.sess.Open() 57 if err != nil { 58 return nil, err 59 } 60 return &peerConn{conn}, nil 61 } 62 63 func (p *peer) accept() (modules.PeerConn, error) { 64 conn, err := p.sess.Accept() 65 if err != nil { 66 return nil, err 67 } 68 return &peerConn{conn}, nil 69 } 70 71 // addPeer adds a peer to the Gateway's peer list and spawns a listener thread 72 // to handle its requests. 73 func (g *Gateway) addPeer(p *peer) { 74 g.peers[p.NetAddress] = p 75 go g.listenPeer(p) 76 } 77 78 // randomPeer returns a random peer from the gateway's peer list. 79 func (g *Gateway) randomPeer() (modules.NetAddress, error) { 80 if len(g.peers) > 0 { 81 r, _ := crypto.RandIntn(len(g.peers)) 82 for addr := range g.peers { 83 if r <= 0 { 84 return addr, nil 85 } 86 r-- 87 } 88 } 89 90 return "", errNoPeers 91 } 92 93 // randomInboundPeer returns a random peer that initiated its connection. 94 func (g *Gateway) randomInboundPeer() (modules.NetAddress, error) { 95 if len(g.peers) > 0 { 96 r, _ := crypto.RandIntn(len(g.peers)) 97 for addr, p := range g.peers { 98 // only select inbound peers 99 if !p.Inbound { 100 continue 101 } 102 if r <= 0 { 103 return addr, nil 104 } 105 r-- 106 } 107 } 108 109 return "", errNoPeers 110 } 111 112 // listen handles incoming connection requests. If the connection is accepted, 113 // the peer will be added to the Gateway's peer list. 114 func (g *Gateway) listen() { 115 for { 116 conn, err := g.listener.Accept() 117 if err != nil { 118 return 119 } 120 121 go g.acceptConn(conn) 122 123 // Sleep after each accept. This limits the rate at which the Gateway 124 // will accept new connections. The intent here is to prevent new 125 // incoming connections from kicking out old ones before they have a 126 // chance to request additional nodes. 127 time.Sleep(acceptInterval) 128 } 129 } 130 131 // acceptConn adds a connecting node as a peer. 132 func (g *Gateway) acceptConn(conn net.Conn) { 133 addr := modules.NetAddress(conn.RemoteAddr().String()) 134 g.log.Printf("INFO: %v wants to connect", addr) 135 136 // read version 137 var remoteVersion string 138 if err := encoding.ReadObject(conn, &remoteVersion, maxAddrLength); err != nil { 139 conn.Close() 140 g.log.Printf("INFO: %v wanted to connect, but we could not read their version: %v", addr, err) 141 return 142 } 143 144 // Check that version is acceptable. 145 // 146 // Reject peers < v0.4.0 as the previous version is v0.3.3 which is 147 // pre-hardfork. 148 // 149 // NOTE: this version must be bumped whenever the gateway or consensus 150 // breaks compatibility. 151 if build.VersionCmp(remoteVersion, "0.4.0") < 0 { 152 encoding.WriteObject(conn, "reject") 153 conn.Close() 154 g.log.Printf("INFO: %v wanted to connect, but their version (%v) was unacceptable", addr, remoteVersion) 155 return 156 } 157 158 // respond with our version 159 if err := encoding.WriteObject(conn, build.Version); err != nil { 160 conn.Close() 161 g.log.Printf("INFO: could not write version ack to %v: %v", addr, err) 162 return 163 } 164 165 // If we are already fully connected, kick out an old peer to make room 166 // for the new one. Importantly, prioritize kicking a peer with the same 167 // IP as the connecting peer. This protects against Sybil attacks. 168 id := g.mu.Lock() 169 if len(g.peers) >= fullyConnectedThreshold { 170 // first choose a random peer, preferably inbound. If have only 171 // outbound peers, we'll wind up kicking an outbound peer; but 172 // subsequent inbound connections will kick each other instead of 173 // continuing to replace outbound peers. 174 kick, err := g.randomInboundPeer() 175 if err != nil { 176 kick, _ = g.randomPeer() 177 } 178 // if another peer shares this IP, choose that one instead 179 for p := range g.peers { 180 if p.Host() == addr.Host() { 181 kick = p 182 break 183 } 184 } 185 g.peers[kick].sess.Close() 186 delete(g.peers, kick) 187 g.log.Printf("INFO: disconnected from %v to make room for %v", kick, addr) 188 } 189 // add the peer 190 g.addPeer(&peer{ 191 Peer: modules.Peer{ 192 NetAddress: addr, 193 Inbound: true, 194 Version: remoteVersion, 195 }, 196 sess: muxado.Server(conn), 197 }) 198 g.mu.Unlock(id) 199 200 g.log.Printf("INFO: accepted connection from new peer %v (v%v)", addr, remoteVersion) 201 } 202 203 // Connect establishes a persistent connection to a peer, and adds it to the 204 // Gateway's peer list. 205 func (g *Gateway) Connect(addr modules.NetAddress) error { 206 if addr == g.Address() { 207 return errors.New("can't connect to our own address") 208 } 209 if err := addr.IsValid(); err != nil { 210 return errors.New("can't connect to invalid address") 211 } 212 213 id := g.mu.RLock() 214 _, exists := g.peers[addr] 215 g.mu.RUnlock(id) 216 if exists { 217 return errors.New("peer already added") 218 } 219 220 conn, err := net.DialTimeout("tcp", string(addr), dialTimeout) 221 if err != nil { 222 return err 223 } 224 // send our version 225 if err := encoding.WriteObject(conn, build.Version); err != nil { 226 return err 227 } 228 // read version ack 229 var remoteVersion string 230 if err := encoding.ReadObject(conn, &remoteVersion, maxAddrLength); err != nil { 231 return err 232 } 233 // decide whether to accept this version 234 if remoteVersion == "reject" { 235 return errPeerRejectedConn 236 } 237 // Check that version is acceptable. 238 // 239 // Reject peers < v0.4.0 as the previous version is v0.3.3 which is 240 // pre-hardfork. 241 // 242 // NOTE: this version must be bumped whenever the gateway or consensus 243 // breaks compatibility. 244 if build.VersionCmp(remoteVersion, "0.4.0") < 0 { 245 conn.Close() 246 return insufficientVersionError(remoteVersion) 247 } 248 249 g.log.Println("INFO: connected to new peer", addr) 250 251 id = g.mu.Lock() 252 g.addPeer(&peer{ 253 Peer: modules.Peer{ 254 NetAddress: addr, 255 Inbound: false, 256 Version: remoteVersion, 257 }, 258 sess: muxado.Client(conn), 259 }) 260 g.mu.Unlock(id) 261 262 // call initRPCs 263 id = g.mu.RLock() 264 for name, fn := range g.initRPCs { 265 go g.RPC(addr, name, fn) 266 } 267 g.mu.RUnlock(id) 268 269 return nil 270 } 271 272 // Disconnect terminates a connection to a peer and removes it from the 273 // Gateway's peer list. The peer's address remains in the node list. 274 func (g *Gateway) Disconnect(addr modules.NetAddress) error { 275 id := g.mu.RLock() 276 p, exists := g.peers[addr] 277 g.mu.RUnlock(id) 278 if !exists { 279 return errors.New("not connected to that node") 280 } 281 p.sess.Close() 282 id = g.mu.Lock() 283 delete(g.peers, addr) 284 g.mu.Unlock(id) 285 286 g.log.Println("INFO: disconnected from peer", addr) 287 return nil 288 } 289 290 // threadedPeerManager tries to keep the Gateway well-connected. As long as 291 // the Gateway is not well-connected, it tries to connect to random nodes. 292 func (g *Gateway) threadedPeerManager() { 293 for { 294 // If we are well-connected, sleep in increments of five minutes until 295 // we are no longer well-connected. 296 id := g.mu.RLock() 297 numOutboundPeers := 0 298 for _, p := range g.peers { 299 if !p.Inbound { 300 numOutboundPeers++ 301 } 302 } 303 addr, err := g.randomNode() 304 g.mu.RUnlock(id) 305 if numOutboundPeers >= modules.WellConnectedThreshold { 306 select { 307 case <-time.After(5 * time.Minute): 308 case <-g.closeChan: 309 return 310 } 311 continue 312 } 313 314 // Try to connect to a random node. Instead of blocking on Connect, we 315 // spawn a goroutine and sleep for five seconds. This allows us to 316 // continue making connections if the node is unresponsive. 317 if err == nil { 318 go g.Connect(addr) 319 } 320 select { 321 case <-time.After(5 * time.Second): 322 case <-g.closeChan: 323 return 324 } 325 } 326 } 327 328 // Peers returns the addresses currently connected to the Gateway. 329 func (g *Gateway) Peers() []modules.Peer { 330 id := g.mu.RLock() 331 defer g.mu.RUnlock(id) 332 var peers []modules.Peer 333 for _, p := range g.peers { 334 peers = append(peers, p.Peer) 335 } 336 return peers 337 }