Introducing Jim, the Chaos Monkey

This commit is contained in:
Ian Kent 2014-12-24 13:21:21 +00:00
parent 9e8d990e72
commit 69fd88323f
9 changed files with 262 additions and 8 deletions

53
JIM.md Normal file
View file

@ -0,0 +1,53 @@
Introduction to Jim
===================
Jim is the MailHog Chaos Monkey, inspired by Netflix.
You can invite Jim to the party using the `invite-jim` flag:
MailHog -invite-jim
With Jim around, things aren't going to work how you expect.
### What can Jim do?
* Reject connections
* Rate limit connections
* Reject authentication
* Reject senders
* Reject recipients
It does this randomly, but within defined parameters.
You can control these using the following command line flags:
| Flag | Default | Description
| --------------------- | ------- | ----
| -invite-jim | false | Set to true to invite Jim
| -jim-disconnect | 0.005 | Chance of randomly disconnecting a session
| -jim-accept | 0.99 | Chance of accepting an incoming connection
| -jim-linkspeed-affect | 0.1 | Chance of applying a rate limit
| -jim-linkspeed-min | 1024 | Minimum link speed (in bytes per second)
| -jim-linkspeed-max | 10240 | Maximum link speed (in bytes per second)
| -jim-reject-sender | 0.05 | Chance of rejecting a MAIL FROM command
| -jim-reject-recipient | 0.05 | Chance of rejecting a RCPT TO command
| -jim-reject-auth | 0.05 | Chance of rejecting an AUTH command
If you enable Jim, you enable all parts. To disable individual parts, set the chance
of it happening to 0, e.g. to disable connection rate limiting:
MailHog -invite-jim -jim-linkspeed-affect=0
### Examples
Always rate limit to 1 byte per second:
MailHog -invite-jim -jim-linkspeed-affect=1 -jim-linkspeed-max=1 -jim-linkspeed-min=1
Disconnect clients after approximately 5 commands:
MailHog -invite-jim -jim-disconnect=0.2
Simulate a mobile connection (at 10-100kbps) for 10% of clients:
MailHog -invite-jim -jim-linkspeed-affect=0.1 -jim-linkspeed-min=1250 -jim-linkspeed-max=12500

View file

@ -4,6 +4,7 @@ import (
"flag" "flag"
"log" "log"
"github.com/ian-kent/Go-MailHog/MailHog-Server/monkey"
"github.com/ian-kent/Go-MailHog/data" "github.com/ian-kent/Go-MailHog/data"
"github.com/ian-kent/Go-MailHog/storage" "github.com/ian-kent/Go-MailHog/storage"
"github.com/ian-kent/envconf" "github.com/ian-kent/envconf"
@ -30,12 +31,15 @@ type Config struct {
MongoDb string MongoDb string
MongoColl string MongoColl string
StorageType string StorageType string
InviteJim bool
Storage storage.Storage Storage storage.Storage
MessageChan chan *data.Message MessageChan chan *data.Message
Assets func(asset string) ([]byte, error) Assets func(asset string) ([]byte, error)
Monkey monkey.ChaosMonkey
} }
var cfg = DefaultConfig() var cfg = DefaultConfig()
var jim = &monkey.Jim{}
func Configure() *Config { func Configure() *Config {
switch cfg.StorageType { switch cfg.StorageType {
@ -56,6 +60,13 @@ func Configure() *Config {
log.Fatalf("Invalid storage type %s", cfg.StorageType) log.Fatalf("Invalid storage type %s", cfg.StorageType)
} }
if cfg.InviteJim {
jim.Configure(func(message string, args ...interface{}) {
log.Printf(message, args...)
})
cfg.Monkey = jim
}
return cfg return cfg
} }
@ -67,4 +78,6 @@ func RegisterFlags() {
flag.StringVar(&cfg.MongoUri, "mongouri", envconf.FromEnvP("MH_MONGO_URI", "127.0.0.1:27017").(string), "MongoDB URI, e.g. 127.0.0.1:27017") flag.StringVar(&cfg.MongoUri, "mongouri", envconf.FromEnvP("MH_MONGO_URI", "127.0.0.1:27017").(string), "MongoDB URI, e.g. 127.0.0.1:27017")
flag.StringVar(&cfg.MongoDb, "mongodb", envconf.FromEnvP("MH_MONGO_DB", "mailhog").(string), "MongoDB database, e.g. mailhog") flag.StringVar(&cfg.MongoDb, "mongodb", envconf.FromEnvP("MH_MONGO_DB", "mailhog").(string), "MongoDB database, e.g. mailhog")
flag.StringVar(&cfg.MongoColl, "mongocoll", envconf.FromEnvP("MH_MONGO_COLLECTION", "messages").(string), "MongoDB collection, e.g. messages") flag.StringVar(&cfg.MongoColl, "mongocoll", envconf.FromEnvP("MH_MONGO_COLLECTION", "messages").(string), "MongoDB collection, e.g. messages")
flag.BoolVar(&cfg.InviteJim, "invite-jim", envconf.FromEnvP("MH_INVITE_JIM", false).(bool), "Decide whether to invite Jim (beware, he causes trouble)")
jim.RegisterFlags()
} }

