Skip to content

Commit 350a806

Browse files
Netty factory: Close TCP connection when receive TLS close notify
TLS has special TLS Alert message "close_notify". After receiving such packet any conversation should be immediately stopped. Some TLS implementations (OpenSSL for example) closes underlying TCP connection when TLS connection being closed, some (Docker SSL server and docker-java before current commit) does not close. AttachContainerCmd executes `onComplete` callback only when TCP connection closes, so without this patch `onComplete` called after a long timeout, with this patch it called when container exits.
1 parent 5c3ba9a commit 350a806

File tree

2 files changed

+91
-1
lines changed

2 files changed

+91
-1
lines changed

src/main/java/com/github/dockerjava/netty/NettyDockerCmdExecFactory.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
import com.github.dockerjava.core.AbstractDockerCmdExecFactory;
1717
import com.github.dockerjava.core.WebTarget;
18+
import io.netty.util.concurrent.Future;
19+
import io.netty.util.concurrent.GenericFutureListener;
1820
import org.apache.commons.lang.SystemUtils;
1921

2022
import com.github.dockerjava.api.command.DockerCmdExecFactory;
@@ -232,12 +234,34 @@ public DuplexChannel connect(Bootstrap bootstrap) throws InterruptedException {
232234
throw new RuntimeException("no port configured for " + host);
233235
}
234236

235-
DuplexChannel channel = (DuplexChannel) bootstrap.connect(host, port).sync().channel();
237+
final DuplexChannel channel = (DuplexChannel) bootstrap.connect(host, port).sync().channel();
236238

237239
final SslHandler ssl = initSsl(dockerClientConfig);
238240

239241
if (ssl != null) {
240242
channel.pipeline().addFirst(ssl);
243+
244+
// https://tools.ietf.org/html/rfc5246#section-7.2.1
245+
// TLS has its own special message about connection termination. Because TLS is a
246+
// session-level protocol, it can be covered by any transport-level protocol like
247+
// TCP, UTP and so on. But we know exactly that data being transferred over TCP and
248+
// that other side will never send any byte into this TCP connection, so this
249+
// channel should be closed.
250+
// RFC says that we must notify opposite side about closing. This could be done only
251+
// in sun.security.ssl.SSLEngineImpl and unfortunately it does not send this
252+
// message. On the other hand RFC does not enforce the opposite side to wait for
253+
// such message.
254+
ssl.sslCloseFuture().addListener(new GenericFutureListener<Future<? super Channel>>() {
255+
@Override
256+
public void operationComplete(Future<? super Channel> future) {
257+
channel.eventLoop().execute(new Runnable() {
258+
@Override
259+
public void run() {
260+
channel.close();
261+
}
262+
});
263+
}
264+
});
241265
}
242266

243267
return channel;

src/test/java/com/github/dockerjava/cmd/AttachContainerCmdIT.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
import org.slf4j.LoggerFactory;
1414

1515
import java.io.ByteArrayInputStream;
16+
import java.io.Closeable;
1617
import java.io.File;
1718
import java.io.InputStream;
1819
import java.io.PipedInputStream;
1920
import java.io.PipedOutputStream;
21+
import java.util.concurrent.CountDownLatch;
2022
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.atomic.AtomicLong;
2124

2225
import static com.github.dockerjava.junit.DockerRule.DEFAULT_IMAGE;
2326
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -28,6 +31,7 @@
2831
import static org.hamcrest.Matchers.isEmptyString;
2932
import static org.hamcrest.Matchers.not;
3033
import static org.junit.Assert.assertEquals;
34+
import static org.junit.Assert.assertTrue;
3135
import static org.junit.Assume.assumeThat;
3236

3337
/**
@@ -205,6 +209,68 @@ public void onNext(Frame frame) {
205209
callback.close();
206210
}
207211

212+
/**
213+
* {@link AttachContainerResultCallback#onComplete()} should be called immediately after
214+
* container exit. It was broken for Netty and TLS connection.
215+
*/
216+
@Test
217+
public void attachContainerClosesStdoutWhenContainerExits() throws Exception {
218+
DockerClient dockerClient = dockerRule.getClient();
219+
220+
CreateContainerResponse container = dockerClient.createContainerCmd(DEFAULT_IMAGE)
221+
.withCmd("echo", "hello world")
222+
.withTty(false)
223+
.exec();
224+
LOG.info("Created container: {}", container.toString());
225+
226+
final CountDownLatch started = new CountDownLatch(1);
227+
final AtomicLong startedAtNanos = new AtomicLong();
228+
final CountDownLatch gotLine = new CountDownLatch(1);
229+
final CountDownLatch completed = new CountDownLatch(1);
230+
final AtomicLong gotLineAtNanos = new AtomicLong();
231+
AttachContainerTestCallback callback = new AttachContainerTestCallback() {
232+
@Override
233+
public void onStart(Closeable stream) {
234+
startedAtNanos.set(System.nanoTime());
235+
started.countDown();
236+
super.onStart(stream);
237+
}
238+
239+
@Override
240+
public void onNext(Frame item) {
241+
if (item.getStreamType() == StreamType.STDOUT) {
242+
gotLineAtNanos.set(System.nanoTime());
243+
gotLine.countDown();
244+
}
245+
super.onNext(item);
246+
}
247+
248+
@Override
249+
public void onComplete() {
250+
completed.countDown();
251+
super.onComplete();
252+
}
253+
};
254+
255+
try (Closeable ignored = callback) {
256+
dockerClient.attachContainerCmd(container.getId())
257+
.withStdOut(true)
258+
.withFollowStream(true)
259+
.exec(callback);
260+
261+
dockerClient.startContainerCmd(container.getId()).exec();
262+
263+
assertTrue("Should start in a reasonable time", started.await(30, SECONDS));
264+
assertTrue("Should get first line quickly after the start", gotLine.await(15, SECONDS));
265+
266+
long gotLineDurationSeconds = (gotLineAtNanos.get() - startedAtNanos.get()) / 1_000_000_000L;
267+
LOG.info("Got line from {} for {} seconds", container.getId(), gotLineDurationSeconds);
268+
269+
boolean finished = completed.await(1L + gotLineDurationSeconds, SECONDS);
270+
assertTrue("Should get EOF in a time close to time of getting the first line", finished);
271+
}
272+
}
273+
208274
public static class AttachContainerTestCallback extends AttachContainerResultCallback {
209275
private StringBuffer log = new StringBuffer();
210276

0 commit comments

Comments
 (0)