├── .github └── workflows │ ├── build-main.yml │ └── release-to-maven-central.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── steadybit │ └── testcontainers │ ├── AbstractTrafficControlAttack.java │ ├── Bandwidth.java │ ├── ContainerAttack.java │ ├── NetworkBlackholeAttack.java │ ├── NetworkCorruptPackagesAttack.java │ ├── NetworkDelayPackagesAttack.java │ ├── NetworkLimitBandwidthAttack.java │ ├── NetworkLoosePackagesAttack.java │ ├── Steadybit.java │ ├── dns │ └── TestcontainersDnsResolver.java │ ├── iprule │ ├── IpRule.java │ ├── IpRuleException.java │ └── TestcontainersIpRule.java │ └── trafficcontrol │ ├── TestcontainersTrafficControl.java │ ├── TrafficControl.java │ └── TrafficControlException.java └── test ├── java └── com │ └── steadybit │ └── testcontainers │ ├── NetworkBlackholeAttackTest.java │ ├── NetworkCorruptPackagesAttackTest.java │ ├── NetworkDelayPackagesAttackTest.java │ ├── NetworkLimitBandwidthAttackTest.java │ ├── NetworkLoosePackagesAttackTest.java │ ├── dns │ └── TestcontainersDnsResolverTest.java │ └── measure │ ├── EchoTcpContainer.java │ ├── Iperf3ClientContainer.java │ └── Iperf3ServerContainer.java └── resources └── logback-test.xml /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: build main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up JDK 15 | uses: actions/setup-java@v3 16 | with: 17 | distribution: 'zulu' 18 | java-version: '11' 19 | cache: 'maven' 20 | server-id: 'ossrh' 21 | server-username: MAVEN_USERNAME 22 | server-password: MAVEN_CENTRAL_TOKEN 23 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 24 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 25 | 26 | - name: Build with Maven 27 | env: 28 | MAVEN_USERNAME: ${{ secrets.MAVEN_SERVER_OSSRH_USERNAME }} 29 | MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_SERVER_OSSRH_PASSWORD }} 30 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PRIVATE_KEY_PASSWORD }} 31 | run: mvn -B deploy -Prelease -------------------------------------------------------------------------------- /.github/workflows/release-to-maven-central.yml: -------------------------------------------------------------------------------- 1 | name: release to maven central 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseversion: 7 | description: 'Release version' 8 | required: true 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up JDK 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'zulu' 20 | java-version: '11' 21 | cache: 'maven' 22 | server-id: 'ossrh' 23 | server-username: MAVEN_USERNAME 24 | server-password: MAVEN_CENTRAL_TOKEN 25 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 26 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 27 | 28 | - name: Build with Maven 29 | env: 30 | MAVEN_USERNAME: ${{ secrets.MAVEN_SERVER_OSSRH_USERNAME }} 31 | MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_SERVER_OSSRH_PASSWORD }} 32 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PRIVATE_KEY_PASSWORD }} 33 | run: mvn -B deploy -Prelease -D"revision=${{ github.event.inputs.releaseversion }}" 34 | 35 | - name: Generate changelog 36 | id: changelog 37 | uses: metcalfc/changelog-generator@v4.0.1 38 | with: 39 | myToken: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Create GitHub Release 42 | uses: actions/create-release@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | tag_name: ${{ github.event.inputs.releaseversion }} 47 | release_name: ${{ github.event.inputs.releaseversion }} 48 | body: | 49 | ### Changes 50 | ${{ steps.changelog.outputs.changelog }} 51 | draft: false 52 | prerelease: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | .idea/ 15 | *.iml 16 | *.iws 17 | 18 | *.log 19 | maven-archiver* 20 | target/ 21 | 22 | #flattened POMs 23 | .flattened-pom.xml 24 | 25 | # gnupg keyring 26 | /.gnupg -------------------------------------------------------------------------------- /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 | # Steadybit Testcontainers 2 | 3 | ## What is it? 4 | 5 | Steadybit Testcontainers is a helper library to the [Testcontainers Project](https://testcontainers.org) to implement Resilience Tests. 6 | 7 | ## Getting Started 8 | 9 | > We have a [blog post discussing "Resilience Tests with Testcontainers"](https://www.steadybit.com/blog/resilience-testing-using-testcontainers/) which gives a more detailed explanation. 10 | 11 | ### 1. Add Steadybit Testcontainers to your project: 12 | Add this to the test dependencies in your `pom.xml`: 13 | ```xml 14 | 15 | com.steadybit 16 | steadybit-testcontainers 17 | 1.0.1 18 | test 19 | 20 | ``` 21 | 22 | ### 2. Add some Chaos to your Testcontainers Test: 23 | 24 | Here is an example for delaying the Redis traffic: 25 | ```java 26 | @Testcontainers 27 | public class RedisBackedCacheIntTest { 28 | private RedisBackedCache underTest; 29 | 30 | @Container 31 | public GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:5.0.3-alpine")).withExposedPorts(6379); 32 | 33 | @BeforeEach 34 | public void setUp() { 35 | underTest = new RedisBackedCache(redis.getHost(), redis.getFirstMappedPort()); 36 | } 37 | 38 | @Test 39 | public void testFindingAnInsertedValue() { 40 | underTest.put("foo", "FOO"); 41 | 42 | Optional foundObject = Steadybit.networkDelayPackages(Duration.ofSeconds(2)) 43 | .forContainers(redis) 44 | .exec(() -> { 45 | //this code runs after the attack was started. 46 | //As soon as this lambda exits the attack will be stopped. 47 | return underTest.get("foo", String.class); 48 | }); 49 | 50 | assertTrue("When an object in the cache is retrieved, it can be found", foundObject.isPresent()); 51 | assertEquals("When we put a String in to the cache and retrieve it, the value is the same", "FOO", foundObject.get()); 52 | } 53 | } 54 | ``` 55 | 56 | ## Available Attacks 57 | 58 | - `networkDelayPackages`: Delays egress tcp/udp network packages for containers (on eth0 by default) 59 | - `networkLoosePackages`: Looses egress tcp/udp network packages for containers (on eth0 by default) 60 | - `networkCorruptPackages`: Corrupts egress tcp/udp network packages for containers (on eth0 by default) 61 | - `networkLimitBandwidth`: Limits tcp/udp network bandwidth for containers (on eth0 by default) 62 | - `networkBlackhole`: Blocks all network traffic for containers 63 | - `networkBlockDns`: Blocks all network traffic for containers on dns port (53) 64 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | steadybit-testcontainers 8 | com.steadybit 9 | ${revision} 10 | jar 11 | 12 | Steadybit Testcontainers Module 13 | Testcontainers module from Steadybit to integrate Chaos Experiments into your integration tests. 14 | https://steadybit.com 15 | 2022 16 | 17 | 18 | 1.0.0-SNAPSHOT 19 | UTF-8 20 | 1.8 21 | ${java.version} 22 | ${java.version} 23 | 24 | 25 | 5.9.2 26 | 1.17.6 27 | 3.24.2 28 | 1.4.12 29 | 4.2.0 30 | 3.9.0 31 | 32 | 33 | 3.9.0 34 | 3.2.2 35 | 3.0.0-M5 36 | 1.6.8 37 | 3.3.1 38 | 3.2.1 39 | 3.0.1 40 | 1.2.7 41 | 42 | 43 | 44 | Steadybit GmbH 45 | http://steadybit.com 46 | 47 | 48 | 49 | 50 | The Apache Software License, Version 2.0 51 | http://www.apache.org/licenses/LICENSE-2.0.txt 52 | repo 53 | 54 | 55 | 56 | 57 | https://github.com/steadybit/testcontainers 58 | scm:git@github.com:steadybit/testcontainers.git 59 | scm:git:git@github.com:steadybit/testcontainers.git 60 | HEAD 61 | 62 | 63 | 64 | GitHub Issues 65 | https://github.com/steadybit/testcontainers/issues 66 | 67 | 68 | 69 | 70 | ossrh 71 | https://s01.oss.sonatype.org/content/repositories/snapshots 72 | 73 | 74 | 75 | 76 | 77 | joshiste 78 | Johannes Edmeier 79 | johannes.edmeier@steadybit.com 80 | Steadybit GmbH 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.junit 88 | junit-bom 89 | ${junit-bom.version} 90 | pom 91 | import 92 | 93 | 94 | org.testcontainers 95 | testcontainers-bom 96 | ${testcontainers-bom.version} 97 | pom 98 | import 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.testcontainers 106 | testcontainers 107 | 108 | 109 | 110 | 111 | org.junit.jupiter 112 | junit-jupiter 113 | test 114 | 115 | 116 | org.testcontainers 117 | junit-jupiter 118 | test 119 | 120 | 121 | ch.qos.logback 122 | logback-classic 123 | ${logback-classic.version} 124 | test 125 | 126 | 127 | org.assertj 128 | assertj-core 129 | ${assertj-core.version} 130 | test 131 | 132 | 133 | org.awaitility 134 | awaitility 135 | ${awaitility.version} 136 | 137 | 138 | commons-net 139 | commons-net 140 | ${commons-net.version} 141 | test 142 | 143 | 144 | 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-compiler-plugin 150 | ${maven-compiler-plugin.version} 151 | 152 | 153 | org.apache.maven.plugins 154 | maven-jar-plugin 155 | ${maven-jar-plugin.version} 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-surefire-plugin 160 | ${maven-surefire-plugin.version} 161 | 162 | 163 | **/*Tests.java 164 | **/*Test.java 165 | 166 | 167 | **/Abstract*.java 168 | 169 | 170 | 171 | 172 | org.codehaus.mojo 173 | flatten-maven-plugin 174 | ${flatten-maven-plugin.version} 175 | 176 | true 177 | oss 178 | 179 | remove 180 | remove 181 | 182 | 183 | 184 | 185 | flatten 186 | process-resources 187 | 188 | flatten 189 | 190 | 191 | 192 | flatten-clean 193 | clean 194 | 195 | clean 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | release 206 | 207 | 208 | 209 | org.apache.maven.plugins 210 | maven-source-plugin 211 | ${maven-source-plugin.version} 212 | 213 | 214 | attach-sources 215 | 216 | jar-no-fork 217 | 218 | 219 | 220 | 221 | 222 | org.apache.maven.plugins 223 | maven-javadoc-plugin 224 | ${maven-javadoc-plugin.version} 225 | 226 | 227 | attach-javadocs 228 | 229 | jar 230 | 231 | 232 | 233 | 234 | 235 | org.apache.maven.plugins 236 | maven-gpg-plugin 237 | ${maven-gpg-plugin.version} 238 | 239 | 240 | sign-artifacts 241 | verify 242 | 243 | sign 244 | 245 | 246 | 247 | 248 | 249 | --pinentry-mode 250 | loopback 251 | 252 | 253 | 254 | 255 | org.sonatype.plugins 256 | nexus-staging-maven-plugin 257 | ${nexus-staging-maven-plugin.version} 258 | true 259 | 260 | ossrh 261 | https://s01.oss.sonatype.org/ 262 | true 263 | 264 | 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/AbstractTrafficControlAttack.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.dns.TestcontainersDnsResolver; 4 | import com.steadybit.testcontainers.trafficcontrol.TestcontainersTrafficControl; 5 | import com.steadybit.testcontainers.trafficcontrol.TrafficControl; 6 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.Filter.U32; 7 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.Protocol.IP; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.testcontainers.containers.Container; 11 | 12 | import java.util.Arrays; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Set; 19 | import java.util.function.Function; 20 | 21 | public abstract class AbstractTrafficControlAttack implements ContainerAttack { 22 | private static final Logger log = LoggerFactory.getLogger(AbstractTrafficControlAttack.class); 23 | protected final static String HANDLE_AFFECTED = "1:3"; 24 | private final List> containers; 25 | private final String networkInterface; 26 | private final Set destAddresses; 27 | private final Integer destPort; 28 | private final Map rules = new HashMap<>(); 29 | 30 | private final Function factory; 31 | 32 | protected AbstractTrafficControlAttack(Builder builder) { 33 | this.containers = builder.containers; 34 | if (builder.networkInterface == null || builder.networkInterface.isEmpty()) { 35 | throw new IllegalArgumentException("networkInterface must not be null or empry"); 36 | } 37 | this.networkInterface = builder.networkInterface; 38 | this.destAddresses = builder.destAddresses; 39 | this.destPort = builder.destPort; 40 | this.factory = builder.factory; 41 | } 42 | 43 | @Override 44 | public synchronized void start() { 45 | for (Container container : this.containers) { 46 | TrafficControl.RuleSet rulesToAdd = this.getRules(container); 47 | this.rules.put(container.getContainerId(), rulesToAdd); 48 | factory.apply(container.getContainerId()).add(rulesToAdd); 49 | log.info("Started {} on {}", this.getClass().getSimpleName(), container.getContainerName()); 50 | } 51 | } 52 | 53 | @Override 54 | public synchronized void stop() { 55 | for (Container container : this.containers) { 56 | TrafficControl.RuleSet rulesToDelete = this.rules.get(container.getContainerId()); 57 | if (rulesToDelete != null) { 58 | factory.apply(container.getContainerId()).delete(rulesToDelete); 59 | log.info("Stopped {} on {}", this.getClass().getSimpleName(), container.getContainerName()); 60 | } 61 | } 62 | } 63 | 64 | private TrafficControl.RuleSet getRules(Container container) { 65 | TrafficControl.RuleSet rules = new TrafficControl.RuleSet(this.networkInterface); 66 | this.addQdisc(container, rules); 67 | this.addFilter(container, rules); 68 | return rules; 69 | } 70 | 71 | private void addFilter(Container container, TrafficControl.RuleSet rules) { 72 | int prio = 1; 73 | List ips = this.resolveAddresses(container, this.destAddresses); 74 | if (ips.isEmpty()) { 75 | if (this.destPort == null) { 76 | //Matches all traffic 77 | rules.filterRule("1:", IP, prio++, U32, "match", "u32", "0", "0", "flowid", HANDLE_AFFECTED); 78 | } else { 79 | //Matches all traffic on port 80 | rules.filterRule("1:", IP, prio++, U32, "match", "ip", "dport", Integer.toString(this.destPort), "0xffff", "flowid", HANDLE_AFFECTED); 81 | rules.filterRule("1:", IP, prio++, U32, "match", "ip", "sport", Integer.toString(this.destPort), "0xffff", "flowid", HANDLE_AFFECTED); 82 | } 83 | } else { 84 | for (String ip : ips) { 85 | if (this.destPort == null) { 86 | rules.filterRule("1:", IP, prio++, U32, "match", "ip", "dst", ip, "flowid", HANDLE_AFFECTED); 87 | rules.filterRule("1:", IP, prio++, U32, "match", "ip", "src", ip, "flowid", HANDLE_AFFECTED); 88 | } else { 89 | rules.filterRule("1:", IP, prio++, U32, "match", "ip", "dst", ip, "match", "ip", "dport", Integer.toString(this.destPort), "0xffff", 90 | "flowid", HANDLE_AFFECTED); 91 | rules.filterRule("1:", IP, prio++, U32, "match", "ip", "src", ip, "match", "ip", "dport", Integer.toString(this.destPort), "0xffff", 92 | "flowid", HANDLE_AFFECTED); 93 | } 94 | } 95 | } 96 | } 97 | 98 | protected abstract void addQdisc(Container container, TrafficControl.RuleSet rules); 99 | 100 | private List resolveAddresses(Container container, Set addresses) { 101 | return new TestcontainersDnsResolver(container).resolve(addresses); 102 | } 103 | 104 | public static abstract class Builder extends ContainerAttack.Builder { 105 | public Function factory = TestcontainersTrafficControl::forContainer; 106 | private String networkInterface = "eth0"; 107 | private Set destAddresses = Collections.emptySet(); 108 | private Integer destPort = null; 109 | 110 | protected Builder() { 111 | } 112 | 113 | public Builder networkInterface(String networkInterface) { 114 | this.networkInterface = networkInterface; 115 | return this; 116 | } 117 | 118 | public Builder destAddress(String... destAddresses) { 119 | this.destAddresses = new HashSet<>(Arrays.asList(destAddresses)); 120 | return this; 121 | } 122 | 123 | public Builder destPort(Integer destPort) { 124 | this.destPort = destPort; 125 | return this; 126 | } 127 | 128 | public Builder factory(Function factory) { 129 | this.factory = factory; 130 | return this; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/Bandwidth.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | public final class Bandwidth { 4 | private final int value; 5 | private final String unit; 6 | 7 | private Bandwidth(int value, String unit) { 8 | this.value = value; 9 | this.unit = unit; 10 | } 11 | 12 | /** 13 | * Bits per second 14 | */ 15 | public static Bandwidth bit(int value) { 16 | return new Bandwidth(value, "bit"); 17 | } 18 | 19 | /** 20 | * Kilobits per second 21 | */ 22 | public static Bandwidth kbit(int value) { 23 | return new Bandwidth(value, "kbit"); 24 | } 25 | 26 | /** 27 | * Megabits per second 28 | */ 29 | public static Bandwidth mbit(int value) { 30 | return new Bandwidth(value, "mbit"); 31 | } 32 | 33 | /** 34 | * Gigabits per second 35 | */ 36 | public static Bandwidth gbit(int value) { 37 | return new Bandwidth(value, "gbit"); 38 | } 39 | 40 | /** 41 | * Terabits per second 42 | */ 43 | public static Bandwidth tbit(int value) { 44 | return new Bandwidth(value, "tbit"); 45 | } 46 | 47 | /** 48 | * bytes per second 49 | */ 50 | public static Bandwidth bps(int value) { 51 | return new Bandwidth(value, "bps"); 52 | } 53 | 54 | /** 55 | * Kilobytes per second 56 | */ 57 | public static Bandwidth kbps(int value) { 58 | return new Bandwidth(value, "kbps"); 59 | } 60 | 61 | /** 62 | * Megabytes per second 63 | */ 64 | public static Bandwidth mbps(int value) { 65 | return new Bandwidth(value, "mbps"); 66 | } 67 | 68 | /** 69 | * Gigabytes per second 70 | */ 71 | public static Bandwidth gbps(int value) { 72 | return new Bandwidth(value, "gbps"); 73 | } 74 | 75 | /** 76 | * Terabytes per second 77 | */ 78 | public static Bandwidth tbps(int value) { 79 | return new Bandwidth(value, "tbps"); 80 | } 81 | 82 | public int getValue() { 83 | return value; 84 | } 85 | 86 | public String getUnit() { 87 | return unit; 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return value + unit; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/ContainerAttack.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import org.testcontainers.containers.Container; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.function.Supplier; 9 | 10 | public interface ContainerAttack extends AutoCloseable { 11 | 12 | default T exec(Supplier run) { 13 | try (ContainerAttack self = this) { 14 | this.start(); 15 | return run.get(); 16 | } 17 | } 18 | 19 | default void exec(Runnable run) { 20 | this.exec(() -> { 21 | run.run(); 22 | return null; 23 | }); 24 | } 25 | 26 | void stop(); 27 | 28 | void start(); 29 | 30 | @Override 31 | default void close() { 32 | this.stop(); 33 | } 34 | 35 | abstract class Builder { 36 | protected List> containers; 37 | 38 | public T forContainers(Container... containers) { 39 | this.containers = new ArrayList<>(Arrays.asList(containers)); 40 | return this.build(); 41 | } 42 | 43 | protected abstract T build(); 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/NetworkBlackholeAttack.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.dns.TestcontainersDnsResolver; 4 | import com.steadybit.testcontainers.iprule.IpRule; 5 | import com.steadybit.testcontainers.iprule.TestcontainersIpRule; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.testcontainers.containers.Container; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.HashSet; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Set; 18 | import java.util.function.Function; 19 | 20 | public class NetworkBlackholeAttack implements ContainerAttack { 21 | private static final Logger log = LoggerFactory.getLogger(NetworkBlackholeAttack.class); 22 | private final List> containers; 23 | private final Set addresses; 24 | private final Integer port; 25 | private final Map> rules = new HashMap<>(); 26 | private final Function factory; 27 | 28 | protected NetworkBlackholeAttack(Builder builder) { 29 | this.containers = builder.containers; 30 | this.addresses = builder.addresses; 31 | this.port = builder.port; 32 | this.factory = builder.factory; 33 | } 34 | 35 | @Override 36 | public synchronized void start() { 37 | for (Container container : this.containers) { 38 | List rulesToAdd = this.getRules(container); 39 | this.rules.put(container.getContainerId(), rulesToAdd); 40 | factory.apply(container.getContainerId()).add(rulesToAdd); 41 | log.info("Started {} on {}", this.getClass().getSimpleName(), container.getContainerName()); 42 | } 43 | } 44 | 45 | @Override 46 | public synchronized void stop() { 47 | for (Container container : this.containers) { 48 | List rulesToDelete = this.rules.get(container.getContainerId()); 49 | if (rulesToDelete != null) { 50 | factory.apply(container.getContainerId()).delete(rulesToDelete); 51 | log.info("Started {} on {}", this.getClass().getSimpleName(), container.getContainerName()); 52 | } 53 | } 54 | } 55 | 56 | private List getRules(Container container) { 57 | List rules = new ArrayList<>(); 58 | List ips = this.resolveAddresses(container, this.addresses); 59 | 60 | if (this.port == null && this.addresses.isEmpty()) { 61 | rules.add(new String[] { "blackhole" }); 62 | } else if (this.port == null) { 63 | for (String ip : ips) { 64 | rules.add(new String[] { "blackhole", "to", ip }); 65 | rules.add(new String[] { "blackhole", "from", ip }); 66 | } 67 | } else if (this.addresses.isEmpty()) { 68 | rules.add(new String[] { "blackhole", "dport", Integer.toString(port) }); 69 | rules.add(new String[] { "blackhole", "sport", Integer.toString(port) }); 70 | } else { 71 | for (String address : addresses) { 72 | rules.add(new String[] { "blackhole", "to", address, "dport", Integer.toString(port) }); 73 | rules.add(new String[] { "blackhole", "from", address, "sport", Integer.toString(port) }); 74 | } 75 | } 76 | return rules; 77 | } 78 | 79 | private List resolveAddresses(Container container, Set addresses) { 80 | return new TestcontainersDnsResolver(container).resolve(addresses); 81 | } 82 | 83 | public static class Builder extends ContainerAttack.Builder { 84 | public Function factory = TestcontainersIpRule::forContainer; 85 | private Set addresses = Collections.emptySet(); 86 | private Integer port = null; 87 | 88 | public Builder() { 89 | } 90 | 91 | public Builder address(String... addresses) { 92 | this.addresses = new HashSet<>(Arrays.asList(addresses)); 93 | return this; 94 | } 95 | 96 | public Builder port(Integer port) { 97 | this.port = port; 98 | return this; 99 | } 100 | 101 | public Builder factory(Function factory) { 102 | this.factory = factory; 103 | return this; 104 | } 105 | 106 | @Override 107 | protected NetworkBlackholeAttack build() { 108 | return new NetworkBlackholeAttack(this); 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/NetworkCorruptPackagesAttack.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.trafficcontrol.TrafficControl; 4 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.QDisc.NETEM; 5 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.QDisc.PRIO; 6 | import org.testcontainers.containers.Container; 7 | 8 | public class NetworkCorruptPackagesAttack extends AbstractTrafficControlAttack { 9 | private final int corruptionPercentage; 10 | 11 | private NetworkCorruptPackagesAttack(Builder builder) { 12 | super(builder); 13 | if (builder.corruptionPercentage < 0 || builder.corruptionPercentage > 100) { 14 | throw new IllegalArgumentException("corruptionPercentage must be between 0-100"); 15 | } 16 | this.corruptionPercentage = builder.corruptionPercentage; 17 | } 18 | 19 | @Override 20 | protected void addQdisc(Container container, TrafficControl.RuleSet rules) { 21 | rules.qdiscRule(PRIO, "1:", null); 22 | rules.qdiscRule(NETEM, "30:", HANDLE_AFFECTED, "corrupt", corruptionPercentage + "%"); 23 | } 24 | 25 | public static class Builder extends AbstractTrafficControlAttack.Builder { 26 | private int corruptionPercentage; 27 | 28 | public Builder() { 29 | } 30 | 31 | public Builder corruptionPercentage(int corruptionPercentage) { 32 | this.corruptionPercentage = corruptionPercentage; 33 | return this; 34 | } 35 | 36 | @Override 37 | protected NetworkCorruptPackagesAttack build() { 38 | return new NetworkCorruptPackagesAttack(this); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/NetworkDelayPackagesAttack.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.trafficcontrol.TrafficControl; 4 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.QDisc.NETEM; 5 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.QDisc.PRIO; 6 | import org.testcontainers.containers.Container; 7 | 8 | import java.time.Duration; 9 | 10 | public class NetworkDelayPackagesAttack extends AbstractTrafficControlAttack { 11 | private final Duration delay; 12 | private final Duration jitter; 13 | 14 | private NetworkDelayPackagesAttack(Builder builder) { 15 | super(builder); 16 | if (builder.delay == null) { 17 | throw new IllegalArgumentException("delay must not be null"); 18 | } 19 | this.delay = builder.delay; 20 | this.jitter = builder.jitter != null ? builder.jitter : Duration.ZERO; 21 | } 22 | 23 | @Override 24 | protected void addQdisc(Container container, TrafficControl.RuleSet rules) { 25 | rules.qdiscRule(PRIO, "1:", null); 26 | rules.qdiscRule(NETEM, "30:", HANDLE_AFFECTED, "delay", delay.toMillis() + "ms", jitter.toMillis() + "ms"); 27 | } 28 | 29 | public static class Builder extends AbstractTrafficControlAttack.Builder { 30 | private Duration delay = Duration.ZERO; 31 | private Duration jitter = Duration.ZERO; 32 | 33 | public Builder() { 34 | } 35 | 36 | public Builder delay(Duration delay) { 37 | this.delay = delay; 38 | return this; 39 | } 40 | 41 | public Builder jitter(Duration jitter) { 42 | this.jitter = jitter; 43 | return this; 44 | } 45 | 46 | @Override 47 | protected NetworkDelayPackagesAttack build() { 48 | return new NetworkDelayPackagesAttack(this); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/NetworkLimitBandwidthAttack.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.trafficcontrol.TrafficControl; 4 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.QDisc.HTB; 5 | import org.testcontainers.containers.Container; 6 | 7 | public class NetworkLimitBandwidthAttack extends AbstractTrafficControlAttack { 8 | private final Bandwidth bandwidth; 9 | 10 | private NetworkLimitBandwidthAttack(Builder builder) { 11 | super(builder); 12 | if (builder.bandwidth == null) { 13 | throw new IllegalArgumentException("bandwidth must not be null"); 14 | } 15 | this.bandwidth = builder.bandwidth; 16 | } 17 | 18 | @Override 19 | protected void addQdisc(Container container, TrafficControl.RuleSet rules) { 20 | rules.qdiscRule(HTB, "1:", null, "default", "30"); 21 | rules.classRule(HTB, HANDLE_AFFECTED, "1:", "rate", this.bandwidth.toString()); 22 | } 23 | 24 | public static class Builder extends AbstractTrafficControlAttack.Builder { 25 | private Bandwidth bandwidth; 26 | 27 | public Builder() { 28 | } 29 | 30 | public Builder bandwidth(Bandwidth bandwidth) { 31 | this.bandwidth = bandwidth; 32 | return this; 33 | } 34 | 35 | @Override 36 | protected NetworkLimitBandwidthAttack build() { 37 | return new NetworkLimitBandwidthAttack(this); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/NetworkLoosePackagesAttack.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.trafficcontrol.TrafficControl; 4 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.QDisc.NETEM; 5 | import static com.steadybit.testcontainers.trafficcontrol.TrafficControl.QDisc.PRIO; 6 | import org.testcontainers.containers.Container; 7 | 8 | public class NetworkLoosePackagesAttack extends AbstractTrafficControlAttack { 9 | private final int lossPercentage; 10 | 11 | private NetworkLoosePackagesAttack(Builder builder) { 12 | super(builder); 13 | if (builder.lossPercentage < 0 || builder.lossPercentage > 100) { 14 | throw new IllegalArgumentException("lossPercentage must be between 0-100"); 15 | } 16 | this.lossPercentage = builder.lossPercentage; 17 | } 18 | 19 | @Override 20 | protected void addQdisc(Container container, TrafficControl.RuleSet rules) { 21 | rules.qdiscRule(PRIO, "1:", null); 22 | rules.qdiscRule(NETEM, "30:", HANDLE_AFFECTED, "loss", "random", lossPercentage + "%"); 23 | } 24 | 25 | public static class Builder extends AbstractTrafficControlAttack.Builder { 26 | private int lossPercentage; 27 | 28 | public Builder() { 29 | } 30 | 31 | public Builder lossPercentage(int lossPercentage) { 32 | this.lossPercentage = lossPercentage; 33 | return this; 34 | } 35 | 36 | @Override 37 | protected NetworkLoosePackagesAttack build() { 38 | return new NetworkLoosePackagesAttack(this); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/Steadybit.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import java.time.Duration; 4 | 5 | public class Steadybit { 6 | private static final Integer DEFAULT_DNS_PORT = 53; 7 | 8 | /** 9 | * Delays egress tcp/udp network packages for containers (on eth0 by default) 10 | */ 11 | public static NetworkDelayPackagesAttack.Builder networkDelayPackages(Duration delay) { 12 | return new NetworkDelayPackagesAttack.Builder().delay(delay); 13 | } 14 | 15 | /** 16 | * Looses egress tcp/udp network packages for containers (on eth0 by default) 17 | */ 18 | public static NetworkLoosePackagesAttack.Builder networkLoosePackages(int lossPercentage) { 19 | return new NetworkLoosePackagesAttack.Builder().lossPercentage(lossPercentage); 20 | } 21 | 22 | /** 23 | * Corrupts egress tcp/udp network packages for containers (on eth0 by default) 24 | */ 25 | public static NetworkCorruptPackagesAttack.Builder networkCorruptPackages(int corruptionPercentage) { 26 | return new NetworkCorruptPackagesAttack.Builder().corruptionPercentage(corruptionPercentage); 27 | } 28 | 29 | /** 30 | * Limits tcp/udp network bandwidth for containers (on eth0 by default) 31 | */ 32 | public static NetworkLimitBandwidthAttack.Builder networkLimitBandwidth(Bandwidth bandwidth) { 33 | return new NetworkLimitBandwidthAttack.Builder().bandwidth(bandwidth); 34 | } 35 | 36 | /** 37 | * Blocks all network traffic for containers 38 | */ 39 | public static NetworkBlackholeAttack.Builder networkBlackhole() { 40 | return new NetworkBlackholeAttack.Builder(); 41 | } 42 | 43 | /** 44 | * Blocks all network traffic for containers on dns port (53) 45 | */ 46 | public static NetworkBlackholeAttack.Builder networkBlockDns() { 47 | return new NetworkBlackholeAttack.Builder().port(DEFAULT_DNS_PORT); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/dns/TestcontainersDnsResolver.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers.dns; 2 | 3 | import com.github.dockerjava.api.DockerClient; 4 | import com.github.dockerjava.zerodep.shaded.org.apache.hc.core5.net.InetAddressUtils; 5 | import static java.util.stream.Collectors.groupingBy; 6 | import static java.util.stream.Collectors.mapping; 7 | import static java.util.stream.Collectors.toList; 8 | import org.testcontainers.DockerClientFactory; 9 | import org.testcontainers.containers.Container; 10 | import org.testcontainers.containers.GenericContainer; 11 | import org.testcontainers.containers.output.OutputFrame; 12 | import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | import java.util.Collection; 17 | import java.util.Collections; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Scanner; 21 | 22 | public class TestcontainersDnsResolver { 23 | private final String targetContainerId; 24 | private final DockerClient dockerClient = DockerClientFactory.lazyClient(); 25 | 26 | public TestcontainersDnsResolver(Container targetContainer) { 27 | targetContainerId = targetContainer.getContainerId(); 28 | } 29 | 30 | public List resolve(Collection addresses) { 31 | if (addresses.isEmpty()) { 32 | return Collections.emptyList(); 33 | } 34 | 35 | List unresolved = new ArrayList<>(); 36 | List resolved = new ArrayList<>(); 37 | 38 | for (String address : addresses) { 39 | if (InetAddressUtils.isIPv4Address(address) | InetAddressUtils.isIPv6Address(address)) { 40 | resolved.add(address); 41 | } else { 42 | unresolved.add(address); 43 | } 44 | } 45 | 46 | resolveUsingExtraHosts(unresolved, resolved); 47 | resolveUsingContainer(unresolved, resolved); 48 | 49 | if (!unresolved.isEmpty()) { 50 | throw new RuntimeException("Could not resolve hosts: " + unresolved); 51 | } 52 | 53 | return resolved; 54 | } 55 | 56 | private void resolveUsingExtraHosts(List unresolved, List resolved) { 57 | if (unresolved.isEmpty()) { 58 | return; 59 | } 60 | 61 | Map> extraHosts = Arrays.stream(this.dockerClient.inspectContainerCmd(targetContainerId).exec() 62 | .getHostConfig() 63 | .getExtraHosts()) 64 | .map(s -> s.split(":", 2)) 65 | .collect(groupingBy(s -> s[0], mapping(s -> s[1], toList()))); 66 | 67 | for (String address : new ArrayList<>(unresolved)) { 68 | List ips = extraHosts.get(address); 69 | if (ips != null) { 70 | unresolved.remove(address); 71 | resolved.addAll(ips); 72 | } 73 | } 74 | } 75 | 76 | private void resolveUsingContainer(List unresolved, List resolved) { 77 | if (unresolved.isEmpty()) { 78 | return; 79 | } 80 | 81 | try (DigContainer container = new DigContainer("praqma/network-multitool:latest") 82 | .withCommand(unresolved.toArray(new String[0])) 83 | .withNetworkMode("container:" + targetContainerId)) { 84 | container.start(); 85 | try (Scanner scanner = new Scanner(container.getLogs(OutputFrame.OutputType.STDOUT))) { 86 | while (scanner.hasNext()) { 87 | String hostname = scanner.next(); 88 | scanner.next(); 89 | scanner.next(); 90 | scanner.next(); 91 | String ip = scanner.next(); 92 | scanner.nextLine(); 93 | resolved.add(ip); 94 | unresolved.remove(hostname.substring(0, hostname.length() - 1)); 95 | } 96 | } 97 | } 98 | } 99 | 100 | private static class DigContainer extends GenericContainer { 101 | public DigContainer(String image) { 102 | super(image); 103 | } 104 | 105 | @Override 106 | protected void configure() { 107 | this.withStartupCheckStrategy(new OneShotStartupCheckStrategy()); 108 | this.withCreateContainerCmdModifier(cmd -> { 109 | cmd.withEntrypoint("dig", "+noall", "+answer"); 110 | }); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/iprule/IpRule.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers.iprule; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | public abstract class IpRule { 11 | private static final Logger log = LoggerFactory.getLogger(IpRule.class); 12 | 13 | public void add(List rules) { 14 | try { 15 | this.apply("add", rules); 16 | } catch (Exception e) { 17 | try { 18 | this.apply("del", rules); 19 | } catch (Exception nested) { 20 | e.addSuppressed(nested); 21 | } 22 | throw e; 23 | } 24 | } 25 | 26 | public void delete(List rules) { 27 | List orderedRules = new ArrayList<>(rules); 28 | Collections.reverse(orderedRules); 29 | this.apply("del", rules); 30 | } 31 | 32 | private void apply(String mode, List rules) { 33 | if (rules.isEmpty()) { 34 | return; 35 | } 36 | 37 | try { 38 | String[] ipCommands = rules.stream().map(rule -> "rule " + mode + " " + String.join(" ", rule)).toArray(String[]::new); 39 | if (log.isTraceEnabled()) { 40 | log.trace("Executing ip commands:\n{}", String.join("\n", ipCommands)); 41 | } 42 | 43 | Result result = this.executeBatch(ipCommands); 44 | if (log.isTraceEnabled()) { 45 | log.trace("Executed ip commands (exitcode={}):\n{}", result.getExitcode(), result.getStdOut()); 46 | } 47 | 48 | if (!result.isSuccessful()) { 49 | throw new IpRuleException("Error when executing ip commands:\n" + String.join("\n", ipCommands)); 50 | } 51 | } catch (Exception ex) { 52 | throw new IpRuleException("Could not execute tcCommands", ex); 53 | } 54 | } 55 | 56 | public String getCurrentRules() { 57 | try { 58 | return this.executeBatch("rule list").stdOut; 59 | } catch (Exception e) { 60 | log.warn("Failed to read ip rule list", e); 61 | return null; 62 | } 63 | } 64 | 65 | protected abstract Result executeBatch(String... command); 66 | 67 | protected static class Result { 68 | private final int exitcode; 69 | private final String stdOut; 70 | private final String stdErr; 71 | 72 | protected Result(int exitcode, String stdOut, String stdErr) { 73 | this.exitcode = exitcode; 74 | this.stdOut = stdOut; 75 | this.stdErr = stdErr; 76 | } 77 | 78 | private boolean isSuccessful() { 79 | return this.exitcode == 0; 80 | } 81 | 82 | public int getExitcode() { 83 | return exitcode; 84 | } 85 | 86 | public String getStdOut() { 87 | return stdOut; 88 | } 89 | 90 | public String getStdErr() { 91 | return stdErr; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/iprule/IpRuleException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 steadybit GmbH. All rights reserved. 3 | */ 4 | 5 | package com.steadybit.testcontainers.iprule; 6 | 7 | public class IpRuleException extends RuntimeException { 8 | 9 | public IpRuleException(String message) { 10 | super(message); 11 | } 12 | 13 | public IpRuleException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/iprule/TestcontainersIpRule.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers.iprule; 2 | 3 | import com.github.dockerjava.api.command.WaitContainerResultCallback; 4 | import com.github.dockerjava.api.model.Capability; 5 | import org.testcontainers.containers.GenericContainer; 6 | import org.testcontainers.containers.output.OutputFrame; 7 | import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; 8 | 9 | public class TestcontainersIpRule extends IpRule { 10 | private final String containerId; 11 | private final String image; 12 | 13 | private TestcontainersIpRule(String image, String containerId) { 14 | this.image = image; 15 | this.containerId = containerId; 16 | } 17 | 18 | public static TestcontainersIpRule forContainer(String containerId) { 19 | return TestcontainersIpRule.usingImage("praqma/network-multitool:latest").forContainer(containerId); 20 | } 21 | 22 | public static TestcontainersIpRule.ImageSpec usingImage(String image) { 23 | return new TestcontainersIpRule.ImageSpec(image); 24 | } 25 | 26 | public static class ImageSpec { 27 | private final String image; 28 | 29 | private ImageSpec(String image) { 30 | this.image = image; 31 | } 32 | 33 | public TestcontainersIpRule forContainer(String containerId) { 34 | return new TestcontainersIpRule(this.image, containerId); 35 | } 36 | } 37 | 38 | @Override 39 | protected Result executeBatch(String... ipRuleCommands) { 40 | try (IpRuleContainer container = new IpRuleContainer(this.image) 41 | .withCommand(ipRuleCommands) 42 | .withNetworkMode("container:" + containerId)) { 43 | container.start(); 44 | return container.getResult(); 45 | } 46 | } 47 | 48 | private static class IpRuleContainer extends GenericContainer { 49 | public IpRuleContainer(String image) { 50 | super(image); 51 | } 52 | 53 | @Override 54 | protected void configure() { 55 | this.withStartupCheckStrategy(new OneShotStartupCheckStrategy()); 56 | this.withCreateContainerCmdModifier(cmd -> { 57 | StringBuilder shCommand = new StringBuilder("("); 58 | for (String tcCommand : cmd.getCmd()) { 59 | shCommand.append("echo '").append(tcCommand).append("';"); 60 | } 61 | shCommand.append(") | ip -batch -"); 62 | 63 | cmd.getHostConfig().withCapAdd(Capability.NET_ADMIN, Capability.NET_RAW); 64 | cmd.withEntrypoint("sh", "-c"); 65 | cmd.withCmd(shCommand.toString()); 66 | }); 67 | } 68 | 69 | public Result getResult() { 70 | Integer statusCode = this.dockerClient.waitContainerCmd(this.getContainerId()).exec(new WaitContainerResultCallback()).awaitStatusCode(); 71 | return new Result(statusCode != null ? statusCode : -1, 72 | this.getLogs(OutputFrame.OutputType.STDOUT), 73 | this.getLogs(OutputFrame.OutputType.STDERR) 74 | ); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/trafficcontrol/TestcontainersTrafficControl.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers.trafficcontrol; 2 | 3 | import com.github.dockerjava.api.command.WaitContainerResultCallback; 4 | import com.github.dockerjava.api.model.Capability; 5 | import org.testcontainers.containers.GenericContainer; 6 | import org.testcontainers.containers.output.OutputFrame; 7 | import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; 8 | 9 | public class TestcontainersTrafficControl extends TrafficControl { 10 | private final String containerId; 11 | private final String image; 12 | 13 | private TestcontainersTrafficControl(String image, String containerId) { 14 | this.containerId = containerId; 15 | this.image = image; 16 | } 17 | 18 | public static TestcontainersTrafficControl forContainer(String containerId) { 19 | return TestcontainersTrafficControl.usingImage("praqma/network-multitool:latest").forContainer(containerId); 20 | } 21 | 22 | public static TestcontainersTrafficControl.ImageSpec usingImage(String image) { 23 | return new TestcontainersTrafficControl.ImageSpec(image); 24 | } 25 | 26 | public static class ImageSpec { 27 | private final String image; 28 | 29 | private ImageSpec(String image) { 30 | this.image = image; 31 | } 32 | 33 | public TestcontainersTrafficControl forContainer(String containerId) { 34 | return new TestcontainersTrafficControl(this.image, containerId); 35 | } 36 | } 37 | 38 | @Override 39 | protected Result executeBatch(String... tcCommands) { 40 | TcContainer container = new TcContainer(this.image) 41 | .withCommand(tcCommands) 42 | .withNetworkMode("container:" + containerId); 43 | 44 | try { 45 | container.start(); 46 | return container.getResult(); 47 | } finally { 48 | container.stop(); 49 | } 50 | } 51 | 52 | private static class TcContainer extends GenericContainer { 53 | public TcContainer(String dockerImageName) { 54 | super(dockerImageName); 55 | } 56 | 57 | @Override 58 | protected void configure() { 59 | this.withStartupCheckStrategy(new OneShotStartupCheckStrategy()); 60 | this.withCreateContainerCmdModifier(cmd -> { 61 | StringBuilder shCommand = new StringBuilder("("); 62 | for (String tcCommand : cmd.getCmd()) { 63 | shCommand.append("echo '").append(tcCommand).append("';"); 64 | } 65 | shCommand.append(") | tc -force -batch -"); 66 | 67 | cmd.getHostConfig().withCapAdd(Capability.NET_ADMIN, Capability.NET_RAW); 68 | cmd.withEntrypoint("sh", "-c"); 69 | cmd.withCmd(shCommand.toString()); 70 | }); 71 | } 72 | 73 | public Result getResult() { 74 | Integer statusCode = this.dockerClient.waitContainerCmd(this.getContainerId()).exec(new WaitContainerResultCallback()).awaitStatusCode(); 75 | return new Result(statusCode != null ? statusCode : -1, 76 | this.getLogs(OutputFrame.OutputType.STDOUT), 77 | this.getLogs(OutputFrame.OutputType.STDERR) 78 | ); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/trafficcontrol/TrafficControl.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers.trafficcontrol; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.Scanner; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | public abstract class TrafficControl { 15 | private static final Logger log = LoggerFactory.getLogger(TrafficControl.class); 16 | private static final Pattern COMMAND_FAILED_PATTERN = Pattern.compile("Command failed -:(\\d+)"); 17 | 18 | public void add(RuleSet rules) { 19 | this.apply(Action.ADD, rules); 20 | } 21 | 22 | public void delete(RuleSet rules) { 23 | this.apply(Action.DELETE, rules); 24 | } 25 | 26 | private void apply(Action action, RuleSet rules) { 27 | if (rules.isEmpty()) { 28 | return; 29 | } 30 | 31 | try { 32 | String[] tcCommands = rules.render(action); 33 | if (log.isTraceEnabled()) { 34 | log.trace("Executing tc commands:\n{}", String.join("\n", tcCommands)); 35 | } 36 | 37 | Result result = this.executeBatch(tcCommands); 38 | if (log.isTraceEnabled()) { 39 | log.trace("Executed tc commands (exitcode={}):\n{}", result.getExitcode(), result.getStdOut()); 40 | } 41 | 42 | if (!result.isSuccessful()) { 43 | this.handleError(action, tcCommands, result); 44 | } 45 | } catch (Exception ex) { 46 | throw new TrafficControlException("Could not execute tcCommands", ex); 47 | } 48 | } 49 | 50 | public String dumpRules(String nic) { 51 | try { 52 | Result result = this.executeBatch("qdisc show dev " + nic, "filter show dev " + nic, "class show dev " + nic); 53 | return result.isSuccessful() ? result.getStdOut() : "failed: " + result.getStdOut(); 54 | } catch (Exception e) { 55 | return "failed: " + e; 56 | } 57 | } 58 | 59 | private void handleError(Action action, String[] tcCommands, Result result) { 60 | List notIgnored = new ArrayList<>(tcCommands.length); 61 | log.trace("TC-StdOut\n{}", result.stdOut); 62 | log.trace("TC-StdErr\n{}", result.stdErr); 63 | try (Scanner scanner = new Scanner(result.getStdErr())) { 64 | scanner.useDelimiter("\\n"); 65 | 66 | while (scanner.hasNext()) { 67 | int ruleIndex = -1; 68 | String errorMessage = ""; 69 | StringBuilder sb = new StringBuilder(); 70 | while (scanner.hasNext()) { 71 | String line = scanner.next(); 72 | Matcher m = COMMAND_FAILED_PATTERN.matcher(line); 73 | if (m.find()) { 74 | ruleIndex = Integer.parseInt(m.group(1)) - 1; 75 | errorMessage = sb.toString(); 76 | sb.setLength(0); 77 | break; 78 | } else { 79 | if (sb.length() > 0) { 80 | sb.append('\n'); 81 | } 82 | sb.append(line); 83 | } 84 | } 85 | 86 | String command = ruleIndex >= 0 && ruleIndex < tcCommands.length ? tcCommands[ruleIndex] : ""; 87 | 88 | if (Action.ADD.equals(action)) { 89 | if (errorMessage.equalsIgnoreCase("Error: Exclusivity flag on, cannot modify.") || errorMessage.equalsIgnoreCase( 90 | "RTNETLINK answers: File exists")) { 91 | log.debug("Rule '{}' was not added. Error ignored: {}", command, errorMessage); 92 | continue; 93 | } 94 | } else if (Action.DELETE.equals(action)) { 95 | log.debug("Rule '{}' was not deleted. Error ignored: {}", command, errorMessage); 96 | continue; 97 | } 98 | 99 | notIgnored.add("'" + command + "' failed: " + errorMessage); 100 | } 101 | } 102 | if (!notIgnored.isEmpty()) { 103 | throw new TrafficControlException("Error when executing tc commands:\n" + String.join("\n", notIgnored)); 104 | } 105 | } 106 | 107 | protected abstract Result executeBatch(String... tcCommands); 108 | 109 | public static class RuleSet { 110 | private final String nic; 111 | private final List rules; 112 | 113 | public RuleSet(String nic) { 114 | this(nic, Collections.emptyList()); 115 | } 116 | 117 | private RuleSet(String nic, Collection rules) { 118 | this.nic = nic; 119 | this.rules = new ArrayList<>(rules); 120 | } 121 | 122 | public void qdiscRule(QDisc kind, String handle, String parent, String... options) { 123 | this.rules.add(new QDiscRule(kind, handle, parent, options)); 124 | } 125 | 126 | public void classRule(QDisc kind, String classId, String parent, String... options) { 127 | this.rules.add(new ClassRule(kind, classId, parent, options)); 128 | } 129 | 130 | public void filterRule(String parent, Protocol protocol, int prio, Filter kind, String... options) { 131 | this.rules.add(new FilterRule(parent, protocol, prio, kind, options)); 132 | } 133 | 134 | private String[] render(Action action) { 135 | List orderedRules = new ArrayList<>(this.rules); 136 | if (Action.DELETE.equals(action)) { 137 | Collections.reverse(orderedRules); 138 | } 139 | return orderedRules.stream().map(r -> r.render(this.nic, action)).toArray(String[]::new); 140 | } 141 | 142 | public List getRules() { 143 | return Collections.unmodifiableList(this.rules); 144 | } 145 | 146 | public String getNic() { 147 | return this.nic; 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | return "RuleSet{" + "nic='" + this.nic + '\'' + ", rules=" + this.rules + '}'; 153 | } 154 | 155 | public boolean isEmpty() { 156 | return this.rules.isEmpty(); 157 | } 158 | } 159 | 160 | public enum QDisc { 161 | PRIO, NETEM, HTB; 162 | 163 | @Override 164 | public String toString() { 165 | return this.name().toLowerCase(); 166 | } 167 | } 168 | 169 | public enum Filter { 170 | U32; 171 | 172 | @Override 173 | public String toString() { 174 | return this.name().toLowerCase(); 175 | } 176 | } 177 | 178 | public enum Protocol { 179 | IP, IP6; 180 | 181 | @Override 182 | public String toString() { 183 | return this.name().toLowerCase(); 184 | } 185 | } 186 | 187 | private enum Action { 188 | ADD, DELETE; 189 | 190 | @Override 191 | public String toString() { 192 | return this.name().toLowerCase(); 193 | } 194 | } 195 | 196 | public static abstract class Rule { 197 | protected abstract String render(String nic, Action action); 198 | } 199 | 200 | public static class QDiscRule extends Rule { 201 | private final QDisc kind; 202 | private final String handle; 203 | private final String parent; 204 | private final String[] options; 205 | 206 | QDiscRule(QDisc kind, String handle, String parent, String[] options) { 207 | this.kind = kind; 208 | this.handle = handle; 209 | this.parent = parent; 210 | this.options = options; 211 | } 212 | 213 | @Override 214 | protected String render(String nic, Action action) { 215 | return "qdisc " + action + " dev " + nic + " " + (this.parent != null ? "parent " + this.parent : "root") + " handle " + this.handle + " " 216 | + this.kind + (this.options != null ? " " + String.join(" ", this.options) : ""); 217 | } 218 | } 219 | 220 | public static class ClassRule extends Rule { 221 | private final QDisc kind; 222 | private final String classid; 223 | private final String parent; 224 | private final String[] options; 225 | 226 | ClassRule(QDisc kind, String classid, String parent, String[] options) { 227 | this.kind = kind; 228 | this.classid = classid; 229 | this.parent = parent; 230 | this.options = options; 231 | } 232 | 233 | @Override 234 | protected String render(String nic, Action action) { 235 | return "class " + action + " dev " + nic + " parent " + this.parent + " classid " + this.classid + " " + this.kind + (this.options != null ? 236 | " " + String.join(" ", this.options) : ""); 237 | } 238 | } 239 | 240 | public static class FilterRule extends Rule { 241 | private final String parent; 242 | private final Protocol protocol; 243 | private final int prio; 244 | private final Filter kind; 245 | private final String[] options; 246 | 247 | FilterRule(String parent, Protocol protocol, int prio, Filter kind, String[] options) { 248 | this.parent = parent; 249 | this.protocol = protocol; 250 | this.prio = prio; 251 | this.kind = kind; 252 | this.options = options; 253 | } 254 | 255 | @Override 256 | protected String render(String nic, Action action) { 257 | return "filter " + action + " dev " + nic + " protocol " + this.protocol + " " + (this.parent != null ? "parent " + this.parent : "root") + " prio " 258 | + this.prio + " " + this.kind + (this.options != null ? " " + String.join(" ", this.options) : ""); 259 | 260 | } 261 | } 262 | 263 | protected static class Result { 264 | private final int exitcode; 265 | private final String stdOut; 266 | private final String stdErr; 267 | 268 | protected Result(int exitcode, String stdOut, String stdErr) { 269 | this.exitcode = exitcode; 270 | this.stdOut = stdOut; 271 | this.stdErr = stdErr; 272 | } 273 | 274 | private boolean isSuccessful() { 275 | return this.exitcode == 0; 276 | } 277 | 278 | public int getExitcode() { 279 | return exitcode; 280 | } 281 | 282 | public String getStdOut() { 283 | return stdOut; 284 | } 285 | 286 | public String getStdErr() { 287 | return stdErr; 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/main/java/com/steadybit/testcontainers/trafficcontrol/TrafficControlException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 steadybit GmbH. All rights reserved. 3 | */ 4 | 5 | package com.steadybit.testcontainers.trafficcontrol; 6 | 7 | public class TrafficControlException extends RuntimeException { 8 | 9 | public TrafficControlException(String message) { 10 | super(message); 11 | } 12 | 13 | public TrafficControlException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/NetworkBlackholeAttackTest.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.iprule.TestcontainersIpRule; 4 | import com.steadybit.testcontainers.measure.EchoTcpContainer; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import org.junit.jupiter.api.Test; 7 | import org.testcontainers.junit.jupiter.Container; 8 | import org.testcontainers.junit.jupiter.Testcontainers; 9 | 10 | @Testcontainers 11 | class NetworkBlackholeAttackTest { 12 | @Container 13 | private final EchoTcpContainer target = new EchoTcpContainer(); 14 | 15 | @Test 16 | void should_blackhole_all_traffic() { 17 | Steadybit.networkBlackhole() 18 | .forContainers(target) 19 | .exec(() -> { 20 | assertThat(target.ping()).isFalse(); 21 | }); 22 | 23 | assertThat(target.ping()).isTrue(); 24 | } 25 | 26 | @Test 27 | void should_blackhole_traffic_using_port_filter() { 28 | //match 29 | Steadybit.networkBlackhole() 30 | .port(target.getEchoPortInContainer()) 31 | .factory(containerId -> TestcontainersIpRule.usingImage("praqma/network-multitool:latest").forContainer(containerId)) 32 | .forContainers(target).exec(() -> assertThat(target.ping()).isFalse()); 33 | 34 | //mismatch 35 | Steadybit.networkBlackhole() 36 | .port(target.getEchoPortInContainer() + 999) 37 | .forContainers(target).exec(() -> assertThat(target.ping()).isTrue()); 38 | 39 | assertThat(target.ping()).isTrue(); 40 | } 41 | 42 | @Test 43 | void should_blackhole_traffic_using_ip_filter() { 44 | //match 45 | Steadybit.networkBlackhole() 46 | .address(target.getEchoAddressInContainer()) 47 | .forContainers(target).exec(() -> assertThat(target.ping()).isFalse()); 48 | 49 | //mismatch 50 | Steadybit.networkBlackhole() 51 | .address("1.1.1.1") 52 | .forContainers(target).exec(() -> assertThat(target.ping()).isTrue()); 53 | 54 | assertThat(target.ping()).isTrue(); 55 | } 56 | 57 | @Test 58 | void should_blackhole_traffic_using_ip_and_port_filter() { 59 | //match 60 | Steadybit.networkBlackhole() 61 | .address(target.getEchoAddressInContainer()) 62 | .port(target.getEchoPortInContainer()) 63 | .forContainers(target).exec(() -> assertThat(target.ping()).isFalse()); 64 | 65 | //mismatch address 66 | Steadybit.networkBlackhole() 67 | .address("1.1.1.1") 68 | .port(target.getEchoPortInContainer()) 69 | .forContainers(target).exec(() -> assertThat(target.ping()).isTrue()); 70 | 71 | //mismatch port 72 | Steadybit.networkBlackhole() 73 | .address(target.getEchoAddressInContainer()) 74 | .port(target.getEchoPortInContainer() + 999) 75 | .forContainers(target).exec(() -> assertThat(target.ping()).isTrue()); 76 | 77 | assertThat(target.ping()).isTrue(); 78 | } 79 | } -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/NetworkCorruptPackagesAttackTest.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.measure.Iperf3ClientContainer; 4 | import com.steadybit.testcontainers.measure.Iperf3ServerContainer; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.data.Offset.offset; 7 | import static org.junit.Assert.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.testcontainers.junit.jupiter.Container; 12 | import org.testcontainers.junit.jupiter.Testcontainers; 13 | 14 | @Testcontainers 15 | class NetworkCorruptPackagesAttackTest { 16 | @Container 17 | private static final Iperf3ServerContainer target = new Iperf3ServerContainer(); 18 | @Container 19 | private static final Iperf3ClientContainer tester = new Iperf3ClientContainer(target); 20 | 21 | @BeforeEach 22 | void setUp() { 23 | tester.stopRunningMeasures(); 24 | } 25 | 26 | @Test 27 | void should_corrupt_packages() { 28 | Steadybit.networkCorruptPackages(20) 29 | .forContainers(target) 30 | .exec(() -> { 31 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 32 | }); 33 | assertThat(tester.measureLoss()).isLessThan(5); 34 | } 35 | 36 | @Test 37 | void should_corrupt_packages_using_port_filter() { 38 | //match 39 | Steadybit.networkCorruptPackages(20) 40 | .destPort(tester.getDataPort()) 41 | .forContainers(target) 42 | .exec(() -> { 43 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 44 | }); 45 | 46 | // mismatch 47 | Steadybit.networkCorruptPackages(20) 48 | .destPort(tester.getDataPort() + 999) 49 | .forContainers(target) 50 | .exec(() -> { 51 | assertThat(tester.measureLoss()).isLessThan(5); 52 | }); 53 | assertThat(tester.measureLoss()).isLessThan(5); 54 | } 55 | 56 | @Test 57 | void should_corrupt_packages_using_ip_filter() { 58 | //match 59 | Steadybit.networkCorruptPackages(20) 60 | .destAddress(tester.getIperfClientAddress()) 61 | .forContainers(target) 62 | .exec(() -> { 63 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 64 | }); 65 | 66 | // mismatch 67 | Steadybit.networkCorruptPackages(20) 68 | .destAddress("1.1.1.1") 69 | .forContainers(target) 70 | .exec(() -> { 71 | assertThat(tester.measureLoss()).isLessThan(5); 72 | }); 73 | assertThat(tester.measureLoss()).isLessThan(5); 74 | } 75 | 76 | @Test 77 | void should_corrupt_packages_using_ip_and_port_filter() { 78 | //match 79 | Steadybit.networkCorruptPackages(20) 80 | .destAddress(tester.getIperfClientAddress()) 81 | .forContainers(target) 82 | .exec(() -> { 83 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 84 | }); 85 | 86 | // mismatch address 87 | Steadybit.networkCorruptPackages(20) 88 | .destAddress("1.1.1.1") 89 | .destPort(tester.getDataPort()) 90 | .forContainers(target) 91 | .exec(() -> { 92 | assertThat(tester.measureLoss()).isLessThan(5); 93 | }); 94 | 95 | // mismatch port 96 | Steadybit.networkCorruptPackages(20) 97 | .destAddress(tester.getIperfClientAddress()) 98 | .destPort(tester.getDataPort() + 999) 99 | .forContainers(target) 100 | .exec(() -> { 101 | assertThat(tester.measureLoss()).isLessThan(5); 102 | }); 103 | 104 | assertThat(tester.measureLoss()).isLessThan(5); 105 | } 106 | 107 | @Test 108 | void should_validate_corruptionPercentage() { 109 | Exception exceptionToLow = assertThrows(RuntimeException.class, () -> { 110 | Steadybit.networkCorruptPackages(-1) 111 | .forContainers(target); 112 | }); 113 | assertThat(exceptionToLow).hasMessage("corruptionPercentage must be between 0-100"); 114 | 115 | Exception exceptionToHigh = assertThrows(RuntimeException.class, () -> { 116 | Steadybit.networkCorruptPackages(101) 117 | .forContainers(target); 118 | }); 119 | assertThat(exceptionToHigh).hasMessage("corruptionPercentage must be between 0-100"); 120 | 121 | assertDoesNotThrow(() -> { 122 | Steadybit.networkCorruptPackages(99) 123 | .forContainers(target); 124 | }); 125 | } 126 | } -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/NetworkDelayPackagesAttackTest.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.measure.EchoTcpContainer; 4 | import com.steadybit.testcontainers.trafficcontrol.TestcontainersTrafficControl; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.data.Offset.offset; 7 | import org.junit.jupiter.api.Test; 8 | import org.testcontainers.junit.jupiter.Container; 9 | import org.testcontainers.junit.jupiter.Testcontainers; 10 | 11 | import java.time.Duration; 12 | 13 | @Testcontainers 14 | class NetworkDelayPackagesAttackTest { 15 | 16 | @Container 17 | private static final EchoTcpContainer target = new EchoTcpContainer(); 18 | 19 | @Test 20 | void should_delay_egress_traffic() { 21 | long withoutAttack = target.measureRoundtrip(); 22 | 23 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 24 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack + 200L, offset(50L))); 25 | 26 | assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L)); 27 | } 28 | 29 | @Test 30 | void should_delay_egress_traffic_using_port_filter() { 31 | long withoutAttack = target.measureRoundtrip(); 32 | 33 | //match 34 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 35 | .factory(containerId -> TestcontainersTrafficControl.usingImage("praqma/network-multitool:latest").forContainer(containerId)) 36 | .destPort(target.getEchoPortInContainer()) 37 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack + 200L, offset(50L))); 38 | 39 | //mismatch 40 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 41 | .destPort(target.getEchoPortInContainer() + 999) 42 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L))); 43 | 44 | assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L)); 45 | } 46 | 47 | @Test 48 | void should_delay_egress_traffic_using_ip_filter() { 49 | long withoutAttack = target.measureRoundtrip(); 50 | 51 | //match 52 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 53 | .destAddress(target.getEchoAddressInContainer()) 54 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack + 200L, offset(50L))); 55 | 56 | //mismatch 57 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 58 | .destAddress("1.1.1.1") 59 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L))); 60 | 61 | assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L)); 62 | } 63 | 64 | @Test 65 | void should_delay_egress_traffic_using_ip_and_port_filter() { 66 | long withoutAttack = target.measureRoundtrip(); 67 | 68 | //match 69 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 70 | .destAddress(target.getEchoAddressInContainer()) 71 | .destPort(target.getEchoPortInContainer()) 72 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack + 200L, offset(50L))); 73 | 74 | //mismatch address 75 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 76 | .destAddress("1.1.1.1") 77 | .destPort(target.getEchoPortInContainer()) 78 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L))); 79 | 80 | //mismatch port 81 | Steadybit.networkDelayPackages(Duration.ofMillis(200)) 82 | .destAddress(target.getEchoAddressInContainer()) 83 | .destPort(target.getEchoPortInContainer() + 999) 84 | .forContainers(target).exec(() -> assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L))); 85 | 86 | assertThat(target.measureRoundtrip()).isCloseTo(withoutAttack, offset(50L)); 87 | } 88 | } -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/NetworkLimitBandwidthAttackTest.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.measure.Iperf3ClientContainer; 4 | import com.steadybit.testcontainers.measure.Iperf3ServerContainer; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.data.Percentage.withPercentage; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.testcontainers.junit.jupiter.Container; 10 | import org.testcontainers.junit.jupiter.Testcontainers; 11 | 12 | @Testcontainers 13 | class NetworkLimitBandwidthAttackTest { 14 | 15 | @Container 16 | private static final Iperf3ServerContainer target = new Iperf3ServerContainer(); 17 | @Container 18 | private static final Iperf3ClientContainer tester = new Iperf3ClientContainer(target); 19 | 20 | private Bandwidth normalBandwidth; 21 | private Bandwidth attackBandwidth; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | tester.stopRunningMeasures(); 26 | 27 | this.normalBandwidth = Bandwidth.mbit((tester.measureBandwidth() + tester.measureBandwidth() + tester.measureBandwidth()) / 3); 28 | this.attackBandwidth = Bandwidth.mbit(normalBandwidth.getValue() / 2); 29 | } 30 | 31 | @Test 32 | void should_limit_all_traffic() { 33 | Steadybit.networkLimitBandwidth(attackBandwidth) 34 | .forContainers(target) 35 | .exec(() -> { 36 | assertThat(tester.measureBandwidth()).isCloseTo(this.attackBandwidth.getValue(), withPercentage(15)); 37 | }); 38 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 39 | } 40 | 41 | @Test 42 | void should_limit_all_traffic_using_port_filter() { 43 | 44 | // match 45 | Steadybit.networkLimitBandwidth(this.attackBandwidth) 46 | .destPort(tester.getDataPort()) 47 | .forContainers(target) 48 | .exec(() -> { 49 | assertThat(tester.measureBandwidth()).isCloseTo(this.attackBandwidth.getValue(), withPercentage(15)); 50 | }); 51 | 52 | // mismatch 53 | Steadybit.networkLimitBandwidth(this.attackBandwidth) 54 | .destPort(tester.getDataPort() + 999) 55 | .forContainers(target) 56 | .exec(() -> { 57 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 58 | }); 59 | 60 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 61 | } 62 | 63 | @Test 64 | void should_limit_all_traffic_using_ip_filter() { 65 | 66 | // match 67 | Steadybit.networkLimitBandwidth(this.attackBandwidth) 68 | .destAddress(tester.getIperfClientAddress()) 69 | .forContainers(target) 70 | .exec(() -> { 71 | assertThat(tester.measureBandwidth()).isCloseTo(this.attackBandwidth.getValue(), withPercentage(15)); 72 | }); 73 | 74 | // mismatch 75 | Steadybit.networkLimitBandwidth(this.attackBandwidth) 76 | .destAddress("1.1.1.1") 77 | .forContainers(target) 78 | .exec(() -> { 79 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 80 | }); 81 | 82 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 83 | } 84 | 85 | @Test 86 | void should_limit_all_traffic_using_ip_and_port_filter() { 87 | 88 | // match 89 | Steadybit.networkLimitBandwidth(this.attackBandwidth) 90 | .destAddress(tester.getIperfClientAddress()) 91 | .destPort(tester.getDataPort()) 92 | .forContainers(target) 93 | .exec(() -> { 94 | assertThat(tester.measureBandwidth()).isCloseTo(this.attackBandwidth.getValue(), withPercentage(15)); 95 | }); 96 | 97 | // mismatch address 98 | Steadybit.networkLimitBandwidth(this.attackBandwidth) 99 | .destAddress("1.1.1.1") 100 | .destPort(tester.getDataPort()) 101 | .forContainers(target) 102 | .exec(() -> { 103 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 104 | }); 105 | 106 | // mismatch port 107 | Steadybit.networkLimitBandwidth(this.attackBandwidth) 108 | .destAddress(tester.getIperfClientAddress()) 109 | .destPort(tester.getDataPort() + 999) 110 | .forContainers(target) 111 | .exec(() -> { 112 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 113 | }); 114 | 115 | assertThat(tester.measureBandwidth()).isCloseTo(this.normalBandwidth.getValue(), withPercentage(15)); 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/NetworkLoosePackagesAttackTest.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers; 2 | 3 | import com.steadybit.testcontainers.measure.Iperf3ClientContainer; 4 | import com.steadybit.testcontainers.measure.Iperf3ServerContainer; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.data.Offset.offset; 7 | import static org.junit.Assert.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.testcontainers.junit.jupiter.Container; 12 | import org.testcontainers.junit.jupiter.Testcontainers; 13 | 14 | @Testcontainers 15 | class NetworkLoosePackagesAttackTest { 16 | 17 | @Container 18 | private static final Iperf3ServerContainer target = new Iperf3ServerContainer(); 19 | @Container 20 | private static final Iperf3ClientContainer tester = new Iperf3ClientContainer(target); 21 | 22 | @BeforeEach 23 | void setUp() { 24 | tester.stopRunningMeasures(); 25 | } 26 | 27 | @Test 28 | void should_loose_some_packages() { 29 | Steadybit.networkLoosePackages(20) 30 | .forContainers(target) 31 | .exec(() -> { 32 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 33 | }); 34 | assertThat(tester.measureLoss()).isLessThan(5); 35 | } 36 | 37 | @Test 38 | void should_loose_some_packages_using_port_filter() { 39 | 40 | //match 41 | Steadybit.networkLoosePackages(20) 42 | .destPort(tester.getDataPort()) 43 | .forContainers(target) 44 | .exec(() -> { 45 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 46 | }); 47 | 48 | // mismatch 49 | Steadybit.networkLoosePackages(20) 50 | .destPort(tester.getDataPort() + 999) 51 | .forContainers(target) 52 | .exec(() -> { 53 | assertThat(tester.measureLoss()).isLessThan(5); 54 | }); 55 | assertThat(tester.measureLoss()).isLessThan(5); 56 | } 57 | 58 | @Test 59 | void should_loose_some_packages_using_ip_filter() { 60 | 61 | //match 62 | Steadybit.networkLoosePackages(20) 63 | .destAddress(tester.getIperfClientAddress()) 64 | .forContainers(target) 65 | .exec(() -> { 66 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 67 | }); 68 | 69 | // mismatch 70 | Steadybit.networkLoosePackages(20) 71 | .destAddress("1.1.1.1") 72 | .forContainers(target) 73 | .exec(() -> { 74 | assertThat(tester.measureLoss()).isLessThan(5); 75 | }); 76 | assertThat(tester.measureLoss()).isLessThan(5); 77 | } 78 | 79 | @Test 80 | void should_loose_some_packages_using_ip_and_port_filter() { 81 | 82 | //match 83 | Steadybit.networkLoosePackages(20) 84 | .destAddress(tester.getIperfClientAddress()) 85 | .forContainers(target) 86 | .exec(() -> { 87 | assertThat(tester.measureLoss()).isCloseTo(20, offset(10)); 88 | }); 89 | 90 | // mismatch address 91 | Steadybit.networkLoosePackages(20) 92 | .destAddress("1.1.1.1") 93 | .destPort(tester.getDataPort()) 94 | .forContainers(target) 95 | .exec(() -> { 96 | assertThat(tester.measureLoss()).isLessThan(5); 97 | }); 98 | 99 | // mismatch port 100 | Steadybit.networkLoosePackages(20) 101 | .destAddress(tester.getIperfClientAddress()) 102 | .destPort(tester.getDataPort() + 999) 103 | .forContainers(target) 104 | .exec(() -> { 105 | assertThat(tester.measureLoss()).isLessThan(5); 106 | }); 107 | 108 | assertThat(tester.measureLoss()).isLessThan(5); 109 | } 110 | 111 | @Test 112 | void should_validate_networkLoosePackages() { 113 | Exception exceptionToLow = assertThrows(RuntimeException.class, () -> { 114 | Steadybit.networkLoosePackages(-1) 115 | .forContainers(target); 116 | }); 117 | assertThat(exceptionToLow.getMessage()).isEqualTo("lossPercentage must be between 0-100"); 118 | 119 | Exception exceptionToHigh = assertThrows(RuntimeException.class, () -> { 120 | Steadybit.networkLoosePackages(101) 121 | .forContainers(target); 122 | }); 123 | assertThat(exceptionToHigh.getMessage()).isEqualTo("lossPercentage must be between 0-100"); 124 | 125 | assertDoesNotThrow(() -> { 126 | Steadybit.networkLoosePackages(99) 127 | .forContainers(target); 128 | }); 129 | } 130 | } -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/dns/TestcontainersDnsResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.steadybit.testcontainers.dns; 2 | 3 | import com.steadybit.testcontainers.measure.EchoTcpContainer; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 6 | import org.junit.jupiter.api.Test; 7 | import org.testcontainers.containers.GenericContainer; 8 | import org.testcontainers.junit.jupiter.Container; 9 | import org.testcontainers.junit.jupiter.Testcontainers; 10 | 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | @Testcontainers 16 | public class TestcontainersDnsResolverTest { 17 | @Container 18 | static GenericContainer target = new EchoTcpContainer() 19 | .withExtraHost("some-extra-host", "192.168.2.1") 20 | .withExtraHost("some-extra-host", "192.168.2.2"); 21 | 22 | @Test 23 | void should_resolve() { 24 | List resolved = new TestcontainersDnsResolver(target).resolve(Arrays.asList("127.0.0.1", "heise.de", "some-extra-host")); 25 | assertThat(resolved).containsExactlyInAnyOrder("127.0.0.1", "193.99.144.80", "192.168.2.1", "192.168.2.2"); 26 | } 27 | 28 | @Test 29 | void should_throw_when_not_resolved() { 30 | assertThatThrownBy(() -> new TestcontainersDnsResolver(target).resolve(Collections.singletonList("doesnt-exist"))) 31 | .hasMessageContaining("doesnt-exist"); 32 | } 33 | } -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/measure/EchoTcpContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 steadybit GmbH. All rights reserved. 3 | */ 4 | 5 | package com.steadybit.testcontainers.measure; 6 | 7 | import com.github.dockerjava.api.command.InspectContainerResponse; 8 | import org.apache.commons.net.echo.EchoTCPClient; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import org.testcontainers.containers.GenericContainer; 11 | import org.testcontainers.shaded.org.apache.commons.io.IOUtils; 12 | 13 | import java.io.IOException; 14 | import java.net.SocketTimeoutException; 15 | import java.time.Duration; 16 | 17 | public class EchoTcpContainer extends GenericContainer { 18 | private final EchoTCPClient echoClient = new EchoTCPClient(); 19 | private int port = 2000; 20 | private Duration pingTimeout = Duration.ofSeconds(1); 21 | 22 | public EchoTcpContainer() { 23 | super("alpine/socat:latest"); 24 | } 25 | 26 | public EchoTcpContainer withEchoPort(int port) { 27 | this.port = port; 28 | return this; 29 | } 30 | 31 | public EchoTcpContainer withPingTimeout(Duration pingTimeout) { 32 | this.pingTimeout = pingTimeout; 33 | return this; 34 | } 35 | 36 | public EchoTCPClient getEchoClient() { 37 | return this.echoClient; 38 | } 39 | 40 | public int getEchoPortInContainer() { 41 | try { 42 | ExecResult result = this.execInContainer("sh", "-c", 43 | "netstat -tn | grep ESTABLISHED | grep \":" + this.port + "\" | cut -c\"45-65\" | cut -d\":\" -f2"); 44 | return Integer.parseInt(result.getStdout().trim()); 45 | } catch (InterruptedException | IOException e) { 46 | throw new RuntimeException("Couldn't get echo port in container"); 47 | } 48 | } 49 | 50 | public String getEchoAddressInContainer() { 51 | return this.getCurrentContainerInfo().getNetworkSettings().getGateway(); 52 | } 53 | 54 | public long measureRoundtrip() { 55 | EchoTCPClient echo = this.getEchoClient(); 56 | byte[] message = "Hello World".getBytes(); 57 | byte[] received = new byte[message.length]; 58 | 59 | try { 60 | long start = System.currentTimeMillis(); 61 | IOUtils.write(message, echo.getOutputStream()); 62 | IOUtils.read(echo.getInputStream(), received, 0, received.length); 63 | long duration = System.currentTimeMillis() - start; 64 | assertThat(received).isEqualTo(message); 65 | return duration; 66 | } catch (IOException e) { 67 | throw new RuntimeException(e); 68 | } 69 | } 70 | 71 | public boolean ping() { 72 | EchoTCPClient echo = this.getEchoClient(); 73 | byte[] message = "Hello World".getBytes(); 74 | byte[] received = new byte[message.length]; 75 | 76 | try { 77 | this.echoClient.setSoTimeout((int) pingTimeout.toMillis()); 78 | IOUtils.write(message, echo.getOutputStream()); 79 | IOUtils.read(echo.getInputStream(), received, 0, received.length); 80 | assertThat(received).isEqualTo(message); 81 | } catch (IOException e) { 82 | if (e instanceof SocketTimeoutException) { 83 | return false; 84 | } 85 | throw new RuntimeException(e); 86 | } 87 | return true; 88 | } 89 | 90 | @Override 91 | protected void containerIsStarted(InspectContainerResponse containerInfo) { 92 | try { 93 | this.echoClient.connect(this.getContainerIpAddress(), this.getMappedPort(this.port)); 94 | } catch (Exception e) { 95 | throw new RuntimeException(e); 96 | } 97 | } 98 | 99 | @Override 100 | protected void containerIsStopping(InspectContainerResponse containerInfo) { 101 | try { 102 | this.echoClient.disconnect(); 103 | } catch (IOException e) { 104 | throw new RuntimeException(e); 105 | } 106 | } 107 | 108 | @Override 109 | protected void configure() { 110 | super.configure(); 111 | this.withPrivilegedMode(true); 112 | this.withExposedPorts(this.port); 113 | this.withCommand("-v", "-d", "-d", "tcp-l:" + this.port + ",fork", "exec:'/bin/cat'"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/measure/Iperf3ClientContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 steadybit GmbH. All rights reserved. 3 | */ 4 | 5 | package com.steadybit.testcontainers.measure; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.testcontainers.containers.GenericContainer; 10 | import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode; 11 | import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; 12 | 13 | import java.io.IOException; 14 | 15 | public class Iperf3ClientContainer extends GenericContainer { 16 | private static final Logger log = LoggerFactory.getLogger(Iperf3ClientContainer.class); 17 | private final ObjectMapper objectMapper = new ObjectMapper(); 18 | private final Iperf3ServerContainer server; 19 | private int dataPort = 5000; 20 | private String maxBitrate = "500M"; 21 | 22 | public Iperf3ClientContainer(Iperf3ServerContainer server) { 23 | super("taoyou/iperf3-alpine:latest"); 24 | this.server = server; 25 | } 26 | 27 | public Iperf3ClientContainer withDataPort(int port) { 28 | this.dataPort = port; 29 | return this; 30 | } 31 | 32 | public Iperf3ClientContainer withMaxBitrate(String maxBitrate) { 33 | this.maxBitrate = maxBitrate; 34 | return this; 35 | } 36 | 37 | public int getDataPort() { 38 | return this.dataPort; 39 | } 40 | 41 | public String getIperfClientAddress() { 42 | return this.getCurrentContainerInfo().getNetworkSettings().getIpAddress(); 43 | } 44 | 45 | public int measureLoss() { 46 | try { 47 | String[] command = { "iperf3", "-c", this.server.getIperf3Address(), "-p", Integer.toString(this.server.getIperf3Port()), "-u", "-t 2", "--bind", 48 | "0.0.0.0", "--reverse", "--cport", Integer.toString(this.dataPort), "--json" }; 49 | ExecResult result = this.execInContainer(command); 50 | if (result.getExitCode() == 0) { 51 | JsonNode root = this.objectMapper.readTree(result.getStdout().replace("\n", "")); 52 | return (int) Math.round(root.at("/end/sum/lost_percent").asDouble()); 53 | } 54 | throw new RuntimeException("Execution [" + String.join(" ", command) + "] failed: RC=" + result.getExitCode() + " " + result.getStdout()); 55 | } catch (IOException | InterruptedException e) { 56 | throw new RuntimeException("Execution failed:", e); 57 | } 58 | } 59 | 60 | public int measureBandwidth() { 61 | try { 62 | String[] command = { "iperf3", "-c", this.server.getIperf3Address(), "-p", Integer.toString(this.server.getIperf3Port()), "-t 1", "--bind", 63 | "0.0.0.0", "--udp", "--bitrate", maxBitrate, "--reverse", "--cport", Integer.toString(this.dataPort), "--json" }; 64 | ExecResult result = this.execInContainer(command); 65 | if (result.getExitCode() == 0) { 66 | JsonNode root = this.objectMapper.readTree(result.getStdout().replace("\n", "")); 67 | return Math.round(root.at("/end/sum/bits_per_second").floatValue() / 1_000_000.0f); 68 | } 69 | throw new RuntimeException("Execution [" + String.join(" ", command) + "] failed: RC=" + result.getExitCode() + " " + result.getStderr()); 70 | } catch (IOException | InterruptedException e) { 71 | throw new RuntimeException("Execution failed:", e); 72 | } 73 | } 74 | 75 | @Override 76 | protected void configure() { 77 | super.configure(); 78 | this.setWaitStrategy(null); 79 | this.withCreateContainerCmdModifier(cmd -> { 80 | cmd.withEntrypoint("/bin/sh"); 81 | cmd.withTty(true); 82 | }); 83 | } 84 | 85 | public void stopRunningMeasures() { 86 | try { 87 | this.execInContainer("pkill", "iperf3"); 88 | } catch (InterruptedException | IOException e) { 89 | log.warn("Error killing iperf3", e); 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/com/steadybit/testcontainers/measure/Iperf3ServerContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 steadybit GmbH. All rights reserved. 3 | */ 4 | 5 | package com.steadybit.testcontainers.measure; 6 | 7 | import org.testcontainers.containers.GenericContainer; 8 | 9 | public class Iperf3ServerContainer extends GenericContainer { 10 | private int port = 5201; 11 | 12 | public Iperf3ServerContainer() { 13 | super("taoyou/iperf3-alpine:latest"); 14 | } 15 | 16 | public Iperf3ServerContainer withIperf3Port(int iperf3Port) { 17 | this.port = iperf3Port; 18 | return this; 19 | } 20 | 21 | public int getIperf3Port() { 22 | return port; 23 | } 24 | 25 | public String getIperf3Address() { 26 | return this.getCurrentContainerInfo().getNetworkSettings().getIpAddress(); 27 | } 28 | 29 | @Override 30 | protected void configure() { 31 | super.configure(); 32 | this.withCommand("-s", "-p", String.valueOf(this.port)); 33 | this.withCreateContainerCmdModifier(cmd -> { 34 | cmd.withEntrypoint("iperf3"); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------