Goal

BadActor aims to help you increase the expense for “bad actors” who are probing or attacking your systems.

It is spirited after Fail2Ban, but unlike Fail2Ban, which derives its signals from regular expressions and logfile scans, BadActor expects the developer to programmatically define and send the signals themselves.

Once you you have jailed a BadActor, you can handle their future actions appropriately (lock their account, add latency, 401, 404, etc).

Benchmark

BadActor is incredibly fast and safe from race conditions. It creates an internal light-weight cache that utilizes the Jump Consistent Hash aglorithm to evenly shard the Actors into seperate buckets, known as Directors. It keeps the cache clean by implementing a Least Recently Used(LRU_CACHE) system along with a reaping goroutine who runs hourly.

These are BadActor’s benchmarks.

➜  badactor git:(master) ✗ go test -bench=. -cpu=4 -benchmem -benchtime=5s | column -t
PASS
BenchmarkIsJailed-4                50000000                          121       ns/op  0    B/op  0  allocs/op
BenchmarkIsJailedFor-4             50000000                          134       ns/op  0    B/op  0  allocs/op
BenchmarkInfraction-4              5000000                           1390      ns/op  528  B/op  7  allocs/op
BenchmarkInfractionlIsJailed-4     3000000                           2755      ns/op  800  B/op  9  allocs/op
BenchmarkInfractionlIsJailedFor-4  3000000                           2733      ns/op  800  B/op  9  allocs/op
BenchmarkStudioInfraction512-4     3000000                           2215      ns/op  591  B/op  9  allocs/op
BenchmarkStudioInfraction1024-4    3000000                           2357      ns/op  612  B/op  9  allocs/op
BenchmarkStudioInfraction2048-4    5000000                           2617      ns/op  621  B/op  9  allocs/op
BenchmarkStudioInfraction4096-4    5000000                           2566      ns/op  671  B/op  9  allocs/op
BenchmarkStudioInfraction65536-4   3000000                           3309      ns/op  667  B/op  9  allocs/op
BenchmarkStudioInfraction262144-4  2000000                           3644      ns/op  674  B/op  9  allocs/op
ok                                 github.com/jaredfolkins/badactor  178.239s
➜  badactor git:(master)

And below is an external benchmark performed using wrk. The intent was to show reads and writes to BadActor underload, while being muxed by HttpRouter and having Negroni do a middleware lookup on every request.

The code is on github.

➜  wrk2 git:(master) ✗ ./wrk http://localhost:9999/bench -t12 -c400 -d1m  -R30000
Running 1m test @ http://localhost:9999/bench
  12 threads and 400 connections
  Thread calibration: mean lat.: 1.469ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.480ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.484ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.479ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.483ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.482ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.487ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.481ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.479ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.488ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.471ms, rate sampling interval: 10ms
  Thread calibration: mean lat.: 1.479ms, rate sampling interval: 10ms
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.49ms  747.80us   9.46ms   74.23%
    Req/Sec     2.63k   248.94     4.44k    73.86%
  1797722 requests in 1.00m, 198.88MB read
Requests/sec:  29963.02
Transfer/sec:      3.31MB
➜  wrk2 git:(master)

Tutorial

In this tutorial we will create a small web server and utilize BadActor to protect the /login endpoint.

First setup your application’s structure and main.go file.

$ go get "github.com/jaredfolkins/badactor"
$ go get "github.com/codegangsta/negroni"
$ go get "github.com/julienschmidt/httprouter"
$ mkdir $GOPATH/badactorTest; 
$ cd $GOPATH/badactorTest;
$ vi main.go

Create the basic boilerplate main function, import the following packages, and create a variable for BadActor’s singlton Studio Struct.

Note, BadActor uses the Observer pattern with the Studio as the Subject. The Studio contains all of the publicly exposed APIs and is the only way of utilizing BadActor while limiting lock contention and avoiding deadlock.

package main

import (
  "log"
  "net"
  "net/http"
  "time"

  "github.com/codegangsta/negroni"
  "github.com/jaredfolkins/badactor"
  "github.com/julienschmidt/httprouter"
)

var st *badactor.Studio

func main() {

  // create new Studio
  st = badactor.NewStudio()

}

Next we need to define a Rule. Please notice the Name field, which is how we will reference the Rule going forward. We safely pass the Rule to the Studio using the AddRule() method. If we wanted to we could add multiple Rules, but for this tutorial we will just use one.

// create and add rule
ru := &badactor.Rule{
  Name:        "Login",
  Message:     "You have failed to login too many times",
  StrikeLimit: 10,
  ExpireBase:  time.Second * 15, 
  Sentence:    time.Minute * 1, 
}

// add the rule to the stack
err := st.AddRule(ru)
if err != nil {
  panic(err)
}

After all the rules are created and added, we call the CreateDirectors() method. We pass this method a int32 value indicating the Capacity of our Studio.

// creates the Directors who act as the Buckets in our sharding cache
err := st.CreateDirectors(256)
if err != nil {
  log.Fatal(err)
}

Your last step for initializing BadActor is to setup your polling duration and begin running the Reaper.

