├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── args.go ├── args_test.go ├── assets ├── combined.svg ├── sawtooth.svg └── speedbump.gif ├── go.mod ├── go.sum ├── lib ├── README.md ├── base.go ├── connection.go ├── connection_test.go ├── latency_generator.go ├── latency_generator_test.go ├── sawtooth.go ├── sawtooth_test.go ├── sine.go ├── speedbump.go ├── speedbump_test.go ├── square.go ├── square_test.go ├── triangle.go └── triangle_test.go └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: CI 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.17.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v3 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Test 18 | run: go test -race -v ./... -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release binaries and Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release-binaries: 9 | name: Build and publish release binaries 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | goos: [linux, windows, darwin] 14 | goarch: [amd64, arm64] 15 | exclude: 16 | - goarch: arm64 17 | goos: windows 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: wangyoucao577/go-release-action@v1.37 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | goos: ${{ matrix.goos }} 24 | goarch: ${{ matrix.goarch }} 25 | goversion: "1.17" 26 | extra_files: LICENSE README.md 27 | build_image: 28 | name: Build and Push Docker image to Docker Hub 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Check out the repo 32 | uses: actions/checkout@v2 33 | 34 | - name: Log in to Docker Hub 35 | uses: docker/login-action@v1 36 | with: 37 | username: ${{ secrets.DOCKER_USERNAME }} 38 | password: ${{ secrets.DOCKER_PASSWORD }} 39 | 40 | - name: Extract metadata (tags, labels) for Docker 41 | id: meta 42 | uses: docker/metadata-action@v3 43 | with: 44 | images: kffl/speedbump 45 | 46 | - name: Build and push Docker image 47 | uses: docker/build-push-action@v2 48 | with: 49 | context: . 50 | push: true 51 | tags: ${{ steps.meta.outputs.tags }} 52 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to speedbump 2 | 3 | ## Bugs and feature requests (Issues) 4 | 5 | 1. If you have found a bug, please check existing Issues prior to opening a new one. 6 | 2. When reporting a bug via issues, please provide commit # (or output of `speedbump --version` if you are using a pre-built binary form a GitHub release). 7 | 3. A bug report should include accurate description of the steps required to reproduce it. 8 | 4. Feature requests are welcome! 9 | 10 | ## Pull requests 11 | 12 | ### Commit naming 13 | 14 | This project uses [conventional commits](https://www.conventionalcommits.org/). 15 | 16 | ### WIP changes 17 | 18 | If you encounter a problem while having work-in-progress changes in your repo, don't hesitate to open a draft pull request. 19 | 20 | Thanks! 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.11-alpine3.16 AS builder 2 | WORKDIR /go/src/github.com/kffl/speedbump/ 3 | COPY ./ ./ 4 | RUN go get ./ 5 | RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -v -o speedbump . 6 | 7 | FROM alpine:3.16 8 | 9 | WORKDIR /root/ 10 | COPY --from=builder /go/src/github.com/kffl/speedbump/speedbump . 11 | ENTRYPOINT ["/root/speedbump"] 12 | CMD ["--help"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # speedbump - TCP proxy with variable latency 2 | 3 |
4 | speedbump logo 5 |
6 | Speedbump is a TCP proxy written in Go which allows for simulating variable network latency. 7 | 8 | [![CI Workflow](https://github.com/kffl/speedbump/workflows/CI/badge.svg)](https://github.com/kffl/speedbump/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/kffl/speedbump)](https://goreportcard.com/report/github.com/kffl/speedbump) [![Docker Pulls](https://img.shields.io/docker/pulls/kffl/speedbump)](https://hub.docker.com/r/kffl/speedbump) [![Docker Image Version](https://img.shields.io/docker/v/kffl/speedbump)](https://hub.docker.com/r/kffl/speedbump) [![GoDoc](https://godoc.org/github.com/kffl/speedbump/lib?status.svg)](https://godoc.org/github.com/kffl/speedbump/lib) 9 | 10 | ## Usage 11 | 12 | ### Installation 13 | 14 | The easiest way to install speedbump is to download pre-built binaries for your platform that are automatically attached to each [release](https://github.com/kffl/speedbump/releases/) under _Assets_. If you wish to build speedbump from source, clone this repository and run `go build`. Alternatively, you can run speedbump as a container using the [kffl/speedbump](https://hub.docker.com/r/kffl/speedbump) image. 15 | 16 | ### Basic usage examples 17 | 18 | Spawn a new instance listening on port 2000 that proxies TCP traffic to localhost:80 with a base latency of 100ms and sine wave amplitude of 100ms (resulting in maximum added latency being 200ms and minimum being 0), period of which is 1 minute: 19 | 20 | ``` 21 | speedbump --latency=100ms --sine-amplitude=100ms --sine-period=1m --port=2000 localhost:80 22 | ``` 23 | 24 | or when running speedbump using the [kffl/speedbump](https://hub.docker.com/r/kffl/speedbump) container image: 25 | 26 | ``` 27 | docker run --net=host kffl/speedbump:latest --latency=100ms --sine-amplitude=100ms \ 28 | --sine-period=1m --port=2000 localhost:80 29 | ``` 30 | 31 | Spawn a new instance with a base latency of 300ms and a sawtooth wave latency summand with amplitude of 200ms and period of 2 minutes (visualized by the graph below): 32 | 33 | ``` 34 | speedbump --latency=300ms --saw-amplitude=200ms --saw-period=2m --port=2000 localhost:80 35 | ``` 36 | 37 |
38 | speedbump sawtooth wave graph 39 |
40 | 41 | ### Combining latency summands 42 | 43 | It is possible to run speedbump with multiple latency summands at once: 44 | 45 |
46 | speedbump sawtooth + sine graph 47 |
48 | 49 | ## CLI Arguments Reference: 50 | 51 | Output of `speedbump --help`: 52 | 53 | ``` 54 | usage: speedbump [] 55 | 56 | TCP proxy for simulating variable network latency. 57 | 58 | Flags: 59 | --help Show context-sensitive help (also try --help-long and 60 | --help-man). 61 | --host="" IP or hostname to listen on. Speedbump will bind to 62 | all available network interfaces if unspecified. 63 | --port=8000 Port number to listen on. 64 | --buffer=64KB Size of the buffer used for TCP reads. 65 | --queue-size=1024 Size of the delay queue storing read buffers. 66 | --latency=5ms Base latency added to proxied traffic. 67 | --log-level=INFO Log level. Possible values: DEBUG, TRACE, INFO, WARN, 68 | ERROR. 69 | --sine-amplitude=0 Amplitude of the latency sine wave. 70 | --sine-period=0 Period of the latency sine wave. 71 | --saw-amplitude=0 Amplitude of the latency sawtooth wave. 72 | --saw-period=0 Period of the latency sawtooth wave. 73 | --square-amplitude=0 Amplitude of the latency square wave. 74 | --square-period=0 Period of the latency square wave. 75 | --triangle-amplitude=0 Amplitude of the latency triangle wave. 76 | --triangle-period=0 Period of the latency triangle wave. 77 | --version Show application version. 78 | 79 | Args: 80 | TCP proxy destination in host:post format. 81 | ``` 82 | 83 | ## Using speedbump as a library 84 | 85 | Speedbump can be used as a Go library via its `lib` package. Check `lib` [README](lib/README.md) for additional information. 86 | 87 | ## License 88 | 89 | Copyright Paweł Kuffel 2022, licensed under Apache 2.0 License. 90 | 91 | Speedbump logo contains the Go Gopher mascot which was originally designed by Renee French (http://reneefrench.blogspot.com/) and licensed under Creative Commons 3.0 Attributions license. 92 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/kffl/speedbump/lib" 5 | "gopkg.in/alecthomas/kingpin.v2" 6 | ) 7 | 8 | func parseArgs(args []string) (*lib.SpeedbumpCfg, error) { 9 | var app = kingpin.New("speedbump", "TCP proxy for simulating variable network latency.") 10 | 11 | var ( 12 | host = app.Flag("host", "IP or hostname to listen on. Speedbump will bind to all network interfaces if unspecified."). 13 | Default(""). 14 | String() 15 | port = app.Flag("port", "Port number to listen on.").Default("8000").Int() 16 | bufferSize = app.Flag("buffer", "Size of the buffer used for TCP reads."). 17 | Default("64KB"). 18 | Bytes() 19 | queueSize = app.Flag("queue-size", "Size of the delay queue storing read buffers."). 20 | Default("1024"). 21 | Int() 22 | latency = app.Flag("latency", "Base latency added to proxied traffic."). 23 | Default("5ms"). 24 | Duration() 25 | logLevel = app.Flag("log-level", "Log level. Possible values: DEBUG, TRACE, INFO, WARN, ERROR."). 26 | Default("INFO"). 27 | Enum("DEBUG", "TRACE", "INFO", "WARN", "ERROR") 28 | sineAmplitude = app.Flag("sine-amplitude", "Amplitude of the latency sine wave."). 29 | PlaceHolder("0"). 30 | Duration() 31 | sinePeriod = app.Flag("sine-period", "Period of the latency sine wave."). 32 | PlaceHolder("0"). 33 | Duration() 34 | sawAmplitude = app.Flag("saw-amplitude", "Amplitude of the latency sawtooth wave."). 35 | PlaceHolder("0"). 36 | Duration() 37 | sawPeriod = app.Flag("saw-period", "Period of the latency sawtooth wave."). 38 | PlaceHolder("0"). 39 | Duration() 40 | squareAmplitude = app.Flag("square-amplitude", "Amplitude of the latency square wave."). 41 | PlaceHolder("0"). 42 | Duration() 43 | squarePeriod = app.Flag("square-period", "Period of the latency square wave."). 44 | PlaceHolder("0"). 45 | Duration() 46 | triangleAmplitude = app.Flag("triangle-amplitude", "Amplitude of the latency triangle wave."). 47 | PlaceHolder("0"). 48 | Duration() 49 | trianglePeriod = app.Flag("triangle-period", "Period of the latency triangle wave."). 50 | PlaceHolder("0"). 51 | Duration() 52 | destAddr = app.Arg("destination", "TCP proxy destination in host:post format."). 53 | Required(). 54 | String() 55 | ) 56 | 57 | app.Version("1.1.0") 58 | _, err := app.Parse(args) 59 | 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | var cfg = lib.SpeedbumpCfg{ 65 | Host: *host, 66 | Port: *port, 67 | DestAddr: *destAddr, 68 | BufferSize: int(*bufferSize), 69 | QueueSize: *queueSize, 70 | Latency: &lib.LatencyCfg{ 71 | Base: *latency, 72 | SineAmplitude: *sineAmplitude, 73 | SinePeriod: *sinePeriod, 74 | SawAmplitude: *sawAmplitude, 75 | SawPeriod: *sawPeriod, 76 | SquareAmplitude: *squareAmplitude, 77 | SquarePeriod: *squarePeriod, 78 | TriangleAmplitude: *triangleAmplitude, 79 | TrianglePeriod: *trianglePeriod, 80 | }, 81 | LogLevel: *logLevel, 82 | } 83 | 84 | return &cfg, err 85 | } 86 | -------------------------------------------------------------------------------- /args_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseArgsDefault(t *testing.T) { 11 | cfg, err := parseArgs([]string{"localhost:80"}) 12 | assert.Nil(t, err) 13 | assert.Equal(t, cfg.DestAddr, "localhost:80") 14 | assert.Equal(t, cfg.Port, 8000) 15 | assert.Equal(t, 0xffff+1, cfg.BufferSize) 16 | assert.Equal(t, time.Millisecond*5, cfg.Latency.Base) 17 | assert.Equal(t, time.Duration(0), cfg.Latency.SineAmplitude) 18 | } 19 | 20 | func TestParseArgsError(t *testing.T) { 21 | _, err := parseArgs([]string{"--nope", "localhost:80"}) 22 | assert.NotNil(t, err) 23 | } 24 | 25 | func TestParseArgsAll(t *testing.T) { 26 | cfg, err := parseArgs( 27 | []string{ 28 | "--host=somehost", 29 | "--port=1234", 30 | "--buffer=200B", 31 | "--latency=100ms", 32 | "--sine-amplitude=50ms", 33 | "--sine-period=1m", 34 | "--square-amplitude=123ms", 35 | "--square-period=3m", 36 | "--triangle-amplitude=150ms", 37 | "--triangle-period=2m", 38 | "host:777", 39 | }, 40 | ) 41 | assert.Nil(t, err) 42 | assert.Equal(t, cfg.DestAddr, "host:777") 43 | assert.Equal(t, cfg.Host, "somehost") 44 | assert.Equal(t, cfg.Port, 1234) 45 | assert.Equal(t, 200, cfg.BufferSize) 46 | assert.Equal(t, time.Millisecond*100, cfg.Latency.Base) 47 | assert.Equal(t, time.Millisecond*50, cfg.Latency.SineAmplitude) 48 | assert.Equal(t, time.Minute, cfg.Latency.SinePeriod) 49 | assert.Equal(t, time.Duration(0), cfg.Latency.SawAmplitude) 50 | assert.Equal(t, time.Duration(0), cfg.Latency.SawPeriod) 51 | assert.Equal(t, time.Millisecond*123, cfg.Latency.SquareAmplitude) 52 | assert.Equal(t, time.Minute*3, cfg.Latency.SquarePeriod) 53 | assert.Equal(t, time.Millisecond*150, cfg.Latency.TriangleAmplitude) 54 | assert.Equal(t, time.Minute*2, cfg.Latency.TrianglePeriod) 55 | } 56 | -------------------------------------------------------------------------------- /assets/combined.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 2022-07-19T20:45:55.087052 10 | image/svg+xml 11 | 12 | 13 | Matplotlib v3.5.2, https://matplotlib.org/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 366 | 379 | 409 | 434 | 435 | 466 | 485 | 506 | 527 | 560 | 577 | 590 | 612 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 661 | 662 | 663 | 664 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 819 | 845 | 852 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 1182 | 1183 | 1184 | 1187 | 1188 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1204 | 1205 | 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | 1217 | 1218 | 1221 | 1222 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1238 | 1239 | 1245 | 1246 | 1247 | 1248 | 1249 | 1250 | 1251 | 1252 | 1255 | 1256 | 1257 | 1260 | 1261 | 1262 | 1265 | 1266 | 1267 | 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 1301 | 1327 | 1334 | 1347 | 1363 | 1384 | 1385 | 1386 | 1387 | 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 1395 | 1396 | 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | 1403 | 1404 | 1405 | 1406 | 1407 | 1408 | 1409 | 1410 | 1411 | 1412 | 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | 1476 | 1477 | 1478 | 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 1485 | 1486 | 1487 | 1488 | 1489 | 1490 | 1491 | 1492 | 1493 | 1494 | 1505 | 1506 | 1507 | 1511 | 1512 | 1513 | 1514 | 1515 | 1516 | 1517 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 | 1527 | 1528 | 1529 | 1530 | 1531 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 | 1575 | 1576 | 1577 | 1578 | 1579 | 1580 | 1581 | 1582 | 1583 | 1584 | 1585 | 1586 | 1587 | 1588 | 1589 | 1590 | 1591 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1634 | 1653 | 1654 | 1655 | 1656 | 1657 | 1658 | 1659 | 1660 | 1661 | 1662 | 1663 | 1664 | 1665 | 1666 | 1667 | 1668 | 1669 | 1670 | 1671 | 1672 | 1673 | 1674 | 1675 | 1679 | 1680 | 1681 | 1682 | 1683 | 1684 | 1685 | 1686 | 1687 | 1708 | 1709 | 1710 | 1711 | 1712 | 1713 | 1714 | 1715 | 1716 | 1717 | 1718 | 1719 | 1720 | 1721 | 1722 | 1723 | 1724 | 1725 | 1726 | 1727 | 1731 | 1732 | 1733 | 1734 | 1735 | 1736 | 1737 | 1738 | 1739 | 1740 | 1741 | 1742 | 1743 | 1744 | 1745 | 1746 | 1747 | 1748 | 1749 | 1750 | 1751 | 1752 | 1753 | 1754 | 1755 | 1756 | 1757 | 1758 | 1759 | 1760 | -------------------------------------------------------------------------------- /assets/sawtooth.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 2022-07-19T20:17:48.714337 10 | image/svg+xml 11 | 12 | 13 | Matplotlib v3.5.2, https://matplotlib.org/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 366 | 379 | 409 | 434 | 435 | 466 | 485 | 506 | 527 | 560 | 577 | 590 | 612 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 661 | 662 | 663 | 664 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 819 | 845 | 852 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 902 | 903 | 904 | 907 | 908 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 924 | 925 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 941 | 942 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 958 | 959 | 960 | 963 | 964 | 965 | 968 | 969 | 970 | 973 | 974 | 975 | 976 | 977 | 978 | 1004 | 1030 | 1037 | 1050 | 1066 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1166 | 1167 | 1168 | 1172 | 1173 | 1174 | 1175 | 1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | 1249 | 1250 | 1251 | 1252 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 1267 | 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1308 | 1309 | 1310 | 1311 | 1312 | 1313 | 1314 | 1315 | 1316 | 1317 | 1318 | 1319 | 1320 | 1321 | 1322 | 1323 | 1324 | 1325 | 1326 | -------------------------------------------------------------------------------- /assets/speedbump.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kffl/speedbump/04ce6c72868086bfc65fd1691751a3db124f296e/assets/speedbump.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kffl/speedbump 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 7 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/fatih/color v1.13.0 // indirect 10 | github.com/hashicorp/go-hclog v1.2.1 // indirect 11 | github.com/mattn/go-colorable v0.1.12 // indirect 12 | github.com/mattn/go-isatty v0.0.14 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | github.com/stretchr/objx v0.4.0 // indirect 15 | github.com/stretchr/testify v1.8.0 // indirect 16 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect 17 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 4 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 9 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 10 | github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= 11 | github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 12 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 13 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 14 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 15 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 16 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 17 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 22 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 23 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 24 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 25 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 26 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 27 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 28 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= 33 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 35 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # speedbump `lib` package 2 | 3 | This package allows for using speedbump as external library in Go code. It can be useful for adding programmatic delay to TCP connections while running load tests (i.e. between the SUT and a database). 4 | 5 | Consult GoDoc for API reference: 6 | 7 | [![GoDoc](https://godoc.org/github.com/kffl/speedbump/lib?status.svg)](https://godoc.org/github.com/kffl/speedbump/lib) 8 | 9 | ## Installation 10 | 11 | ``` 12 | go get github.com/kffl/speedbump/lib 13 | ``` 14 | 15 | ## Example usage 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "time" 22 | 23 | speedbump "github.com/kffl/speedbump/lib" 24 | ) 25 | 26 | func main() { 27 | cfg := speedbump.SpeedbumpCfg{ 28 | Port: 8000, 29 | DestAddr: "localhost:80", 30 | BufferSize: 16384, 31 | QueueSize: 2048, 32 | Latency: &speedbump.LatencyCfg{ 33 | Base: time.Millisecond * 100, 34 | SineAmplitude: time.Millisecond * 50, 35 | SinePeriod: time.Minute, 36 | }, 37 | LogLevel: "TRACE", 38 | } 39 | 40 | s, err := speedbump.NewSpeedbump(&cfg) 41 | 42 | if err != nil { 43 | // handle creation error 44 | return 45 | } 46 | 47 | // Start() will unblock as soon as the proxy is started 48 | // or return an error if there is a startup error 49 | err = s.Start() 50 | 51 | if err != nil { 52 | // handle startup error 53 | return 54 | } 55 | 56 | // let's stop the proxy after 5 mins 57 | time.Sleep(time.Minute * 5) 58 | 59 | s.Stop() 60 | 61 | // DONE 62 | } 63 | 64 | ``` 65 | 66 | ## `v1` Upgrade guide 67 | 68 | In an effort to make the `lib` package easier to work with when used as a dependency for Go tests, the following changes were made to its API in the `v1` release: 69 | 70 | - `Start()` is no longer blocking. It will either unblock as soon as the proxy starts listening or return an error if proxy startup fails; 71 | - `Stop()` waits for all proxy connections to close before returning; 72 | - a field name typo `sawAmplitute` was fixed in `LatencyCfg` struct (renamed to `SawAmplitude`). -------------------------------------------------------------------------------- /lib/base.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import "time" 4 | 5 | type baseLatencySummand struct { 6 | latency time.Duration 7 | } 8 | 9 | func (b baseLatencySummand) getLatency(elapsed time.Duration) time.Duration { 10 | return b.latency 11 | } 12 | -------------------------------------------------------------------------------- /lib/connection.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hashicorp/go-hclog" 12 | ) 13 | 14 | type transitBuffer struct { 15 | data []byte 16 | delayUntil time.Time 17 | } 18 | 19 | type connection struct { 20 | srcConn, destConn io.ReadWriteCloser 21 | bufferSize int 22 | latencyGen LatencyGenerator 23 | delayQueue chan transitBuffer 24 | done chan error 25 | ctx context.Context 26 | log hclog.Logger 27 | } 28 | 29 | func (c *connection) readFromSrc() { 30 | for { 31 | buffer := make([]byte, c.bufferSize) 32 | bytes, err := c.srcConn.Read(buffer) 33 | receivedAt := time.Now() 34 | if err != nil { 35 | c.done <- fmt.Errorf("Error reading data from client %s", err) 36 | return 37 | } 38 | trimmedBuffer := buffer[:bytes] 39 | desiredLatency := c.latencyGen.generateLatency(receivedAt) 40 | delayUntil := receivedAt.Add(desiredLatency) 41 | 42 | t := transitBuffer{ 43 | data: trimmedBuffer, 44 | delayUntil: delayUntil, 45 | } 46 | 47 | c.log.Trace("Writing to delay queue", "bytes", bytes, "delay", desiredLatency) 48 | 49 | c.delayQueue <- t 50 | 51 | } 52 | } 53 | 54 | func (c *connection) readFromDest() { 55 | buffer := make([]byte, c.bufferSize) 56 | for { 57 | bytes, err := c.destConn.Read(buffer) 58 | if err != nil { 59 | c.done <- fmt.Errorf("Error reading data from proxy destination: %s", err) 60 | return 61 | } 62 | trimmedBuffer := buffer[:bytes] 63 | 64 | bytes, err = c.srcConn.Write(trimmedBuffer) 65 | if err != nil { 66 | c.done <- fmt.Errorf("Error writing data back to proxy client: %s", err) 67 | return 68 | } 69 | } 70 | } 71 | 72 | func (c *connection) readFromDelayQueue() { 73 | for { 74 | t := <-c.delayQueue 75 | 76 | c.log.Trace("Read from delay queue", "bytes", len(t.data)) 77 | 78 | time.Sleep(time.Until(t.delayUntil)) 79 | 80 | _, err := c.destConn.Write(t.data) 81 | if err != nil { 82 | c.done <- fmt.Errorf("Error writing from delay queue to proxy destination: %s", err) 83 | return 84 | } 85 | } 86 | } 87 | 88 | // start launches 3 goroutines responsible for handling a proxy connection 89 | // (dest->src, src->queue, queue->dest). This operation will block until 90 | // either an error is sent via the done channel or the context is cancelled. 91 | func (c *connection) start() { 92 | c.log.Debug("Starting a new proxy connection") 93 | go c.readFromDest() 94 | go c.readFromSrc() 95 | go c.readFromDelayQueue() 96 | for { 97 | select { 98 | case err := <-c.done: 99 | c.handleError(err) 100 | return 101 | case <-c.ctx.Done(): 102 | c.handleStop() 103 | return 104 | } 105 | } 106 | } 107 | 108 | func (c *connection) handleError(err error) { 109 | if !strings.HasSuffix(err.Error(), io.EOF.Error()) { 110 | c.log.Warn("Closing proxy connection due to an unexpected error", "err", err) 111 | } else { 112 | c.log.Debug("Closing proxy connection (EOF)") 113 | } 114 | c.closeProxyConnections() 115 | } 116 | 117 | func (c *connection) handleStop() { 118 | c.log.Info("Stopping proxy connection") 119 | c.closeProxyConnections() 120 | } 121 | 122 | func (c *connection) closeProxyConnections() { 123 | c.srcConn.Close() 124 | c.destConn.Close() 125 | } 126 | 127 | func newProxyConnection( 128 | ctx context.Context, 129 | clientConn io.ReadWriteCloser, 130 | srcAddr *net.TCPAddr, 131 | destAddr *net.TCPAddr, 132 | bufferSize int, 133 | queueSize int, 134 | latencyGen LatencyGenerator, 135 | logger hclog.Logger, 136 | ) (*connection, error) { 137 | destConn, err := net.DialTCP("tcp", nil, destAddr) 138 | if err != nil { 139 | return nil, fmt.Errorf("Error dialing remote address: %s", err) 140 | } 141 | c := &connection{ 142 | srcConn: clientConn, 143 | destConn: destConn, 144 | bufferSize: bufferSize, 145 | latencyGen: latencyGen, 146 | delayQueue: make(chan transitBuffer, queueSize), 147 | done: make(chan error, 3), 148 | ctx: ctx, 149 | log: logger, 150 | } 151 | 152 | return c, nil 153 | } 154 | -------------------------------------------------------------------------------- /lib/connection_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "testing" 8 | "time" 9 | 10 | "github.com/hashicorp/go-hclog" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type readReturn struct { 15 | n int 16 | data []byte 17 | err error 18 | } 19 | type writeReturn struct { 20 | n int 21 | err error 22 | } 23 | 24 | type mockConn struct { 25 | readCount *int 26 | writeCount *int 27 | closeCount *int 28 | readRes []readReturn 29 | writeRes []writeReturn 30 | closeRes []error 31 | } 32 | 33 | func (m mockConn) Read(p []byte) (int, error) { 34 | invocation := *m.readCount 35 | *m.readCount++ 36 | res := m.readRes[invocation%len(m.readRes)] 37 | copy(p, res.data) 38 | return res.n, res.err 39 | } 40 | 41 | func (m mockConn) Write(p []byte) (int, error) { 42 | invocation := *m.writeCount 43 | *m.writeCount++ 44 | res := m.writeRes[invocation%len(m.writeRes)] 45 | return res.n, res.err 46 | } 47 | 48 | func (m mockConn) Close() error { 49 | invocation := *m.closeCount 50 | *m.closeCount++ 51 | res := m.closeRes[invocation%len(m.closeRes)] 52 | return res 53 | } 54 | 55 | type mockLatencyGenerator struct { 56 | delay time.Duration 57 | } 58 | 59 | func (m *mockLatencyGenerator) generateLatency(when time.Time) time.Duration { 60 | return m.delay 61 | } 62 | 63 | func TestReadFromSrc(t *testing.T) { 64 | readCnt := new(int) 65 | writeCtn := new(int) 66 | closeCnt := new(int) 67 | mockSrc := mockConn{ 68 | readCount: readCnt, 69 | writeCount: writeCtn, 70 | closeCount: closeCnt, 71 | readRes: []readReturn{ 72 | {10, []byte("testdata12jibberish"), nil}, 73 | {0, []byte(""), errors.New("some-error")}, 74 | }, 75 | } 76 | 77 | delayQueue := make(chan transitBuffer, 10) 78 | done := make(chan error, 3) 79 | 80 | c := &connection{ 81 | srcConn: mockSrc, 82 | bufferSize: 20, 83 | latencyGen: &mockLatencyGenerator{time.Millisecond * 2}, 84 | delayQueue: delayQueue, 85 | done: done, 86 | log: hclog.NewNullLogger(), 87 | } 88 | 89 | c.readFromSrc() 90 | 91 | transitBuff := <-delayQueue 92 | err := <-done 93 | 94 | assert.Equal(t, 2, *mockSrc.readCount) 95 | assert.Equal(t, []byte("testdata12"), transitBuff.data) 96 | assert.EqualError(t, err, "Error reading data from client some-error") 97 | } 98 | 99 | func TestReadFromDest(t *testing.T) { 100 | readCnt := new(int) 101 | writeCtn := new(int) 102 | closeCnt := new(int) 103 | mockDest := mockConn{ 104 | readCount: readCnt, 105 | writeCount: writeCtn, 106 | closeCount: closeCnt, 107 | readRes: []readReturn{ 108 | {10, []byte("testdata12jibberish"), nil}, 109 | {0, []byte(""), errors.New("some-error")}, 110 | }, 111 | } 112 | 113 | readCnt = new(int) 114 | writeCtn = new(int) 115 | closeCnt = new(int) 116 | mockSrc := mockConn{ 117 | readCount: readCnt, 118 | writeCount: writeCtn, 119 | closeCount: closeCnt, 120 | writeRes: []writeReturn{ 121 | {10, nil}, 122 | {0, errors.New("other-error")}, 123 | }, 124 | } 125 | 126 | done := make(chan error, 3) 127 | 128 | c := &connection{ 129 | srcConn: mockSrc, 130 | destConn: mockDest, 131 | bufferSize: 20, 132 | latencyGen: &mockLatencyGenerator{time.Millisecond * 2}, 133 | done: done, 134 | } 135 | 136 | c.readFromDest() 137 | 138 | err := <-done 139 | 140 | assert.Equal(t, 2, *mockDest.readCount) 141 | assert.Equal(t, 1, *mockSrc.writeCount) 142 | assert.EqualError(t, err, "Error reading data from proxy destination: some-error") 143 | } 144 | 145 | func TestReadFromDestSrcWriteError(t *testing.T) { 146 | readCnt := new(int) 147 | writeCtn := new(int) 148 | closeCnt := new(int) 149 | mockDest := mockConn{ 150 | readCount: readCnt, 151 | writeCount: writeCtn, 152 | closeCount: closeCnt, 153 | readRes: []readReturn{ 154 | {10, []byte("testdata12jibberish"), nil}, 155 | {10, []byte("testdata34jibberish"), nil}, 156 | }, 157 | } 158 | 159 | readCnt = new(int) 160 | writeCtn = new(int) 161 | closeCnt = new(int) 162 | mockSrc := mockConn{ 163 | readCount: readCnt, 164 | writeCount: writeCtn, 165 | closeCount: closeCnt, 166 | writeRes: []writeReturn{ 167 | {10, nil}, 168 | {0, errors.New("other-error")}, 169 | }, 170 | } 171 | 172 | done := make(chan error, 3) 173 | 174 | c := &connection{ 175 | srcConn: mockSrc, 176 | destConn: mockDest, 177 | bufferSize: 20, 178 | latencyGen: &mockLatencyGenerator{time.Millisecond * 2}, 179 | done: done, 180 | } 181 | 182 | c.readFromDest() 183 | 184 | err := <-done 185 | 186 | assert.Equal(t, 2, *mockDest.readCount) 187 | assert.Equal(t, 2, *mockSrc.writeCount) 188 | assert.EqualError(t, err, "Error writing data back to proxy client: other-error") 189 | } 190 | 191 | func TestReadFromDelayQueue(t *testing.T) { 192 | readCnt := new(int) 193 | writeCtn := new(int) 194 | closeCnt := new(int) 195 | mockDest := mockConn{ 196 | readCount: readCnt, 197 | writeCount: writeCtn, 198 | closeCount: closeCnt, 199 | writeRes: []writeReturn{ 200 | {10, nil}, 201 | {0, errors.New("write-error")}, 202 | }, 203 | } 204 | 205 | delayQueue := make(chan transitBuffer, 10) 206 | done := make(chan error, 3) 207 | 208 | c := &connection{ 209 | destConn: mockDest, 210 | bufferSize: 20, 211 | delayQueue: delayQueue, 212 | done: done, 213 | log: hclog.NewNullLogger(), 214 | } 215 | 216 | delayQueue <- transitBuffer{[]byte("testdata"), time.Now().Add(time.Millisecond)} 217 | delayQueue <- transitBuffer{[]byte("testdata"), time.Now().Add(time.Millisecond * 2)} 218 | 219 | c.readFromDelayQueue() 220 | 221 | err := <-done 222 | 223 | assert.Equal(t, 2, *mockDest.writeCount) 224 | assert.EqualError(t, err, "Error writing from delay queue to proxy destination: write-error") 225 | } 226 | 227 | func TestStart(t *testing.T) { 228 | readCnt := new(int) 229 | writeCtn := new(int) 230 | closeCnt := new(int) 231 | mockDest := mockConn{ 232 | readCount: readCnt, 233 | writeCount: writeCtn, 234 | closeCount: closeCnt, 235 | readRes: []readReturn{ 236 | {10, []byte("testdata12jibberish"), nil}, 237 | {10, []byte("testdata34jibberish"), nil}, 238 | {10, []byte("testdata56jibberish"), nil}, 239 | }, 240 | writeRes: []writeReturn{ 241 | {10, nil}, 242 | {0, errors.New("dest-write-err")}, 243 | }, 244 | closeRes: []error{nil}, 245 | } 246 | 247 | readCnt = new(int) 248 | writeCtn = new(int) 249 | closeCnt = new(int) 250 | mockSrc := mockConn{ 251 | readCount: readCnt, 252 | writeCount: writeCtn, 253 | closeCount: closeCnt, 254 | readRes: []readReturn{ 255 | {10, []byte("testdata12jibberish"), nil}, 256 | {10, []byte("testdata34jibberish"), nil}, 257 | }, 258 | writeRes: []writeReturn{ 259 | {10, nil}, 260 | }, 261 | closeRes: []error{nil}, 262 | } 263 | 264 | delayQueue := make(chan transitBuffer, 10) 265 | done := make(chan error, 3) 266 | 267 | c := &connection{ 268 | srcConn: mockSrc, 269 | destConn: mockDest, 270 | bufferSize: 20, 271 | latencyGen: &mockLatencyGenerator{time.Millisecond * 10}, 272 | delayQueue: delayQueue, 273 | done: done, 274 | ctx: context.TODO(), 275 | log: hclog.NewNullLogger(), 276 | } 277 | 278 | c.start() 279 | 280 | time.Sleep(time.Millisecond * 30) 281 | 282 | assert.Equal(t, 2, *mockDest.writeCount) 283 | assert.Equal(t, 1, *mockDest.closeCount) 284 | assert.Equal(t, 1, *mockSrc.closeCount) 285 | } 286 | 287 | func TestNewProxyConnectionError(t *testing.T) { 288 | localAddr, _ := net.ResolveTCPAddr("tcp", ":8000") 289 | destAddr, _ := net.ResolveTCPAddr("tcp", "nope:3000") 290 | 291 | mockClientConn := mockConn{} 292 | 293 | _, err := newProxyConnection( 294 | context.TODO(), 295 | mockClientConn, 296 | localAddr, 297 | destAddr, 298 | 0xffff, 299 | 100, 300 | &mockLatencyGenerator{time.Millisecond * 10}, 301 | hclog.Default(), 302 | ) 303 | 304 | assert.NotNil(t, err) 305 | } 306 | -------------------------------------------------------------------------------- /lib/latency_generator.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type LatencyGenerator interface { 8 | generateLatency(time.Time) time.Duration 9 | } 10 | 11 | type LatencyCfg struct { 12 | Base time.Duration 13 | SineAmplitude time.Duration 14 | SinePeriod time.Duration 15 | SawAmplitude time.Duration 16 | SawPeriod time.Duration 17 | SquareAmplitude time.Duration 18 | SquarePeriod time.Duration 19 | TriangleAmplitude time.Duration 20 | TrianglePeriod time.Duration 21 | } 22 | 23 | type latencySummand interface { 24 | getLatency(elapsed time.Duration) time.Duration 25 | } 26 | 27 | type simpleLatencyGenerator struct { 28 | start time.Time 29 | summands []latencySummand 30 | } 31 | 32 | func newSimpleLatencyGenerator(start time.Time, cfg *LatencyCfg) simpleLatencyGenerator { 33 | summands := []latencySummand{baseLatencySummand{cfg.Base}} 34 | if cfg.SineAmplitude > 0 && cfg.SinePeriod > 0 { 35 | summands = append(summands, sineLatencySummand{ 36 | cfg.SineAmplitude, 37 | cfg.SinePeriod, 38 | }) 39 | } 40 | if cfg.SawAmplitude > 0 && cfg.SawPeriod > 0 { 41 | summands = append(summands, sawtoothLatencySummand{ 42 | cfg.SawAmplitude, 43 | cfg.SawPeriod, 44 | }) 45 | } 46 | if cfg.SquareAmplitude > 0 && cfg.SquarePeriod > 0 { 47 | summands = append(summands, squareLatencySummand{ 48 | cfg.SquareAmplitude, 49 | cfg.SquarePeriod, 50 | }) 51 | } 52 | if cfg.TriangleAmplitude > 0 && cfg.TrianglePeriod > 0 { 53 | summands = append(summands, triangleLatencySummand{ 54 | cfg.TriangleAmplitude, 55 | cfg.TrianglePeriod, 56 | }) 57 | } 58 | return simpleLatencyGenerator{ 59 | start: start, 60 | summands: summands, 61 | } 62 | } 63 | 64 | func (g simpleLatencyGenerator) generateLatency(when time.Time) time.Duration { 65 | var latency time.Duration = 0 66 | elapsed := when.Sub(g.start) 67 | for _, s := range g.summands { 68 | latency += s.getLatency(elapsed) 69 | } 70 | return latency 71 | } 72 | -------------------------------------------------------------------------------- /lib/latency_generator_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSimpleLatencyGeneratorWithSine(t *testing.T) { 11 | start := time.Now() 12 | g := newSimpleLatencyGenerator(start, &LatencyCfg{ 13 | Base: time.Second * 3, 14 | SineAmplitude: time.Second * 2, 15 | SinePeriod: time.Second * 8, 16 | }) 17 | 18 | startingVal := g.generateLatency(start) 19 | after2Sec := g.generateLatency(start.Add(time.Second * 2)) 20 | after4Sec := g.generateLatency(start.Add(time.Second * 4)) 21 | after2Periods := g.generateLatency(start.Add(time.Second * 16)) 22 | assert.Equal(t, time.Second*3, startingVal) 23 | assert.Equal(t, time.Second*5, after2Sec) 24 | assert.Equal(t, time.Second*3, after4Sec) 25 | assert.Equal(t, time.Second*3, after2Periods) 26 | } 27 | 28 | func TestSimpleLatencyGeneratorWithSawtooth(t *testing.T) { 29 | start := time.Now() 30 | g := newSimpleLatencyGenerator(start, &LatencyCfg{ 31 | Base: time.Second * 3, 32 | SawAmplitude: time.Second * 2, 33 | SawPeriod: time.Second * 8, 34 | }) 35 | 36 | startingVal := g.generateLatency(start) 37 | after2Sec := g.generateLatency(start.Add(time.Second * 2)) 38 | after4Sec := g.generateLatency(start.Add(time.Second * 4)) 39 | after2Periods := g.generateLatency(start.Add(time.Second * 16)) 40 | assert.Equal(t, time.Second*3, startingVal) 41 | assert.Equal(t, time.Second*4, after2Sec) 42 | assert.Equal(t, time.Second*1, after4Sec) 43 | assert.Equal(t, time.Second*3, after2Periods) 44 | } 45 | 46 | func TestSimpleLatencyGeneratorWithTriangle(t *testing.T) { 47 | start := time.Now() 48 | g := newSimpleLatencyGenerator(start, &LatencyCfg{ 49 | Base: time.Second * 3, 50 | TriangleAmplitude: time.Second * 2, 51 | TrianglePeriod: time.Second * 8, 52 | }) 53 | 54 | startingVal := g.generateLatency(start) 55 | after2Sec := g.generateLatency(start.Add(time.Second * 2)) 56 | after4Sec := g.generateLatency(start.Add(time.Second * 4)) 57 | after6Sec := g.generateLatency(start.Add(time.Second * 6)) 58 | after2Periods := g.generateLatency(start.Add(time.Second * 16)) 59 | assert.Equal(t, time.Second*3, startingVal) 60 | assert.Equal(t, time.Second*5, after2Sec) 61 | assert.Equal(t, time.Second*3, after4Sec) 62 | assert.Equal(t, time.Second*1, after6Sec) 63 | assert.Equal(t, time.Second*3, after2Periods) 64 | } 65 | -------------------------------------------------------------------------------- /lib/sawtooth.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import "time" 4 | 5 | type sawtoothLatencySummand struct { 6 | amplitude time.Duration 7 | period time.Duration 8 | } 9 | 10 | func (s sawtoothLatencySummand) getLatency(elapsed time.Duration) time.Duration { 11 | return time.Duration( 12 | (float64((elapsed+s.period/2)%s.period)/float64(s.period))*float64(s.amplitude*2), 13 | ) - s.amplitude 14 | } 15 | -------------------------------------------------------------------------------- /lib/sawtooth_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSawtoothLatencySummand(t *testing.T) { 11 | s := sawtoothLatencySummand{ 12 | amplitude: time.Second, 13 | period: time.Minute, 14 | } 15 | 16 | assert.Equal(t, time.Millisecond*0, s.getLatency(time.Duration(0))) 17 | assert.Equal(t, time.Millisecond*500, s.getLatency(time.Second*15)) 18 | assert.Equal(t, time.Millisecond*-1000, s.getLatency(time.Second*30)) 19 | assert.Equal(t, time.Millisecond*-800, s.getLatency(time.Second*36)) 20 | assert.Equal(t, time.Millisecond*0, s.getLatency(time.Second*60)) 21 | } 22 | -------------------------------------------------------------------------------- /lib/sine.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | type sineLatencySummand struct { 9 | amplitude time.Duration 10 | period time.Duration 11 | } 12 | 13 | func (s sineLatencySummand) getLatency(elapsed time.Duration) time.Duration { 14 | return time.Duration( 15 | math.Sin( 16 | float64(elapsed)/float64(s.period)*math.Pi*2, 17 | ) * float64( 18 | s.amplitude, 19 | )) 20 | } 21 | -------------------------------------------------------------------------------- /lib/speedbump.go: -------------------------------------------------------------------------------- 1 | // Package lib allows for using speedbump as a library. 2 | package lib 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/hashicorp/go-hclog" 13 | ) 14 | 15 | // Speedbump is a proxy instance returned by NewSpeedbump 16 | type Speedbump struct { 17 | bufferSize int 18 | queueSize int 19 | srcAddr, destAddr net.TCPAddr 20 | listener *net.TCPListener 21 | latencyGen LatencyGenerator 22 | nextConnId int 23 | // active keeps track of proxy connections that are running 24 | active sync.WaitGroup 25 | // ctx is used for notifying proxy connections once Stop() is invoked 26 | ctx context.Context 27 | ctxCancel context.CancelFunc 28 | log hclog.Logger 29 | } 30 | 31 | // SpeedbumpCfg contains Spedbump instance configuration 32 | type SpeedbumpCfg struct { 33 | // IP or a hostname to listen on (binds to all network interfaces if unspecified) 34 | Host string 35 | // Port specifies the local port number to listen on 36 | Port int 37 | // DestAddr specifies the proxy desination address in host:port format 38 | DestAddr string 39 | // BufferSize specifies the number of bytes in a buffer used for TCP reads 40 | BufferSize int 41 | // The size of the delay queue containing read buffers (defaults to 1024) 42 | QueueSize int 43 | // LatencyCfg specifies parameters of the desired latency summands 44 | Latency *LatencyCfg 45 | // LogLevel can be one of: DEBUG, TRACE, INFO, WARN, ERROR 46 | LogLevel string 47 | } 48 | 49 | // NewSpeedbump creates a Speedbump instance based on a provided config 50 | func NewSpeedbump(cfg *SpeedbumpCfg) (*Speedbump, error) { 51 | localTCPAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)) 52 | if err != nil { 53 | return nil, fmt.Errorf("Error resolving local address: %s", err) 54 | } 55 | destTCPAddr, err := net.ResolveTCPAddr("tcp", cfg.DestAddr) 56 | if err != nil { 57 | return nil, fmt.Errorf("Error resolving destination address: %s", err) 58 | } 59 | l := hclog.New(&hclog.LoggerOptions{ 60 | Level: hclog.LevelFromString(cfg.LogLevel), 61 | }) 62 | queueSize := cfg.QueueSize 63 | // setting a default queueSize in order to maintain compatibility 64 | // with speedbump @v0.1.0 used as a dependency in other Go programs 65 | if queueSize == 0 { 66 | queueSize = 1024 67 | } 68 | s := &Speedbump{ 69 | bufferSize: int(cfg.BufferSize), 70 | queueSize: queueSize, 71 | srcAddr: *localTCPAddr, 72 | destAddr: *destTCPAddr, 73 | latencyGen: newSimpleLatencyGenerator(time.Now(), cfg.Latency), 74 | log: l, 75 | } 76 | return s, nil 77 | } 78 | 79 | func (s *Speedbump) startAcceptLoop() { 80 | for { 81 | conn, err := s.listener.AcceptTCP() 82 | if err != nil { 83 | if strings.Contains(err.Error(), "use of closed") { 84 | // the listener was closed, which means that Stop() was called 85 | return 86 | } else { 87 | s.log.Warn("Accepting incoming TCP conn failed", "err", err) 88 | continue 89 | } 90 | } 91 | l := s.log.With("connection", s.nextConnId) 92 | p, err := newProxyConnection( 93 | s.ctx, 94 | conn, 95 | &s.srcAddr, 96 | &s.destAddr, 97 | s.bufferSize, 98 | s.queueSize, 99 | s.latencyGen, 100 | l, 101 | ) 102 | if err != nil { 103 | s.log.Warn("Creating new proxy conn failed", "err", err) 104 | conn.Close() 105 | continue 106 | } 107 | s.nextConnId++ 108 | s.active.Add(1) 109 | go s.startProxyConnection(p) 110 | } 111 | } 112 | 113 | func (s *Speedbump) startProxyConnection(p *connection) { 114 | defer s.active.Done() 115 | // start will block until a proxy connection is closed 116 | p.start() 117 | } 118 | 119 | // Start launches a Speedbump instance. This operation will unblock either 120 | // as soon as the proxy starts listening or when a startup error occurrs. 121 | func (s *Speedbump) Start() error { 122 | listener, err := net.ListenTCP("tcp", &s.srcAddr) 123 | if err != nil { 124 | return fmt.Errorf("Error starting TCP listener: %s", err) 125 | } 126 | s.listener = listener 127 | 128 | ctx, cancel := context.WithCancel(context.Background()) 129 | s.ctx = ctx 130 | s.ctxCancel = cancel 131 | 132 | s.log.Info("Started speedbump", "port", s.srcAddr.Port, "dest", s.destAddr.String()) 133 | 134 | go s.startAcceptLoop() 135 | return nil 136 | } 137 | 138 | // Stop closes the Speedbump instance's TCP listener and notifies all existing 139 | // proxy connections that Speedbump is shutting down. It waits for individual 140 | // proxy connections to close before returning. 141 | func (s *Speedbump) Stop() { 142 | s.log.Info("Stopping speedbump") 143 | // close TCP listener so that startAcceptLoop returns 144 | s.listener.Close() 145 | // notify all proxy connections 146 | s.ctxCancel() 147 | s.log.Debug("Waiting for active connections to be closed") 148 | s.active.Wait() 149 | s.log.Info("Speedbump stopped") 150 | } 151 | -------------------------------------------------------------------------------- /lib/speedbump_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var defaultLatencyCfg = &LatencyCfg{ 15 | Base: time.Millisecond * 5, 16 | SineAmplitude: time.Duration(0), 17 | SinePeriod: time.Minute, 18 | } 19 | 20 | func startEchoSrv(port int) error { 21 | srv, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) 22 | if err != nil { 23 | return err 24 | } 25 | defer srv.Close() 26 | for { 27 | conn, err := srv.Accept() 28 | if err != nil { 29 | continue 30 | } 31 | go func(c net.Conn) { 32 | defer c.Close() 33 | io.Copy(c, c) 34 | }(conn) 35 | } 36 | } 37 | 38 | func TestNewSpeedbump(t *testing.T) { 39 | cfg := SpeedbumpCfg{ 40 | "localhost", 41 | 8000, 42 | "localhost:1234", 43 | 0xffff, 44 | 100, 45 | defaultLatencyCfg, 46 | "WARN", 47 | } 48 | s, err := NewSpeedbump(&cfg) 49 | assert.Nil(t, err) 50 | assert.Equal(t, 0xffff, s.bufferSize) 51 | } 52 | 53 | func TestNewSpeedbumpInvalidHost(t *testing.T) { 54 | cfg := SpeedbumpCfg{ 55 | "nope", 56 | 8080, 57 | "localhost:1234", 58 | 0xffff, 59 | 100, 60 | defaultLatencyCfg, 61 | "WARN", 62 | } 63 | s, err := NewSpeedbump(&cfg) 64 | assert.Nil(t, s) 65 | assert.ErrorContains(t, err, "lookup nope") 66 | } 67 | 68 | func TestNewSpeedbumpErrorResolvingLocal(t *testing.T) { 69 | cfg := SpeedbumpCfg{ 70 | "localhost", 71 | -1, 72 | "localhost:1234", 73 | 0xffff, 74 | 100, 75 | defaultLatencyCfg, 76 | "WARN", 77 | } 78 | s, err := NewSpeedbump(&cfg) 79 | assert.Nil(t, s) 80 | assert.True(t, strings.HasPrefix(err.Error(), "Error resolving local")) 81 | } 82 | 83 | func TestNewSpeedbumpErrorResolvingDest(t *testing.T) { 84 | cfg := SpeedbumpCfg{ 85 | "localhost", 86 | 8000, 87 | "nope:1234", 88 | 0xffff, 89 | 100, 90 | defaultLatencyCfg, 91 | "WARN", 92 | } 93 | s, err := NewSpeedbump(&cfg) 94 | assert.Nil(t, s) 95 | assert.True(t, strings.HasPrefix(err.Error(), "Error resolving destination")) 96 | } 97 | 98 | func TestNewSpeedbumpDefaultQueueSize(t *testing.T) { 99 | cfg := SpeedbumpCfg{ 100 | Port: 8000, 101 | DestAddr: "localhost:1234", 102 | BufferSize: 0xffff, 103 | // QueueSize is ommitted 104 | Latency: defaultLatencyCfg, 105 | LogLevel: "WARN", 106 | } 107 | s, err := NewSpeedbump(&cfg) 108 | assert.Nil(t, err) 109 | assert.Equal(t, 1024, s.queueSize) 110 | } 111 | 112 | func TestStartListenError(t *testing.T) { 113 | cfg := SpeedbumpCfg{ 114 | "localhost", 115 | 1, // a privileged port 116 | "localhost:1234", 117 | 0xffff, 118 | 100, 119 | defaultLatencyCfg, 120 | "WARN", 121 | } 122 | s, _ := NewSpeedbump(&cfg) 123 | 124 | err := s.Start() 125 | 126 | assert.True(t, strings.HasPrefix(err.Error(), "Error starting TCP listener")) 127 | } 128 | 129 | func isDurationCloseTo(expected time.Duration, obtianed time.Duration, percentage int) bool { 130 | absoluteError := int(expected) - int(obtianed) 131 | if absoluteError < 0 { 132 | absoluteError *= -1 133 | } 134 | errorPercentage := float64(absoluteError) / float64(expected) * 100.0 135 | return errorPercentage < float64(percentage) 136 | } 137 | 138 | func TestSpeedbumpWithEchoServer(t *testing.T) { 139 | port := 9006 140 | testSrvAddr := fmt.Sprintf("localhost:%d", port) 141 | 142 | go startEchoSrv(port) 143 | 144 | cfg := SpeedbumpCfg{ 145 | "localhost", 146 | 8000, 147 | testSrvAddr, 148 | 0xffff, 149 | 100, 150 | &LatencyCfg{ 151 | Base: time.Millisecond * 100, 152 | SineAmplitude: time.Millisecond * 100, 153 | SinePeriod: time.Millisecond * 400, 154 | }, 155 | "WARN", 156 | } 157 | s, err := NewSpeedbump(&cfg) 158 | s.Start() 159 | 160 | assert.Nil(t, err) 161 | 162 | tcpAddr, _ := net.ResolveTCPAddr("tcp", "localhost:8000") 163 | 164 | conn, err := net.DialTCP("tcp", nil, tcpAddr) 165 | if err != nil { 166 | panic(err) 167 | } 168 | 169 | firstOpStart := time.Now() 170 | 171 | conn.Write([]byte("test-string")) 172 | res := make([]byte, 1024) 173 | bytes, _ := conn.Read(res) 174 | 175 | firstOpElapsed := time.Since(firstOpStart) 176 | 177 | trimmedRes := res[:bytes] 178 | 179 | assert.Equal(t, []byte("test-string"), trimmedRes) 180 | assert.True(t, isDurationCloseTo(time.Millisecond*100, firstOpElapsed, 20)) 181 | 182 | // after ~100ms since test start the added delay will be at 200ms (100ms base + 100ms sine wave max) 183 | secondOpStart := time.Now() 184 | 185 | conn.Write([]byte("another-test")) 186 | res = make([]byte, 1024) 187 | bytes, _ = conn.Read(res) 188 | 189 | secondOpElapsed := time.Since(secondOpStart) 190 | 191 | trimmedRes = res[:bytes] 192 | 193 | s.Stop() 194 | 195 | assert.Equal(t, []byte("another-test"), trimmedRes) 196 | assert.True(t, isDurationCloseTo(time.Millisecond*200, secondOpElapsed, 20)) 197 | } 198 | -------------------------------------------------------------------------------- /lib/square.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import "time" 4 | 5 | 6 | type squareLatencySummand struct { 7 | amplitude time.Duration 8 | period time.Duration 9 | } 10 | 11 | func (s squareLatencySummand) getLatency(elapsed time.Duration) time.Duration { 12 | return time.Duration( 13 | (4 * (elapsed / s.period) - 2 * ((2 * elapsed) / s.period) + 1) * s.amplitude, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /lib/square_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSquareLatencySummand(t *testing.T) { 11 | s := squareLatencySummand{ 12 | amplitude: time.Second * 2, 13 | period: time.Minute, 14 | } 15 | 16 | assert.Equal(t, s.getLatency(time.Duration(0)), time.Second * 2) 17 | assert.Equal(t, s.getLatency(time.Second * 15), time.Second * 2) 18 | assert.Equal(t, s.getLatency(time.Second * 30), time.Second * -2) 19 | assert.Equal(t, s.getLatency(time.Second * 45), time.Second * -2) 20 | assert.Equal(t, s.getLatency(time.Second * 60), time.Second * 2) 21 | assert.Equal(t, s.getLatency(time.Second * 84), time.Second * 2) 22 | assert.Equal(t, s.getLatency(time.Second * 90), time.Second * -2) 23 | } 24 | -------------------------------------------------------------------------------- /lib/triangle.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | type triangleLatencySummand struct { 9 | amplitude time.Duration 10 | period time.Duration 11 | } 12 | 13 | func (t triangleLatencySummand) getLatency(elapsed time.Duration) time.Duration { 14 | a, p, x := float64(t.amplitude), float64(t.period), float64(elapsed) 15 | return time.Duration(4*a/p*math.Abs(math.Mod(((math.Mod((x-p/4), p))+p), p)-p/2) - a) 16 | } 17 | -------------------------------------------------------------------------------- /lib/triangle_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTriangleLatencySummand(t *testing.T) { 11 | s := triangleLatencySummand{ 12 | amplitude: time.Second, 13 | period: time.Minute, 14 | } 15 | 16 | assert.Equal(t, time.Millisecond*0, s.getLatency(time.Duration(0))) 17 | assert.Equal(t, time.Millisecond*500, s.getLatency(time.Millisecond*7500)) 18 | assert.Equal(t, time.Millisecond*1000, s.getLatency(time.Millisecond*15000)) 19 | assert.Equal(t, time.Millisecond*500, s.getLatency(time.Millisecond*22500)) 20 | assert.Equal(t, time.Millisecond*0, s.getLatency(time.Millisecond*30000)) 21 | assert.Equal(t, -time.Millisecond*500, s.getLatency(time.Millisecond*37500)) 22 | assert.Equal(t, -time.Millisecond*1000, s.getLatency(time.Millisecond*45000)) 23 | assert.Equal(t, -time.Millisecond*500, s.getLatency(time.Millisecond*52500)) 24 | assert.Equal(t, time.Millisecond*0, s.getLatency(time.Millisecond*60000)) 25 | } 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/kffl/speedbump/lib" 10 | ) 11 | 12 | func exitWithError(err error) { 13 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 14 | os.Exit(1) 15 | } 16 | 17 | func main() { 18 | cfg, err := parseArgs(os.Args[1:]) 19 | 20 | if err != nil { 21 | exitWithError(err) 22 | } 23 | 24 | s, err := lib.NewSpeedbump(cfg) 25 | 26 | if err != nil { 27 | exitWithError(err) 28 | } 29 | 30 | sigs := make(chan os.Signal, 1) 31 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 32 | 33 | done := make(chan bool) 34 | 35 | go func() { 36 | <-sigs 37 | // signal was caught for the first time 38 | // stop the speedbump instance 39 | go func() { 40 | s.Stop() 41 | done <- true 42 | }() 43 | <-sigs 44 | // signal was caught for the second time 45 | // force the process to exit 46 | os.Exit(1) 47 | }() 48 | 49 | err = s.Start() 50 | 51 | if err != nil { 52 | exitWithError(err) 53 | } 54 | 55 | <-done 56 | } 57 | --------------------------------------------------------------------------------