toxiproxy-1.2.1/000077500000000000000000000000001255450513500136055ustar00rootroot00000000000000toxiproxy-1.2.1/.gitignore000066400000000000000000000001321255450513500155710ustar00rootroot00000000000000toxiproxy toxiproxy.test testing testing.test cpu.out cover*.out coverage.html *.deb tmp/ toxiproxy-1.2.1/CHANGELOG.md000066400000000000000000000011711255450513500154160ustar00rootroot00000000000000# 1.2.1 * Fix proxy name conflicts leaking an open port #69 # 1.2.0 * Add a Toxic and Toxics type for the Go client * Add `Dockerfile` * Fix latency toxic limiting bandwidth #67 * Add Slicer toxic # 1.1.0 * Remove /toxics endpoint in favour of /proxies * Add bandwidth toxic # 1.0.3 * Rename Go library package to Toxiproxy from Client * Fix latency toxic send to closed channel panic #46 * Fix latency toxic accumulating delay #47 # 1.0.2 * Added Toxic support to Go client # 1.0.1 * Various improvements to the documentation * Initial version of Go client * Fix toxic disabling bug #42 # 1.0.0 Initial public release. toxiproxy-1.2.1/Dockerfile000066400000000000000000000002411255450513500155740ustar00rootroot00000000000000FROM golang:1.4 ADD . /app RUN cd /app && GOPATH=/app/Godeps/_workspace go build -o toxiproxy EXPOSE 8474 ENTRYPOINT ["/app/toxiproxy"] CMD ["-host=0.0.0.0"] toxiproxy-1.2.1/LICENSE000066400000000000000000000020631255450513500146130ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Shopify Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. toxiproxy-1.2.1/Makefile000066400000000000000000000025631255450513500152530ustar00rootroot00000000000000NAME=toxiproxy VERSION=$(shell cat VERSION) DEB=pkg/$(NAME)_$(VERSION)_amd64.deb GODEP_PATH=$(shell pwd)/Godeps/_workspace ORIGINAL_PATH=$(shell echo $(GOPATH)) COMBINED_GOPATH=$(GODEP_PATH):$(ORIGINAL_PATH) .PHONY: packages deb test linux darwin all: deb linux darwin docker deb: $(DEB) darwin: tmp/build/toxiproxy-darwin-amd64 linux: tmp/build/toxiproxy-linux-amd64 build: GOPATH=$(COMBINED_GOPATH) go build -o toxiproxy clean: rm tmp/build/* rm *.deb test: GOMAXPROCS=4 GOPATH=$(COMBINED_GOPATH) go test -v tmp/build/toxiproxy-linux-amd64: GOOS=linux GOARCH=amd64 GOPATH=$(COMBINED_GOPATH) go build -o $(@) tmp/build/toxiproxy-darwin-amd64: GOOS=darwin GOARCH=amd64 GOPATH=$(COMBINED_GOPATH) go build -o $(@) docker: docker build --tag="shopify/toxiproxy:$(VERSION)" . docker tag -f shopify/toxiproxy:$(VERSION) shopify/toxiproxy:latest docker push shopify/toxiproxy:$(VERSION) docker push shopify/toxiproxy:latest $(DEB): tmp/build/toxiproxy-linux-amd64 fpm -t deb \ -s dir \ --name "toxiproxy" \ --version $(VERSION) \ --license MIT \ --no-depends \ --no-auto-depends \ --architecture amd64 \ --maintainer "Simon Eskildsen " \ --description "TCP proxy to simulate network and system conditions" \ --url "https://github.com/Shopify/toxiproxy" \ $<=/usr/bin/toxiproxy \ ./share/toxiproxy.conf=/etc/init/toxiproxy.conf toxiproxy-1.2.1/README.md000066400000000000000000000370251255450513500150730ustar00rootroot00000000000000# Toxiproxy ![](http://i.imgur.com/sOaNw0o.png) Toxiproxy is a framework for simulating network conditions. It's made specifically to work in testing, CI and development environments, supporting deterministic tampering with connections, but with support for randomized chaos and customization. **Toxiproxy is the tool you need to prove with tests that your application doesn't have single points of failure.** We've been successfully using it in all development and test environments at Shopify since October, 2014. See our [blog post][blog] on resiliency for more information. Toxiproxy usage consists of two parts. A TCP proxy written in Go (what this repository contains) and a client communicating with the proxy over HTTP. You configure your application to make all test connections go through Toxiproxy and can then manipulate their health via HTTP. See [Usage](#usage) below on how to set up your project. For example, to add 1000ms of latency to the response of MySQL from the [Ruby client](https://github.com/Shopify/toxiproxy-ruby): ```ruby Toxiproxy[:mysql_master].downstream(:latency, latency: 1000).apply do Shop.first # this takes at least 1s end ``` To take down all Redis instances: ```ruby Toxiproxy[/redis/].down do Shop.first # this will throw an exception end ``` While the examples in this README are currently in Ruby, there's nothing stopping you from creating a client in any other language (see [Clients](#clients)). ## Table of Contents 1. [Why yet another chaotic TCP proxy?](#why-yet-another-chaotic-tcp-proxy) 2. [Clients](#clients) 3. [Example](#example) 4. [Usage](#usage) 1. [Installing](#1-installing-toxiproxy) 2. [Populating](#2-populating-toxiproxy) 3. [Using](#3-using-toxiproxy) 5. [Toxics](#toxics) 1. [Latency](#latency) 2. [Down](#down) 3. [Bandwidth](#bandwidth) 4. [Slow close](#slow_close) 5. [Timeout](#timeout) 6. [Slicer](#slicer) 6. [HTTP API](#http-api) 1. [Proxy fields](#proxy-fields) 2. [Curl example](#curl-example) 7. [FAQ](#frequently-asked-questions) 8. [Development](#development) ## Why yet another chaotic TCP proxy? The existing ones we found didn't provide the kind of dynamic API we needed for integration and unit testing. Linux tools like `nc` and so on are not cross-platform and require root, which makes them problematic in test, development and CI environments. ## Clients * [toxiproxy-ruby](https://github.com/Shopify/toxiproxy-ruby) * [toxiproxy-go](https://github.com/Shopify/toxiproxy/tree/master/client) * [toxiproxy.net](https://github.com/mdevilliers/Toxiproxy.Net) * [toxiproxy-php-client](https://github.com/ihsw/toxiproxy-php-client) * [toxiproxy-node](https://github.com/dlion/toxiproxy-node) ## Example Let's walk through an example with a Rails application. Note that Toxiproxy is in no way tied to Ruby, it's just been our first use case and it's currently the only language that has a client. You can see the full example at [Sirupsen/toxiproxy-rails-example](https://github.com/Sirupsen/toxiproxy-rails-example). To get started right away, jump down to [Usage](#usage). For our popular blog, for some reason we're storing the tags for our posts in Redis and the posts themselves in MySQL. We might have a `Post` class that includes some methods to manipulate tags in a [Redis set](http://redis.io/commands#set): ```ruby class Post < ActiveRecord::Base # Return an Array of all the tags. def tags TagRedis.smembers(tag_key) end # Add a tag to the post. def add_tag(tag) TagRedis.sadd(tag_key, tag) end # Remove a tag from the post. def remove_tag(tag) TagRedis.srem(tag_key, tag) end # Return the key in Redis for the set of tags for the post. def tag_key "post:tags:#{self.id}" end end ``` We've decided that erroring while writing to the tag data store (adding/removing) is OK. However, if the tag data store is down, we should be able to see the post with no tags. We could simply rescue the `Redis::CannotConnectError` around the `SMEMBERS` Redis call in the `tags` method. Let's use Toxiproxy to test that. Since we've already installed Toxiproxy and it's running on our machine, we can skip to step 2. This is where we need to make sure Toxiproxy has a mapping for Redis tags. To `config/boot.rb` (before any connection is made) we add: ```ruby require 'toxiproxy' Toxiproxy.populate([ { "name": "toxiproxy_test_redis_tags", "listen": "127.0.0.1:22222", "upstream": "127.0.0.1:6379" } ]) ``` Then in `config/environments/test.rb` we set the `TagRedis` to be a Redis client that connects to Redis through Toxiproxy by adding this line: ```ruby TagRedis = Redis.new(port: 22222) ``` All calls in the test environment now go through Toxiproxy. That means we can add a unit test where we simulate a failure: ```ruby test "should return empty array when tag redis is down when listing tags" do @post.add_tag "mammals" # Take down all Redises in Toxiproxy Toxiproxy[/redis/].down do assert_equal [], @post.tags end end ``` The test fails with `Redis::CannotConnectError`. Perfect! Toxiproxy took down the Redis successfully for the duration of the closure. Let's fix the `tags` method to be resilient: ```ruby def tags TagRedis.smembers(tag_key) rescue Redis::CannotConnectError [] end ``` The tests pass! We now have a unit test that proves fetching the tags when Redis is down returns an empty array, instead of throwing an exception. For full coverage you should also write an integration test that wraps fetching the entire blog post page when Redis is down. Full example application is at [Sirupsen/toxiproxy-rails-example](https://github.com/Sirupsen/toxiproxy-rails-example). ## Usage Configuring a project to use Toxiproxy consists of four steps: 1. Installing Toxiproxy 2. Populating Toxiproxy 3. Using Toxiproxy ### 1. Installing Toxiproxy **Linux** See [`Releases`](https://github.com/Shopify/toxiproxy/releases) for the latest binaries and system packages for your architecture. **Ubuntu** ```bash $ wget -O toxiproxy-1.2.1.deb https://github.com/Shopify/toxiproxy/releases/download/v1.2.1/toxiproxy_1.2.1_amd64.deb $ sudo dpkg -i toxiproxy-1.2.1.deb $ sudo service toxiproxy start ``` **OS X** ```bash $ brew tap shopify/shopify $ brew install toxiproxy ``` **Docker** Toxiproxy is available on [Docker Hub](https://registry.hub.docker.com/u/shopify/toxiproxy/). ```bash $ docker pull shopify/toxiproxy $ docker run -it shopify/toxiproxy ``` ### 2. Populating Toxiproxy When your application boots, it needs to make sure that Toxiproxy knows which endpoints to proxy where. The main parameters are: name, address for Toxiproxy to **listen** on and the address of the upstream. Some client libraries have helpers for this task, which is essentially just making sure each proxy in a list is created. Example from the Ruby client: ```ruby # Make sure `shopify_test_redis_master` and `shopify_test_mysql_master` are # present in Toxiproxy Toxiproxy.populate([ { "name": "shopify_test_redis_master", "listen": "127.0.0.1:22220", "upstream": "127.0.0.1:6379" }, { "name": "shopify_test_mysql_master", "listen": "127.0.0.1:24220", "upstream": "127.0.0.1:3306" } ]) ``` This code needs to run as early in boot as possible, before any code establishes a connection through Toxiproxy. Please check your client library for documentation on the population helpers. Alternatively use the HTTP API directly to create proxies, e.g.: ```bash curl -i -d '{"name": "shopify_test_redis_master", "upstream": "localhost:6379", "listen": "localhost:26379"}' localhost:8474/proxies ``` We recommend a naming such as the above: `___`. This makes sure there are no clashes between applications using the same Toxiproxy. For large application we recommend storing the Toxiproxy configurations in a separate configuration file. We use `config/toxiproxy.json`. Use ports outside the ephemeral port range to avoid random port conflicts. It's `32,768` to `61,000` on Linux by default, see `/proc/sys/net/ipv4/ip_local_port_range`. ### 3. Using Toxiproxy To use Toxiproxy, you now need to configure your application to connect through Toxiproxy. Continuing with our example from step two, we can configure our Redis client to connect through Toxiproxy: ```ruby # old straight to redis redis = Redis.new(port: 6380) # new through toxiproxy redis = Redis.new(port: 22220) ``` Now you can tamper with it through the Toxiproxy API. In Ruby: ```ruby redis = Redis.new(port: 22220) Toxiproxy[:shopify_test_redis_master].downstream(:latency, latency: 1000).apply do redis.get("test") # will take 1s end ``` Please consult your respective client library on usage. ### Toxics Toxics manipulate the pipe between the client and upstream. If the `enabled` field is not provided when creating the toxic, it will default to being disabled. #### latency Add a delay to all data going through the proxy. The delay is equal to `latency` +/- `jitter`. Fields: - `enabled`: true/false - `latency`: time in milliseconds - `jitter`: time in milliseconds #### down Bringing a service down is not technically a toxic in the implementation of Toxiproxy. This is done by `POST`ing to `/proxies/{proxy}` and setting the `enabled` field to `false`. #### bandwidth Limit a connection to a maximum number of kilobytes per second. Fields: - `enabled`: true/false - `rate`: rate in KB/s #### slow_close Delay the TCP socket from closing until `delay` has elapsed. Fields: - `enabled`: true/false - `delay`: time in milliseconds #### timeout Stops all data from getting through, and close the connection after `timeout`. If `timeout` is 0, the connection won't close, and data will be delayed until the toxic is disabled. Fields: - `enabled`: true/false - `timeout`: time in milliseconds #### slicer Slices TCP data up into small bits, optionally adding a delay between each sliced "packet". Fields: - `enabled`: true/false - `average_size`: size in bytes of an average packet - `size_variation`: variation in bytes of an average packet (should be smaller than average_size) - `delay`: time in microseconds to delay each packet by ### HTTP API All communication with the Toxiproxy daemon from the client happens through the HTTP interface, which is described here. Toxiproxy listens for HTTP on port **8474**. #### Proxy Fields: - `name`: proxy name (string) - `listen`: listen address (string) - `upstream`: proxy upstream address (string) - `enabled`: true/false (defaults to true on creation) To change a proxy's name, it must be deleted and recreated. Changing the `listen` or `upstream` fields will restart the proxy and drop any active connections. If `listen` is specified with a port of 0, toxiproxy will pick an ephemeral port. The `listen` field in the response will be updated with the actual port. If you change `enabled` to `false`, it'll take down the proxy. You can switch it back to `true` to reenable it. All endpoints are JSON. - **GET /proxies** - List existing proxies and their toxics - **POST /proxies** - Create a new proxy - **GET /proxies/{proxy}** - Show the proxy with both its upstream and downstream toxics - **POST /proxies/{proxy}** - Update a proxy's fields - **DELETE /proxies/{proxy}** - Delete an existing proxy - **GET /proxies/{proxy}/upstream/toxics** - List upstream toxics - **GET /proxies/{proxy}/downstream/toxics** - List downstream toxics - **POST /proxies/{proxy}/upstream/toxics/{toxic}** - Update upstream toxic - **POST /proxies/{proxy}/downstream/toxics/{toxic}** - Update downstream toxic - **GET /reset** - Enable all proxies and disable all toxics ### Curl Example ```bash $ curl -i -d '{"name": "redis", "upstream": "localhost:6379", "listen": "localhost:26379"}' localhost:8474/proxies HTTP/1.1 201 Created Content-Type: application/json Date: Sun, 12 Apr 2015 19:52:08 GMT Content-Length: 392 {"name":"redis","listen":"127.0.0.1:26379","upstream":"localhost:6379","enabled":true,"upstream_toxics":{"latency":{"enabled":false,"latency":0,"jitter":0},"slow_close":{"enabled":false,"delay":0},"timeout":{"enabled":false,"timeout":0}},"downstream_toxics":{"latency":{"enabled":false,"latency":0,"jitter":0},"slow_close":{"enabled":false,"delay":0},"timeout":{"enabled":false,"timeout":0}}} ``` ```bash $ redis-cli -p 26379 127.0.0.1:26379> SET omg pandas OK 127.0.0.1:26379> GET omg "pandas" ``` ```bash $ curl -i localhost:8474/proxies HTTP/1.1 200 OK Content-Type: application/json Date: Sun, 12 Apr 2015 19:52:49 GMT Content-Length: 96 {"redis":{"name":"redis","listen":"127.0.0.1:26379","upstream":"localhost:6379","enabled":true}} ``` ```bash $ curl -i -d '{"enabled":true, "latency":1000}' localhost:8474/proxies/redis/downstream/toxics/latency HTTP/1.1 200 OK Content-Type: application/json Date: Mon, 10 Nov 2014 16:37:25 GMT Content-Length: 42 {"enabled":true,"latency":1000,"jitter":0} ``` ```bash $ redis-cli -p 26379 127.0.0.1:26379> GET "omg" "pandas" (1.00s) 127.0.0.1:26379> DEL "omg" (integer) 1 (1.00s) ``` ```bash $ curl -i -d '{"enabled":false}' localhost:8474/proxies/redis/downstream/toxics/latency HTTP/1.1 200 OK Content-Type: application/json Date: Mon, 10 Nov 2014 16:39:49 GMT Content-Length: 43 {"enabled":false,"latency":1000,"jitter":0} ``` ```bash $ redis-cli -p 26379 127.0.0.1:26379> GET "omg" (nil) ``` ```bash $ curl -i -X DELETE localhost:8474/proxies/redis HTTP/1.1 204 No Content Date: Mon, 10 Nov 2014 16:07:36 GMT ``` ```bash $ redis-cli -p 26379 Could not connect to Redis at 127.0.0.1:26379: Connection refused ``` ### Frequently Asked Questions **How fast is Toxiproxy?** The speed of Toxiproxy depends largely on your hardware, but you can expect a latency of *< 100µs* when no toxics are enabled. When running with `GOMAXPROCS=4` on a Macbook Pro we acheived *~1000MB/s* throuput, and as high as *2400MB/s* on a higher end desktop. Basically, you can expect Toxiproxy to move data around at least as fast the app you're testing. **I am not seeing my Toxiproxy actions reflected for MySQL**. MySQL will prefer the local Unix domain socket for some clients, no matter which port you pass it if the host is set to `localhost`. Configure your MySQL server to not create a socket, and use `127.0.0.1` as the host. Remember to remove the old socket after you restart the server. **Toxiproxy causes intermittent connection failures**. Use ports outside the ephemeral port range to avoid random port conflicts. It's `32,768` to `61,000` on Linux by default, see `/proc/sys/net/ipv4/ip_local_port_range`. **Should I run a Toxiproxy for each application?** No, we recommend using the same Toxiproxy for all applications. To distinguish between services we recommend naming your proxies with the scheme: `___`. For example, `shopify_test_redis_master` or `shopify_development_mysql_1`. ### Development * `make all`. Build Toxiproxy binaries and packages for all platforms. Requires to have Go compiled with cross compilation enabled on Linux and Darwin (amd64) as well as [`fpm`](https://github.com/jordansissel/fpm) in your `$PATH` to build the Debian package. * `make test`. Run the Toxiproxy tests. * `make darwin`. Build binary for Darwin. * `make linux`. Build binary for Linux. ### Release 1. Update `CHANGELOG.md` 2. Bump `VERSION` and `toxiproxy.go` 3. Change versions in `README.md` 4. Commit 5. Tag 6. `make` to create binaries, packages and push new Docker image 7. Create [Github draft release](https://github.com/Shopify/toxiproxy/releases/new) against new tag and upload binaries and Debian package 8. [Bump version for Homebrew](https://github.com/Shopify/homebrew-shopify/blob/master/toxiproxy.rb#L9) [blog]: http://www.shopify.com/technology/16906928-building-and-testing-resilient-ruby-on-rails-applications toxiproxy-1.2.1/VERSION000066400000000000000000000000061255450513500146510ustar00rootroot000000000000001.2.1 toxiproxy-1.2.1/api.go000066400000000000000000000243341255450513500147130ustar00rootroot00000000000000package main import ( "encoding/json" "errors" "log" "net" "net/http" "github.com/Sirupsen/logrus" "github.com/gorilla/mux" ) type server struct { collection *ProxyCollection } func NewServer(collection *ProxyCollection) *server { return &server{ collection: collection, } } func (server *server) Listen(host string, port string) { r := mux.NewRouter() r.HandleFunc("/reset", server.ResetState).Methods("GET") r.HandleFunc("/proxies", server.ProxyIndex).Methods("GET") r.HandleFunc("/proxies", server.ProxyCreate).Methods("POST") r.HandleFunc("/proxies/{proxy}", server.ProxyShow).Methods("GET") r.HandleFunc("/proxies/{proxy}", server.ProxyUpdate).Methods("POST") r.HandleFunc("/proxies/{proxy}", server.ProxyDelete).Methods("DELETE") r.HandleFunc("/proxies/{proxy}/upstream/toxics", server.ToxicIndexUpstream).Methods("GET") r.HandleFunc("/proxies/{proxy}/downstream/toxics", server.ToxicIndexDownstream).Methods("GET") r.HandleFunc("/proxies/{proxy}/upstream/toxics/{toxic}", server.ToxicSetUpstream).Methods("POST") r.HandleFunc("/proxies/{proxy}/downstream/toxics/{toxic}", server.ToxicSetDownstream).Methods("POST") r.HandleFunc("/version", server.Version).Methods("GET") http.Handle("/", r) logrus.WithFields(logrus.Fields{ "host": host, "port": port, "version": Version, }).Info("API HTTP server starting") err := http.ListenAndServe(net.JoinHostPort(host, port), nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func (server *server) ProxyIndex(response http.ResponseWriter, request *http.Request) { proxies := server.collection.Proxies() marshalData := make(map[string]interface{}, len(proxies)) for name, proxy := range proxies { marshalData[name] = proxyWithToxics(proxy) } data, err := json.Marshal(marshalData) if err != nil { response.Header().Set("Content-Type", "application/json") http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } response.Header().Set("Content-Type", "application/json") _, err = response.Write(data) if err != nil { logrus.Warn("ProxyIndex: Failed to write response to client", err) } } func (server *server) ResetState(response http.ResponseWriter, request *http.Request) { proxies := server.collection.Proxies() for _, proxy := range proxies { err := proxy.Start() if err != nil && err != ErrProxyAlreadyStarted { response.Header().Set("Content-Type", "application/json") http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } proxy.upToxics.ResetToxics() proxy.downToxics.ResetToxics() } response.WriteHeader(http.StatusNoContent) _, err := response.Write(nil) if err != nil { logrus.Warn("ResetState: Failed to write headers to client", err) } } func (server *server) ProxyCreate(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") // Default fields enable to proxy right away input := Proxy{Enabled: true} err := json.NewDecoder(request.Body).Decode(&input) if err != nil { http.Error(response, server.apiError(err, http.StatusBadRequest), http.StatusBadRequest) return } if len(input.Name) < 1 { http.Error(response, server.apiError(errors.New("Missing required field: name"), http.StatusBadRequest), http.StatusBadRequest) return } if len(input.Upstream) < 1 { http.Error(response, server.apiError(errors.New("Missing required field: upstream"), http.StatusBadRequest), http.StatusBadRequest) return } proxy := NewProxy() proxy.Name = input.Name proxy.Listen = input.Listen proxy.Upstream = input.Upstream err = server.collection.Add(proxy, input.Enabled) if err != nil { http.Error(response, server.apiError(err, http.StatusConflict), http.StatusConflict) return } data, err := json.Marshal(proxyWithToxics(proxy)) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } response.WriteHeader(http.StatusCreated) _, err = response.Write(data) if err != nil { logrus.Warn("ProxyCreate: Failed to write response to client", err) } } func (server *server) ProxyUpdate(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") vars := mux.Vars(request) proxy, err := server.collection.Get(vars["proxy"]) if err != nil { http.Error(response, server.apiError(err, http.StatusNotFound), http.StatusNotFound) return } // Default fields are the same as existing proxy input := Proxy{Listen: proxy.Listen, Upstream: proxy.Upstream, Enabled: proxy.Enabled} err = json.NewDecoder(request.Body).Decode(&input) if err != nil { http.Error(response, server.apiError(err, http.StatusBadRequest), http.StatusBadRequest) return } err = proxy.Update(&input) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } data, err := json.Marshal(proxyWithToxics(proxy)) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } _, err = response.Write(data) if err != nil { logrus.Warn("ProxyAction: Failed to write response to client", err) } } func (server *server) ProxyDelete(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) err := server.collection.Remove(vars["proxy"]) if err != nil { response.Header().Set("Content-Type", "application/json") http.Error(response, server.apiError(err, http.StatusNotFound), http.StatusNotFound) return } response.WriteHeader(http.StatusNoContent) _, err = response.Write(nil) if err != nil { logrus.Warn("ProxyDelete: Failed to write headers to client", err) } } func (server *server) ProxyShow(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") vars := mux.Vars(request) proxy, err := server.collection.Get(vars["proxy"]) if err != nil { http.Error(response, server.apiError(err, http.StatusNotFound), http.StatusNotFound) return } data, err := json.Marshal(proxyWithToxics(proxy)) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } _, err = response.Write(data) if err != nil { logrus.Warn("ToxicIndex: Failed to write response to client", err) } } func (server *server) ToxicIndexUpstream(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") vars := mux.Vars(request) proxy, err := server.collection.Get(vars["proxy"]) if err != nil { http.Error(response, server.apiError(err, http.StatusNotFound), http.StatusNotFound) return } data, err := json.Marshal(proxy.upToxics.GetToxicMap()) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } _, err = response.Write(data) if err != nil { logrus.Warn("ToxicIndex: Failed to write response to client", err) } } func (server *server) ToxicIndexDownstream(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") vars := mux.Vars(request) proxy, err := server.collection.Get(vars["proxy"]) if err != nil { http.Error(response, server.apiError(err, http.StatusNotFound), http.StatusNotFound) return } data, err := json.Marshal(proxy.downToxics.GetToxicMap()) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } _, err = response.Write(data) if err != nil { logrus.Warn("ToxicIndex: Failed to write response to client", err) } } func (server *server) ToxicSetUpstream(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") vars := mux.Vars(request) proxy, err := server.collection.Get(vars["proxy"]) if err != nil { http.Error(response, server.apiError(err, http.StatusNotFound), http.StatusNotFound) return } toxic, err := proxy.upToxics.SetToxicJson(vars["toxic"], request.Body) if err != nil { http.Error(response, server.apiError(err, http.StatusBadRequest), http.StatusBadRequest) return } data, err := json.Marshal(toxic) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } _, err = response.Write(data) if err != nil { logrus.Warn("ToxicSet: Failed to write response to client", err) } } func (server *server) ToxicSetDownstream(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") vars := mux.Vars(request) proxy, err := server.collection.Get(vars["proxy"]) if err != nil { http.Error(response, server.apiError(err, http.StatusNotFound), http.StatusNotFound) return } toxic, err := proxy.downToxics.SetToxicJson(vars["toxic"], request.Body) if err != nil { http.Error(response, server.apiError(err, http.StatusBadRequest), http.StatusBadRequest) return } data, err := json.Marshal(toxic) if err != nil { http.Error(response, server.apiError(err, http.StatusInternalServerError), http.StatusInternalServerError) return } _, err = response.Write(data) if err != nil { logrus.Warn("ToxicSet: Failed to write response to client", err) } } func (server *server) Version(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "text/plain") _, err := response.Write([]byte(Version)) if err != nil { logrus.Warn("Version: Failed to write response to client", err) } } func (server *server) apiError(err error, code int) string { data, err2 := json.Marshal(struct { Title string `json:"title"` Status int `json:"status"` }{err.Error(), code}) if err2 != nil { logrus.Warn("Error json encoding error (╯°□°)╯︵ ┻━┻", err2) return "" } return string(data) } func proxyWithToxics(proxy *Proxy) (result struct { *Proxy UpstreamToxics map[string]Toxic `json:"upstream_toxics"` DownstreamToxics map[string]Toxic `json:"downstream_toxics"` }) { result.Proxy = proxy result.UpstreamToxics = proxy.upToxics.GetToxicMap() result.DownstreamToxics = proxy.downToxics.GetToxicMap() return } toxiproxy-1.2.1/api_test.go000066400000000000000000000271141255450513500157510ustar00rootroot00000000000000package main import ( "io/ioutil" "net/http" "testing" "time" tclient "github.com/Shopify/toxiproxy/client" ) var testServer *server var client = tclient.NewClient("http://127.0.0.1:8475") var testProxy = client.NewProxy(&tclient.Proxy{ Name: "mysql_master", Listen: "localhost:3310", Upstream: "localhost:20001", Enabled: true, }) func WithServer(t *testing.T, f func(string)) { // Make sure only one server is running at a time. Apparently there's no clean // way to shut it down between each test run. if testServer == nil { testServer = NewServer(NewProxyCollection()) go testServer.Listen("localhost", "8475") // Allow server to start. There's no clean way to know when it listens. time.Sleep(50 * time.Millisecond) } f("http://localhost:8475") err := testServer.collection.Clear() if err != nil { t.Error("Failed to clear collection", err) } } func TestIndexWithNoProxies(t *testing.T) { WithServer(t, func(addr string) { client := tclient.NewClient(addr) proxies, err := client.Proxies() if err != nil { t.Fatal("Failed getting proxies: ", err) } if len(proxies) > 0 { t.Fatal("Expected no proxies, got: ", proxies) } }) } func TestCreateProxy(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } }) } func TestCreateProxyBlankName(t *testing.T) { WithServer(t, func(addr string) { blankProxy := client.NewProxy(&tclient.Proxy{}) err := blankProxy.Create() if err == nil { t.Fatal("Expected error creating proxy, got nil") } else if err.Error() != "Create: HTTP 400: Missing required field: name" { t.Fatal("Expected different error creating proxy:", err) } }) } func TestCreateProxyBlankUpstream(t *testing.T) { WithServer(t, func(addr string) { blankProxy := client.NewProxy(&tclient.Proxy{Name: "test"}) err := blankProxy.Create() if err == nil { t.Fatal("Expected error creating proxy, got nil") } else if err.Error() != "Create: HTTP 400: Missing required field: upstream" { t.Fatal("Expected different error creating proxy:", err) } }) } func TestIndexWithToxics(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy") } proxies, err := client.Proxies() if err != nil { t.Fatal("Error listing proxies: ", err) } if len(proxies) == 0 { t.Fatal("Expected new proxy in list") } proxy, ok := proxies["mysql_master"] if !ok { t.Fatal("Expected to see mysql_master proxy in list") } if proxy.Name != "mysql_master" || proxy.Listen != "127.0.0.1:3310" || proxy.Upstream != "localhost:20001" { t.Fatalf("Unexpected proxy metadata: %s, %s, %s", proxy.Name, proxy.Listen, proxy.Upstream) } AssertToxicEnabled(t, proxy.ToxicsUpstream, "latency", false) AssertToxicEnabled(t, proxy.ToxicsDownstream, "latency", false) }) } func TestGetProxy(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy") } proxy, err := client.Proxy("mysql_master") if err != nil { t.Fatal("Unable to retriecve proxy: ", err) } if proxy.Name != "mysql_master" || proxy.Listen != "127.0.0.1:3310" || proxy.Upstream != "localhost:20001" || !proxy.Enabled { t.Fatalf("Unexpected proxy metadata: %s, %s, %s, %v", proxy.Name, proxy.Listen, proxy.Upstream, proxy.Enabled) } AssertToxicEnabled(t, proxy.ToxicsUpstream, "latency", false) AssertToxicEnabled(t, proxy.ToxicsDownstream, "latency", false) }) } func TestCreateDisabledProxy(t *testing.T) { WithServer(t, func(addr string) { disabledProxy := *testProxy disabledProxy.Enabled = false err := disabledProxy.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } proxy, err := client.Proxy("mysql_master") if err != nil { t.Fatal("Unable to retriecve proxy: ", err) } if proxy.Name != "mysql_master" || proxy.Listen != "localhost:3310" || proxy.Upstream != "localhost:20001" || proxy.Enabled { t.Fatalf("Unexpected proxy metadata: %s, %s, %s, %v", proxy.Name, proxy.Listen, proxy.Upstream, proxy.Enabled) } AssertProxyUp(t, proxy.Listen, false) }) } func TestCreateDisabledProxyAndEnable(t *testing.T) { WithServer(t, func(addr string) { disabledProxy := *testProxy disabledProxy.Enabled = false err := disabledProxy.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } proxy, err := client.Proxy("mysql_master") if err != nil { t.Fatal("Unable to retriecve proxy: ", err) } if proxy.Name != "mysql_master" || proxy.Listen != "localhost:3310" || proxy.Upstream != "localhost:20001" || proxy.Enabled { t.Fatalf("Unexpected proxy metadata: %s, %s, %s, %v", proxy.Name, proxy.Listen, proxy.Upstream, proxy.Enabled) } proxy.Enabled = true err = proxy.Save() if err != nil { t.Fatal("Failed to update proxy: ", err) } AssertProxyUp(t, proxy.Listen, true) proxy.Enabled = false err = proxy.Save() if err != nil { t.Fatal("Failed to update proxy: ", err) } AssertProxyUp(t, proxy.Listen, false) }) } func TestDeleteProxy(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } proxies, err := client.Proxies() if err != nil { t.Fatal("Error listing proxies: ", err) } if len(proxies) == 0 { t.Fatal("Expected new proxy in list") } AssertProxyUp(t, testProxy.Listen, true) err = testProxy.Delete() if err != nil { t.Fatal("Failed deleting proxy: ", err) } AssertProxyUp(t, testProxy.Listen, false) proxies, err = client.Proxies() if err != nil { t.Fatal("Error listing proxies: ", err) } if len(proxies) > 0 { t.Fatal("Expected proxy to be deleted from list") } }) } func TestCreateProxyPortConflict(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy") } testProxy2 := *testProxy testProxy2.Name = "test" err = testProxy2.Create() if err == nil { t.Fatal("Proxy did not result in conflict.") } else if err.Error() != "Create: HTTP 409: listen tcp 127.0.0.1:3310: bind: address already in use" { t.Fatal("Incorrect error adding proxy:", err) } err = testProxy.Delete() if err != nil { t.Fatal("Unable to delete proxy: ", err) } err = testProxy2.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } }) } func TestCreateProxyNameConflict(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } testProxy2 := *testProxy testProxy2.Listen = "localhost:3311" err = testProxy2.Create() if err == nil { t.Fatal("Proxy did not result in conflict.") } else if err.Error() != "Create: HTTP 409: Proxy with name mysql_master already exists" { t.Fatal("Incorrect error adding proxy:", err) } err = testProxy.Delete() if err != nil { t.Fatal("Unable to delete proxy: ", err) } err = testProxy2.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } }) } func TestDeleteNonExistantProxy(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Delete() if err == nil { t.Fatal("Expected error when deleting proxy that doesn't exist") } }) } func TestResetState(t *testing.T) { WithServer(t, func(addr string) { disabledProxy := *testProxy disabledProxy.Enabled = false err := disabledProxy.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } latency, err := disabledProxy.SetToxic("latency", "downstream", tclient.Toxic{ "enabled": true, "latency": 100, "jitter": 10, }) if err != nil { t.Fatal("Error setting toxic: %+v", err) } if latency["enabled"] != true { t.Fatal("Latency toxic did not start up") } if latency["latency"] != 100.0 || latency["jitter"] != 10.0 { t.Fatal("Latency toxic did not start up with correct settings") } err = client.ResetState() if err != nil { t.Fatal("unable to reset state: ", err) } proxies, err := client.Proxies() if err != nil { t.Fatal("Error listing proxies: ", err) } proxy, ok := proxies["mysql_master"] if !ok { t.Fatal("Expected proxy to still exist") } if !proxy.Enabled { t.Fatal("Expected proxy to be enabled") } toxics, err := proxy.Toxics("downstream") if err != nil { t.Fatal("Error requesting toxics: %+v", err) } latency = AssertToxicEnabled(t, toxics, "latency", false) if latency["latency"] != 100.0 || latency["jitter"] != 10.0 { t.Fatal("Latency toxic did not keep settings on reset") } AssertProxyUp(t, proxy.Listen, true) }) } func TestListingToxics(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy") } toxics, err := testProxy.Toxics("upstream") if err != nil { t.Fatal("Error returning toxics: %+v", err) } AssertToxicEnabled(t, toxics, "latency", false) }) } func TestSetToxics(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy") } latency, err := testProxy.SetToxic("latency", "downstream", tclient.Toxic{ "enabled": true, "latency": 100, "jitter": 10, }) if err != nil { t.Fatal("Error setting toxic: %+v", err) } if latency["enabled"] != true { t.Fatal("Latency toxic did not start up") } if latency["latency"] != 100.0 || latency["jitter"] != 10.0 { t.Fatal("Latency toxic did not start up with correct settings") } toxics, err := testProxy.Toxics("downstream") if err != nil { t.Fatal("Error returning toxics: %+v", err) } AssertToxicEnabled(t, toxics, "latency", true) toxics, err = testProxy.Toxics("upstream") if err != nil { t.Fatal("Error returning toxics: %+v", err) } AssertToxicEnabled(t, toxics, "latency", false) }) } func TestUpdateToxics(t *testing.T) { WithServer(t, func(addr string) { err := testProxy.Create() if err != nil { t.Fatal("Unable to create proxy: ", err) } latency, err := testProxy.SetToxic("latency", "downstream", tclient.Toxic{ "enabled": true, "latency": 100, "jitter": 10, }) if err != nil { t.Fatal("Error setting toxic: %+v", err) } if latency["enabled"] != true { t.Fatal("Latency toxic did not start up") } if latency["latency"] != 100.0 || latency["jitter"] != 10.0 { t.Fatal("Latency toxic did not start up with correct settings: %+v", latency) } latency, err = testProxy.SetToxic("latency", "downstream", tclient.Toxic{ "latency": 1000, }) if err != nil { t.Fatal("Error setting toxic: %+v", err) } if latency["enabled"] != true { t.Fatal("Latency toxic did not stay enabled") } if latency["latency"] != 1000.0 || latency["jitter"] != 10.0 { t.Fatal("Latency toxic did not get updated with the correct settings") } }) } func TestVersionEndpointReturnsVersion(t *testing.T) { WithServer(t, func(addr string) { resp, err := http.Get(addr + "/version") if err != nil { t.Fatal("Failed to get index", err) } body, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal("Unable to read body from response") } if string(body) != Version { t.Fatal("Expected to return Version from /version, got:", string(body)) } }) } func AssertToxicEnabled(t *testing.T, toxics tclient.Toxics, name string, enabled bool) tclient.Toxic { toxic, ok := toxics[name] if !ok { t.Fatalf("Expected to see %s toxic in list", name) return nil } if toxic["enabled"] != enabled { t.Fatal("%s toxic should have had enabled = %v", name, enabled) return nil } return toxic } toxiproxy-1.2.1/circle.yml000066400000000000000000000002661255450513500155750ustar00rootroot00000000000000# Don't let Circle try to do anything clever with this Go project. We isolate # with godep to avoid `go get` going nuts. dependencies: override: test: override: - make test toxiproxy-1.2.1/client/000077500000000000000000000000001255450513500150635ustar00rootroot00000000000000toxiproxy-1.2.1/client/README.md000066400000000000000000000005551255450513500163470ustar00rootroot00000000000000# toxiproxy-go This is the Go client library for the [Toxiproxy](https://github.com/shopify/toxiproxy) API. Please read the [usage section in the Toxiproxy README](https://github.com/shopify/toxiproxy#usage) before attempting to use the client. For Usage please [see the Godoc documentation](http://godoc.org/github.com/Shopify/toxiproxy/client) for the package. toxiproxy-1.2.1/client/client.go000066400000000000000000000136351255450513500167000ustar00rootroot00000000000000// Package Toxiproxy provides a client wrapper around the Toxiproxy HTTP API for // testing the resiliency of Go applications. package toxiproxy import ( "bytes" "encoding/json" "fmt" "net/http" ) // Client holds information about where to connect to Toxiproxy. type Client struct { endpoint string } type Toxic map[string]interface{} type Toxics map[string]Toxic // Proxy represents a Proxy. type Proxy struct { Name string `json:"name"` // The name of the proxy Listen string `json:"listen"` // The address the proxy listens on Upstream string `json:"upstream"` // The upstream address to proxy to Enabled bool `json:"enabled"` // Whether the proxy is enabled ToxicsUpstream Toxics `json:"upstream_toxics"` // Toxics in the upstream direction ToxicsDownstream Toxics `json:"downstream_toxics"` // Toxics in the downstream direction client *Client } // NewClient creates a new client which provides the base of all communication // with Toxiproxy. Endpoint is the address to the proxy (e.g. localhost:8474 if // not overriden) func NewClient(endpoint string) *Client { return &Client{endpoint: endpoint} } // Proxies returns a map with all the proxies and their toxics. func (client *Client) Proxies() (map[string]*Proxy, error) { resp, err := http.Get(client.endpoint + "/proxies") if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "Proxies") if err != nil { return nil, err } proxies := make(map[string]*Proxy) err = json.NewDecoder(resp.Body).Decode(&proxies) if err != nil { return nil, err } for _, proxy := range proxies { proxy.client = client } return proxies, nil } // NewProxy instantiates a new proxy instance. Note Create() must be called on // it to create it. The Enabled field must be set to true, otherwise the Proxy // will not be enabled when created. func (client *Client) NewProxy(proxy *Proxy) *Proxy { if proxy == nil { proxy = &Proxy{} } proxy.client = client return proxy } // Create creates a new proxy. func (proxy *Proxy) Create() error { request, err := json.Marshal(proxy) if err != nil { return err } resp, err := http.Post(proxy.client.endpoint+"/proxies", "application/json", bytes.NewReader(request)) if err != nil { return err } err = checkError(resp, http.StatusCreated, "Create") if err != nil { return err } proxy = new(Proxy) err = json.NewDecoder(resp.Body).Decode(&proxy) if err != nil { return err } return nil } // Proxy returns a proxy by name. func (client *Client) Proxy(name string) (*Proxy, error) { // TODO url encode resp, err := http.Get(client.endpoint + "/proxies/" + name) if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "Proxy") if err != nil { return nil, err } proxy := client.NewProxy(nil) err = json.NewDecoder(resp.Body).Decode(proxy) if err != nil { return nil, err } return proxy, nil } // Save saves changes to a proxy such as its enabled status. func (proxy *Proxy) Save() error { request, err := json.Marshal(proxy) if err != nil { return err } resp, err := http.Post(proxy.client.endpoint+"/proxies/"+proxy.Name, "application/json", bytes.NewReader(request)) if err != nil { return err } err = checkError(resp, http.StatusOK, "Save") if err != nil { return err } err = json.NewDecoder(resp.Body).Decode(proxy) if err != nil { return err } return nil } // Delete a proxy which will cause it to stop listening and delete all // information associated with it. If you just wish to stop and later enable a // proxy, set the `Enabled` field to `false` and call `Save()`. func (proxy *Proxy) Delete() error { httpClient := &http.Client{} req, err := http.NewRequest("DELETE", proxy.client.endpoint+"/proxies/"+proxy.Name, nil) if err != nil { return err } resp, err := httpClient.Do(req) if err != nil { return err } return checkError(resp, http.StatusNoContent, "Delete") } // Toxics returns a map of all the toxics and their attributes for a direction. func (proxy *Proxy) Toxics(direction string) (Toxics, error) { resp, err := http.Get(proxy.client.endpoint + "/proxies/" + proxy.Name + "/" + direction + "/toxics") if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "Toxics") if err != nil { return nil, err } toxics := make(Toxics) err = json.NewDecoder(resp.Body).Decode(&toxics) if err != nil { return nil, err } return toxics, nil } // SetToxic sets the parameters for a toxic with a given name in the direction. // See https://github.com/Shopify/toxiproxy#toxics for a list of all Toxics. func (proxy *Proxy) SetToxic(name string, direction string, toxic Toxic) (Toxic, error) { request, err := json.Marshal(toxic) if err != nil { return nil, err } resp, err := http.Post(proxy.client.endpoint+"/proxies/"+proxy.Name+"/"+direction+"/toxics/"+name, "application/json", bytes.NewReader(request)) if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "SetToxic") if err != nil { return nil, err } toxics := make(Toxic) err = json.NewDecoder(resp.Body).Decode(&toxics) if err != nil { return nil, err } return toxics, nil } // ResetState resets the state of all proxies and toxics in Toxiproxy. func (client *Client) ResetState() error { resp, err := http.Get(client.endpoint + "/reset") if err != nil { return err } return checkError(resp, http.StatusNoContent, "ResetState") } type ApiError struct { Title string `json:"title"` Status int `json:"status"` } func (err *ApiError) Error() string { return fmt.Sprintf("HTTP %d: %s", err.Status, err.Title) } func checkError(resp *http.Response, expectedCode int, caller string) error { if resp.StatusCode != expectedCode { apiError := new(ApiError) err := json.NewDecoder(resp.Body).Decode(apiError) if err != nil { apiError.Title = fmt.Sprintf("Unexpected response code, expected %d", expectedCode) apiError.Status = resp.StatusCode } return fmt.Errorf("%s: %v", caller, apiError) } return nil } toxiproxy-1.2.1/io_chan.go000066400000000000000000000030521255450513500155340ustar00rootroot00000000000000package main import ( "io" "time" ) // Stores a slice of bytes with its receive timestmap type StreamChunk struct { data []byte timestamp time.Time } // Implements the io.WriteCloser interface for a chan []byte type ChanWriter struct { output chan<- *StreamChunk } func NewChanWriter(output chan<- *StreamChunk) *ChanWriter { return &ChanWriter{output} } func (c *ChanWriter) Write(buf []byte) (int, error) { packet := &StreamChunk{make([]byte, len(buf)), time.Now()} copy(packet.data, buf) // Make a copy before sending it to the channel c.output <- packet return len(buf), nil } func (c *ChanWriter) Close() error { close(c.output) return nil } // Implements the io.Reader interface for a chan []byte type ChanReader struct { input <-chan *StreamChunk buffer []byte } func NewChanReader(input <-chan *StreamChunk) *ChanReader { return &ChanReader{input, []byte{}} } func (c *ChanReader) Read(out []byte) (int, error) { if c.buffer == nil { return 0, io.EOF } n := copy(out, c.buffer) c.buffer = c.buffer[n:] if len(out) <= len(c.buffer) { return n, nil } else if n > 0 { // We have some data to return, so make the channel read optional select { case p := <-c.input: if p == nil { // Stream was closed c.buffer = nil return n, io.EOF } n2 := copy(out[n:], p.data) c.buffer = p.data[n2:] return n + n2, nil default: return n, nil } } p := <-c.input if p == nil { // Stream was closed c.buffer = nil return 0, io.EOF } n2 := copy(out[n:], p.data) c.buffer = p.data[n2:] return n + n2, nil } toxiproxy-1.2.1/io_chan_test.go000066400000000000000000000075051255450513500166020ustar00rootroot00000000000000package main import ( "bytes" "io" "testing" ) func TestBasicReadWrite(t *testing.T) { send := []byte("hello world") c := make(chan *StreamChunk) writer := NewChanWriter(c) reader := NewChanReader(c) go writer.Write(send) buf := make([]byte, len(send)) n, err := reader.Read(buf) if n != len(send) { t.Fatalf("Read wrong number of bytes: %d expected %d", n, len(send)) } if err != nil { t.Fatal("Couldn't read from stream", err) } if !bytes.Equal(buf, send) { t.Fatal("Got wrong message from stream", string(buf)) } writer.Close() n, err = reader.Read(buf) if err != io.EOF { t.Fatal("Read returned wrong error after close:", err) } if n != 0 { t.Fatalf("Read still returned data after close: %d bytes", n) } } func TestReadMoreThanWrite(t *testing.T) { send := []byte("hello world") c := make(chan *StreamChunk) writer := NewChanWriter(c) reader := NewChanReader(c) go writer.Write(send) buf := make([]byte, len(send)+10) n, err := reader.Read(buf) if n != len(send) { t.Fatalf("Read wrong number of bytes: %d expected %d", n, len(send)) } if err != nil { t.Fatal("Couldn't read from stream", err) } if !bytes.Equal(buf[:n], send) { t.Fatal("Got wrong message from stream", string(buf[:n])) } writer.Close() n, err = reader.Read(buf) if err != io.EOF { t.Fatal("Read returned wrong error after close:", err) } if n != 0 { t.Fatalf("Read still returned data after close: %d bytes", n) } } func TestReadLessThanWrite(t *testing.T) { send := []byte("hello world") c := make(chan *StreamChunk) writer := NewChanWriter(c) reader := NewChanReader(c) go writer.Write(send) buf := make([]byte, 6) n, err := reader.Read(buf) if n != len(buf) { t.Fatalf("Read wrong number of bytes: %d expected %d", n, len(buf)) } if err != nil { t.Fatal("Couldn't read from stream", err) } if !bytes.Equal(buf, send[:len(buf)]) { t.Fatal("Got wrong message from stream", string(buf)) } writer.Close() n, err = reader.Read(buf) if n != len(send)-len(buf) { t.Fatalf("Read wrong number of bytes: %d expected %d", n, len(send)-len(buf)) } if err != io.EOF { t.Fatal("Read returned wrong error after close:", err) } if !bytes.Equal(buf[:n], send[len(buf):]) { t.Fatal("Got wrong message from stream", string(buf[:n])) } n, err = reader.Read(buf) if err != io.EOF { t.Fatal("Read returned wrong error after close:", err) } if n != 0 { t.Fatalf("Read still returned data after close: %d bytes", n) } } func TestMultiReadWrite(t *testing.T) { send := []byte("hello world, this message is longer") c := make(chan *StreamChunk) writer := NewChanWriter(c) reader := NewChanReader(c) go func() { writer.Write(send[:9]) writer.Write(send[9:19]) writer.Write(send[19:]) writer.Close() }() buf := make([]byte, 10) read := 0 for i := 0; i < len(send)/10; i++ { n, err := reader.Read(buf) if err != nil { t.Fatal("Couldn't read from stream", err) } if !bytes.Equal(buf[:n], send[read:read+n]) { t.Fatal("Got wrong message from stream", string(buf)) } read += n } n, err := reader.Read(buf) if err != io.EOF { t.Fatal("Read returned wrong error after close:", err) } if !bytes.Equal(buf[:n], send[len(send)-n:]) { t.Fatal("Got wrong message from stream", string(buf[:n])) } } func TestMultiWriteWithCopy(t *testing.T) { send := []byte("hello world, this message is longer") c := make(chan *StreamChunk) writer := NewChanWriter(c) reader := NewChanReader(c) go func() { writer.Write(send[:9]) writer.Write(send[9:19]) writer.Write(send[19:]) writer.Close() }() buf := new(bytes.Buffer) n, err := io.Copy(buf, reader) if int(n) != len(send) { t.Fatalf("Read wrong number of bytes: %d expected %d", n, len(send)) } if err != nil { t.Fatal("Couldn't read from stream", err) } if !bytes.Equal(buf.Bytes(), send) { t.Fatal("Got wrong message from stream", buf.String()) } } toxiproxy-1.2.1/link.go000066400000000000000000000040651255450513500150760ustar00rootroot00000000000000package main import ( "io" "github.com/Sirupsen/logrus" ) // ToxicLinks are single direction pipelines that connects an input and output via // a chain of toxics. There is a fixed number of toxics in the chain, such that a // toxic always maps to the same toxic stub. Toxics are replaced with noops when // disabled. // // NoopToxic LatencyToxic NoopToxic // v v v // Input > ToxicStub > ToxicStub > ToxicStub > Output // type ToxicLink struct { stubs []*ToxicStub proxy *Proxy toxics *ToxicCollection input *ChanWriter output *ChanReader } func NewToxicLink(proxy *Proxy, toxics *ToxicCollection) *ToxicLink { link := &ToxicLink{ stubs: make([]*ToxicStub, len(toxics.chain)), proxy: proxy, toxics: toxics, } // Initialize the link with ToxicStubs last := make(chan *StreamChunk, 1024) link.input = NewChanWriter(last) for i := 0; i < len(link.stubs); i++ { next := make(chan *StreamChunk, 1024) link.stubs[i] = NewToxicStub(last, next) last = next } link.output = NewChanReader(last) return link } // Start the link with the specified toxics func (link *ToxicLink) Start(name string, source io.Reader, dest io.WriteCloser) { go func() { bytes, err := io.Copy(link.input, source) if err != nil { logrus.WithFields(logrus.Fields{ "name": link.proxy.Name, "upstream": link.proxy.Upstream, "bytes": bytes, "err": err, }).Warn("Source terminated") } link.input.Close() }() for i, toxic := range link.toxics.chain { go link.stubs[i].Run(toxic) } go func() { bytes, err := io.Copy(dest, link.output) if err != nil { logrus.WithFields(logrus.Fields{ "name": link.proxy.Name, "upstream": link.proxy.Upstream, "bytes": bytes, "err": err, }).Warn("Destination terminated") } dest.Close() link.toxics.RemoveLink(name) link.proxy.RemoveConnection(name) }() } // Replace the toxic at the specified index func (link *ToxicLink) SetToxic(toxic Toxic, index int) { if link.stubs[index].Interrupt() { go link.stubs[index].Run(toxic) } } toxiproxy-1.2.1/proxy.go000066400000000000000000000120361255450513500153170ustar00rootroot00000000000000package main import ( "errors" "sync" "github.com/Sirupsen/logrus" "gopkg.in/tomb.v1" "net" ) // Proxy represents the proxy in its entirity with all its links. The main // responsibility of Proxy is to accept new client and create Links between the // client and upstream. // // Client <-> toxiproxy <-> Upstream // type Proxy struct { sync.Mutex Name string `json:"name"` Listen string `json:"listen"` Upstream string `json:"upstream"` Enabled bool `json:"enabled"` started chan error tomb tomb.Tomb connections ConnectionList upToxics *ToxicCollection downToxics *ToxicCollection } type ConnectionList struct { list map[string]net.Conn lock sync.Mutex } func (c *ConnectionList) Lock() { c.lock.Lock() } func (c *ConnectionList) Unlock() { c.lock.Unlock() } var ErrProxyAlreadyStarted = errors.New("Proxy already started") func NewProxy() *Proxy { proxy := &Proxy{ started: make(chan error), connections: ConnectionList{list: make(map[string]net.Conn)}, } proxy.upToxics = NewToxicCollection(proxy) proxy.downToxics = NewToxicCollection(proxy) return proxy } func (proxy *Proxy) Start() error { proxy.Lock() defer proxy.Unlock() return start(proxy) } func (proxy *Proxy) Update(input *Proxy) error { proxy.Lock() defer proxy.Unlock() if input.Listen != proxy.Listen || input.Upstream != proxy.Upstream { stop(proxy) proxy.Listen = input.Listen proxy.Upstream = input.Upstream } if input.Enabled != proxy.Enabled { if input.Enabled { return start(proxy) } stop(proxy) } return nil } func (proxy *Proxy) Stop() { proxy.Lock() defer proxy.Unlock() stop(proxy) } // server runs the Proxy server, accepting new clients and creating Links to // connect them to upstreams. func (proxy *Proxy) server() { ln, err := net.Listen("tcp", proxy.Listen) if err != nil { proxy.started <- err return } proxy.Listen = ln.Addr().String() proxy.started <- nil logrus.WithFields(logrus.Fields{ "name": proxy.Name, "proxy": proxy.Listen, "upstream": proxy.Upstream, }).Info("Started proxy") acceptTomb := tomb.Tomb{} defer acceptTomb.Done() // This channel is to kill the blocking Accept() call below by closing the // net.Listener. go func() { <-proxy.tomb.Dying() // Notify ln.Accept() that the shutdown was safe acceptTomb.Killf("Shutting down from stop()") // Unblock ln.Accept() err := ln.Close() if err != nil { logrus.WithFields(logrus.Fields{ "proxy": proxy.Name, "listen": proxy.Listen, "err": err, }).Warn("Attempted to close an already closed proxy server") } // Wait for the accept loop to finish processing acceptTomb.Wait() proxy.tomb.Done() }() for { client, err := ln.Accept() if err != nil { // This is to confirm we're being shut down in a legit way. Unfortunately, // Go doesn't export the error when it's closed from Close() so we have to // sync up with a channel here. // // See http://zhen.org/blog/graceful-shutdown-of-go-net-dot-listeners/ select { case <-acceptTomb.Dying(): default: logrus.WithFields(logrus.Fields{ "proxy": proxy.Name, "listen": proxy.Listen, "err": err, }).Warn("Error while accepting client") } return } logrus.WithFields(logrus.Fields{ "name": proxy.Name, "client": client.RemoteAddr(), "proxy": proxy.Listen, "upstream": proxy.Upstream, }).Info("Accepted client") upstream, err := net.Dial("tcp", proxy.Upstream) if err != nil { logrus.WithFields(logrus.Fields{ "name": proxy.Name, "client": client.RemoteAddr(), "proxy": proxy.Listen, "upstream": proxy.Upstream, }).Error("Unable to open connection to upstream") client.Close() continue } name := client.RemoteAddr().String() proxy.connections.Lock() proxy.connections.list[name+"client"] = client proxy.connections.list[name+"upstream"] = upstream proxy.connections.Unlock() proxy.upToxics.StartLink(name+"client", client, upstream) proxy.downToxics.StartLink(name+"upstream", upstream, client) } } func (proxy *Proxy) RemoveConnection(name string) { proxy.connections.Lock() defer proxy.connections.Unlock() delete(proxy.connections.list, name) } // Starts a proxy, assumes the lock has already been taken func start(proxy *Proxy) error { if proxy.Enabled { return ErrProxyAlreadyStarted } proxy.tomb = tomb.Tomb{} // Reset tomb, from previous starts/stops go proxy.server() err := <-proxy.started // Only enable the proxy if it successfully started proxy.Enabled = err == nil return err } // Stops a proxy, assumes the lock has already been taken func stop(proxy *Proxy) { if !proxy.Enabled { return } proxy.Enabled = false proxy.tomb.Killf("Shutting down from stop()") proxy.tomb.Wait() // Wait until we stop accepting new connections proxy.connections.Lock() defer proxy.connections.Unlock() for _, conn := range proxy.connections.list { conn.Close() } logrus.WithFields(logrus.Fields{ "name": proxy.Name, "proxy": proxy.Listen, "upstream": proxy.Upstream, }).Info("Terminated proxy") } toxiproxy-1.2.1/proxy_collection.go000066400000000000000000000041641255450513500175350ustar00rootroot00000000000000package main import ( "fmt" "sync" ) // ProxyCollection is a collection of proxies. It's the interface for anything // to add and remove proxies from the toxiproxy instance. It's responsibilty is // to maintain the integrity of the proxy set, by guarding for things such as // duplicate names. type ProxyCollection struct { sync.RWMutex proxies map[string]*Proxy } func NewProxyCollection() *ProxyCollection { return &ProxyCollection{ proxies: make(map[string]*Proxy), } } func (collection *ProxyCollection) Add(proxy *Proxy, start bool) error { collection.Lock() defer collection.Unlock() if _, exists := collection.proxies[proxy.Name]; exists { return fmt.Errorf("Proxy with name %s already exists", proxy.Name) } if start { err := proxy.Start() if err != nil { return err } } collection.proxies[proxy.Name] = proxy return nil } func (collection *ProxyCollection) Proxies() map[string]*Proxy { collection.RLock() defer collection.RUnlock() // Copy the map since using the existing one isn't thread-safe proxies := make(map[string]*Proxy, len(collection.proxies)) for k, v := range collection.proxies { proxies[k] = v } return proxies } func (collection *ProxyCollection) Get(name string) (*Proxy, error) { collection.RLock() defer collection.RUnlock() return collection.getByName(name) } func (collection *ProxyCollection) Remove(name string) error { collection.Lock() defer collection.Unlock() proxy, err := collection.getByName(name) if err != nil { return err } proxy.Stop() delete(collection.proxies, proxy.Name) return nil } func (collection *ProxyCollection) Clear() error { collection.Lock() defer collection.Unlock() for _, proxy := range collection.proxies { proxy.Stop() delete(collection.proxies, proxy.Name) } return nil } // getByName returns a proxy by its name. Its used from #remove and #get. // It assumes the lock has already been acquired. func (collection *ProxyCollection) getByName(name string) (*Proxy, error) { proxy, exists := collection.proxies[name] if !exists { return nil, fmt.Errorf("Proxy with name %s doesn't exist", name) } return proxy, nil } toxiproxy-1.2.1/proxy_collection_test.go000066400000000000000000000054051255450513500205730ustar00rootroot00000000000000package main import ( "bytes" "net" "testing" ) func TestAddProxyToCollection(t *testing.T) { collection := NewProxyCollection() proxy := NewTestProxy("test", "localhost:20000") if _, err := collection.Get(proxy.Name); err == nil { t.Error("Expected proxies to be empty") } err := collection.Add(proxy, false) if err != nil { t.Error("Expected to be able to add first proxy to collection") } if _, err := collection.Get(proxy.Name); err != nil { t.Error("Expected proxy to be added to map") } } func TestAddTwoProxiesToCollection(t *testing.T) { collection := NewProxyCollection() proxy := NewTestProxy("test", "localhost:20000") err := collection.Add(proxy, false) if err != nil { t.Error("Expected to be able to add first proxy to collection") } err = collection.Add(proxy, false) if err == nil { t.Error("Expected to not be able to add proxy with same name") } } func TestListProxies(t *testing.T) { collection := NewProxyCollection() proxy := NewTestProxy("test", "localhost:20000") err := collection.Add(proxy, false) if err != nil { t.Error("Expected to be able to add first proxy to collection") } proxies := collection.Proxies() proxy, ok := proxies[proxy.Name] if !ok { t.Error("Expected to be able to see existing proxy") } else if proxy.Enabled { t.Error("Expected proxy not to be running") } } func TestAddProxyAndStart(t *testing.T) { collection := NewProxyCollection() proxy := NewTestProxy("test", "localhost:20000") err := collection.Add(proxy, true) if err != nil { t.Error("Expected to be able to add proxy to collection:", err) } proxies := collection.Proxies() proxy, ok := proxies[proxy.Name] if !ok { t.Error("Expected to be able to see existing proxy") } else if !proxy.Enabled { t.Error("Expected proxy to be running") } } func TestAddAndRemoveProxyFromCollection(t *testing.T) { WithTCPProxy(t, func(conn net.Conn, response chan []byte, proxy *Proxy) { collection := NewProxyCollection() if _, err := collection.Get(proxy.Name); err == nil { t.Error("Expected proxies to be empty") } err := collection.Add(proxy, false) if err != nil { t.Error("Expected to be able to add first proxy to collection") } if _, err := collection.Get(proxy.Name); err != nil { t.Error("Expected proxy to be added to map") } msg := []byte("go away") _, err = conn.Write(msg) if err != nil { t.Error("Failed writing to socket to shut down server") } conn.Close() resp := <-response if !bytes.Equal(resp, msg) { t.Error("Server didn't read bytes from client") } err = collection.Remove(proxy.Name) if err != nil { t.Error("Expected to remove proxy from collection") } if _, err := collection.Get(proxy.Name); err == nil { t.Error("Expected proxies to be empty") } }) } toxiproxy-1.2.1/proxy_test.go000066400000000000000000000155451255450513500163660ustar00rootroot00000000000000package main import ( "bytes" "encoding/hex" "io" "io/ioutil" "net" "testing" "time" "github.com/Sirupsen/logrus" "gopkg.in/tomb.v1" ) func init() { logrus.SetLevel(logrus.FatalLevel) } func NewTestProxy(name, upstream string) *Proxy { proxy := NewProxy() proxy.Name = name proxy.Listen = "localhost:0" proxy.Upstream = upstream return proxy } func WithTCPServer(t *testing.T, f func(string, chan []byte)) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() response := make(chan []byte, 1) tomb := tomb.Tomb{} go func() { defer tomb.Done() src, err := ln.Accept() if err != nil { select { case <-tomb.Dying(): default: t.Fatal("Failed to accept client") } return } ln.Close() val, err := ioutil.ReadAll(src) if err != nil { t.Fatal("Failed to read from client") } response <- val }() f(ln.Addr().String(), response) tomb.Killf("Function body finished") ln.Close() tomb.Wait() close(response) } func TestSimpleServer(t *testing.T) { WithTCPServer(t, func(addr string, response chan []byte) { conn, err := net.Dial("tcp", addr) if err != nil { t.Error("Unable to dial TCP server", err) } msg := []byte("hello world") _, err = conn.Write(msg) if err != nil { t.Error("Failed writing to TCP server", err) } err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } resp := <-response if !bytes.Equal(resp, msg) { t.Error("Server didn't read bytes from client") } }) } func WithTCPProxy(t *testing.T, f func(proxy net.Conn, response chan []byte, proxyServer *Proxy)) { WithTCPServer(t, func(upstream string, response chan []byte) { proxy := NewTestProxy("test", upstream) proxy.Start() conn := AssertProxyUp(t, proxy.Listen, true) f(conn, response, proxy) proxy.Stop() }) } func TestProxySimpleMessage(t *testing.T) { WithTCPProxy(t, func(conn net.Conn, response chan []byte, proxy *Proxy) { msg := []byte("hello world") _, err := conn.Write(msg) if err != nil { t.Error("Failed writing to TCP server", err) } err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } resp := <-response if !bytes.Equal(resp, msg) { t.Error("Server didn't read correct bytes from client", resp) } }) } func TestProxyToDownUpstream(t *testing.T) { proxy := NewTestProxy("test", "localhost:20009") proxy.Start() conn := AssertProxyUp(t, proxy.Listen, true) // Check to make sure the connection is closed conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) _, err := conn.Read(make([]byte, 1)) if err != io.EOF { t.Error("Proxy did not close connection when upstream down", err) } proxy.Stop() } func TestProxyBigMessage(t *testing.T) { WithTCPProxy(t, func(conn net.Conn, response chan []byte, proxy *Proxy) { buf := make([]byte, 32*1024) msg := make([]byte, len(buf)*2) hex.Encode(msg, buf) _, err := conn.Write(msg) if err != nil { t.Error("Failed writing to TCP server", err) } err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } resp := <-response if !bytes.Equal(resp, msg) { t.Error("Server didn't read correct bytes from client", resp) } }) } func TestProxyTwoPartMessage(t *testing.T) { WithTCPProxy(t, func(conn net.Conn, response chan []byte, proxy *Proxy) { msg1 := []byte("hello world") msg2 := []byte("hello world") _, err := conn.Write(msg1) if err != nil { t.Error("Failed writing to TCP server", err) } _, err = conn.Write(msg2) if err != nil { t.Error("Failed writing to TCP server", err) } err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } msg1 = append(msg1, msg2...) resp := <-response if !bytes.Equal(resp, msg1) { t.Error("Server didn't read correct bytes from client", resp) } }) } func TestClosingProxyMultipleTimes(t *testing.T) { WithTCPProxy(t, func(conn net.Conn, response chan []byte, proxy *Proxy) { proxy.Stop() proxy.Stop() proxy.Stop() }) } func TestStartTwoProxiesOnSameAddress(t *testing.T) { WithTCPProxy(t, func(conn net.Conn, response chan []byte, proxy *Proxy) { proxy2 := NewTestProxy("proxy_2", "localhost:3306") proxy2.Listen = proxy.Listen if err := proxy2.Start(); err == nil { t.Fatal("Expected an err back from start") } }) } func TestStopProxyBeforeStarting(t *testing.T) { WithTCPServer(t, func(upstream string, response chan []byte) { proxy := NewTestProxy("test", upstream) AssertProxyUp(t, proxy.Listen, false) proxy.Stop() err := proxy.Start() if err != nil { t.Error("Proxy failed to start", err) } err = proxy.Start() if err != ErrProxyAlreadyStarted { t.Error("Proxy did not fail to start when already started", err) } AssertProxyUp(t, proxy.Listen, true) proxy.Stop() AssertProxyUp(t, proxy.Listen, false) }) } func TestProxyUpdate(t *testing.T) { WithTCPServer(t, func(upstream string, response chan []byte) { proxy := NewTestProxy("test", upstream) err := proxy.Start() if err != nil { t.Error("Proxy failed to start", err) } AssertProxyUp(t, proxy.Listen, true) before := proxy.Listen input := &Proxy{Listen: "localhost:0", Upstream: proxy.Upstream, Enabled: true} err = proxy.Update(input) if err != nil { t.Error("Failed to update proxy", err) } if proxy.Listen == before || proxy.Listen == input.Listen { t.Errorf("Proxy update didn't change listen address: %s to %s", before, proxy.Listen) } AssertProxyUp(t, proxy.Listen, true) input.Listen = proxy.Listen err = proxy.Update(input) if err != nil { t.Error("Failed to update proxy", err) } AssertProxyUp(t, proxy.Listen, true) input.Enabled = false err = proxy.Update(input) if err != nil { t.Error("Failed to update proxy", err) } AssertProxyUp(t, proxy.Listen, false) }) } func TestRestartFailedToStartProxy(t *testing.T) { WithTCPServer(t, func(upstream string, response chan []byte) { proxy := NewTestProxy("test", upstream) conflict := NewTestProxy("test2", upstream) err := conflict.Start() if err != nil { t.Error("Proxy failed to start", err) } AssertProxyUp(t, conflict.Listen, true) proxy.Listen = conflict.Listen err = proxy.Start() if err == nil || err == ErrProxyAlreadyStarted { t.Error("Proxy started when it should have conflicted") } conflict.Stop() AssertProxyUp(t, conflict.Listen, false) err = proxy.Start() if err != nil { t.Error("Proxy failed to start after conflict went away", err) } AssertProxyUp(t, proxy.Listen, true) proxy.Stop() AssertProxyUp(t, proxy.Listen, false) }) } func AssertProxyUp(t *testing.T, addr string, up bool) net.Conn { conn, err := net.Dial("tcp", addr) if err != nil && up { t.Error("Expected proxy to be up", err) } else if err == nil && !up { t.Error("Expected proxy to be down") } return conn } toxiproxy-1.2.1/share/000077500000000000000000000000001255450513500147075ustar00rootroot00000000000000toxiproxy-1.2.1/share/toxiproxy.conf000066400000000000000000000004141255450513500176420ustar00rootroot00000000000000description "TCP proxy to simulate network and system conditions" author "Simon Eskildsen & Jacob Wirth" start on startup stop on shutdown env HOST="localhost" env PORT="8474" env BINARY="/usr/bin/toxiproxy" script exec $BINARY -port $PORT -host $HOST end script toxiproxy-1.2.1/testing/000077500000000000000000000000001255450513500152625ustar00rootroot00000000000000toxiproxy-1.2.1/testing/benchmark_test.go000066400000000000000000000037701255450513500206110ustar00rootroot00000000000000package main import ( "io/ioutil" "net/http" "testing" ) // Benchmark numbers: // // 1x Toxics: // BenchmarkDirect 2000 694467 ns/op // BenchmarkProxy 2000 1136668 ns/op // BenchmarkDirectSmall 5000 423319 ns/op // BenchmarkProxySmall 2000 769262 ns/op // // 5x Toxics: // BenchmarkDirect 5000 695102 ns/op // BenchmarkProxy 2000 1232454 ns/op // BenchmarkDirectSmall 5000 424712 ns/op // BenchmarkProxySmall 2000 798016 ns/op // Test the backend server directly, use 64k random endpoint func BenchmarkDirect(b *testing.B) { client := http.Client{} for i := 0; i < b.N; i++ { resp, err := client.Get("http://localhost:20002/test1") if err != nil { b.Fatal(err) } _, err = ioutil.ReadAll(resp.Body) if err != nil { b.Fatal(err) } resp.Body.Close() } } // Test the backend through toxiproxy, use 64k random endpoint func BenchmarkProxy(b *testing.B) { client := http.Client{} for i := 0; i < b.N; i++ { resp, err := client.Get("http://localhost:20000/test1") if err != nil { b.Fatal(err) } _, err = ioutil.ReadAll(resp.Body) if err != nil { b.Fatal(err) } resp.Body.Close() } } // Test the backend server directly, use "hello world" endpoint func BenchmarkDirectSmall(b *testing.B) { client := http.Client{} for i := 0; i < b.N; i++ { resp, err := client.Get("http://localhost:20002/test2") if err != nil { b.Fatal(err) } _, err = ioutil.ReadAll(resp.Body) if err != nil { b.Fatal(err) } resp.Body.Close() } } // Test the backend through toxiproxy, use "hello world" endpoint func BenchmarkProxySmall(b *testing.B) { client := http.Client{} for i := 0; i < b.N; i++ { resp, err := client.Get("http://localhost:20000/test2") if err != nil { b.Fatal(err) } _, err = ioutil.ReadAll(resp.Body) if err != nil { b.Fatal(err) } resp.Body.Close() } } toxiproxy-1.2.1/testing/endpoint.go000066400000000000000000000014041255450513500174300ustar00rootroot00000000000000package main import ( "encoding/hex" "fmt" "net/http" ) var stuff []byte var out []byte var out2 []byte func handler1(w http.ResponseWriter, r *http.Request) { n, err := w.Write(out) if n != len(out) { fmt.Println("Short write!") } if err != nil { fmt.Println(err) } } func handler2(w http.ResponseWriter, r *http.Request) { n, err := w.Write(out2) if n != len(out2) { fmt.Println("Short write!") } if err != nil { fmt.Println(err) } } func main() { stuff = make([]byte, 32*1024) out = make([]byte, len(stuff)*2) out2 = []byte("hello world") for i := 0; i < len(stuff); i++ { stuff[i] = byte(i % 256) } hex.Encode(out, stuff) http.HandleFunc("/test1", handler1) http.HandleFunc("/test2", handler2) http.ListenAndServe(":20002", nil) } toxiproxy-1.2.1/toxic.go000066400000000000000000000036321255450513500152660ustar00rootroot00000000000000package main // A Toxic is something that can be attatched to a link to modify the way // data can be passed through (for example, by adding latency) // // Toxic // v // Client <-> ToxicStub <-> Upstream // // Toxic's work in a pipeline fashion, and can be chained together // with channels. The toxic itself only defines the settings and // Pipe() function definition, and uses the ToxicStub struct to store // per-connection information. This allows the same toxic to be used // for multiple connections. type Toxic interface { // Return the unique name of the toxic, as used by the json api. Name() string // Returns true if the toxic is enabled. Disabled toxics are not used and are replaced with NoopToxics. IsEnabled() bool // Sets the enabled field of the toxic, does not replace the toxic when set. SetEnabled(bool) // Defines how packets flow through a ToxicStub. Pipe() blocks until the link is closed or interrupted. Pipe(*ToxicStub) } type ToxicStub struct { input <-chan *StreamChunk output chan<- *StreamChunk interrupt chan struct{} running chan struct{} closed chan struct{} } func NewToxicStub(input <-chan *StreamChunk, output chan<- *StreamChunk) *ToxicStub { return &ToxicStub{ interrupt: make(chan struct{}), closed: make(chan struct{}), input: input, output: output, } } // Begin running a toxic on this stub, can be interrupted. func (s *ToxicStub) Run(toxic Toxic) { s.running = make(chan struct{}) defer close(s.running) toxic.Pipe(s) } // Interrupt the flow of data so that the toxic controlling the stub can be replaced. // Returns true if the stream was successfully interrupted. func (s *ToxicStub) Interrupt() bool { select { case <-s.closed: return false case s.interrupt <- struct{}{}: <-s.running // Wait for the running toxic to exit return true } } func (s *ToxicStub) Close() { close(s.closed) close(s.output) } toxiproxy-1.2.1/toxic_bandwidth.go000066400000000000000000000027321255450513500173120ustar00rootroot00000000000000package main import "time" // The BandwidthToxic passes data through at a limited rate type BandwidthToxic struct { Enabled bool `json:"enabled"` // Rate in KB/s Rate int64 `json:"rate"` } func (t *BandwidthToxic) Name() string { return "bandwidth" } func (t *BandwidthToxic) IsEnabled() bool { return t.Enabled } func (t *BandwidthToxic) SetEnabled(enabled bool) { t.Enabled = enabled } func (t *BandwidthToxic) Pipe(stub *ToxicStub) { var sleep time.Duration = 0 for { select { case <-stub.interrupt: return case p := <-stub.input: if p == nil { stub.Close() return } if t.Rate <= 0 { sleep = 0 } else { sleep += time.Duration(len(p.data)) * time.Millisecond / time.Duration(t.Rate) } // If the rate is low enough, split the packet up and send in 100 millisecond intervals for int64(len(p.data)) > t.Rate*100 { select { case <-time.After(100 * time.Millisecond): stub.output <- &StreamChunk{p.data[:t.Rate*100], p.timestamp} p.data = p.data[t.Rate*100:] sleep -= 100 * time.Millisecond case <-stub.interrupt: stub.output <- p // Don't drop any data on the floor return } } start := time.Now() select { case <-time.After(sleep): // time.After only seems to have ~1ms prevision, so offset the next sleep by the error sleep -= time.Now().Sub(start) stub.output <- p case <-stub.interrupt: stub.output <- p // Don't drop any data on the floor return } } } } toxiproxy-1.2.1/toxic_collection.go000066400000000000000000000046101255450513500174760ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "io" "sync" ) type ToxicCollection struct { sync.Mutex noop *NoopToxic proxy *Proxy chain []Toxic toxics []Toxic links map[string]*ToxicLink } func NewToxicCollection(proxy *Proxy) *ToxicCollection { toxicOrder := []Toxic{ new(SlowCloseToxic), new(LatencyToxic), new(BandwidthToxic), new(SlicerToxic), new(TimeoutToxic), } collection := &ToxicCollection{ noop: new(NoopToxic), proxy: proxy, chain: make([]Toxic, len(toxicOrder)), toxics: toxicOrder, links: make(map[string]*ToxicLink), } for i := 0; i < len(collection.chain); i++ { collection.chain[i] = collection.noop } return collection } func (c *ToxicCollection) ResetToxics() { c.Lock() defer c.Unlock() for index, toxic := range c.toxics { toxic.SetEnabled(false) c.setToxic(toxic, index) } } func (c *ToxicCollection) GetToxicMap() map[string]Toxic { result := make(map[string]Toxic) for _, toxic := range c.toxics { result[toxic.Name()] = toxic } return result } func (c *ToxicCollection) SetToxicJson(name string, data io.Reader) (Toxic, error) { c.Lock() defer c.Unlock() for index, toxic := range c.toxics { if toxic.Name() == name { err := json.NewDecoder(data).Decode(toxic) if err != nil { return nil, err } c.setToxic(toxic, index) return toxic, nil } } return nil, fmt.Errorf("Bad toxic type: %s", name) } func (c *ToxicCollection) SetToxicValue(toxic Toxic) error { c.Lock() defer c.Unlock() for index, toxic2 := range c.toxics { if toxic2.Name() == toxic.Name() { c.setToxic(toxic, index) return nil } } return fmt.Errorf("Bad toxic type: %v", toxic) } // Assumes lock has already been grabbed func (c *ToxicCollection) setToxic(toxic Toxic, index int) { if !toxic.IsEnabled() { c.chain[index] = c.noop } else { c.chain[index] = toxic } // Asynchronously update the toxic in each link group := sync.WaitGroup{} for _, link := range c.links { group.Add(1) go func(link *ToxicLink) { defer group.Done() link.SetToxic(c.chain[index], index) }(link) } group.Wait() } func (c *ToxicCollection) StartLink(name string, input io.Reader, output io.WriteCloser) { c.Lock() defer c.Unlock() link := NewToxicLink(c.proxy, c) link.Start(name, input, output) c.links[name] = link } func (c *ToxicCollection) RemoveLink(name string) { c.Lock() defer c.Unlock() delete(c.links, name) } toxiproxy-1.2.1/toxic_latency.go000066400000000000000000000021401255450513500167760ustar00rootroot00000000000000package main import ( "math/rand" "time" ) // The LatencyToxic passes data through with the a delay of latency +/- jitter added. type LatencyToxic struct { Enabled bool `json:"enabled"` // Times in milliseconds Latency int64 `json:"latency"` Jitter int64 `json:"jitter"` } func (t *LatencyToxic) Name() string { return "latency" } func (t *LatencyToxic) IsEnabled() bool { return t.Enabled } func (t *LatencyToxic) SetEnabled(enabled bool) { t.Enabled = enabled } func (t *LatencyToxic) delay() time.Duration { // Delay = t.Latency +/- t.Jitter delay := t.Latency jitter := int64(t.Jitter) if jitter > 0 { delay += rand.Int63n(jitter*2) - jitter } return time.Duration(delay) * time.Millisecond } func (t *LatencyToxic) Pipe(stub *ToxicStub) { for { select { case <-stub.interrupt: return case c := <-stub.input: if c == nil { stub.Close() return } sleep := t.delay() - time.Now().Sub(c.timestamp) select { case <-time.After(sleep): stub.output <- c case <-stub.interrupt: stub.output <- c // Don't drop any data on the floor return } } } } toxiproxy-1.2.1/toxic_noop.go000066400000000000000000000007231255450513500163170ustar00rootroot00000000000000package main // The NoopToxic passes all data through without any toxic effects. type NoopToxic struct{} func (t *NoopToxic) Name() string { return "noop" } func (t *NoopToxic) IsEnabled() bool { return true } func (t *NoopToxic) SetEnabled(enabled bool) {} func (t *NoopToxic) Pipe(stub *ToxicStub) { for { select { case <-stub.interrupt: return case c := <-stub.input: if c == nil { stub.Close() return } stub.output <- c } } } toxiproxy-1.2.1/toxic_slicer.go000066400000000000000000000044131255450513500166250ustar00rootroot00000000000000package main import ( "math/rand" "time" ) // The SlicerToxic slices data into multiple smaller packets // to simulate real-world TCP behaviour. type SlicerToxic struct { Enabled bool `json:"enabled"` // Average number of bytes to slice at AverageSize int `json:"average_size"` // +/- bytes to vary sliced amounts. Must be less than // the average size SizeVariation int `json:"size_variation"` // Microseconds to delay each packet. May be useful since there's // usually some kind of buffering of network data Delay int `json:"delay"` } func (t *SlicerToxic) Name() string { return "slicer" } func (t *SlicerToxic) IsEnabled() bool { return t.Enabled } func (t *SlicerToxic) SetEnabled(enabled bool) { t.Enabled = enabled } // Returns a list of chunk offsets to slice up a packet of the // given total size. For example, for a size of 100, output might be: // // []int{0, 18, 18, 43, 43, 67, 67, 77, 77, 100} // ^---^ ^----^ ^----^ ^----^ ^-----^ // // This tries to get fairly evenly-varying chunks (no tendency // to have a small/large chunk at the start/end). func (t *SlicerToxic) chunk(start int, end int) []int { // Base case: // If the size is within the random varation, _or already // less than the average size_, just return it. // Otherwise split the chunk in about two, and recurse. if (end-start)-t.AverageSize <= t.SizeVariation { return []int{start, end} } // +1 in the size variation to offset favoring of smaller // numbers by integer division mid := start + (end-start)/2 + (rand.Intn(t.SizeVariation*2) - t.SizeVariation) + rand.Intn(1) left := t.chunk(start, mid) right := t.chunk(mid, end) return append(left, right...) } func (t *SlicerToxic) Pipe(stub *ToxicStub) { for { select { case <-stub.interrupt: return case c := <-stub.input: if c == nil { stub.Close() return } chunks := t.chunk(0, len(c.data)) for i := 1; i < len(chunks); i += 2 { stub.output <- &StreamChunk{ data: c.data[chunks[i-1]:chunks[i]], timestamp: c.timestamp, } select { case <-stub.interrupt: stub.output <- &StreamChunk{ data: c.data[chunks[i]:], timestamp: c.timestamp, } return case <-time.After(time.Duration(t.Delay) * time.Microsecond): } } } } } toxiproxy-1.2.1/toxic_slow_close.go000066400000000000000000000014211255450513500175110ustar00rootroot00000000000000package main import "time" // The SlowCloseToxic stops the TCP connection from closing until after a delay. type SlowCloseToxic struct { Enabled bool `json:"enabled"` // Times in milliseconds Delay int64 `json:"delay"` } func (t *SlowCloseToxic) Name() string { return "slow_close" } func (t *SlowCloseToxic) IsEnabled() bool { return t.Enabled } func (t *SlowCloseToxic) SetEnabled(enabled bool) { t.Enabled = enabled } func (t *SlowCloseToxic) Pipe(stub *ToxicStub) { for { select { case <-stub.interrupt: return case c := <-stub.input: if c == nil { delay := time.Duration(t.Delay) * time.Millisecond select { case <-time.After(delay): stub.Close() return case <-stub.interrupt: return } } stub.output <- c } } } toxiproxy-1.2.1/toxic_test.go000066400000000000000000000336531255450513500163330ustar00rootroot00000000000000package main import ( "bufio" "bytes" "io" "net" "strings" "testing" "time" "gopkg.in/tomb.v1" ) func WithEchoServer(t *testing.T, f func(string, chan []byte)) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() response := make(chan []byte, 1) tomb := tomb.Tomb{} go func() { defer tomb.Done() src, err := ln.Accept() if err != nil { select { case <-tomb.Dying(): default: t.Fatal("Failed to accept client") } return } ln.Close() scan := bufio.NewScanner(src) if scan.Scan() { received := append(scan.Bytes(), '\n') response <- received src.Write(received) } }() f(ln.Addr().String(), response) tomb.Killf("Function body finished") ln.Close() tomb.Wait() close(response) } func WithEchoProxy(t *testing.T, f func(proxy net.Conn, response chan []byte, proxyServer *Proxy)) { WithEchoServer(t, func(upstream string, response chan []byte) { proxy := NewTestProxy("test", upstream) proxy.Start() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { t.Error("Unable to dial TCP server", err) } f(conn, response, proxy) proxy.Stop() }) } func AssertDeltaTime(t *testing.T, message string, actual, expected, delta time.Duration) { diff := actual - expected if diff < 0 { diff *= -1 } if diff > delta { t.Errorf("[%s] Time was more than %v off: got %v expected %v", message, delta, actual, expected) } else { t.Logf("[%s] Time was correct: %v (expected %v)", message, actual, expected) } } func DoLatencyTest(t *testing.T, upLatency, downLatency *LatencyToxic) { WithEchoProxy(t, func(conn net.Conn, response chan []byte, proxy *Proxy) { t.Logf("Using latency: Up: %dms +/- %dms, Down: %dms +/- %dms", upLatency.Latency, upLatency.Jitter, downLatency.Latency, downLatency.Jitter) proxy.upToxics.SetToxicValue(upLatency) proxy.downToxics.SetToxicValue(downLatency) msg := []byte("hello world " + strings.Repeat("a", 32*1024) + "\n") timer := time.Now() _, err := conn.Write(msg) if err != nil { t.Error("Failed writing to TCP server", err) } resp := <-response if !bytes.Equal(resp, msg) { t.Error("Server didn't read correct bytes from client:", string(resp)) } AssertDeltaTime(t, "Server read", time.Now().Sub(timer), time.Duration(upLatency.Latency)*time.Millisecond, time.Duration(upLatency.Jitter+10)*time.Millisecond, ) timer2 := time.Now() scan := bufio.NewScanner(conn) if scan.Scan() { resp = append(scan.Bytes(), '\n') if !bytes.Equal(resp, msg) { t.Error("Client didn't read correct bytes from server:", string(resp)) } } AssertDeltaTime(t, "Client read", time.Now().Sub(timer2), time.Duration(downLatency.Latency)*time.Millisecond, time.Duration(downLatency.Jitter+10)*time.Millisecond, ) AssertDeltaTime(t, "Round trip", time.Now().Sub(timer), time.Duration(upLatency.Latency+downLatency.Latency)*time.Millisecond, time.Duration(upLatency.Jitter+downLatency.Jitter+10)*time.Millisecond, ) upLatency.Enabled = false downLatency.Enabled = false proxy.upToxics.SetToxicValue(upLatency) proxy.downToxics.SetToxicValue(downLatency) err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } }) } func TestUpstreamLatency(t *testing.T) { DoLatencyTest(t, &LatencyToxic{Enabled: true, Latency: 100}, &LatencyToxic{Enabled: false}) } func TestDownstreamLatency(t *testing.T) { DoLatencyTest(t, &LatencyToxic{Enabled: false}, &LatencyToxic{Enabled: true, Latency: 100}) } func TestFullstreamLatencyEven(t *testing.T) { DoLatencyTest(t, &LatencyToxic{Enabled: true, Latency: 100}, &LatencyToxic{Enabled: true, Latency: 100}) } func TestFullstreamLatencyBiasUp(t *testing.T) { DoLatencyTest(t, &LatencyToxic{Enabled: true, Latency: 1000}, &LatencyToxic{Enabled: true, Latency: 100}) } func TestFullstreamLatencyBiasDown(t *testing.T) { DoLatencyTest(t, &LatencyToxic{Enabled: true, Latency: 100}, &LatencyToxic{Enabled: true, Latency: 1000}) } func TestZeroLatency(t *testing.T) { DoLatencyTest(t, &LatencyToxic{Enabled: true, Latency: 0}, &LatencyToxic{Enabled: true, Latency: 0}) } func AssertEchoResponse(t *testing.T, client, server net.Conn) { msg := []byte("hello world\n") _, err := client.Write(msg) if err != nil { t.Error("Failed writing to TCP server", err) } scan := bufio.NewScanner(server) if !scan.Scan() { t.Error("Client unexpectedly closed connection") } resp := append(scan.Bytes(), '\n') if !bytes.Equal(resp, msg) { t.Error("Server didn't read correct bytes from client:", string(resp)) } _, err = server.Write(resp) if err != nil { t.Error("Failed writing to TCP client", err) } scan = bufio.NewScanner(client) if !scan.Scan() { t.Error("Server unexpectedly closed connection") } resp = append(scan.Bytes(), '\n') if !bytes.Equal(resp, msg) { t.Error("Client didn't read correct bytes from server:", string(resp)) } } func TestPersistentConnections(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() serverConnRecv := make(chan net.Conn) go func() { conn, err := ln.Accept() if err != nil { t.Error("Unable to accept TCP connection", err) } serverConnRecv <- conn }() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { t.Error("Unable to dial TCP server", err) } serverConn := <-serverConnRecv proxy.upToxics.SetToxicValue(&LatencyToxic{Enabled: true, Latency: 0}) proxy.downToxics.SetToxicValue(&LatencyToxic{Enabled: true, Latency: 0}) AssertEchoResponse(t, conn, serverConn) proxy.upToxics.ResetToxics() proxy.downToxics.ResetToxics() AssertEchoResponse(t, conn, serverConn) proxy.upToxics.ResetToxics() proxy.downToxics.ResetToxics() AssertEchoResponse(t, conn, serverConn) err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } } func TestLatencyToxicCloseRace(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() go func() { for { _, err := ln.Accept() if err != nil { return } } }() // Check for potential race conditions when interrupting toxics for i := 0; i < 1000; i++ { proxy.upToxics.SetToxicValue(&LatencyToxic{Enabled: true, Latency: 10}) conn, err := net.Dial("tcp", proxy.Listen) if err != nil { t.Error("Unable to dial TCP server", err) } conn.Write([]byte("hello")) conn.Close() proxy.upToxics.SetToxicValue(&LatencyToxic{Enabled: false}) } } func TestLatencyToxicBandwidth(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() buf := []byte(strings.Repeat("hello world ", 1000)) go func() { conn, err := ln.Accept() if err != nil { t.Error("Unable to accept TCP connection", err) } for err == nil { _, err = conn.Write(buf) } }() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { t.Error("Unable to dial TCP server", err) } proxy.downToxics.SetToxicValue(&LatencyToxic{Enabled: true, Latency: 100}) time.Sleep(100 * time.Millisecond) // Wait for latency toxic buf2 := make([]byte, len(buf)) start := time.Now() count := 0 for i := 0; i < 100; i++ { n, err := io.ReadFull(conn, buf2) count += n if err != nil { t.Error(err) break } } // Assert the transfer was at least 100MB/s AssertDeltaTime(t, "Latency toxic bandwidth", time.Since(start), 0, time.Duration(count/100000)*time.Millisecond) err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } } func TestProxyLatency(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() serverConnRecv := make(chan net.Conn) go func() { conn, err := ln.Accept() if err != nil { t.Error("Unable to accept TCP connection", err) } serverConnRecv <- conn }() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { t.Error("Unable to dial TCP server", err) } serverConn := <-serverConnRecv start := time.Now() for i := 0; i < 100; i++ { AssertEchoResponse(t, conn, serverConn) } latency := time.Now().Sub(start) / 200 if latency > 300*time.Microsecond { t.Errorf("Average proxy latency > 300µs (%v)", latency) } else { t.Logf("Average proxy latency: %v", latency) } err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } } func TestBandwidthToxic(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() serverConnRecv := make(chan net.Conn) go func() { conn, err := ln.Accept() if err != nil { t.Error("Unable to accept TCP connection", err) } serverConnRecv <- conn }() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { t.Error("Unable to dial TCP server", err) } serverConn := <-serverConnRecv rate := 1000 // 1MB/s proxy.upToxics.SetToxicValue(&BandwidthToxic{Enabled: true, Rate: int64(rate)}) buf := []byte(strings.Repeat("hello world ", 40000)) // 480KB go func() { n, err := conn.Write(buf) conn.Close() if n != len(buf) || err != nil { t.Errorf("Failed to write buffer: (%d == %d) %v", n, len(buf), err) } }() buf2 := make([]byte, len(buf)) start := time.Now() _, err = io.ReadAtLeast(serverConn, buf2, len(buf2)) if err != nil { t.Errorf("Proxy read failed: %v", err) } else if bytes.Compare(buf, buf2) != 0 { t.Errorf("Server did not read correct buffer from client!") } AssertDeltaTime(t, "Bandwidth", time.Now().Sub(start), time.Duration(len(buf))*time.Second/time.Duration(rate*1000), 10*time.Millisecond, ) } func TestSlicerToxic(t *testing.T) { data := []byte(strings.Repeat("hello world ", 40000)) // 480 kb slicer := &SlicerToxic{Enabled: true, AverageSize: 1024, SizeVariation: 512, Delay: 10} input := make(chan *StreamChunk) output := make(chan *StreamChunk) stub := NewToxicStub(input, output) done := make(chan bool) go func() { slicer.Pipe(stub) done <- true }() defer func() { input <- nil <-done }() input <- &StreamChunk{data: data} buf := make([]byte, 0, len(data)) reads := 0 L: for { select { case c := <-output: reads++ buf = append(buf, c.data...) case <-time.After(5 * time.Millisecond): break L } } if reads < 480/2 || reads > 480/2+480 { t.Errorf("Expected to read about 480 times, but read %d times.", reads) } if bytes.Compare(buf, data) != 0 { t.Errorf("Server did not read correct buffer from client!") } } func TestToxicUpdate(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() serverConnRecv := make(chan net.Conn) go func() { conn, err := ln.Accept() if err != nil { t.Error("Unable to accept TCP connection", err) } serverConnRecv <- conn }() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { t.Error("Unable to dial TCP server", err) } serverConn := <-serverConnRecv running := make(chan struct{}) go func() { enabled := false for { select { case <-running: return default: proxy.upToxics.SetToxicValue(&LatencyToxic{Enabled: enabled}) enabled = !enabled proxy.downToxics.SetToxicValue(&LatencyToxic{Enabled: enabled}) } } }() for i := 0; i < 100; i++ { AssertEchoResponse(t, conn, serverConn) } close(running) err = conn.Close() if err != nil { t.Error("Failed to close TCP connection", err) } } func BenchmarkBandwidthToxic100MB(b *testing.B) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { b.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() buf := []byte(strings.Repeat("hello world ", 1000)) go func() { conn, err := ln.Accept() if err != nil { b.Error("Unable to accept TCP connection", err) } buf2 := make([]byte, len(buf)) for err == nil { _, err = conn.Read(buf2) } }() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { b.Error("Unable to dial TCP server", err) } proxy.upToxics.SetToxicValue(&BandwidthToxic{Enabled: true, Rate: 100 * 1000}) b.SetBytes(int64(len(buf))) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { n, err := conn.Write(buf) if err != nil || n != len(buf) { b.Errorf("%v, %d == %d", err, n, len(buf)) break } } err = conn.Close() if err != nil { b.Error("Failed to close TCP connection", err) } } func BenchmarkNoopToxic(b *testing.B) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { b.Fatal("Failed to create TCP server", err) } defer ln.Close() proxy := NewTestProxy("test", ln.Addr().String()) proxy.Start() defer proxy.Stop() buf := []byte(strings.Repeat("hello world ", 1000)) go func() { conn, err := ln.Accept() if err != nil { b.Error("Unable to accept TCP connection", err) } buf2 := make([]byte, len(buf)) for err == nil { _, err = conn.Read(buf2) } }() conn, err := net.Dial("tcp", proxy.Listen) if err != nil { b.Error("Unable to dial TCP server", err) } b.SetBytes(int64(len(buf))) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { n, err := conn.Write(buf) if err != nil || n != len(buf) { b.Errorf("%v, %d == %d", err, n, len(buf)) break } } err = conn.Close() if err != nil { b.Error("Failed to close TCP connection", err) } } toxiproxy-1.2.1/toxic_timeout.go000066400000000000000000000014261255450513500170330ustar00rootroot00000000000000package main import "time" // The TimeoutToxic stops any data from flowing through, and will close the connection after a timeout. // If the timeout is set to 0, then the connection will not be closed. type TimeoutToxic struct { Enabled bool `json:"enabled"` // Times in milliseconds Timeout int64 `json:"timeout"` } func (t *TimeoutToxic) Name() string { return "timeout" } func (t *TimeoutToxic) IsEnabled() bool { return t.Enabled } func (t *TimeoutToxic) SetEnabled(enabled bool) { t.Enabled = enabled } func (t *TimeoutToxic) Pipe(stub *ToxicStub) { timeout := time.Duration(t.Timeout) * time.Millisecond if timeout > 0 { select { case <-time.After(timeout): stub.Close() return case <-stub.interrupt: return } } else { <-stub.interrupt return } } toxiproxy-1.2.1/toxiproxy.go000066400000000000000000000010141255450513500162150ustar00rootroot00000000000000package main import ( "flag" "math/rand" "time" ) var Version = "1.2.1" var host string var port string func init() { flag.StringVar(&host, "host", "localhost", "Host for toxiproxy's API to listen on") flag.StringVar(&port, "port", "8474", "Port for toxiproxy's API to listen on") seed := flag.Int64("seed", time.Now().UTC().UnixNano(), "Seed for randomizing toxics with") flag.Parse() rand.Seed(*seed) } func main() { proxies := NewProxyCollection() server := NewServer(proxies) server.Listen(host, port) }