├── .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 extends AbstractTrafficControlAttack> 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 |
--------------------------------------------------------------------------------