From 81e37cf16fe4620f7b26a2ac45e81997f8ac4410 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Sun, 9 Oct 2022 14:46:31 -0300 Subject: [PATCH 1/2] feat: add `corrupt` toxic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a toxic that flips random bits of the input stream, corrupting it. Example run: ``` ͳ curl -v http://localhost:4001/api/echo -H "Content-Type: application/json" -d '{"echo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' * Trying 127.0.0.1:4001... * Connected to localhost (127.0.0.1) port 4001 (#0) > POST /api/echo HTTP/1.1 > Host: localhost:4001 > User-Agent: curl/7.85.0 > Accept: */* > Content-Type: application/json > Content-Length: 77 > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < cache-control: max-age=0, private, must-revalidate < content-length: 76 < content-type: application/json; charset=utf-8 < date: Sun, 09 Oct 2022 17:55:36 GMT < server: Cowboy < x-request-id: Fxx4NnPZWF6J4lwAAABG < * Connection #0 to host localhost left intact {"echo":"aaqaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} ``` (Note that it changed one of the `a`'s to a `q`. Many times it may corrupt the request such that a response is not sent back.) --- README.md | 6 ++++ toxics/corrupt.go | 63 ++++++++++++++++++++++++++++++++++ toxics/corrupt_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 toxics/corrupt.go create mode 100644 toxics/corrupt_test.go 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/toxics/corrupt.go b/toxics/corrupt.go new file mode 100644 index 00000000..95d554b6 --- /dev/null +++ b/toxics/corrupt.go @@ -0,0 +1,63 @@ +package toxics + +import ( + "math/rand" +) + +type CorruptToxic struct { + // probability of bit flips + Prob float64 `json:"probability"` +} + +// reference: https://stackoverflow.com/questions/2075912/generate-a-random-binary-number-with-a-variable-proportion-of-1-bits +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) { + for { + select { + case <-stub.Interrupt: + return + case c := <-stub.Input: + if c == nil { + stub.Close() + return + } + t.corrupt(c.Data) + stub.Output <- c + } + } +} + +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)") + } +} From 292a7250a3fd1fc369d8d1320ccd691cf8143f83 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Sun, 16 Oct 2022 14:48:47 -0300 Subject: [PATCH 2/2] chore: use `io{reader,writer}`; add e2e test --- .golangci.yml | 7 +++++++ scripts/test-e2e | 12 ++++++++++++ toxics/corrupt.go | 27 +++++++++++++++++---------- 3 files changed, 36 insertions(+), 10 deletions(-) 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/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 index 95d554b6..58f5e4ee 100644 --- a/toxics/corrupt.go +++ b/toxics/corrupt.go @@ -1,7 +1,10 @@ package toxics import ( + "io" "math/rand" + + "github.com/Shopify/toxiproxy/v2/stream" ) type CorruptToxic struct { @@ -9,7 +12,7 @@ type CorruptToxic struct { Prob float64 `json:"probability"` } -// reference: https://stackoverflow.com/questions/2075912/generate-a-random-binary-number-with-a-variable-proportion-of-1-bits +// 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) @@ -43,18 +46,22 @@ func (t *CorruptToxic) corrupt(data []byte) { } 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 { - select { - case <-stub.Interrupt: + 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 - case c := <-stub.Input: - if c == nil { - stub.Close() - return - } - t.corrupt(c.Data) - stub.Output <- c } + t.corrupt(buf[:n]) + writer.Write(buf[:n]) } }