From 88f1cc0c98c9bf6b1b091ea63f55c4cc0b547389 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 20 Feb 2020 15:14:24 -0800 Subject: [PATCH] derp, cmd/derper: add rate limiting support, add default 5Mbps limit Signed-off-by: Brad Fitzpatrick --- cmd/derper/derper.go | 12 ++++++++---- derp/derp.go | 9 +++++---- derp/derp_client.go | 2 +- derp/derp_server.go | 24 +++++++++++++++++++++--- go.mod | 1 + go.sum | 2 ++ 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/cmd/derper/derper.go b/cmd/derper/derper.go index 7fd7c4df2..4ce529cff 100644 --- a/cmd/derper/derper.go +++ b/cmd/derper/derper.go @@ -25,10 +25,11 @@ import ( ) var ( - addr = flag.String("a", ":443", "server address") - configPath = flag.String("c", "", "config file path") - certDir = flag.String("certdir", defaultCertDir(), "directory to store LetsEncrypt certs, if addr's port is :443") - hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443") + addr = flag.String("a", ":443", "server address") + configPath = flag.String("c", "", "config file path") + certDir = flag.String("certdir", defaultCertDir(), "directory to store LetsEncrypt certs, if addr's port is :443") + hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443") + bytesPerSec = flag.Int("mbps", 5, "Mbps (mebibit/s) per-client rate limit; 0 means unlimited") ) func defaultCertDir() string { @@ -96,6 +97,9 @@ func main() { } s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf) + if *bytesPerSec != 0 { + s.BytesPerSecond = (*bytesPerSec << 20) / 8 + } mux := http.NewServeMux() mux.Handle("/derp", derphttp.Handler(s)) diff --git a/derp/derp.go b/derp/derp.go index 8209a8d78..aa8b04f9d 100644 --- a/derp/derp.go +++ b/derp/derp.go @@ -27,10 +27,11 @@ import ( const magic = "DERP🔑" // 8 bytes: 0x44 45 52 50 f0 9f 94 91 const ( - nonceLen = 24 - keyLen = 32 - maxInfoLen = 1 << 20 - keepAlive = 60 * time.Second + nonceLen = 24 + keyLen = 32 + maxInfoLen = 1 << 20 + keepAlive = 60 * time.Second + maxPacketData = 64 << 10 ) // frameType is the one byte frame type at the beginning of the frame diff --git a/derp/derp_client.go b/derp/derp_client.go index ff726bb1b..0c3468c88 100644 --- a/derp/derp_client.go +++ b/derp/derp_client.go @@ -131,7 +131,7 @@ func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) { } }() - if len(pkt) > 64<<10 { + if len(pkt) > maxPacketData { return fmt.Errorf("packet too big: %d", len(pkt)) } diff --git a/derp/derp_server.go b/derp/derp_server.go index 53d0bc262..60006b418 100644 --- a/derp/derp_server.go +++ b/derp/derp_server.go @@ -21,12 +21,17 @@ import ( "time" "golang.org/x/crypto/nacl/box" + "golang.org/x/time/rate" "tailscale.com/types/key" "tailscale.com/types/logger" ) // Server is a DERP server. type Server struct { + // BytesPerSecond, if non-zero, specifies how many bytes per + // second to cap per-client reads at. + BytesPerSecond int + privateKey key.Private publicKey key.Public logf logger.Logf @@ -179,6 +184,13 @@ func (s *Server) accept(nc net.Conn, brw *bufio.ReadWriter) error { defer cancel() go s.sendClientKeepAlives(ctx, c) + lim := rate.Inf + if s.BytesPerSecond != 0 { + lim = rate.Limit(s.BytesPerSecond) + } + const burstBytes = 1 << 20 // generous bandwidth delay product? must be over 64k max packet size. + limiter := rate.NewLimiter(lim, burstBytes) + for { ft, fl, err := readFrameHeader(c.br) if err != nil { @@ -188,8 +200,7 @@ func (s *Server) accept(nc net.Conn, brw *bufio.ReadWriter) error { // TODO: nothing else yet supported return fmt.Errorf("client %x: unsupported frame %v", c.key, ft) } - - dstKey, contents, err := s.recvPacket(c.br, fl) + dstKey, contents, err := s.recvPacket(ctx, c.br, fl, limiter) if err != nil { return fmt.Errorf("client %x: recvPacket: %v", c.key, err) } @@ -229,6 +240,7 @@ func (s *Server) sendClientKeepAlives(ctx context.Context, c *sclient) { func (s *Server) verifyClient(clientKey key.Public, info *sclientInfo) error { // TODO(crawshaw): implement policy constraints on who can use the DERP server + // TODO(bradfitz): ... and at what rate. return nil } @@ -308,7 +320,7 @@ func (s *Server) sendPacket(bw *bufio.Writer, srcKey key.Public, contents []byte return bw.Flush() } -func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Public, contents []byte, err error) { +func (s *Server) recvPacket(ctx context.Context, br *bufio.Reader, frameLen uint32, limiter *rate.Limiter) (dstKey key.Public, contents []byte, err error) { if frameLen < keyLen { return key.Public{}, nil, errors.New("short send packet frame") } @@ -316,6 +328,12 @@ func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Publi return key.Public{}, nil, err } packetLen := frameLen - keyLen + if packetLen > maxPacketData { + return key.Public{}, nil, fmt.Errorf("data packet longer (%d) than max of %v", packetLen, maxPacketData) + } + if err := limiter.WaitN(ctx, int(packetLen)); err != nil { + return key.Public{}, nil, fmt.Errorf("rate limit: %v", err) + } contents = make([]byte, packetLen) if _, err := io.ReadFull(br, contents); err != nil { return key.Public{}, nil, err diff --git a/go.mod b/go.mod index 40eddb31d..91ead36d6 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20200217220822-9197077df867 + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 gortc.io/stun v1.22.1 honnef.co/go/tools v0.0.1-2019.2.3 // indirect rsc.io/goversion v1.2.0 diff --git a/go.sum b/go.sum index 96d305e83..76db0182e 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,8 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=