diff --git a/.travis.yml b/.travis.yml index 3f24fdb3..6aebcb12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: go +services: + - docker sudo: false go: - tip @@ -6,7 +8,19 @@ install: - go get -v golang.org/x/lint/golint - go get -d -t -v ./... - go build -v ./... -script: - - go vet ./... - - $HOME/gopath/bin/golint -set_exit_status $(go list ./... | grep -v /vendor/) - - go test -v ./... +jobs: + include: + - stage: Test + script: + - go vet ./... + - $HOME/gopath/bin/golint -set_exit_status $(go list ./... | grep -v /vendor/) + - go test -v ./... + - stage: Fuzz regression + go: 1.12.x + dist: bionic + script: ./fuzzit.sh local-regression + - stage: Fuzz + if: branch = master AND type IN (push) + go: 1.12.x + dist: bionic + script: ./fuzzit.sh fuzzing diff --git a/README.md b/README.md index 0307fa43..282ab9c2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Vegeta [![Build Status](https://secure.travis-ci.org/tsenart/vegeta.svg?branch=master)](http://travis-ci.org/tsenart/vegeta) [![Go Report Card](https://goreportcard.com/badge/github.com/tsenart/vegeta)](https://goreportcard.com/report/github.com/tsenart/vegeta) [![GoDoc](https://godoc.org/github.com/tsenart/vegeta?status.svg)](https://godoc.org/github.com/tsenart/vegeta) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/tsenart/vegeta?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Donate](https://img.shields.io/badge/donate-bitcoin-yellow.svg)](#donate) +# Vegeta [![Build Status](https://secure.travis-ci.org/tsenart/vegeta.svg?branch=master)](http://travis-ci.org/tsenart/vegeta) [![Fuzzit Status](https://app.fuzzit.dev/badge?org_id=vegeta)](https://app.fuzzit.dev/orgs/vegeta/dashboard) [![Go Report Card](https://goreportcard.com/badge/github.com/tsenart/vegeta)](https://goreportcard.com/report/github.com/tsenart/vegeta) [![GoDoc](https://godoc.org/github.com/tsenart/vegeta?status.svg)](https://godoc.org/github.com/tsenart/vegeta) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/tsenart/vegeta?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Donate](https://img.shields.io/badge/donate-bitcoin-yellow.svg)](#donate) Vegeta is a versatile HTTP load testing tool built out of a need to drill HTTP services with a constant request rate. diff --git a/fuzzit.sh b/fuzzit.sh new file mode 100755 index 00000000..5f5e006a --- /dev/null +++ b/fuzzit.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -xe + +# Validate arguments +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Configure +NAME=vegeta +ROOT=./lib +TYPE=$1 + +# Setup +export GO111MODULE="off" +go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build +go get -d -v -u ./... +if [ ! -f fuzzit ]; then + wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.29/fuzzit_Linux_x86_64 + chmod a+x fuzzit +fi + +# Fuzz +function fuzz { + FUNC=Fuzz$1 + TARGET=$2 + DIR=${3:-$ROOT} + go-fuzz-build -libfuzzer -func $FUNC -o fuzzer.a $DIR + clang -fsanitize=fuzzer fuzzer.a -o fuzzer + ./fuzzit create job --type $TYPE $NAME/$TARGET fuzzer +} +fuzz HTTPTargeter http-targeter +fuzz JSONTargeter json-targeter +fuzz ResultsFormatDetection results-format-detection +fuzz GobDecoder gob-decoder +fuzz CSVDecoder csv-decoder +fuzz JSONDecoder json-decoder +fuzz AttackerTCP attacker-tcp +fuzz AttackerHTTP attacker-http diff --git a/lib/attack_fuzz.go b/lib/attack_fuzz.go new file mode 100644 index 00000000..e14eef61 --- /dev/null +++ b/lib/attack_fuzz.go @@ -0,0 +1,154 @@ +// +build gofuzz + +package vegeta + +import ( + "encoding/binary" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "time" +) + +// FuzzAttackerTCP fuzzes binary responses to attacker. +func FuzzAttackerTCP(fuzz []byte) int { + // Ignore empty fuzz + if len(fuzz) == 0 { + return -1 + } + + // Start server + directory, err := ioutil.TempDir("/tmp", "fuzz") + if err != nil { + panic(err.Error()) + } + socket := fmt.Sprintf("%s/attacker.sock", directory) + listener, err := net.Listen("unix", socket) + if err != nil { + panic(err.Error()) + } + go func() { + connection, err := listener.Accept() + if err != nil { + panic(err.Error()) + } + _, err = connection.Write(fuzz) + if err != nil { + panic(err.Error()) + } + err = connection.Close() + if err != nil { + panic(err.Error()) + } + }() + defer listener.Close() + defer os.RemoveAll(directory) + + // Setup targeter + targeter := Targeter(func(target *Target) error { + target.Method = "GET" + target.URL = "http://vegeta.test" + return nil + }) + + // Deliver a single hit + attacker := NewAttacker( + UnixSocket(socket), + Workers(1), + MaxWorkers(1), + Timeout(time.Second), + KeepAlive(false), + ) + result := attacker.hit(targeter, "fuzz") + if result.Error != "" { + return 0 + } + return 1 +} + +// FuzzAttackerHTTP fuzzes valid HTTP responses to attacker. +func FuzzAttackerHTTP(fuzz []byte) int { + // Decode response + code, headers, body, ok := decodeFuzzResponse(fuzz) + if !ok { + return -1 + } + + // Start server + directory, err := ioutil.TempDir("/tmp", "fuzz") + if err != nil { + panic(err.Error()) + } + socket := fmt.Sprintf("%s/attacker.sock", directory) + listener, err := net.Listen("unix", socket) + if err != nil { + panic(err.Error()) + } + handler := func(response http.ResponseWriter, request *http.Request) { + for name, values := range headers { + for _, value := range values { + response.Header().Add(name, value) + } + } + response.WriteHeader(int(code)) + _, err := response.Write(body) + if err != nil { + panic(err.Error()) + } + } + server := http.Server{ + Handler: http.HandlerFunc(handler), + } + defer server.Close() + defer listener.Close() + defer os.RemoveAll(directory) + go server.Serve(listener) + + // Setup targeter + targeter := Targeter(func(target *Target) error { + target.Method = "GET" + target.URL = "http://vegeta.test" + return nil + }) + + // Deliver a single hit + attacker := NewAttacker( + UnixSocket(socket), + Workers(1), + MaxWorkers(1), + Timeout(time.Second), + KeepAlive(false), + ) + result := attacker.hit(targeter, "fuzz") + if result.Error != "" { + return 0 + } + return 1 +} + +func decodeFuzzResponse(fuzz []byte) ( + code int, + headers map[string][]string, + body []byte, + ok bool, +) { + if len(fuzz) < 2 { + return + } + headers = make(map[string][]string) + body = []byte{} + code = int(binary.LittleEndian.Uint16(fuzz[0:2])) + if len(fuzz) == 2 { + ok = true + return + } + fuzz, ok = decodeFuzzHeaders(fuzz[2:], headers) + if !ok { + return + } + body = fuzz + ok = true + return +} diff --git a/lib/results_fuzz.go b/lib/results_fuzz.go new file mode 100644 index 00000000..fced203d --- /dev/null +++ b/lib/results_fuzz.go @@ -0,0 +1,63 @@ +// +build gofuzz + +package vegeta + +import ( + "bytes" + "io" +) + +// FuzzResultsFormatDetection tests result list format detection. +func FuzzResultsFormatDetection(fuzz []byte) int { + decoder := DecoderFor(bytes.NewReader(fuzz)) + if decoder == nil { + return 0 + } + ok := readAllResults(decoder) + if !ok { + return 0 + } + return 1 +} + +// FuzzGobDecoder tests decoding a gob format result list. +func FuzzGobDecoder(fuzz []byte) int { + decoder := NewDecoder(bytes.NewReader(fuzz)) + ok := readAllResults(decoder) + if !ok { + return 0 + } + return 1 +} + +// FuzzCSVDecoder tests decoding a CSV format result list. +func FuzzCSVDecoder(fuzz []byte) int { + decoder := NewCSVDecoder(bytes.NewReader(fuzz)) + ok := readAllResults(decoder) + if !ok { + return 0 + } + return 1 +} + +// FuzzJSONDecoder tests decoding a JSON format result list. +func FuzzJSONDecoder(fuzz []byte) int { + decoder := NewJSONDecoder(bytes.NewReader(fuzz)) + ok := readAllResults(decoder) + if !ok { + return 0 + } + return 1 +} + +func readAllResults(decoder Decoder) (ok bool) { + for { + result := &Result{} + err := decoder.Decode(result) + if err == io.EOF { + return true + } else if err != nil { + return false + } + } +} diff --git a/lib/targets_fuzz.go b/lib/targets_fuzz.go new file mode 100644 index 00000000..87a925fe --- /dev/null +++ b/lib/targets_fuzz.go @@ -0,0 +1,68 @@ +// +build gofuzz + +package vegeta + +import ( + "bytes" + "net/http" +) + +// FuzzHTTPTargeter tests decoding an HTTP encoded target list. +func FuzzHTTPTargeter(fuzz []byte) int { + headers, body, fuzz, ok := decodeFuzzTargetDefaults(fuzz) + if !ok { + return -1 + } + targeter := NewHTTPTargeter( + bytes.NewReader(fuzz), + body, + headers, + ) + _, err := ReadAllTargets(targeter) + if err != nil { + return 0 + } + return 1 +} + +// FuzzJSONTargeter tests decoding a JSON encoded target list. +func FuzzJSONTargeter(fuzz []byte) int { + headers, body, fuzz, ok := decodeFuzzTargetDefaults(fuzz) + if !ok { + return -1 + } + targeter := NewJSONTargeter( + bytes.NewReader(fuzz), + body, + headers, + ) + _, err := ReadAllTargets(targeter) + if err != nil { + return 0 + } + return 1 +} + +func decodeFuzzTargetDefaults(fuzz []byte) ( + headers http.Header, + body []byte, + rest []byte, + ok bool, +) { + if len(fuzz) < 2 { + return + } + headers = make(map[string][]string) + body = []byte{} + rest = []byte{} + rest, ok = decodeFuzzHeaders(fuzz, headers) + if !ok { + return + } + if len(rest) == 0 { + ok = true + return + } + body, rest, ok = extractFuzzByteString(rest) + return +} diff --git a/lib/util_fuzz.go b/lib/util_fuzz.go new file mode 100644 index 00000000..60c758fe --- /dev/null +++ b/lib/util_fuzz.go @@ -0,0 +1,120 @@ +// +build gofuzz + +package vegeta + +func decodeFuzzHeaders(fuzz []byte, headers map[string][]string) ( + rest []byte, + ok bool, +) { + rest = fuzz + for { + if len(rest) == 0 { + // Consumed all fuzz + ok = true + return + } + if fuzz[0] == 0 { + // Headers terminated + if len(rest) == 1 { + rest = []byte{} + } else { + rest = rest[1:] + } + ok = true + return + } + if len(fuzz) == 1 { + // Invalid headers encoding + return + } + rest, ok = decodeFuzzHeader(rest[1:], headers) + if !ok { + return + } + } +} + +func decodeFuzzHeader(fuzz []byte, headers map[string][]string) ( + rest []byte, + ok bool, +) { + if len(fuzz) == 0 { + ok = true + return + } + name, rest, ok := extractFuzzString(fuzz) + if !ok { + return + } + value, rest, ok := extractFuzzString(rest) + if !ok { + return + } + if header, ok := headers[name]; ok { + headers[name] = append(header, value) + } else { + headers[name] = []string{value} + } + ok = true + return +} + +func extractFuzzString(fuzz []byte) ( + value string, + rest []byte, + ok bool, +) { + if len(fuzz) < 2 { + // Invalid string encoding + return + } + length := int(fuzz[0]) + if length == 0 { + // Invalid length + return + } + if len(fuzz) < (length + 1) { + // Insufficient fuzz + return + } + value = string(fuzz[1 : length+1]) + if len(fuzz) == (length + 1) { + // Consumed all fuzz + rest = []byte{} + } else { + // More fuzz + rest = fuzz[length+1:] + } + ok = true + return +} + +func extractFuzzByteString(fuzz []byte) ( + value []byte, + rest []byte, + ok bool, +) { + if len(fuzz) < 2 { + // Invalid byte string encoding + return + } + length := int(fuzz[0]) + if length == 0 { + // Invalid length + return + } + if len(fuzz) < (length + 1) { + // Insufficient fuzz + return + } + value = fuzz[1 : length+1] + if len(fuzz) == (length + 1) { + // Consumed all fuzz + rest = []byte{} + } else { + // More fuzz + rest = fuzz[length+1:] + } + ok = true + return +}