Merge pull request #840 from karalabe/throttled-dialing
p2p: throttled handshakes
This commit is contained in:
commit
23454dcfcb
@ -242,6 +242,7 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso
|
||||
utils.JSpathFlag,
|
||||
utils.ListenPortFlag,
|
||||
utils.MaxPeersFlag,
|
||||
utils.MaxPendingPeersFlag,
|
||||
utils.EtherbaseFlag,
|
||||
utils.MinerThreadsFlag,
|
||||
utils.MiningEnabledFlag,
|
||||
|
@ -75,6 +75,7 @@ func init() {
|
||||
utils.LogFileFlag,
|
||||
utils.LogLevelFlag,
|
||||
utils.MaxPeersFlag,
|
||||
utils.MaxPendingPeersFlag,
|
||||
utils.MinerThreadsFlag,
|
||||
utils.NATFlag,
|
||||
utils.NodeKeyFileFlag,
|
||||
|
@ -197,6 +197,11 @@ var (
|
||||
Usage: "Maximum number of network peers (network disabled if set to 0)",
|
||||
Value: 16,
|
||||
}
|
||||
MaxPendingPeersFlag = cli.IntFlag{
|
||||
Name: "maxpendpeers",
|
||||
Usage: "Maximum number of pending connection attempts (defaults used if set to 0)",
|
||||
Value: 0,
|
||||
}
|
||||
ListenPortFlag = cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Network listening port",
|
||||
@ -292,6 +297,7 @@ func MakeEthConfig(clientID, version string, ctx *cli.Context) *eth.Config {
|
||||
AccountManager: GetAccountManager(ctx),
|
||||
VmDebug: ctx.GlobalBool(VMDebugFlag.Name),
|
||||
MaxPeers: ctx.GlobalInt(MaxPeersFlag.Name),
|
||||
MaxPendingPeers: ctx.GlobalInt(MaxPendingPeersFlag.Name),
|
||||
Port: ctx.GlobalString(ListenPortFlag.Name),
|
||||
NAT: GetNAT(ctx),
|
||||
NatSpec: ctx.GlobalBool(NatspecEnabledFlag.Name),
|
||||
|
@ -60,8 +60,9 @@ type Config struct {
|
||||
VmDebug bool
|
||||
NatSpec bool
|
||||
|
||||
MaxPeers int
|
||||
Port string
|
||||
MaxPeers int
|
||||
MaxPendingPeers int
|
||||
Port string
|
||||
|
||||
// Space-separated list of discovery node URLs
|
||||
BootNodes string
|
||||
@ -280,16 +281,17 @@ func New(config *Config) (*Ethereum, error) {
|
||||
protocols = append(protocols, eth.whisper.Protocol())
|
||||
}
|
||||
eth.net = &p2p.Server{
|
||||
PrivateKey: netprv,
|
||||
Name: config.Name,
|
||||
MaxPeers: config.MaxPeers,
|
||||
Protocols: protocols,
|
||||
NAT: config.NAT,
|
||||
NoDial: !config.Dial,
|
||||
BootstrapNodes: config.parseBootNodes(),
|
||||
StaticNodes: config.parseNodes(staticNodes),
|
||||
TrustedNodes: config.parseNodes(trustedNodes),
|
||||
NodeDatabase: nodeDb,
|
||||
PrivateKey: netprv,
|
||||
Name: config.Name,
|
||||
MaxPeers: config.MaxPeers,
|
||||
MaxPendingPeers: config.MaxPendingPeers,
|
||||
Protocols: protocols,
|
||||
NAT: config.NAT,
|
||||
NoDial: !config.Dial,
|
||||
BootstrapNodes: config.parseBootNodes(),
|
||||
StaticNodes: config.parseNodes(staticNodes),
|
||||
TrustedNodes: config.parseNodes(trustedNodes),
|
||||
NodeDatabase: nodeDb,
|
||||
}
|
||||
if len(config.Port) > 0 {
|
||||
eth.net.ListenAddr = ":" + config.Port
|
||||
|
@ -22,10 +22,11 @@ const (
|
||||
refreshPeersInterval = 30 * time.Second
|
||||
staticPeerCheckInterval = 15 * time.Second
|
||||
|
||||
// This is the maximum number of inbound connection
|
||||
// that are allowed to linger between 'accepted' and
|
||||
// 'added as peer'.
|
||||
maxAcceptConns = 50
|
||||
// Maximum number of concurrently handshaking inbound connections.
|
||||
maxAcceptConns = 10
|
||||
|
||||
// Maximum number of concurrently dialing outbound connections.
|
||||
maxDialingConns = 10
|
||||
|
||||
// total timeout for encryption handshake and protocol
|
||||
// handshake in both directions.
|
||||
@ -52,6 +53,11 @@ type Server struct {
|
||||
// connected. It must be greater than zero.
|
||||
MaxPeers int
|
||||
|
||||
// MaxPendingPeers is the maximum number of peers that can be pending in the
|
||||
// handshake phase, counted separately for inbound and outbound connections.
|
||||
// Zero defaults to preset values.
|
||||
MaxPendingPeers int
|
||||
|
||||
// Name sets the node name of this server.
|
||||
// Use common.MakeName to create a name that follows existing conventions.
|
||||
Name string
|
||||
@ -331,8 +337,12 @@ func (srv *Server) listenLoop() {
|
||||
// This channel acts as a semaphore limiting
|
||||
// active inbound connections that are lingering pre-handshake.
|
||||
// If all slots are taken, no further connections are accepted.
|
||||
slots := make(chan struct{}, maxAcceptConns)
|
||||
for i := 0; i < maxAcceptConns; i++ {
|
||||
tokens := maxAcceptConns
|
||||
if srv.MaxPendingPeers > 0 {
|
||||
tokens = srv.MaxPendingPeers
|
||||
}
|
||||
slots := make(chan struct{}, tokens)
|
||||
for i := 0; i < tokens; i++ {
|
||||
slots <- struct{}{}
|
||||
}
|
||||
|
||||
@ -401,7 +411,15 @@ func (srv *Server) dialLoop() {
|
||||
defer srv.loopWG.Done()
|
||||
defer refresh.Stop()
|
||||
|
||||
// TODO: maybe limit number of active dials
|
||||
// Limit the number of concurrent dials
|
||||
tokens := maxAcceptConns
|
||||
if srv.MaxPendingPeers > 0 {
|
||||
tokens = srv.MaxPendingPeers
|
||||
}
|
||||
slots := make(chan struct{}, tokens)
|
||||
for i := 0; i < tokens; i++ {
|
||||
slots <- struct{}{}
|
||||
}
|
||||
dial := func(dest *discover.Node) {
|
||||
// Don't dial nodes that would fail the checks in addPeer.
|
||||
// This is important because the connection handshake is a lot
|
||||
@ -413,11 +431,14 @@ func (srv *Server) dialLoop() {
|
||||
if !ok || dialing[dest.ID] {
|
||||
return
|
||||
}
|
||||
// Request a dial slot to prevent CPU exhaustion
|
||||
<-slots
|
||||
|
||||
dialing[dest.ID] = true
|
||||
srv.peerWG.Add(1)
|
||||
go func() {
|
||||
srv.dialNode(dest)
|
||||
slots <- struct{}{}
|
||||
dialed <- dest
|
||||
}()
|
||||
}
|
||||
|
@ -369,6 +369,136 @@ func TestServerTrustedPeers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that a failed dial will temporarily throttle a peer.
|
||||
func TestServerMaxPendingDials(t *testing.T) {
|
||||
defer testlog(t).detach()
|
||||
|
||||
// Start a simple test server
|
||||
server := &Server{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
PrivateKey: newkey(),
|
||||
MaxPeers: 10,
|
||||
MaxPendingPeers: 1,
|
||||
}
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatal("failed to start test server: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
// Simulate two separate remote peers
|
||||
peers := make(chan *discover.Node, 2)
|
||||
conns := make(chan net.Conn, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listener %d: failed to setup: %v", i, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
peers <- &discover.Node{
|
||||
ID: discover.PubkeyID(&newkey().PublicKey),
|
||||
IP: addr.IP,
|
||||
TCP: uint16(addr.Port),
|
||||
}
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
conns <- conn
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Request a dial for both peers
|
||||
go func() {
|
||||
for i := 0; i < 2; i++ {
|
||||
server.staticDial <- <-peers // hack piggybacking the static implementation
|
||||
}
|
||||
}()
|
||||
|
||||
// Make sure only one outbound connection goes through
|
||||
var conn net.Conn
|
||||
|
||||
select {
|
||||
case conn = <-conns:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("first dial timeout")
|
||||
}
|
||||
select {
|
||||
case conn = <-conns:
|
||||
t.Fatalf("second dial completed prematurely")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
// Finish the first dial, check the second
|
||||
conn.Close()
|
||||
select {
|
||||
case conn = <-conns:
|
||||
conn.Close()
|
||||
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("second dial timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerMaxPendingAccepts(t *testing.T) {
|
||||
defer testlog(t).detach()
|
||||
|
||||
// Start a test server and a peer sink for synchronization
|
||||
started := make(chan *Peer)
|
||||
server := &Server{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
PrivateKey: newkey(),
|
||||
MaxPeers: 10,
|
||||
MaxPendingPeers: 1,
|
||||
NoDial: true,
|
||||
newPeerHook: func(p *Peer) { started <- p },
|
||||
}
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatal("failed to start test server: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
// Try and connect to the server on multiple threads concurrently
|
||||
conns := make([]net.Conn, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
dialer := &net.Dialer{Deadline: time.Now().Add(3 * time.Second)}
|
||||
|
||||
conn, err := dialer.Dial("tcp", server.ListenAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial server: %v", err)
|
||||
}
|
||||
conns[i] = conn
|
||||
}
|
||||
// Check that a handshake on the second doesn't pass
|
||||
go func() {
|
||||
key := newkey()
|
||||
shake := &protoHandshake{Version: baseProtocolVersion, ID: discover.PubkeyID(&key.PublicKey)}
|
||||
if _, err := setupConn(conns[1], key, shake, server.Self(), false, server.trustedNodes); err != nil {
|
||||
t.Fatalf("failed to run handshake: %v", err)
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
t.Fatalf("handshake on second connection accepted")
|
||||
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
// Shake on first, check that both go through
|
||||
go func() {
|
||||
key := newkey()
|
||||
shake := &protoHandshake{Version: baseProtocolVersion, ID: discover.PubkeyID(&key.PublicKey)}
|
||||
if _, err := setupConn(conns[0], key, shake, server.Self(), false, server.trustedNodes); err != nil {
|
||||
t.Fatalf("failed to run handshake: %v", err)
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("peer %d: handshake timeout", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newkey() *ecdsa.PrivateKey {
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user