p2p: add ban peer feature

This commit is contained in:
rjl493456442 2020-03-12 16:19:20 +08:00
parent 0bdb21f0cb
commit 13712f52d8
4 changed files with 78 additions and 7 deletions

View file

@ -77,6 +77,7 @@ var (
errAlreadyDialing = errors.New("already dialing") errAlreadyDialing = errors.New("already dialing")
errAlreadyConnected = errors.New("already connected") errAlreadyConnected = errors.New("already connected")
errRecentlyDialed = errors.New("recently dialed") errRecentlyDialed = errors.New("recently dialed")
errRecentlyBanned = errors.New("recently banned")
errNotWhitelisted = errors.New("not contained in netrestrict whitelist") errNotWhitelisted = errors.New("not contained in netrestrict whitelist")
) )
@ -102,6 +103,7 @@ type dialScheduler struct {
remStaticCh chan *enode.Node remStaticCh chan *enode.Node
addPeerCh chan *conn addPeerCh chan *conn
remPeerCh chan *conn remPeerCh chan *conn
banPeerCh chan *conn
// Everything below here belongs to loop and // Everything below here belongs to loop and
// should only be accessed by code on the loop goroutine. // should only be accessed by code on the loop goroutine.
@ -121,6 +123,9 @@ type dialScheduler struct {
historyTimer mclock.Timer historyTimer mclock.Timer
historyTimerTime mclock.AbsTime historyTimerTime mclock.AbsTime
// The bannedHistory keeps all recently banned nodes which won't be dialed for a short time
bannedHistory expHeap
// for logStats // for logStats
lastStatsLog mclock.AbsTime lastStatsLog mclock.AbsTime
doneSinceLastLog int doneSinceLastLog int
@ -172,6 +177,7 @@ func newDialScheduler(config dialConfig, it enode.Iterator, setupFunc dialSetupF
remStaticCh: make(chan *enode.Node), remStaticCh: make(chan *enode.Node),
addPeerCh: make(chan *conn), addPeerCh: make(chan *conn),
remPeerCh: make(chan *conn), remPeerCh: make(chan *conn),
banPeerCh: make(chan *conn),
} }
d.lastStatsLog = d.clock.Now() d.lastStatsLog = d.clock.Now()
d.ctx, d.cancel = context.WithCancel(context.Background()) d.ctx, d.cancel = context.WithCancel(context.Background())
@ -219,6 +225,14 @@ func (d *dialScheduler) peerRemoved(c *conn) {
} }
} }
// peerBanned notifies the dialer about banned peer
func (d *dialScheduler) peerBanned(c *conn) {
select {
case d.banPeerCh <- c:
case <-d.ctx.Done():
}
}
// loop is the main loop of the dialer. // loop is the main loop of the dialer.
func (d *dialScheduler) loop(it enode.Iterator) { func (d *dialScheduler) loop(it enode.Iterator) {
var ( var (
@ -273,6 +287,14 @@ loop:
delete(d.peers, c.node.ID()) delete(d.peers, c.node.ID())
d.updateStaticPool(c.node.ID()) d.updateStaticPool(c.node.ID())
case c := <-d.banPeerCh:
// If it's not a static node and been banned,
// add to the blacklist for dialing.
if d.static[c.node.ID()] == nil {
remoteIP := netutil.AddrIP(c.fd.RemoteAddr())
d.bannedHistory.add(remoteIP.String(), d.clock.Now().Add(banIPThrottleTime))
}
case node := <-d.addStaticCh: case node := <-d.addStaticCh:
id := node.ID() id := node.ID()
_, exists := d.static[id] _, exists := d.static[id]
@ -400,6 +422,10 @@ func (d *dialScheduler) checkDial(n *enode.Node) error {
if d.history.contains(string(n.ID().Bytes())) { if d.history.contains(string(n.ID().Bytes())) {
return errRecentlyDialed return errRecentlyDialed
} }
d.bannedHistory.expire(d.clock.Now(), nil)
if !n.Incomplete() && !netutil.IsLAN(n.IP()) && d.bannedHistory.contains(n.IP().String()) {
return errRecentlyBanned
}
return nil return nil
} }

View file

@ -113,6 +113,8 @@ type Peer struct {
protoErr chan error protoErr chan error
closed chan struct{} closed chan struct{}
disc chan DiscReason disc chan DiscReason
banOnce sync.Once
ban chan struct{}
// events receives message send / receive events if set // events receives message send / receive events if set
events *event.Feed events *event.Feed
@ -168,6 +170,14 @@ func (p *Peer) Disconnect(reason DiscReason) {
} }
} }
// Ban terminates the peer connection if the peer acts really bad.
// The banned peer will be forbidden for re-connection in a short time(5 mins).
func (p *Peer) Ban() {
p.banOnce.Do(func() {
close(p.ban)
})
}
// String implements fmt.Stringer. // String implements fmt.Stringer.
func (p *Peer) String() string { func (p *Peer) String() string {
id := p.ID() id := p.ID()
@ -186,6 +196,7 @@ func newPeer(log log.Logger, conn *conn, protocols []Protocol) *Peer {
running: protomap, running: protomap,
created: mclock.Now(), created: mclock.Now(),
disc: make(chan DiscReason), disc: make(chan DiscReason),
ban: make(chan struct{}),
protoErr: make(chan error, len(protomap)+1), // protocols + pingLoop protoErr: make(chan error, len(protomap)+1), // protocols + pingLoop
closed: make(chan struct{}), closed: make(chan struct{}),
log: log.New("id", conn.node.ID(), "conn", conn.flags), log: log.New("id", conn.node.ID(), "conn", conn.flags),
@ -197,7 +208,7 @@ func (p *Peer) Log() log.Logger {
return p.log return p.log
} }
func (p *Peer) run() (remoteRequested bool, err error) { func (p *Peer) run() (remoteRequested bool, banned bool, err error) {
var ( var (
writeStart = make(chan struct{}, 1) writeStart = make(chan struct{}, 1)
writeErr = make(chan error, 1) writeErr = make(chan error, 1)
@ -238,13 +249,24 @@ loop:
case err = <-p.disc: case err = <-p.disc:
reason = discReasonForError(err) reason = discReasonForError(err)
break loop break loop
case <-p.ban:
banned = true
break loop
} }
} }
close(p.closed) close(p.closed)
// If the peer is banned by up upper level protocol, just send the
// DiscRequested to peer. The reason is:
// - The banned disconnection reason is not yet defined
// - It's might unnecessary to tell the adversary the specified reason for
// disconnection.
if banned {
reason = DiscRequested
}
p.rw.close(reason) p.rw.close(reason)
p.wg.Wait() p.wg.Wait()
return remoteRequested, err return remoteRequested, banned, err
} }
func (p *Peer) pingLoop() { func (p *Peer) pingLoop() {

View file

@ -97,7 +97,7 @@ func testPeer(protos []Protocol) (func(), *conn, *Peer, <-chan error) {
peer := newPeer(log.Root(), c1, protos) peer := newPeer(log.Root(), c1, protos)
errc := make(chan error, 1) errc := make(chan error, 1)
go func() { go func() {
_, err := peer.run() _, _, err := peer.run()
errc <- err errc <- err
}() }()

View file

@ -57,6 +57,10 @@ const (
// This time limits inbound connection attempts per source IP. // This time limits inbound connection attempts per source IP.
inboundThrottleTime = 30 * time.Second inboundThrottleTime = 30 * time.Second
// This time limits inbound connection attempts per source IP
// when the IP is banned.
banIPThrottleTime = 5 * time.Minute
// Maximum time allowed for reading a complete message. // Maximum time allowed for reading a complete message.
// This is effectively the amount of time a connection can be idle. // This is effectively the amount of time a connection can be idle.
frameReadTimeout = 30 * time.Second frameReadTimeout = 30 * time.Second
@ -198,6 +202,9 @@ type Server struct {
// State of run loop and listenLoop. // State of run loop and listenLoop.
inboundHistory expHeap inboundHistory expHeap
bannedLock sync.RWMutex // protects bannedHistory
bannedHistory expHeap
} }
type peerOpFunc func(map[enode.ID]*Peer) type peerOpFunc func(map[enode.ID]*Peer)
@ -206,6 +213,7 @@ type peerDrop struct {
*Peer *Peer
err error err error
requested bool // true if signaled by the peer requested bool // true if signaled by the peer
banned bool // true if the peer is banned
} }
type connFlag int32 type connFlag int32
@ -770,6 +778,14 @@ running:
if pd.Inbound() { if pd.Inbound() {
inboundCount-- inboundCount--
} }
// Ban the IP if a peer is marked as bad peer.
if pd.banned {
remoteIP := netutil.AddrIP(pd.rw.fd.RemoteAddr())
srv.bannedLock.Lock()
srv.bannedHistory.add(remoteIP.String(), srv.clock.Now().Add(banIPThrottleTime))
srv.bannedLock.Unlock()
srv.dialsched.peerBanned(pd.rw)
}
} }
} }
@ -896,8 +912,15 @@ func (srv *Server) checkInboundConn(fd net.Conn, remoteIP net.IP) error {
if srv.NetRestrict != nil && !srv.NetRestrict.Contains(remoteIP) { if srv.NetRestrict != nil && !srv.NetRestrict.Contains(remoteIP) {
return fmt.Errorf("not whitelisted in NetRestrict") return fmt.Errorf("not whitelisted in NetRestrict")
} }
// Reject Internet peers that try too often. // Reject Internet peers if they are marked as bad.
now := srv.clock.Now() now := srv.clock.Now()
srv.bannedLock.Lock()
defer srv.bannedLock.Unlock()
srv.bannedHistory.expire(now, nil)
if !netutil.IsLAN(remoteIP) && srv.bannedHistory.contains(remoteIP.String()) {
return fmt.Errorf("bad peer")
}
// Reject Internet peers that try too often.
srv.inboundHistory.expire(now, nil) srv.inboundHistory.expire(now, nil)
if !netutil.IsLAN(remoteIP) && srv.inboundHistory.contains(remoteIP.String()) { if !netutil.IsLAN(remoteIP) && srv.inboundHistory.contains(remoteIP.String()) {
return fmt.Errorf("too many attempts") return fmt.Errorf("too many attempts")
@ -1032,12 +1055,12 @@ func (srv *Server) runPeer(p *Peer) {
}) })
// Run the per-peer main loop. // Run the per-peer main loop.
remoteRequested, err := p.run() remoteRequested, banned, err := p.run()
// Announce disconnect on the main loop to update the peer set. // Announce disconnect on the main loop to update the peer set.
// The main loop waits for existing peers to be sent on srv.delpeer // The main loop waits for existing peers to be sent on srv.delpeer
// before returning, so this send should not select on srv.quit. // before returning, so this send should not select on srv.quit.
srv.delpeer <- peerDrop{p, err, remoteRequested} srv.delpeer <- peerDrop{p, err, remoteRequested, banned}
// Broadcast peer drop to external subscribers. This needs to be // Broadcast peer drop to external subscribers. This needs to be
// after the send to delpeer so subscribers have a consistent view of // after the send to delpeer so subscribers have a consistent view of