View file

@ -0,0 +1,105 @@
package monkey
import (
"flag"
"math/rand"
"net"
"time"
"github.com/ian-kent/linkio"
)
// Jim is a chaos monkey
type Jim struct {
disconnectChance float64
acceptChance float64
linkSpeedAffect float64
linkSpeedMin float64
linkSpeedMax float64
rejectSenderChance float64
rejectRecipientChance float64
rejectAuthChance float64
logf func(message string, args ...interface{})
}
// RegisterFlags implements ChaosMonkey.RegisterFlags
func (j *Jim) RegisterFlags() {
flag.Float64Var(&j.disconnectChance, "jim-disconnect", 0.005, "Chance of disconnect")
flag.Float64Var(&j.acceptChance, "jim-accept", 0.99, "Chance of accept")
flag.Float64Var(&j.linkSpeedAffect, "jim-linkspeed-affect", 0.1, "Chance of affecting link speed")
flag.Float64Var(&j.linkSpeedMin, "jim-linkspeed-min", 1024, "Minimum link speed (in bytes per second)")
flag.Float64Var(&j.linkSpeedMax, "jim-linkspeed-max", 10240, "Maximum link speed (in bytes per second)")
flag.Float64Var(&j.rejectSenderChance, "jim-reject-sender", 0.05, "Chance of rejecting a sender (MAIL FROM)")
flag.Float64Var(&j.rejectRecipientChance, "jim-reject-recipient", 0.05, "Chance of rejecting a recipient (RCPT TO)")
flag.Float64Var(&j.rejectAuthChance, "jim-reject-auth", 0.05, "Chance of rejecting authentication (AUTH)")
}
// Configure implements ChaosMonkey.Configure
func (j *Jim) Configure(logf func(string, ...interface{})) {
j.logf = logf
rand.Seed(time.Now().Unix())
}
// Accept implements ChaosMonkey.Accept
func (j *Jim) Accept(conn net.Conn) bool {
if rand.Float64() > j.acceptChance {
j.logf("Jim: Rejecting connection\n")
return false
}
j.logf("Jim: Allowing connection\n")
return true
}
// LinkSpeed implements ChaosMonkey.LinkSpeed
func (j *Jim) LinkSpeed() *linkio.Throughput {
rand.Seed(time.Now().Unix())
if rand.Float64() < j.linkSpeedAffect {
lsDiff := j.linkSpeedMax - j.linkSpeedMin
lsAffect := j.linkSpeedMin + (lsDiff * rand.Float64())
f := linkio.Throughput(lsAffect) * linkio.BytePerSecond
j.logf("Jim: Restricting throughput to %s\n", f)
return &f
}
j.logf("Jim: Allowing unrestricted throughput")
return nil
}
// ValidRCPT implements ChaosMonkey.ValidRCPT
func (j *Jim) ValidRCPT(rcpt string) bool {
if rand.Float64() < j.rejectRecipientChance {
j.logf("Jim: Rejecting recipient %s\n", rcpt)
return false
}
j.logf("Jim: Allowing recipient%s\n", rcpt)
return true
}
// ValidMAIL implements ChaosMonkey.ValidMAIL
func (j *Jim) ValidMAIL(mail string) bool {
if rand.Float64() < j.rejectSenderChance {
j.logf("Jim: Rejecting sender %s\n", mail)
return false
}
j.logf("Jim: Allowing sender %s\n", mail)
return true
}
// ValidAUTH implements ChaosMonkey.ValidAUTH
func (j *Jim) ValidAUTH(mechanism string, args ...string) bool {
if rand.Float64() < j.rejectAuthChance {
j.logf("Jim: Rejecting authentication %s: %s\n", mechanism, args)
return false
}
j.logf("Jim: Allowing authentication %s: %s\n", mechanism, args)
return true
}
// Disconnect implements ChaosMonkey.Disconnect
func (j *Jim) Disconnect() bool {
if rand.Float64() < j.disconnectChance {
j.logf("Jim: Being nasty, kicking them off\n")
return true
}
j.logf("Jim: Being nice, letting them stay\n")
return false
}

