diff --git a/router/router.go b/router/router.go index 141dfc38..b498690b 100644 --- a/router/router.go +++ b/router/router.go @@ -16,16 +16,17 @@ import ( ) const ( - macMaxAge = 10 * time.Minute // [1] - tcpAcceptDelay = 1 * time.Millisecond // [2] + macMaxAge = 10 * time.Minute // [1] + acceptMaxTokens = 100 // [2] + acceptTokenDelay = 100 * time.Millisecond // [3] ) // [1] should be greater than typical ARP cache expiries, i.e. > 3/2 * // /proc/sys/net/ipv4_neigh/*/base_reachable_time_ms on Linux -// [2] time to wait between accepting tcp connections. This guards -// against brute-force attacks on the password when encryption is in -// use. It is also a basic DoS defence. +// [2] capacity of token bucket for rate limiting accepts + +// [3] control rate at which new tokens are added to the bucket type LogFrameFunc func(string, []byte, *layers.Ethernet) @@ -50,6 +51,7 @@ type Router struct { gossipChannels GossipChannels TopologyGossip Gossip UDPListener *net.UDPConn + acceptLimiter *TokenBucket } type PacketSource interface { @@ -81,6 +83,7 @@ func NewRouter(config Config, name PeerName, nickName string) *Router { router.Routes = NewRoutes(router.Ourself, router.Peers) router.ConnectionMaker = NewConnectionMaker(router.Ourself, router.Peers, router.Port, router.PeerDiscovery) router.TopologyGossip = router.NewGossip("topology", router) + router.acceptLimiter = NewTokenBucket(acceptMaxTokens, acceptTokenDelay) return router } @@ -212,7 +215,7 @@ func (router *Router) listenTCP(localPort int) { continue } router.acceptTCP(tcpConn) - time.Sleep(tcpAcceptDelay) + router.acceptLimiter.Wait() } }() } diff --git a/router/token_bucket.go b/router/token_bucket.go new file mode 100644 index 00000000..254de6eb --- /dev/null +++ b/router/token_bucket.go @@ -0,0 +1,42 @@ +package router + +import ( + "time" +) + +type TokenBucket struct { + capacity int64 // Maximum capacity of bucket + tokenInterval time.Duration // Token replenishment rate + refillDuration time.Duration // Time to refill from empty + earliestUnspentToken time.Time +} + +func NewTokenBucket(capacity int64, tokenInterval time.Duration) *TokenBucket { + tb := TokenBucket{ + capacity: capacity, + tokenInterval: tokenInterval, + refillDuration: tokenInterval * time.Duration(capacity)} + + tb.earliestUnspentToken = tb.capacityToken() + + return &tb +} + +func (tb *TokenBucket) Wait() { + // If earliest unspent token is in the future, sleep until then + time.Sleep(tb.earliestUnspentToken.Sub(time.Now())) + + // Alternatively, enforce bucket capacity if necessary + capacityToken := tb.capacityToken() + if tb.earliestUnspentToken.Before(capacityToken) { + tb.earliestUnspentToken = capacityToken + } + + // 'Remove' a token from the bucket + tb.earliestUnspentToken = tb.earliestUnspentToken.Add(tb.tokenInterval) +} + +// Determine the historic token timestamp representing a full bucket +func (tb *TokenBucket) capacityToken() time.Time { + return time.Now().Add(-tb.refillDuration).Truncate(tb.tokenInterval) +} diff --git a/site/how-it-works.md b/site/how-it-works.md index 4e570c7c..70b97ff1 100644 --- a/site/how-it-works.md +++ b/site/how-it-works.md @@ -278,7 +278,7 @@ password, and the result is hashed through SHA256, to form the final ephemeral session key. Thus the supplied password is never exchanged directly, and is thoroughly mixed into the shared secret. Furthermore, the rate at which TCP connections are accepted is limited by weave to -1kHz, which twarts online dictionary attacks on reasonably strong +10Hz, which twarts online dictionary attacks on reasonably strong passwords. The shared key formed by Diffie-Hellman is 256 bits long, appending @@ -293,6 +293,39 @@ not be able to form valid ephemeral session keys. The same ephemeral session key is used for both TCP and UDP traffic between two peers. + Generating fresh keys for every connection +provides forward secrecy at the cost of placing a demand on the Linux +CSPRNG (accessed by `GenerateKey` via `/dev/urandom`) proportional to +the number of inbound connection attempts. Weave has accept throttling +to mitigate against denial of service attacks that seek to deplete the +CSPRNG entropy pool, however even at the lower bound of ten requests +per second there may not be enough entropy gathered on a headless +system to keep pace. + +Under such conditions, the consequences will be limited to slowing +down processes reading from the blocking `/dev/random` device as the +kernel waits for enough new entropy to be harvested. It is important +to note that contrary to intuition this low entropy state does not +compromise the ongoing use of `/dev/urandom` - [expert +opinion](http://blog.cr.yp.to/20140205-entropy.html) +asserts that as long as the CSPRNG is seeded with enough entropy (e.g. +256 bits) before random number generation commences then the output is +entirely safe for use as key material. + +By way of comparison, this is exactly how OpenSSL works - it reads 256 +bits of entropy at startup, and uses that to seed an internal CSPRNG +which is used thenceforth to generate keys. Whilst we could have taken +the same approach and built our own CSPRNG to work around the +potential `/dev/random` blocking issue, we thought it was much more +prudent to rely on the [heavily +scrutinised](http://eprint.iacr.org/2012/251.pdf) Linux random number +generator as [advised +here](http://cr.yp.to/highspeed/coolnacl-20120725.pdf) (page 10, +'Centralizing randomness'). The aforementioned notwithstanding, if +weave's demand on `/dev/urandom` is causing you problems with blocking +`/dev/random` reads, please get in touch with us - we'd love to hear +about your use case. + #### TCP TCP connection are only used to exchange topology information between