badactor

package module
Version: v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2020 License: MIT Imports: 5 Imported by: 2

README

badactor logo Coverage Status Documentation

BadActor

BadActor is an in-memory, application driven jailer built in the spirit of fail2ban. A middleware with the primary goal to increase the expense for "bad actors" who engage in system probing or attacks.

The BadActor logo is based on Renee French's wonderful gopher. Thanks Renee!

Install

$ go get github.com/jaredfolkins/badactor

Use Case

A common use case for BadActor is jailing an offender who fails to login to your website (N) times as this can signal a bruteforce attempt.

Tutorial

Checkout badactor.org for a tutorial.

Design

  • speed (subsecond response underload and submillisecond with standard operations)
  • no external dependencies
  • solid code coverage and thorough tests

Does It Scale?

BadActor can be included in your go application and ran concurrently. This allows you an easy way to scale up as BadActor's memory footprint is tiny. Because it leverages a light-weight cache with sharding and reaping, it allows most organizations to be confident that BadActor will not be a bottleneck.

Benchmarks

Type Value
Model Name MacBook Pro
Model Identifier MacBookPro11,3
Processor Name Intel Core i7
Processor Speed 2.3 GHz
Number of Processors 1
Total Number of Cores 4
L2 Cache (per Core) 256 KB
L3 Cache 6 MB
Memory 16 GB
1.8.2015
➜  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) ✗
12.30.2014
➜  badactor git:(master) ✗ go test -benchtime=5s -bench=. -benchmem -cpu=4 | column -t
PASS
BenchmarkIsJailed-4                  50000000                          133        ns/op  0          B/op  0        allocs/op
BenchmarkIsJailedFor-4               50000000                          136        ns/op  0          B/op  0        allocs/op
BenchmarkInfraction-4                10000000                          824        ns/op  116        B/op  5        allocs/op
BenchmarkInfractionMostCostly-4      10000000                          891        ns/op  116        B/op  5        allocs/op
BenchmarkInfractionIsJailed-4        3000000                           2569       ns/op  340        B/op  13       allocs/op
BenchmarkInfractionIsJailedFor-4     3000000                           2611       ns/op  340        B/op  13       allocs/op
Benchmark10000Actors1Infraction-4    1000                              8571335    ns/op  1162931    B/op  50023    allocs/op
Benchmark100000Actors1Infraction-4   100                               87687224   ns/op  11630938   B/op  500248   allocs/op
Benchmark1000000Actors1Infraction-4  10                                841989544  ns/op  116292788  B/op  5002740  allocs/op
Benchmark10000Actors4Infractions-4   200                               30728688   ns/op  4522659    B/op  170013   allocs/op
ok                                   github.com/jaredfolkins/badactor  93.868s
➜  badactor git:(master) ✗

12.24.2014
➜  badactor git:(master) ✗ go test -bench=. -benchtime=5s -benchmem | column -t
PASS
BenchmarkIsJailed                 50000000                          138        ns/op  0         B/op  0       allocs/op
BenchmarkIsJailedFor              50000000                          140        ns/op  0         B/op  0       allocs/op
BenchmarkInfraction               10000000                          943        ns/op  128       B/op  4       allocs/op
BenchmarkInfractionMostCostly     10000000                          1008       ns/op  128       B/op  4       allocs/op
Benchmark10000Actors              100                               140566388  ns/op  13150354  B/op  150598  allocs/op
Benchmark10000Actors4Infractions  50                                241030802  ns/op  17278074  B/op  210614  allocs/op
ok                                github.com/jaredfolkins/badactor  73.592s
➜  badactor git:(master) ✗

12.16.2014

This was before a serious refactoring. I am keeping it here because (a) I'd like to encourage others to benchmark their code and (b) I learned many valuable lessons while doing it.

