diff --git a/.golangci.yml b/.golangci.yml index 22897ef9..ca9c66df 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,3 +40,10 @@ linters-settings: misspell: locale: US +issues: + exclude-rules: + - path: toxics/corrupt\.go + linters: + - gosec + # we don't need cryptographically secure RNGs for this + text: "G404:" diff --git a/README.md b/README.md index b8bf9a76..0a2f2e16 100644 --- a/README.md +++ b/README.md @@ -446,6 +446,12 @@ Closes connection when transmitted data exceeded limit. - `bytes`: number of bytes it should transmit before connection is closed +#### corrupt + +Flips random bits of the input stream, corrupting it. + + - `probability`: probability of any given bit in the input of being flipped + ### HTTP API All communication with the Toxiproxy daemon from the client happens through the diff --git a/scripts/test-e2e b/scripts/test-e2e index 42dafba5..f149ef86 100755 --- a/scripts/test-e2e +++ b/scripts/test-e2e @@ -169,6 +169,18 @@ cli toxic delete --toxicName="reset_peer" shopify_http echo -e "-----------------\n" +echo "=== Corrupt toxic" + +cli toxic add --type=corrupt \ + --toxicName="corrupt" \ + --attribute="probability=1.0" \ + --toxicity=1.0 \ + shopify_http +cli inspect shopify_http +cli toxic delete --toxicName="corrupt" shopify_http + +echo -e "-----------------\n" + echo "== Metrics test" wait_for_url http://localhost:20000/test1 curl -s http://localhost:8474/metrics | grep -E '^toxiproxy_proxy_sent_bytes_total{direction="downstream",listener="127.0.0.1:20000",proxy="shopify_http",upstream="localhost:20002"} [0-9]+' diff --git a/toxics/corrupt.go b/toxics/corrupt.go new file mode 100644 index 00000000..58f5e4ee --- /dev/null +++ b/toxics/corrupt.go @@ -0,0 +1,70 @@ +package toxics + +import ( + "io" + "math/rand" + + "github.com/Shopify/toxiproxy/v2/stream" +) + +type CorruptToxic struct { + // probability of bit flips + Prob float64 `json:"probability"` +} + +// reference: https://stackoverflow.com/a/2076028/2708711 +func generate_mask(num_bytes int, prob float64, gas int) []byte { + tol := 0.001 + x := make([]byte, num_bytes) + rand.Read(x) + if gas <= 0 { + return x + } + if prob > 0.5+tol { + y := generate_mask(num_bytes, 2*prob-1, gas-1) + for i := 0; i < num_bytes; i++ { + x[i] |= y[i] + } + return x + } + if prob < 0.5-tol { + y := generate_mask(num_bytes, 2*prob, gas-1) + for i := 0; i < num_bytes; i++ { + x[i] &= y[i] + } + return x + } + return x +} + +func (t *CorruptToxic) corrupt(data []byte) { + gas := 10 + mask := generate_mask(len(data), t.Prob, gas) + for i := 0; i < len(data); i++ { + data[i] ^= mask[i] + } +} + +func (t *CorruptToxic) Pipe(stub *ToxicStub) { + buf := make([]byte, 32*1024) + writer := stream.NewChanWriter(stub.Output) + reader := stream.NewChanReader(stub.Input) + reader.SetInterrupt(stub.Interrupt) + for { + n, err := reader.Read(buf) + if err == stream.ErrInterrupted { + t.corrupt(buf[:n]) + writer.Write(buf[:n]) + return + } else if err == io.EOF { + stub.Close() + return + } + t.corrupt(buf[:n]) + writer.Write(buf[:n]) + } +} + +func init() { + Register("corrupt", new(CorruptToxic)) +} diff --git a/toxics/corrupt_test.go b/toxics/corrupt_test.go new file mode 100644 index 00000000..b1830272 --- /dev/null +++ b/toxics/corrupt_test.go @@ -0,0 +1,77 @@ +package toxics_test + +import ( + "strings" + "testing" + + "github.com/Shopify/toxiproxy/v2/stream" + "github.com/Shopify/toxiproxy/v2/toxics" +) + +func count_flips(before, after []byte) int { + res := 0 + for i := 0; i < len(before); i++ { + if before[i] != after[i] { + res += 1 + } + } + return res +} + +func DoCorruptEcho(corrupt *toxics.CorruptToxic) ([]byte, []byte) { + len_data := 100 + data0 := []byte(strings.Repeat("a", len_data)) + data1 := make([]byte, len_data) + copy(data1, data0) + + input := make(chan *stream.StreamChunk) + output := make(chan *stream.StreamChunk) + stub := toxics.NewToxicStub(input, output) + + done := make(chan bool) + go func() { + corrupt.Pipe(stub) + done <- true + }() + defer func() { + close(input) + for { + select { + case <-done: + return + case <-output: + } + } + }() + + input <- &stream.StreamChunk{Data: data1} + + result := <-output + return data0, result.Data +} + +func TestCorruptToxicLowProb(t *testing.T) { + corrupt := &toxics.CorruptToxic{Prob: 0.001} + original, corrupted := DoCorruptEcho(corrupt) + + num_flips := count_flips(original, corrupted) + + tolerance := 5 + expected := 0 + if num_flips > expected+tolerance { + t.Errorf("Too many bytes flipped! (note: this test has a very low false positive probability)") + } +} + +func TestCorruptToxicHighProb(t *testing.T) { + corrupt := &toxics.CorruptToxic{Prob: 0.999} + original, corrupted := DoCorruptEcho(corrupt) + + num_flips := count_flips(original, corrupted) + + tolerance := 5 + expected := 100 + if num_flips < expected-tolerance { + t.Errorf("Too few bytes flipped! (note: this test has a very low false positive probability)") + } +}