//poll duration 
dur := time.Minute * time.Duration(60)
// Start the reaper
st.StartReaper()

Next we will create our HttpRouter and our Negroni middleware handler.

// router
router := httprouter.New()

// middleware
n := negroni.Classic()

To keep things clean, lets create a LoginHandler function.

What this very naive example LoginHandler does is -

  1. It stores the username and password into two variables
  2. Next it snags the requestor’s IP address which we will use as the Actor’s Name (aka the Key)
  3. Then it validates the username and password
  4. If the username and password is correct, it redirects sending a StatusOK code
  5. If the username and password is incorrect, it increments the Infraction for the Actor based on the “Login” rule we create, then it prints the Strikes (just for clarity) and redirects send a StatusUnathuorized code

// this is a naive login function for example purposes
func LoginHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {

  var err error

  // get the username and password
  un := r.FormValue("username")
  pw := r.FormValue("password")

  // snag the IP for use as the actor's name
  an, _, err := net.SplitHostPort(r.RemoteAddr)
  if err != nil {
    panic(err)
  }

  // mock authentication, not safe, don't do this in a production env
  if un == "example_user" && pw == "example_pass" {
    http.Redirect(w, r, "", http.StatusOK)
    return
  }

  // auth fails, increment infraction
  err = st.Infraction(an, "Login")
  if err != nil {
    log.Printf("[%v] has err %v", an, err)
  }

  // this will display the Strike count
  i, err := st.Strikes(an, "Login")
  log.Printf("%v has %v strikes, %v\n", an, i , err)

  http.Redirect(w, r, "", http.StatusUnauthorized)
  return
}

We now need to implement some middleware for Negroni to utilize. The middleware does the following.

  1. It snags the requestor’s IP address for use as the Actor’s name.
  2. It checks to see if the Actor is jailed.
  3. If the Actor is jailed, throw an error and send the StatusUnathorized code
  4. If the Actor is NOT jailed, continue processing the request as normal

type BadActorMiddleware struct {
	negroni.Handler
}

func NewBadActorMiddleware() *BadActorMiddleware {
	return &BadActorMiddleware{}
}

func (bam *BadActorMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {

	// snag the IP for use as the actor's name
	an, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		panic(err)
	}

	// if the Actor is jailed, send them StatusUnauthorized
	if st.IsJailed(an) {
		http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
		return
	}

	// call the next middleware in the chain
	next(w, r)
}

Finally run your main.go application.

➜  badactorTest git:(master) ✗ go run main.go

Then run the following from the command line, at least 10 times, to simulate someone trying to login with the wrong credentials.

$ curl --data "username=badusername&password=badpassword" http://localhost:9999/login

You’ll recieve the following output.

➜  badactorTest git:(master) ✗ go run main.go
[negroni] listening on :9999
2015/01/10 18:22:48 [::1] has 1 Strikes <nil>
[negroni] Completed 401 Unauthorized in 89.348µs
[negroni] Started POST /login
2015/01/10 18:22:49 [::1] has 2 Strikes <nil>
[negroni] Completed 401 Unauthorized in 81.692µs
[negroni] Started POST /login
2015/01/10 18:22:49 [::1] has 3 Strikes <nil>
[negroni] Completed 401 Unauthorized in 65.213µs
[negroni] Started POST /login
2015/01/10 18:22:50 [::1] has 4 Strikes <nil>
[negroni] Completed 401 Unauthorized in 86µs
[negroni] Started POST /login
2015/01/10 18:22:50 [::1] has 5 Strikes <nil>
[negroni] Completed 401 Unauthorized in 85.37µs
[negroni] Started POST /login
2015/01/10 18:22:51 [::1] has 6 Strikes <nil>
[negroni] Completed 401 Unauthorized in 81.869µs
[negroni] Started POST /login
2015/01/10 18:22:51 [::1] has 7 Strikes <nil>
[negroni] Completed 401 Unauthorized in 105.735µs
[negroni] Started POST /login
2015/01/10 18:22:52 [::1] has 8 Strikes <nil>
[negroni] Completed 401 Unauthorized in 118.018µs
[negroni] Started POST /login
2015/01/10 18:22:52 [::1] has 9 Strikes <nil>
[negroni] Completed 401 Unauthorized in 71.321µs
[negroni] Started POST /login
2015/01/10 18:22:53 [::1] has 0 Strikes director.Strikes() failed, Infraction does not exists
[negroni] Completed 401 Unauthorized in 130.181µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 29.554µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 90.571µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 23.424µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 24.39µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 24.329µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 23.048µs

By continuing to try and Login you keep increasing the Infraction count, once it goes past 10 you break the threshold, as defined by our rule, and the Actor is jailed.

The sentence time is 1 minute.

During that period the middleware intercepts the requestor, sees that they are Jailed, and returns a 404 Not Found.

The code is on github.

Maker

BadActor is built and maintained by the pragmatically paranoid Jared Folkins.

If you are looking to hire a remote Go programmer who is also proficient at writing in the 3rd person, please contact him.