➜  badactor git:(master) go test -bench=. -benchtime=5s -benchmem | column -t
PASS
BenchmarkInfraction1                  2000                              2679694   ns/op  518  B/op  10  allocs/op
BenchmarkInfraction10                 2000                              3050845   ns/op  516  B/op  10  allocs/op
BenchmarkInfraction100                2000                              3430051   ns/op  516  B/op  10  allocs/op
BenchmarkInfraction1000               2000                              3738125   ns/op  516  B/op  10  allocs/op
BenchmarkInfraction10000              2000                              4004534   ns/op  516  B/op  10  allocs/op
BenchmarkInfractionWithIsJailed1      3000                              1832770   ns/op  193  B/op  3   allocs/op
BenchmarkInfractionWithIsJailed10     3000                              1968030   ns/op  193  B/op  3   allocs/op
BenchmarkInfractionWithIsJailed100    3000                              2120179   ns/op  193  B/op  3   allocs/op
BenchmarkInfractionWithIsJailed1000   3000                              1955656   ns/op  193  B/op  3   allocs/op
BenchmarkInfractionWithIsJailed10000  3000                              1943728   ns/op  193  B/op  3   allocs/op
ok                                    github.com/jaredfolkins/badactor  109.879s
➜  badactor git:(master)

Action Interface

The Action Interface has two primary methods, WhenJailed and WhenTimeServed. An excerpt of an implementation is below. They are called when the actor is jailed for the rule or when the actor has served its time for a particular rule.

type MyAction struct{}

func (ma *MyAction) WhenJailed(a *badactor.Actor, r *badactor.Rule) error {
  // Do something here. Log, email, etc...
	return nil
}

func (ma *MyAction) WhenTimeServed(a *badactor.Actor, r *badactor.Rule) error {
  // Do something here. Log, email, etc...
	return nil
}

And assigned to the rule like so.

// define and add the rule to the stack
ru := &badactor.Rule{
  Name:        "Login",
  Message:     "You have failed to login too many times",
  StrikeLimit: 10,
  ExpireBase:  time.Second * 1,
  Sentence:    time.Second * 10,
  Action:      &MyAction{},
}
st.AddRule(ru)

Httprouter & Negroni Example

package main

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

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

var st *badactor.Studio

type MyAction struct{}

func (ma *MyAction) WhenJailed(a *badactor.Actor, r *badactor.Rule) error {
	return nil
}

func (ma *MyAction) WhenTimeServed(a *badactor.Actor, r *badactor.Rule) error {
	return nil
}

func main() {

	//runtime.GOMAXPROCS(4)

	// studio capacity
	var sc int32
	// director capacity
	var dc int32

	sc = 1024
	dc = 1024

	// init new Studio
	st = badactor.NewStudio(sc)

	// define and add the rule to the stack
	ru := &badactor.Rule{
		Name:        "Login",
		Message:     "You have failed to login too many times",
		StrikeLimit: 10,
		ExpireBase:  time.Second * 1,
		Sentence:    time.Second * 10,
		Action:      &MyAction{},
	}
	st.AddRule(ru)

	err := st.CreateDirectors(dc)
	if err != nil {
		log.Fatal(err)
	}

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

	// router
	router := httprouter.New()
	router.POST("/login", LoginHandler)

	// middleware
	n := negroni.Classic()
	n.Use(NewBadActorMiddleware())
	n.UseHandler(router)
	n.Run(":9999")

}

//
// HANDLER
//

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

	var err error

	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
	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)
	}

	// auth fails, increment infraction
	i, err := st.Strikes(an, "Login")
	log.Printf("[%v] has %v Strikes %v", an, i, err)

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

//
// MIDDLEWARE
//
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.StatusNotFound), http.StatusNotFound)
		return
	}

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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

type Action interface {
	WhenJailed(a *Actor, r *Rule) error     // When an Actor isJailed, do this
	WhenTimeServed(a *Actor, r *Rule) error // When an Actor is relased because of timeServed, do this
}

Action is the inferface the Programmer implements to perform event based actions

type Actor

