Your Redis goes slow. Your MySQL stops answering. Your payment provider resets the connection halfway through a request. You have no idea what your application does, because you have never made those things happen on purpose.
Toxiproxy is Shopify's answer to that: a TCP proxy that sits between your application and its dependencies and breaks the network on demand. It is two pieces — a Go server that does the proxying, and a client library per language that tells it what to break.
This package is the PHP client, plus the part every other client leaves to you: it downloads, starts, stops and cleans up the official Go server for you.
composer require mpge/toxiproxy-php --devuseMpge\Toxiproxy\Toxiproxy;
$toxiproxy = Toxiproxy::start();
$redis = $toxiproxy->proxy('redis', '127.0.0.1:6379');
$redis->withLatency(1000, function () {
// verify your application handles a slow Redis connection
});That is the whole setup. No Homebrew, no docker-compose.yml, no binary committed to
your repository, no toxiproxy-server & in a CI script. The first call downloads the
official release binary for your platform, verifies its checksum, starts it, and stops
it again when your process exits.
- Installation
- Quick start
- Proxies
- Toxics
- Scoped chaos
- Testing
- Laravel
- Command line
- Binary management
- Using a server you manage yourself
- Docker
- Continuous integration
- Configuration
- Supported platforms
- Testing against third-party APIs
- Troubleshooting
- How this differs from ihsw/toxiproxy-php-client
- Architecture
- Licence and attribution
composer require mpge/toxiproxy-php --devRequires PHP 8.2 or newer. Dependencies are Symfony Console and Symfony Process, and that is all — there is deliberately no HTTP client dependency, so installing this into an existing project cannot conflict with whatever it already uses. See Transports if you would rather it went through your own.
Optionally pre-download the server binary, so the first test run is not also a download:
vendor/bin/toxiproxy-php installuseMpge\Toxiproxy\Toxiproxy;
// Starts the Go server, or adopts one already running on the same port.$toxiproxy = Toxiproxy::start();
// No listen address given, so Toxiproxy picks a free port and tells us which.$redis = $toxiproxy->proxy(
name: 'redis',
upstream: '127.0.0.1:6379',
);
// Point your client at the proxy instead of the real service.$client = newRedis();
$client->connect($redis->host(), $redis->port());
$client->ping(); // fast$redis->latency(2000);
$client->ping(); // two seconds slower$redis->removeToxics();
$toxiproxy->stop();Toxiproxy::stop() only stops a server this object started. One that was already
running when you connected is left alone — see ownership.
A proxy is a listening socket that forwards to an upstream, with toxics applied in between.
$proxy = $toxiproxy->proxy(
name: 'mysql',
upstream: '127.0.0.1:3306',
listen: '127.0.0.1:13306', // optional
);proxy() is get-or-create: calling it twice with the same arguments returns the same
proxy rather than failing, which is what you want in a test file that runs more than
once. Use createProxy() if you want a duplicate name to be an error.
Leave listen out and Toxiproxy binds an ephemeral port, then reports which one it
got:
$proxy = $toxiproxy->proxy('redis', '127.0.0.1:6379');
$proxy->host(); // '127.0.0.1'$proxy->port(); // 51824$proxy->listen(); // '127.0.0.1:51824'$proxy->address(); // an Address value objectThis is delegated to the server on purpose. Picking a free port in PHP means binding a socket, reading the port, closing it, and hoping nothing else claims it in between. Asking Toxiproxy to bind port 0 has no such window.
$proxy->name(); // 'mysql'$proxy->upstreamAddress(); // '127.0.0.1:3306' (upstream() is the toxic helper)$proxy->isEnabled(); // true$proxy->toxics(); // a ToxicCollection$proxy->disable(); // stop accepting connections, sever existing ones$proxy->enable();
$proxy->update(upstream: '127.0.0.1:3307');
$proxy->refresh(); // re-read from the server$proxy->delete();useMpge\Toxiproxy\Proxy\ProxyDefinition;
$toxiproxy->populate([
newProxyDefinition('redis', '127.0.0.1:16379', '127.0.0.1:6379'),
newProxyDefinition('mysql', '127.0.0.1:13306', '127.0.0.1:3306'),
]);
$toxiproxy->proxies(); // a ProxyCollection, keyed by name$toxiproxy->proxies()->poisoned(); // only the ones carrying toxics$toxiproxy->reset(); // enable every proxy, drop every toxic, keep the proxies$toxiproxy->flush(); // delete the proxies tooreset() is the one you want between test cases: it is a single HTTP call and leaves
your proxy ports stable.
Every helper maps onto a toxic the Go server actually registers, using the attribute names it actually reads.
$proxy->latency(1000); // delay each packet by 1000ms$proxy->latency(latency: 1000, jitter: 250); // 1000ms ± 250ms$proxy->bandwidth(100); // throttle to 100 KB/s$proxy->timeout(5000); // stop data, close after 5s$proxy->timeout(); // stop data, never close$proxy->slowClose(2000); // delay the TCP close by 2s$proxy->resetPeer(); // TCP RST, immediately$proxy->limitData(1024); // close after 1KB has passed$proxy->slicer(averageSize: 64, sizeVariation: 16, delayMicroseconds: 100);
$proxy->packetLoss(0.25); // see the note below$proxy->noop(); // changes nothingEvery helper returns the Toxic the server stored, so you can read back what it made
of your request.
Toxiproxy names directions from your client's point of view. Helpers default to downstream, matching the server's own default.
$proxy->downstream()->latency(1000); // service -> client$proxy->upstream()->bandwidth(50); // client -> service$proxy->latency(1000); // same as ->downstream()->latency(1000)Anything the named helpers do not cover, including a toxic type added to a newer Toxiproxy than this package knows about:
$proxy->addToxic(
type: 'latency',
stream: 'downstream',
toxicity: 1.0,
attributes: [
'latency' => 1000,
'jitter' => 100,
],
);Attribute keys are validated against the type before the request is sent. This matters
more than it looks: the Go server decodes attributes into a typed struct, so a
misspelled key is dropped without complaint and you get a toxic that is accepted, is
listed, and does nothing at all. Sending latencyMs instead of latency fails here
rather than wasting an afternoon.
Every toxic carries a toxicity between 0 and 1: the fraction of connections it is
applied to.
$proxy->latency(1000, toxicity: 0.5); // half of connections are slowToxiproxy has no packet-loss toxic. Its toxics are exactly latency, bandwidth,
slow_close, timeout, reset_peer, slicer, limit_data and noop.
packetLoss(0.25) is therefore a reset_peer toxic at toxicity 0.25: one connection
in four is reset. That models a lossy or flapping link at the granularity Toxiproxy
actually works at, which is connections rather than packets. It is documented here
rather than hidden, because a helper that silently means something different from its
name is worse than no helper.
If you want bytes dropped mid-stream instead of connections dropped, use bandwidth()
or limitData().
$proxy->toxic('latency_downstream'); // ?Toxic$proxy->removeToxic('latency_downstream');
$proxy->removeToxics(); // all of them$proxy->toxics()->names();
$proxy->toxics()->ofType(ToxicType::Latency);
$proxy->toxics()->onStream(ToxicDirection::Upstream);Applying a toxic whose name already exists updates it rather than failing, so repeated calls are safe.
The helpers you will actually reach for. Break something, run some code, put it back:
$proxy->withLatency(1000, function () use ($service) {
$service->call();
});
$proxy->down(function () use ($service) {
$service->call(); // the service is unreachable in here
});The proxy is restored in a finally block, so a failing assertion inside the callback
cannot leak a toxic into the rest of your suite. Restoring means restoring: toxics the
callback added are removed, ones it changed are put back, ones it deleted return.
The callback's return value is passed through:
$duration = $proxy->withLatency(500, fn () => $this->timeCheckout());The full set:
$proxy->withLatency(1000, $callback, jitter: 100);
$proxy->withBandwidth(50, $callback);
$proxy->withTimeout(5000, $callback);
$proxy->withSlowClose(2000, $callback);
$proxy->withLimitData(1024, $callback);
$proxy->withSlicer(64, $callback);
$proxy->withResetPeer(0, $callback);
$proxy->withPacketLoss(0.25, $callback);
$proxy->withToxics([$toxicA, $toxicB], $callback);
$proxy->upstream()->withBandwidth(50, $callback); // directionaluseMpge\Toxiproxy\Testing\InteractsWithToxiproxy;
usePHPUnit\Framework\TestCase;
finalclass CheckoutTest extends TestCase
{
use InteractsWithToxiproxy;
protectedfunctionsetUp(): void
{
$this->proxy('redis', '127.0.0.1:6379');
$this->proxy('mysql', '127.0.0.1:3306');
}
publicfunctiontest_checkout_survives_a_slow_cache(): void
{
$this->withLatency('redis', 2000, function () {
$this->assertTrue($this->checkout()->succeeded());
});
}
publicfunctiontest_checkout_fails_cleanly_when_the_database_is_down(): void
{
$this->withServiceDown('mysql', function () {
$this->assertSame('try again shortly', $this->checkout()->message());
});
}
}The trait gives you:
$this->toxiproxy(); // the shared Toxiproxy$this->proxy('redis', '127.0.0.1:6379'); // get or create$this->proxy('redis'); // get, once created$this->proxies(['redis' => '…', 'mysql' => '…']); // several at once$this->withLatency('redis', 1000, $callback);
$this->withServiceDown('mysql', $callback);
$this->withBandwidth('redis', 50, $callback);
$this->withTimeout('redis', 5000, $callback);
$this->withPacketLoss('redis', 0.25, $callback);
$this->withToxics('redis', [$toxic], $callback);One server per test process, started on first use and shared. Between tests an
#[After] hook calls reset(), so a test that leaves a service broken cannot poison
the next one, while proxy definitions and their ports survive the whole class.
No orphans. The server is tied to the PHP process. Interrupt the suite with Ctrl-C and it goes with it.
Point it somewhere else by overriding one method:
protectedfunctiontoxiproxyConfiguration(): Configuration
{
return Configuration::fromEnvironment()->withPort(19474);
}Pest test cases are PHPUnit test cases, so the trait works unchanged. Wire it up once
in tests/Pest.php:
uses(Mpge\Toxiproxy\Testing\InteractsWithToxiproxy::class)->in('Feature');it('survives a slow cache', function () {
$this->proxy('redis', '127.0.0.1:6379');
$this->withLatency('redis', 2000, function () {
expect(checkout())->toBeSuccessful();
});
});Or skip the trait and use the facade directly:
beforeEach(function () {
$this->toxiproxy = Mpge\Toxiproxy\Toxiproxy::start();
$this->redis = $this->toxiproxy->proxy('redis', '127.0.0.1:6379');
});
afterEach(fn () => $this->toxiproxy->reset());
it('retries when the cache resets the connection', function () {
$this->redis->withPacketLoss(1.0, fn () => expect(cache()->get('k'))->toBeNull());
});Laravel support is optional and auto-discovered. Nothing in the core package references the framework.
php artisan vendor:publish --tag=toxiproxy-config// config/toxiproxy.phpreturn [
'host' => env('TOXIPROXY_HOST', '127.0.0.1'),
'port' => env('TOXIPROXY_PORT', 8474),
'binary' => [
'version' => env('TOXIPROXY_VERSION', '2.12.0'),
'auto_install' => true,
],
'proxies' => [
'redis' => ['upstream' => '127.0.0.1:6379'],
'mysql' => ['upstream' => '127.0.0.1:3306', 'listen' => '127.0.0.1:13306'],
],
];php artisan toxiproxy:install
php artisan toxiproxy:start # starts, then creates the configured proxies
php artisan toxiproxy:status
php artisan toxiproxy:stopResolve it from the container:
useMpge\Toxiproxy\Toxiproxy;
app(Toxiproxy::class)->proxy('redis')->latency(500);Resolving the binding connects but does not start a server, because a container
binding should not spawn a process as a side effect. Set auto_start => true if you
want it to, or start it with php artisan toxiproxy:start.
The bindings are Configuration, ToxiproxyClient, ToxiproxyServer and Toxiproxy
(also aliased to toxiproxy).
vendor/bin/toxiproxy-php install # download the official binary
vendor/bin/toxiproxy-php start # start a server in the background
vendor/bin/toxiproxy-php status # what is running, and what it is proxying
vendor/bin/toxiproxy-php proxies # list proxies and their toxics
vendor/bin/toxiproxy-php reset # enable everything, drop every toxic
vendor/bin/toxiproxy-php stop # stop a server this package started
vendor/bin/toxiproxy-php version # package, binary and server versions
vendor/bin/toxiproxy-php update # install the newest upstream release
vendor/bin/toxiproxy-php doctor # diagnose the environmentEvery command takes --host, --port and --url; the server-related ones also take
--release, --binary, --home, --log-level, --timeout and --docker.
start returns immediately, leaving the server running. Use --foreground to stay
attached and stream its log.
doctor checks PHP version, HTTP transport, platform support, cache writability,
binary presence and version drift, API reachability, whether the port is held by
something that is not Toxiproxy, server ownership, and Docker availability:
[ok] PHP 8.4.3
[ok] Package 0.1.0
[ok] HTTP transport ext-curl
[ok] Platform linux/amd64
[ok] Cache directory /home/dev/.cache/toxiproxy-php
[ok] Server binary 2.12.0 /home/dev/.cache/toxiproxy-php/bin/2.12.0/toxiproxy-server
[ok] API http://127.0.0.1:8474 Toxiproxy 2.12.0
[ok] Ownership started by this package, pid 48213
[ok] Proxy interface 127.0.0.1
[ok] Proxies 2 defined
[ok] Docker available
This package downloads the officialtoxiproxy-server binary from Shopify's GitHub
releases. Nothing is compiled, nothing is vendored, nothing is reimplemented.
Resolution order, most explicit first:
TOXIPROXY_BINARY— used verbatim.- This package's cache, keyed by version.
- A
toxiproxy-serveralready onPATH, from Homebrew, apt or anywhere else. Reused rather than duplicated;doctortells you when this is what is in play, since its version is then outside your control. - Downloaded from GitHub Releases, if auto-install is on.
Downloads are verified against the checksums.txt published with the release. A
mismatch deletes the file and raises rather than leaving something that looks
installed.
The cache lives outside vendor/ — ~/.cache/toxiproxy-php on Linux,
~/Library/Caches/toxiproxy-php on macOS, %LOCALAPPDATA%\toxiproxy-php on Windows.
vendor/ is disposable and rebuilt constantly in CI, and a multi-megabyte binary per
project is waste when one per machine will do. Override with TOXIPROXY_HOME.
The version is pinned, not floating. A test suite whose proxy server changes underneath it on somebody else's release schedule is a flake waiting to happen. Move deliberately:
vendor/bin/toxiproxy-php update # install the newest release
TOXIPROXY_VERSION=2.13.0 vendor/bin/toxiproxy-php startTOXIPROXY_VERSION=latest opts into tracking upstream if you would rather.
The rule this package enforces: only stop what you started.
start() adopts a server already answering on the endpoint instead of failing or
spawning a duplicate, and remembers that it did not start it. stop() on such a server
returns false and does nothing. Your docker-compose Toxiproxy, or one a colleague
left running, survives your test suite untouched.
Servers this package starts are recorded in <cache>/run/, so toxiproxy-php stop
works across process boundaries — and refuses any endpoint it finds no record for.
Nothing is installed, started or stopped:
$toxiproxy = Toxiproxy::connect('http://toxiproxy:8474');Or through the environment, which is usually easier in CI:
TOXIPROXY_URL=http://toxiproxy:8474$toxiproxy = Toxiproxy::connect();The ToxiproxyClient is usable entirely on its own if you want none of the lifecycle
management:
useMpge\Toxiproxy\Client\ToxiproxyClient;
$client = newToxiproxyClient('http://127.0.0.1:8474');
$proxy = $client->createProxy(
name: 'mysql',
upstream: '127.0.0.1:3306',
listen: '127.0.0.1:13306',
);There is no HTTP client dependency. By default the package uses ext-curl, falling
back to stream wrappers. To route Toxiproxy calls through a client you already have:
useMpge\Toxiproxy\Client\Psr18Transport;
$transport = newPsr18Transport($psr18Client, $requestFactory, $streamFactory);
$toxiproxy = Toxiproxy::connect('http://127.0.0.1:8474', $transport);Implement Mpge\Toxiproxy\Client\Transport for anything else. It has one method.
Native binaries are the primary path. Docker is available if your stack already lives there:
Toxiproxy::docker()->start();vendor/bin/toxiproxy-php start --dockerIt runs ghcr.io/shopify/toxiproxy, the image Shopify publishes. Two things behave
differently in a container, and neither can be papered over:
Upstreams resolve inside the container.127.0.0.1:6379 means the container's own
loopback, not your machine's. Use host.docker.internal, or a service name on a shared
Docker network.
Proxy listen ports must be published. A proxy listening on a port that was not published when the container started is unreachable from the host, so declare the range up front:
Toxiproxy::docker()
->publish(30000, 30010)
->start();
$toxiproxy->proxy('redis', 'host.docker.internal:6379', '0.0.0.0:30000');On Linux, host networking removes the problem entirely:
Toxiproxy::docker()->network('host')->start();Stop a container with docker stop, not toxiproxy-php stop.
Nothing special is needed. Toxiproxy::start() works on a bare runner, and caching the
binary keeps it to one download per release:
- uses: actions/cache@v4with:
path: ~/.cache/toxiproxy-phpkey: toxiproxy-${{ runner.os }}-2.12.0
- run: vendor/bin/toxiproxy-php install
- run: vendor/bin/phpunitIf you would rather run Toxiproxy as a service container, point the package at it and turn auto-install off:
services:
toxiproxy:
image: ghcr.io/shopify/toxiproxy:2.12.0ports: ['8474:8474']env:
TOXIPROXY_URL: http://127.0.0.1:8474TOXIPROXY_AUTO_INSTALL: '0'Environment variables, all optional:
| Variable | Default | What it does |
|---|---|---|
TOXIPROXY_HOST | 127.0.0.1 | Host the API listens on |
TOXIPROXY_PORT | 8474 | Port the API listens on |
TOXIPROXY_URL | — | Full base URL, overriding host and port together |
TOXIPROXY_VERSION | 2.12.0 | Release to install, or latest |
TOXIPROXY_BINARY | — | An explicit server binary, skipping the cache |
TOXIPROXY_HOME | per-OS cache dir | Where binaries and run records are kept |
TOXIPROXY_AUTO_INSTALL | true | Download the binary on demand |
TOXIPROXY_VERIFY_CHECKSUMS | true | Verify downloads against the release checksums |
TOXIPROXY_LOG_LEVEL | info | Server log level: trace, debug, info, warn, error |
TOXIPROXY_START_TIMEOUT | 15 | Seconds to wait for the API to answer after spawning |
TOXIPROXY_PROXY_HOST | 127.0.0.1 | Interface new proxies bind to |
TOXIPROXY_DEBUG | false | Extra diagnostic output |
Or in code:
useMpge\Toxiproxy\Configuration;
$toxiproxy = Toxiproxy::make()
->port(19474)
->version('2.11.0')
->logLevel('debug')
->startTimeout(30)
->start();
// Or build a Configuration and pass it around.$config = Configuration::fromEnvironment()->withPort(19474);The platforms Shopify publishes a server binary for:
| OS | amd64 | arm64 |
|---|---|---|
| Linux | ✅ | ✅ |
| macOS | ✅ | ✅ |
| Windows | ✅ | — |
| FreeBSD | ✅ | ✅ |
| OpenBSD | ✅ | ✅ |
| NetBSD | ✅ | — |
| Solaris | ✅ | — |
On anything else the client still works; point TOXIPROXY_BINARY at a server you built
yourself, or connect to one over the network.
Toxiproxy sits in front of things that exist locally: your Redis, your MySQL, a service in your compose file. It cannot help with the half of your dependencies that live on somebody else's servers — Stripe, Shopify, Twilio — because there is nothing local for it to proxy.
For those, Cauldron boots working emulations of the providers your project talks to, locally, from one command. It reads the manifests already in your repo, works out which third-party APIs you depend on, and serves fakes for them.
Its cauldron network command deliberately uses the same vocabulary as this
package — latency, jitter, bandwidth, timeout, reset, slice, limit — so you do not
learn two sets of words for one idea:
cauldron network stripe --latency 800ms --jitter 200ms
cauldron network stripe --reset --probability 0.1The two compose. Point Toxiproxy at your database and Cauldron at your payment provider, and every dependency your application has can be made to misbehave from a single test:
$redis = $toxiproxy->proxy('redis', '127.0.0.1:6379');
$redis->withLatency(1000, function () {
// Redis is slow, and Cauldron has Stripe resetting one call in ten.$this->assertTrue($this->checkout()->succeeded());
});Cauldron is a separate project, Go rather than PHP, and not required by anything here.
vendor/bin/toxiproxy-php doctor answers most of what follows. Try it first.
"Cannot start Toxiproxy: 127.0.0.1:8474 is already in use"
Something holds the port. If it is a Toxiproxy you started elsewhere, use it —
Toxiproxy::start() will adopt it. If it is something else, pick another port with
TOXIPROXY_PORT.
"Refusing to stop the Toxiproxy server ... because this process did not start it"
Working as intended. The server has no record in <cache>/run/, so it belongs to
something else. Stop it however you started it.
A toxic is accepted but nothing happens.
Almost always a misspelled attribute name; the Go server drops unknown keys silently.
This package validates them, so use latency() and friends rather than hand-building
addToxic() payloads, and check the attribute names against the table in
Toxics if you must.
Latency looks doubled.
A toxic applies per direction. latency(1000) on downstream adds 1000ms to the reply;
adding another on upstream adds 1000ms to the request as well.
"Could not reach the Toxiproxy API"
The server is not running, or is on a different port. toxiproxy-php status will say
which.
HTTP 403 from the API.
Toxiproxy refuses any request whose User-Agent starts with Mozilla/, as a guard
against poking the control plane from a browser. Only relevant if you supplied a custom
transport with a browser-like agent.
Connection refused straight after start() returns.start() waits for the API to answer before returning, but the proxy ports come up
when the proxy is created. If you cached a port from an earlier run, re-read it from
$proxy->port().
Downloads fail behind a proxy.
The downloader honours curl's usual HTTP_PROXY / HTTPS_PROXY environment. Failing
that, download the binary by hand and set TOXIPROXY_BINARY.
Shopify's documentation points at ihsw/toxiproxy-php-client,
which is a perfectly reasonable HTTP client and has been around a lot longer. It is
worth being explicit about what is different rather than implying it is bad.
ihsw/toxiproxy-php-client | mpge/toxiproxy-php | |
|---|---|---|
| HTTP API client | ✅ | ✅ |
| On Packagist | ❌ (needs a VCS repository entry) | ✅ |
| Server binary management | ❌ | ✅ download, verify, cache |
| Server process lifecycle | ❌ | ✅ start, stop, adopt, no orphans |
| Composer CLI | ❌ | ✅ vendor/bin/toxiproxy-php |
| PHPUnit / Pest helpers | ❌ | ✅ |
Scoped chaos (withLatency, down) | ❌ | ✅ |
| Automatic port allocation | ❌ | ✅ delegated to the server |
| Laravel integration | ❌ | ✅ optional |
reset_peer toxic | ❌ missing from its enum | ✅ |
| HTTP dependency | Guzzle 7 | none |
| Minimum PHP | 8.3 | 8.2 |
If all you want is a thin client and you already have Guzzle, ihsw will serve you
fine. This package exists for the case where you want Toxiproxy to be one composer require and nothing else.
PHP application / tests
|
v
mpge/toxiproxy-php <- this package
|
| HTTP API (:8474)
v
Shopify toxiproxy-server <- the official Go binary, unmodified
|
| TCP
v
Redis / MySQL / HTTP service / …
The proxying is Shopify's Go server, downloaded from their releases and run as-is. This package never touches a packet. It speaks the HTTP control API and manages the process lifecycle, and that is the whole of it.
Inside, the layers are separable and each usable on its own:
src/
Toxiproxy.php the facade
PendingToxiproxy.php fluent configuration before start
Configuration.php every knob, immutable
Client/ the HTTP API: transports, JSON, errors
Proxy/ Proxy, collections, addresses, port allocation
Toxic/ Toxic, its types and directions
Server/ platform detection, downloads, process lifecycle, Docker
Testing/ the PHPUnit trait
Console/ vendor/bin/toxiproxy-php
Laravel/ optional framework integration
Exception/
composer install
composer test:unit # no network, no processes
composer test:integration # drives a real server; downloads the binary once
composer stan
composer lint # composer fix to apply
composer ci # all of the aboveIntegration tests run on port 18474 rather than 8474, so a Toxiproxy you already have running is neither disturbed nor mistaken for one of theirs.
MIT. See LICENSE.
This is not an official Shopify project and is not affiliated with, endorsed by, or sponsored by Shopify.
Toxiproxy is a separate upstream project, copyright Shopify Inc., also MIT licensed. When this package downloads a Toxiproxy server binary, that binary remains the property of its authors and is governed by upstream's licence, a copy of which ships inside every Toxiproxy release.
If Toxiproxy is useful to you, the credit belongs there.