View file

@ -0,0 +1,28 @@
package monkey
import (
"net"
"github.com/ian-kent/linkio"
)
// ChaosMonkey should be implemented by chaos monkeys!
type ChaosMonkey interface {
RegisterFlags()
Configure(func(string, ...interface{}))
// Accept is called for each incoming connection. Returning false closes the connection.
Accept(conn net.Conn) bool
// LinkSpeed sets the maximum connection throughput (in one direction)
LinkSpeed() *linkio.Throughput
// ValidRCPT is called for the RCPT command. Returning false signals an invalid recipient.
ValidRCPT(rcpt string) bool
// ValidMAIL is called for the MAIL command. Returning false signals an invalid sender.
ValidMAIL(mail string) bool
// ValidAUTH is called after authentication. Returning false signals invalid authentication.
ValidAUTH(mechanism string, args ...string) bool
// Disconnect is called after every read. Returning true will close the connection.
Disconnect() bool
}

View file

@ -7,9 +7,11 @@ import (
"log" "log"
"strings" "strings"
"github.com/ian-kent/Go-MailHog/MailHog-Server/monkey"
"github.com/ian-kent/Go-MailHog/data" "github.com/ian-kent/Go-MailHog/data"
"github.com/ian-kent/Go-MailHog/smtp/protocol" "github.com/ian-kent/Go-MailHog/smtp/protocol"
"github.com/ian-kent/Go-MailHog/storage" "github.com/ian-kent/Go-MailHog/storage"
"github.com/ian-kent/linkio"
) )
// Session represents a SMTP session using net.TCPConn // Session represents a SMTP session using net.TCPConn
@ -21,13 +23,32 @@ type Session struct {
remoteAddress string remoteAddress string
isTLS bool isTLS bool
line string line string
link *linkio.Link
reader io.Reader
writer io.Writer
monkey monkey.ChaosMonkey
} }
// Accept starts a new SMTP session using io.ReadWriteCloser // Accept starts a new SMTP session using io.ReadWriteCloser
func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Storage, messageChan chan *data.Message, hostname string) { func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Storage, messageChan chan *data.Message, hostname string, monkey monkey.ChaosMonkey) {
defer conn.Close()
proto := protocol.NewProtocol() proto := protocol.NewProtocol()
proto.Hostname = hostname proto.Hostname = hostname
session := &Session{conn, proto, storage, messageChan, remoteAddress, false, ""} var link *linkio.Link
reader := io.Reader(conn)
writer := io.Writer(conn)
if monkey != nil {
linkSpeed := monkey.LinkSpeed()
if linkSpeed != nil {
link = linkio.NewLink(*linkSpeed * linkio.BytePerSecond)
reader = link.NewLinkReader(io.Reader(conn))
writer = link.NewLinkWriter(io.Writer(conn))
}
}
session := &Session{conn, proto, storage, messageChan, remoteAddress, false, "", link, reader, writer, monkey}
proto.LogHandler = session.logf proto.LogHandler = session.logf
proto.MessageReceivedHandler = session.acceptMessage proto.MessageReceivedHandler = session.acceptMessage
proto.ValidateSenderHandler = session.validateSender proto.ValidateSenderHandler = session.validateSender
@ -37,19 +58,42 @@ func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Stora
session.logf("Starting session") session.logf("Starting session")
session.Write(proto.Start()) session.Write(proto.Start())
for session.Read() == true { for session.Read() == true {
if monkey != nil && monkey.Disconnect != nil && monkey.Disconnect() {
session.conn.Close()
break
}
} }
session.logf("Session ended") session.logf("Session ended")
} }
func (c *Session) validateAuthentication(mechanism string, args ...string) (errorReply *protocol.Reply, ok bool) { func (c *Session) validateAuthentication(mechanism string, args ...string) (errorReply *protocol.Reply, ok bool) {
if c.monkey != nil {
ok := c.monkey.ValidAUTH(mechanism, args...)
if !ok {
// FIXME better error?
return protocol.ReplyUnrecognisedCommand(), false
}
}
return nil, true return nil, true
} }
func (c *Session) validateRecipient(to string) bool { func (c *Session) validateRecipient(to string) bool {
if c.monkey != nil {
ok := c.monkey.ValidRCPT(to)
if !ok {
return false
}
}
return true return true
} }
func (c *Session) validateSender(from string) bool { func (c *Session) validateSender(from string) bool {
if c.monkey != nil {
ok := c.monkey.ValidMAIL(from)
if !ok {
return false
}
}
return true return true
} }
@ -69,7 +113,7 @@ func (c *Session) logf(message string, args ...interface{}) {
// Read reads from the underlying net.TCPConn // Read reads from the underlying net.TCPConn
func (c *Session) Read() bool { func (c *Session) Read() bool {
buf := make([]byte, 1024) buf := make([]byte, 1024)
n, err := io.Reader(c.conn).Read(buf) n, err := c.reader.Read(buf)
if n == 0 { if n == 0 {
c.logf("Connection closed by remote host\n") c.logf("Connection closed by remote host\n")
@ -112,6 +156,6 @@ func (c *Session) Write(reply *protocol.Reply) {
logText := strings.Replace(l, "\n", "\\n", -1) logText := strings.Replace(l, "\n", "\\n", -1)
logText = strings.Replace(logText, "\r", "\\r", -1) logText = strings.Replace(logText, "\r", "\\r", -1)
c.logf("Sent %d bytes: '%s'", len(l), logText) c.logf("Sent %d bytes: '%s'", len(l), logText)
io.Writer(c.conn).Write([]byte(l)) c.writer.Write([]byte(l))
} }
} }

View file

@ -40,7 +40,7 @@ func TestAccept(t *testing.T) {
Convey("Accept should handle a connection", t, func() { Convey("Accept should handle a connection", t, func() {
frw := &fakeRw{} frw := &fakeRw{}
mChan := make(chan *data.Message) mChan := make(chan *data.Message)
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost") Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
}) })
} }
@ -52,7 +52,7 @@ func TestSocketError(t *testing.T) {
}, },
} }
mChan := make(chan *data.Message) mChan := make(chan *data.Message)
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost") Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
}) })
} }
@ -98,7 +98,7 @@ func TestAcceptMessage(t *testing.T) {
//So(m, ShouldNotBeNil) //So(m, ShouldNotBeNil)
wg.Done() wg.Done()
}() }()
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost") Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
wg.Wait() wg.Wait()
So(handlerCalled, ShouldBeTrue) So(handlerCalled, ShouldBeTrue)
}) })

View file

@ -22,7 +22,14 @@ func Listen(cfg *config.Config, exitCh chan int) *net.TCPListener {
log.Printf("[SMTP] Error accepting connection: %s\n", err) log.Printf("[SMTP] Error accepting connection: %s\n", err)
continue continue
} }
defer conn.Close()
if cfg.Monkey != nil {
ok := cfg.Monkey.Accept(conn)
if !ok {
conn.Close()
continue
}
}
go Accept( go Accept(
conn.(*net.TCPConn).RemoteAddr().String(), conn.(*net.TCPConn).RemoteAddr().String(),
@ -30,6 +37,7 @@ func Listen(cfg *config.Config, exitCh chan int) *net.TCPListener {
cfg.Storage, cfg.Storage,
cfg.MessageChan, cfg.MessageChan,
cfg.Hostname, cfg.Hostname,
cfg.Monkey,
) )
} }
} }

View file

@ -25,6 +25,7 @@ deps:
go get github.com/ian-kent/go-log/log go get github.com/ian-kent/go-log/log
go get github.com/ian-kent/envconf go get github.com/ian-kent/envconf
go get github.com/ian-kent/goose go get github.com/ian-kent/goose
go get github.com/ian-kent/linkio
go get github.com/jteeuwen/go-bindata/... go get github.com/jteeuwen/go-bindata/...
go get labix.org/v2/mgo go get labix.org/v2/mgo
# added to fix travis issues # added to fix travis issues

View file

@ -29,6 +29,8 @@ Go was chosen for portability - MailHog runs without installation on multiple pl
* Supports RFC2047 encoded headers * Supports RFC2047 encoded headers
* Real-time updates using EventSource * Real-time updates using EventSource
* Release messages to real SMTP servers * Release messages to real SMTP servers
* Chaos Monkey for failure testing
* See [Introduction to Jim](JIM.md) for more information
* HTTP API to list, retrieve and delete messages * HTTP API to list, retrieve and delete messages
* See [APIv1 documentation](APIv1.md) for more information * See [APIv1 documentation](APIv1.md) for more information
* Multipart MIME support * Multipart MIME support