type Actor struct {
	// contains filtered or unexported fields
}

func (*Actor) Name added in v1.2.0

func (a *Actor) Name() string

type Director

type Director struct {
	sync.Mutex
	// contains filtered or unexported fields
}

Director is a bucket/shard and contains many Actors

func NewDirector

func NewDirector(ma int32) *Director

NewDirector instantiates a Director Struct

func (*Director) ActorsName added in v1.2.0

func (d *Director) ActorsName(a *Actor) (string, error)

ActorsName accepts an Actor as an argument it then locks the cache, validates the actor exists, and returns the value it does this to make sure that the statement of BadActor is kept intact

type Rule

type Rule struct {
	Name        string
	Message     string
	StrikeLimit int
	ExpireBase  time.Duration
	Sentence    time.Duration
	Action      Action
}

Rule struct is used as a basic ruleset to Judge and Jail an Actor by

func NewClassicRule

func NewClassicRule(n string, m string) *Rule

NewClassicRule returns a Rule with basic default values

type Studio

type Studio struct {
	sync.Mutex
	// contains filtered or unexported fields
}

Studio is the singleton instance, it contains the Directors(buckets) who have many Actors(points)

func NewStudio

func NewStudio(md int32) *Studio

NewStudio returns a init'd Studio struct, you pass it an int32 value which is the capacity and informs the Studio how many Directors will be created, it is also the value that jumpHash uses to mod

func (*Studio) ActorExists

func (st *Studio) ActorExists(an string) bool

ActorExists accepts an ActorName and returns a bool if the Actor is found

func (*Studio) AddRule

func (st *Studio) AddRule(r *Rule)

AddRule accepts a Rule struct and adds it to the rules map if it doesn't exist

func (*Studio) ApplyRules

func (st *Studio) ApplyRules() error

ApplyRules takes the currently stored rules map and applies it to all Directors

func (*Studio) CreateActor

func (st *Studio) CreateActor(an string, rn string) error

CreateActor takes an ActorName and RuleName and creates an Actor

func (*Studio) CreateDirectors

func (st *Studio) CreateDirectors(ma int32) error

CreateDirectors creates and adds the Directors to the director map

func (*Studio) CreateInfraction

func (st *Studio) CreateInfraction(an string, rn string) error

CreateInfraction takes an ActorName and RuleName and creates an Infraction

func (Studio) Director

func (st Studio) Director(an string) *Director

Director takes the name of an Actor as a string, serializes it, uses the jumpHash aglo to determine the Director that the Actor belongs to

func (*Studio) Infraction

func (st *Studio) Infraction(an string, rn string) error

Infraction accepts an ActorName and RuleName and either creates, increments, or increments and jails the Actor

func (*Studio) InfractionExists

func (st *Studio) InfractionExists(an string, rn string) bool

InfractionExists accepts an ActorName and RuleName and returns a bool if the Infraction is found

func (*Studio) IsJailed

func (st *Studio) IsJailed(an string) bool

IsJailed accepts an ActorName and returns a bool if the Actor is Jailed for ANY Rule

func (*Studio) IsJailedFor

func (st *Studio) IsJailedFor(an string, rn string) bool

IsJailedFor accepts an ActorName and a RuleName and returns a bool if the Actor is Jailed for that particular Rule

func (*Studio) KeepAlive

func (st *Studio) KeepAlive(an string) error

KeepAlive accepts an ActorName and allows you to rebase the TTL for the Actor so that it isn't removed from the stack as scheduled, keeping it alive.

func (*Studio) StartReaper

func (st *Studio) StartReaper(dur time.Duration)

StartReaper starts the reaping goroutine and takes a time.Duration on how often you want the Reaper to run

func (*Studio) Status

func (st *Studio) Status() *message

func (*Studio) Strikes

func (st *Studio) Strikes(an string, rn string) (int, error)

Strikes accepts an ActorName and a RuleName and returns the total strikes an Actor holds for a particular Rule

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL