Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add corrupt toxic #451

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions scripts/test-e2e
Original file line number Diff line number Diff line change
Expand Up @@ -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]+'
Expand Down
70 changes: 70 additions & 0 deletions toxics/corrupt.go
Original file line number Diff line number Diff line change
@@ -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))
}
77 changes: 77 additions & 0 deletions toxics/corrupt_test.go
Original file line number Diff line number Diff line change
@@ -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)")
}
}