Skip to content

Commit

Permalink
implement RPC rate limiting
Browse files Browse the repository at this point in the history
early proof-of-concept for providing pre-auth rate limiting at the Nomad
servers, with the ability to observe which users are creating the load.
  • Loading branch information
tgross committed Jun 24, 2022
1 parent cea3a3d commit 5597c57
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 8 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ require (
github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 // indirect
github.com/sethvargo/go-limiter v0.7.2 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d // indirect
github.com/stretchr/objx v0.2.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,8 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 h1:58E
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8=
github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
github.com/shirou/gopsutil v0.0.0-20181107111621-48177ef5f880/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil/v3 v3.21.12 h1:VoGxEW2hpmz0Vt3wUvHIl9fquzYLNpVpgNNB7pGJimA=
github.com/shirou/gopsutil/v3 v3.21.12/go.mod h1:BToYZVTlSVlfazpDDYFnsVZLaoRG+g8ufT6fPQLdJzA=
Expand Down
10 changes: 9 additions & 1 deletion nomad/namespace_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,26 @@ import (
metrics "github.com/armon/go-metrics"
memdb "github.com/hashicorp/go-memdb"
multierror "github.com/hashicorp/go-multierror"

"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
)

// Namespace endpoint is used for manipulating namespaces
type Namespace struct {
srv *Server
srv *Server
limiter *RateLimiter
}

// UpsertNamespaces is used to upsert a set of namespaces
func (n *Namespace) UpsertNamespaces(args *structs.NamespaceUpsertRequest,
reply *structs.GenericResponse) error {

if err := n.srv.CheckRateLimit(n.limiter, args.AuthToken, acl.PolicyWrite); err != nil {
return err
}

args.Region = n.srv.config.AuthoritativeRegion
if done, err := n.srv.forward("Namespace.UpsertNamespaces", args, args, reply); done {
return err
Expand Down
78 changes: 78 additions & 0 deletions nomad/rate_limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package nomad

import (
"context"
"time"

"github.com/armon/go-metrics"
"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/nomad/structs"
limiter "github.com/sethvargo/go-limiter"
"github.com/sethvargo/go-limiter/memorystore"
)

func (srv *Server) CheckRateLimit(limiter *RateLimiter, secretID, op string) error {
token, err := srv.ResolveSecretToken(secretID)
if err != nil {
return err
}
return limiter.check(srv.shutdownCtx, token.AccessorID, op)
}

type RateLimiter struct {
endpoint string
write limiter.Store
read limiter.Store
list limiter.Store
}

func newRateLimiter(endpoint string) *RateLimiter {
// TODO: needs to take a configuration object so we can set tokens
// value
return &RateLimiter{
endpoint: endpoint,
write: newRateLimiterStore(10),
read: newRateLimiterStore(10),
list: newRateLimiterStore(10),
}
}

func (r *RateLimiter) check(ctx context.Context, op, key string) error {
var tokens, remaining uint64
var ok bool
var err error
switch op {
case acl.PolicyWrite:
tokens, remaining, _, ok, err = r.write.Take(ctx, key)
case acl.PolicyRead:
tokens, remaining, _, ok, err = r.read.Take(ctx, key)
case acl.PolicyList:
tokens, remaining, _, ok, err = r.list.Take(ctx, key)
}
used := tokens - remaining
metrics.IncrCounterWithLabels(
[]string{"nomad", "rpc", r.endpoint, op}, 1,
[]metrics.Label{{Name: "id", Value: key}})
metrics.AddSampleWithLabels(
[]string{"nomad", "rpc", r.endpoint, op, "used"}, float32(used),
[]metrics.Label{{Name: "id", Value: key}})

if err != nil {
return err // TODO: what's the source of these errors?
}
if !ok {
metrics.IncrCounterWithLabels([]string{"nomad", "rpc", r.endpoint, op, "limited"}, 1,
[]metrics.Label{{Name: "id", Value: key}})
return structs.ErrTooManyRequests
}
return nil
}

func newRateLimiterStore(tokens uint64) limiter.Store {
// note: the memorystore implementation never returns an error
store, _ := memorystore.New(&memorystore.Config{
Tokens: tokens, // Number of tokens allowed per interval.
Interval: time.Minute, // Interval until tokens reset.
})
return store
}
11 changes: 6 additions & 5 deletions nomad/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import (
log "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
lru "github.com/hashicorp/golang-lru"
"github.com/hashicorp/raft"
raftboltdb "github.com/hashicorp/raft-boltdb/v2"
"github.com/hashicorp/serf/serf"
"go.etcd.io/bbolt"

"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/codec"
Expand All @@ -38,10 +43,6 @@ import (
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/nomad/volumewatcher"
"github.com/hashicorp/nomad/scheduler"
"github.com/hashicorp/raft"
raftboltdb "github.com/hashicorp/raft-boltdb/v2"
"github.com/hashicorp/serf/serf"
"go.etcd.io/bbolt"
)

const (
Expand Down Expand Up @@ -1158,7 +1159,7 @@ func (s *Server) setupRpcServer(server *rpc.Server, ctx *RPCContext) {
s.staticEndpoints.Status = &Status{srv: s, logger: s.logger.Named("status")}
s.staticEndpoints.System = &System{srv: s, logger: s.logger.Named("system")}
s.staticEndpoints.Search = &Search{srv: s, logger: s.logger.Named("search")}
s.staticEndpoints.Namespace = &Namespace{srv: s}
s.staticEndpoints.Namespace = &Namespace{srv: s, limiter: newRateLimiter("Namespace")}
s.staticEndpoints.Enterprise = NewEnterpriseEndpoints(s)

// These endpoints are dynamic because they need access to the
Expand Down
5 changes: 3 additions & 2 deletions nomad/structs/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
errNodeLacksRpc = "Node does not support RPC; requires 0.8 or later"
errMissingAllocID = "Missing allocation ID"
errIncompatibleFiltering = "Filter expression cannot be used with other filter parameters"
errTooManyRequests = "Too many requests"

// Prefix based errors that are used to check if the error is of a given
// type. These errors should be created with the associated constructor.
Expand Down Expand Up @@ -55,8 +56,8 @@ var (
ErrNodeLacksRpc = errors.New(errNodeLacksRpc)
ErrMissingAllocID = errors.New(errMissingAllocID)
ErrIncompatibleFiltering = errors.New(errIncompatibleFiltering)

ErrUnknownNode = errors.New(ErrUnknownNodePrefix)
ErrUnknownNode = errors.New(ErrUnknownNodePrefix)
ErrTooManyRequests = errors.New(errTooManyRequests)

ErrDeploymentTerminalNoCancel = errors.New(errDeploymentTerminalNoCancel)
ErrDeploymentTerminalNoFail = errors.New(errDeploymentTerminalNoFail)
Expand Down

0 comments on commit 5597c57

Please sign in to comment.