You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by ti...@apache.org on 2020/06/10 12:00:39 UTC

[maven-surefire] branch SUREFIRE-1796 updated (5df88c8 -> 0874002)

This is an automated email from the ASF dual-hosted git repository.

tibordigana pushed a change to branch SUREFIRE-1796
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git.


 discard 5df88c8  changes after Michael's review
    omit 8409875  changes after Michael's review
    omit 3ca730c  InetAddress
    omit 1494d88  static method
    omit e5178fe  static method
    omit 4537623  String instead of UUID, removed UUID_STRING_LENGTH, refactoring
    omit c6f52fb  FutureTask.get() checking exception instead of try-catch within the Thread
    omit ea40207  [SUREFIRE-1796] The session of TCP channel should be authenticated
     add e8f65b9  [SUREFIRE-1797] Surefire report with parameterized tests contain all names of test the same
     add 02cca4d  [GH] performance problem on Windows nodes
     new 0874002  [SUREFIRE-1796] The session of TCP channel should be authenticated

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (5df88c8)
            \
             N -- N -- N   refs/heads/SUREFIRE-1796 (0874002)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../its/JUnitPlatformRerunFailingTestsIT.java      | 20 ++++++
 .../src/test/resources/fail-fast-testng/pom.xml    |  1 +
 .../fail-fast-testng/src/test/java/pkg/ATest.java  |  2 +-
 .../fail-fast-testng/src/test/java/pkg/BTest.java  |  2 +-
 .../fail-fast-testng/src/test/java/pkg/CTest.java  |  2 +-
 .../fail-fast-testng/src/test/java/pkg/DTest.java  |  2 +-
 .../junit-platform-rerun-failing-tests/pom.xml     | 40 +++++++++++-
 .../test/java/junitplatform/ParametersTest.java    | 76 ++++++++++++++++++++++
 .../surefire/junitplatform/RunListenerAdapter.java | 20 ++++--
 9 files changed, 155 insertions(+), 10 deletions(-)
 create mode 100644 surefire-its/src/test/resources/junit-platform-rerun-failing-tests/src/test/java/junitplatform/ParametersTest.java


[maven-surefire] 01/01: [SUREFIRE-1796] The session of TCP channel should be authenticated

Posted by ti...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

tibordigana pushed a commit to branch SUREFIRE-1796
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git

commit 08740026d6a2feb6746b5dcd88490f1a24b17003
Author: tibordigana <ti...@apache.org>
AuthorDate: Fri Jun 5 00:28:20 2020 +0200

    [SUREFIRE-1796] The session of TCP channel should be authenticated
---
 .../plugin/surefire/booterclient/ForkStarter.java  |  15 ++-
 .../extensions/InvalidSessionIdException.java      |  34 ++----
 .../surefire/extensions/SurefireForkChannel.java   |  30 +++++-
 ...BooterDeserializerStartupConfigurationTest.java |   4 +-
 .../maven/plugin/surefire/extensions/E2ETest.java  |  83 ++++++++++++++-
 .../maven/surefire/extensions/ForkChannelTest.java |  19 +++-
 .../api/util/internal/AsyncSocketTest.java         |   3 +-
 ...refireMasterProcessChannelProcessorFactory.java |  28 +++++
 .../surefire/booter/ForkedBooterMockTest.java      | 116 +++++++++++++++++----
 .../surefire/extensions/ForkNodeArguments.java     |   3 +
 .../spi/MasterProcessChannelProcessorFactory.java  |   2 +-
 11 files changed, 277 insertions(+), 60 deletions(-)

diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
index 3a8cd4f..0a333f6 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
@@ -83,6 +83,7 @@ import static java.lang.StrictMath.min;
 import static java.lang.System.currentTimeMillis;
 import static java.lang.Thread.currentThread;
 import static java.util.Collections.addAll;
+import static java.util.UUID.randomUUID;
 import static java.util.concurrent.Executors.newScheduledThreadPool;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -573,7 +574,8 @@ public class ForkStarter
         File dumpLogDir = replaceForkThreadsInPath( startupReportConfiguration.getReportsDirectory(), forkNumber );
         try
         {
-            forkChannel = forkNodeFactory.createForkChannel( new ForkedNodeArg( forkNumber, dumpLogDir ) );
+            ForkNodeArguments forkNodeArguments = new ForkedNodeArg( forkNumber, dumpLogDir, randomUUID().toString() );
+            forkChannel = forkNodeFactory.createForkChannel( forkNodeArguments );
             closer.addCloseable( forkChannel );
             tempDir = forkConfiguration.getTempDirectory().getCanonicalPath();
             BooterSerializer booterSerializer = new BooterSerializer( forkConfiguration );
@@ -881,11 +883,20 @@ public class ForkStarter
     {
         private final int forkChannelId;
         private final File dumpLogDir;
+        private final String sessionId;
 
-        ForkedNodeArg( int forkChannelId, File dumpLogDir )
+        ForkedNodeArg( int forkChannelId, File dumpLogDir, String sessionId )
         {
             this.forkChannelId = forkChannelId;
             this.dumpLogDir = dumpLogDir;
+            this.sessionId = sessionId;
+        }
+
+        @Nonnull
+        @Override
+        public String getSessionId()
+        {
+            return sessionId;
         }
 
         @Override
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
similarity index 54%
copy from surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
index bdd0b2e..2ceaf12 100644
--- a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.extensions;
+package org.apache.maven.plugin.surefire.extensions;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,33 +19,21 @@ package org.apache.maven.surefire.extensions;
  * under the License.
  */
 
-import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.extensions.ForkChannel;
+import org.apache.maven.surefire.extensions.util.CommandlineExecutor;
 
-import javax.annotation.Nonnegative;
-import javax.annotation.Nonnull;
-import java.io.File;
+import java.io.IOException;
 
 /**
- * The properties related to the current JVM.
+ * After the authentication has failed, {@link ForkChannel#connectToClient()} throws {@link InvalidSessionIdException}
+ * and {@link org.apache.maven.plugin.surefire.booterclient.ForkStarter} should close {@link CommandlineExecutor}.
  *
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 3.0.0-M5
  */
-public interface ForkNodeArguments
+public class InvalidSessionIdException extends IOException
 {
-    /**
-     * The index of the forked JVM, from 1 to N.
-     *
-     * @return index of the forked JVM
-     */
-    @Nonnegative
-    int getForkChannelId();
-
-    @Nonnull
-    File dumpStreamText( @Nonnull String text );
-
-    void logWarningAtEnd( @Nonnull String text );
-
-    @Nonnull
-    ConsoleLogger getConsoleLogger();
+    public InvalidSessionIdException( String actualSessionId, String expectedSessionId )
+    {
+        super( "The actual sessionId '" + actualSessionId + "' does not match '" + expectedSessionId + "'." );
+    }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
index cf522e8..5049dcf 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
@@ -32,10 +32,10 @@ import org.apache.maven.surefire.extensions.util.LineConsumerThread;
 import javax.annotation.Nonnull;
 import java.io.Closeable;
 import java.io.IOException;
-import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketOption;
+import java.nio.ByteBuffer;
 import java.nio.channels.AsynchronousServerSocketChannel;
 import java.nio.channels.AsynchronousSocketChannel;
 import java.nio.channels.ReadableByteChannel;
@@ -49,6 +49,7 @@ import static java.net.StandardSocketOptions.SO_REUSEADDR;
 import static java.net.StandardSocketOptions.TCP_NODELAY;
 import static java.nio.channels.AsynchronousChannelGroup.withThreadPool;
 import static java.nio.channels.AsynchronousServerSocketChannel.open;
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static org.apache.maven.surefire.api.util.internal.Channels.newBufferedChannel;
 import static org.apache.maven.surefire.api.util.internal.Channels.newChannel;
 import static org.apache.maven.surefire.api.util.internal.Channels.newInputStream;
@@ -76,6 +77,7 @@ final class SurefireForkChannel extends ForkChannel
     private final AsynchronousServerSocketChannel server;
     private final String localHost;
     private final int localPort;
+    private final String sessionId;
     private volatile AsynchronousSocketChannel worker;
     private volatile LineConsumerThread out;
 
@@ -84,11 +86,12 @@ final class SurefireForkChannel extends ForkChannel
         super( arguments );
         server = open( withThreadPool( THREAD_POOL ) );
         setTrueOptions( SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE );
-        InetAddress ip = Inet4Address.getLocalHost();
+        InetAddress ip = InetAddress.getLocalHost();
         server.bind( new InetSocketAddress( ip, 0 ), 1 );
         InetSocketAddress localAddress = (InetSocketAddress) server.getLocalAddress();
         localHost = localAddress.getHostString();
         localPort = localAddress.getPort();
+        sessionId = arguments.getSessionId();
     }
 
     @Override
@@ -102,6 +105,7 @@ final class SurefireForkChannel extends ForkChannel
         try
         {
             worker = server.accept().get();
+            verifySessionId();
         }
         catch ( InterruptedException e )
         {
@@ -113,6 +117,26 @@ final class SurefireForkChannel extends ForkChannel
         }
     }
 
+    private void verifySessionId() throws InterruptedException, ExecutionException, IOException
+    {
+        ByteBuffer buffer = ByteBuffer.allocate( sessionId.length() );
+        int read;
+        do
+        {
+            read = worker.read( buffer ).get();
+        } while ( read != -1 && buffer.hasRemaining() );
+        if ( read == -1 )
+        {
+            throw new IOException( "Channel closed while verifying the client." );
+        }
+        buffer.flip();
+        String clientSessionId = new String( buffer.array(), US_ASCII );
+        if ( !clientSessionId.equals( sessionId ) )
+        {
+            throw new InvalidSessionIdException( clientSessionId, sessionId );
+        }
+    }
+
     @SafeVarargs
     private final void setTrueOptions( SocketOption<Boolean>... options )
         throws IOException
@@ -129,7 +153,7 @@ final class SurefireForkChannel extends ForkChannel
     @Override
     public String getForkNodeConnectionString()
     {
-        return "tcp://" + localHost + ":" + localPort;
+        return "tcp://" + localHost + ":" + localPort + "?sessionId=" + sessionId;
     }
 
     @Override
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java
index 6fe2ddc..669e73c 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java
@@ -179,10 +179,10 @@ public class BooterDeserializerStartupConfigurationTest
         BooterSerializer booterSerializer = new BooterSerializer( forkConfiguration );
         String aTest = "aTest";
         File propsTest = booterSerializer.serialize( props, getProviderConfiguration(), startupConfiguration, aTest,
-                false, null, 1, "tcp://127.0.0.1:63003" );
+                false, null, 1, "tcp://localhost:63003" );
         BooterDeserializer booterDeserializer = new BooterDeserializer( new FileInputStream( propsTest ) );
         assertNull( booterDeserializer.getPluginPid() );
-        assertEquals( "tcp://127.0.0.1:63003", booterDeserializer.getConnectionString() );
+        assertEquals( "tcp://localhost:63003", booterDeserializer.getConnectionString() );
         return booterDeserializer.getStartupConfiguration();
     }
 
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
index 30517f3..34946ef 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
@@ -21,24 +21,31 @@ package org.apache.maven.plugin.surefire.extensions;
 
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
-import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
 import org.apache.maven.surefire.api.event.Event;
+import org.apache.maven.surefire.api.report.ConsoleOutputReceiver;
+import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
 import org.apache.maven.surefire.extensions.EventHandler;
 import org.apache.maven.surefire.extensions.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
-import org.apache.maven.surefire.api.report.ConsoleOutputReceiver;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import javax.annotation.Nonnull;
 import java.io.Closeable;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.channels.ReadableByteChannel;
+import java.util.UUID;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 
 import static org.fest.assertions.Assertions.assertThat;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -52,13 +59,17 @@ public class E2ETest
     private static final String LONG_STRING =
         "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
 
+    @Rule
+    public final ExpectedException e = ExpectedException.none();
+
     @Test
-    public void test() throws Exception
+    public void endToEndTest() throws Exception
     {
         ConsoleLogger logger = mock( ConsoleLogger.class );
         ForkNodeArguments arguments = mock( ForkNodeArguments.class );
         when( arguments.getForkChannelId() ).thenReturn( 1 );
         when( arguments.getConsoleLogger() ).thenReturn( logger );
+        when( arguments.getSessionId() ).thenReturn( UUID.randomUUID().toString() );
         final SurefireForkChannel server = new SurefireForkChannel( arguments );
 
         final String connection = server.getForkNodeConnectionString();
@@ -184,4 +195,70 @@ public class E2ETest
             .isPositive()
             .isLessThanOrEqualTo( 6_000L );
     }
+
+    @Test( timeout = 10_000L )
+    public void shouldVerifyClient() throws Exception
+    {
+        ForkNodeArguments forkNodeArguments = mock( ForkNodeArguments.class );
+        when( forkNodeArguments.getSessionId() ).thenReturn( UUID.randomUUID().toString() );
+
+        try ( SurefireForkChannel server = new SurefireForkChannel( forkNodeArguments );
+              SurefireMasterProcessChannelProcessorFactory client = new SurefireMasterProcessChannelProcessorFactory() )
+        {
+            FutureTask<String> task = new FutureTask<>( new Callable<String>()
+            {
+                @Override
+                public String call() throws Exception
+                {
+                    client.connect( server.getForkNodeConnectionString() );
+                    return "client connected";
+                }
+            } );
+
+            Thread t = new Thread( task );
+            t.setDaemon( true );
+            t.start();
+
+            server.connectToClient();
+
+            assertThat( task.get() )
+                .isEqualTo( "client connected" );
+        }
+    }
+
+    @Test( timeout = 10_000L )
+    public void shouldNotVerifyClient() throws Exception
+    {
+        ForkNodeArguments forkNodeArguments = mock( ForkNodeArguments.class );
+        String serverSessionId = UUID.randomUUID().toString();
+        when( forkNodeArguments.getSessionId() ).thenReturn( serverSessionId );
+
+        try ( SurefireForkChannel server = new SurefireForkChannel( forkNodeArguments );
+              SurefireMasterProcessChannelProcessorFactory client = new SurefireMasterProcessChannelProcessorFactory() )
+        {
+            FutureTask<String> task = new FutureTask<>( new Callable<String>()
+            {
+                @Override
+                public String call() throws Exception
+                {
+                    URI connectionUri = new URI( server.getForkNodeConnectionString() );
+                    client.connect( "tcp://" + connectionUri.getHost() + ":" + connectionUri.getPort()
+                        + "?sessionId=6ba7b812-9dad-11d1-80b4-00c04fd430c8" );
+                    return "client connected";
+                }
+            } );
+
+            Thread t = new Thread( task );
+            t.setDaemon( true );
+            t.start();
+
+            e.expect( InvalidSessionIdException.class );
+            e.expectMessage( "The actual sessionId '6ba7b812-9dad-11d1-80b4-00c04fd430c8' does not match '"
+                + serverSessionId + "'." );
+
+            server.connectToClient();
+
+            fail( task.get() );
+        }
+    }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
index 01bb8f3..fec93c8 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
@@ -39,6 +39,7 @@ import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.channels.ReadableByteChannel;
 import java.util.Queue;
+import java.util.UUID;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -62,8 +63,16 @@ public class ForkChannelTest
     @Test( timeout = TESTCASE_TIMEOUT )
     public void shouldRequestReplyMessagesViaTCP() throws Exception
     {
+        final String sessionId = UUID.randomUUID().toString();
         ForkNodeArguments forkNodeArguments = new ForkNodeArguments()
         {
+            @Nonnull
+            @Override
+            public String getSessionId()
+            {
+                return sessionId;
+            }
+
             @Override
             public int getForkChannelId()
             {
@@ -102,7 +111,8 @@ public class ForkChannelTest
             String localHost = InetAddress.getLocalHost().getHostAddress();
             assertThat( channel.getForkNodeConnectionString() )
                 .startsWith( "tcp://" + localHost + ":" )
-                .isNotEqualTo( "tcp://" + localHost + ":" );
+                .isNotEqualTo( "tcp://" + localHost + ":" )
+                .endsWith( "?sessionId=" + sessionId );
 
             URI uri = new URI( channel.getForkNodeConnectionString() );
 
@@ -123,7 +133,7 @@ public class ForkChannelTest
             CountdownCloseable cc = new CountdownCloseable( closeable, 2 );
             Consumer consumer = new Consumer();
 
-            Client client = new Client( uri.getPort() );
+            Client client = new Client( uri.getPort(), sessionId.toString() );
             client.start();
 
             channel.connectToClient();
@@ -164,10 +174,12 @@ public class ForkChannelTest
     private final class Client extends Thread
     {
         private final int port;
+        private final String sessionId;
 
-        private Client( int port )
+        private Client( int port, String sessionId )
         {
             this.port = port;
+            this.sessionId = sessionId;
         }
 
         @Override
@@ -175,6 +187,7 @@ public class ForkChannelTest
         {
             try ( Socket socket = new Socket( InetAddress.getLocalHost().getHostAddress(), port ) )
             {
+                socket.getOutputStream().write( sessionId.getBytes( US_ASCII ) );
                 byte[] data = new byte[128];
                 int readLength = socket.getInputStream().read( data );
                 String token = new String( data, 0, readLength, US_ASCII );
diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/AsyncSocketTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/AsyncSocketTest.java
index 004d743..f4cb773 100644
--- a/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/AsyncSocketTest.java
+++ b/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/AsyncSocketTest.java
@@ -80,7 +80,8 @@ public class AsyncSocketTest
         AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool( executorService );
         AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open( group );
         setTrueOptions( server, SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE );
-        server.bind( null, 1 );
+        InetAddress ip = InetAddress.getLocalHost();
+        server.bind( new InetSocketAddress( ip, 0 ), 1 );
         address = (InetSocketAddress) server.getLocalAddress();
 
         System.gc();
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java
index f27aa0c..9efff25 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java
@@ -29,7 +29,9 @@ import java.net.MalformedURLException;
 import java.net.SocketOption;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
 import java.nio.channels.AsynchronousSocketChannel;
+import java.util.StringTokenizer;
 import java.util.concurrent.ExecutionException;
 
 import static java.net.StandardSocketOptions.SO_KEEPALIVE;
@@ -37,6 +39,7 @@ import static java.net.StandardSocketOptions.SO_REUSEADDR;
 import static java.net.StandardSocketOptions.TCP_NODELAY;
 import static java.nio.channels.AsynchronousChannelGroup.withFixedThreadPool;
 import static java.nio.channels.AsynchronousSocketChannel.open;
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static org.apache.maven.surefire.api.util.internal.Channels.newBufferedChannel;
 import static org.apache.maven.surefire.api.util.internal.Channels.newInputStream;
 import static org.apache.maven.surefire.api.util.internal.Channels.newOutputStream;
@@ -75,6 +78,12 @@ public class SurefireMasterProcessChannelProcessorFactory
             clientSocketChannel = open( withFixedThreadPool( 2, newDaemonThreadFactory() ) );
             setTrueOptions( SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE );
             clientSocketChannel.connect( hostAddress ).get();
+            String sessionId = extractSessionId( uri );
+            if ( sessionId != null )
+            {
+                ByteBuffer buff = ByteBuffer.wrap( sessionId.getBytes( US_ASCII ) );
+                clientSocketChannel.write( buff );
+            }
         }
         catch ( URISyntaxException | InterruptedException e )
         {
@@ -119,4 +128,23 @@ public class SurefireMasterProcessChannelProcessorFactory
             }
         }
     }
+
+    private static String extractSessionId( URI uri )
+    {
+        String query = uri.getQuery();
+        if ( query == null )
+        {
+            return null;
+        }
+        for ( StringTokenizer tokenizer = new StringTokenizer( query, "&" ); tokenizer.hasMoreTokens(); )
+        {
+            String token = tokenizer.nextToken();
+            int delimiter = token.indexOf( '=' );
+            if ( delimiter != -1 && "sessionId".equals( token.substring( 0, delimiter ) ) )
+            {
+                return token.substring( delimiter + 1 );
+            }
+        }
+        return null;
+    }
 }
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java
index 11a3df2..1982f7e 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java
@@ -21,11 +21,11 @@ package org.apache.maven.surefire.booter;
 
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
+import org.apache.maven.surefire.api.report.StackTraceWriter;
 import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
 import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
 import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelProcessorFactory;
 import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
-import org.apache.maven.surefire.api.report.StackTraceWriter;
 import org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils;
 import org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory;
 import org.junit.Rule;
@@ -45,11 +45,16 @@ import org.powermock.modules.junit4.PowerMockRunner;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.nio.ByteBuffer;
 import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
 
-import static java.net.StandardSocketOptions.SO_KEEPALIVE;
-import static java.net.StandardSocketOptions.SO_REUSEADDR;
-import static java.net.StandardSocketOptions.TCP_NODELAY;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
 import static org.mockito.ArgumentMatchers.any;
@@ -300,31 +305,16 @@ public class ForkedBooterMockTest
 
         try ( ServerSocketChannel server = ServerSocketChannel.open() )
         {
-            if ( server.supportedOptions().contains( SO_REUSEADDR ) )
-            {
-                server.setOption( SO_REUSEADDR, true );
-            }
-
-            if ( server.supportedOptions().contains( TCP_NODELAY ) )
-            {
-                server.setOption( TCP_NODELAY, true );
-            }
-
-            if ( server.supportedOptions().contains( SO_KEEPALIVE ) )
-            {
-                server.setOption( SO_KEEPALIVE, true );
-            }
-
             server.bind( new InetSocketAddress( 0 ) );
             int serverPort = ( (InetSocketAddress) server.getLocalAddress() ).getPort();
 
             try ( MasterProcessChannelProcessorFactory factory =
-                     invokeMethod( ForkedBooter.class, "lookupDecoderFactory", "tcp://127.0.0.1:" + serverPort ) )
+                     invokeMethod( ForkedBooter.class, "lookupDecoderFactory", "tcp://localhost:" + serverPort ) )
             {
                 assertThat( factory )
                     .isInstanceOf( SurefireMasterProcessChannelProcessorFactory.class );
 
-                assertThat( factory.canUse( "tcp://127.0.0.1:" + serverPort ) )
+                assertThat( factory.canUse( "tcp://localhost:" + serverPort ) )
                     .isTrue();
 
                 assertThat( factory.canUse( "-- whatever --" ) )
@@ -350,7 +340,7 @@ public class ForkedBooterMockTest
                     }
                 } );
 
-                factory.connect( "tcp://127.0.0.1:" + serverPort );
+                factory.connect( "tcp://localhost:" + serverPort );
 
                 MasterProcessChannelDecoder decoder = factory.createDecoder();
                 assertThat( decoder )
@@ -363,6 +353,68 @@ public class ForkedBooterMockTest
     }
 
     @Test
+    public void shouldAuthenticate() throws Exception
+    {
+        mockStatic( ForkedBooter.class );
+
+        doCallRealMethod()
+            .when( ForkedBooter.class, "lookupDecoderFactory", anyString() );
+
+        try ( final ServerSocketChannel server = ServerSocketChannel.open() )
+        {
+            server.bind( new InetSocketAddress( 0 ) );
+            int serverPort = ( (InetSocketAddress) server.getLocalAddress() ).getPort();
+            final String uuid = UUID.randomUUID().toString();
+            String url = "tcp://localhost:" + serverPort + "?sessionId=" + uuid;
+            try ( final MasterProcessChannelProcessorFactory factory =
+                      invokeMethod( ForkedBooter.class, "lookupDecoderFactory", url ) )
+            {
+                assertThat( factory )
+                    .isInstanceOf( SurefireMasterProcessChannelProcessorFactory.class );
+
+                FutureTask<Boolean> task = new FutureTask<>( new Callable<Boolean>()
+                {
+                    @Override
+                    public Boolean call()
+                    {
+                        try
+                        {
+                            SocketChannel channel = server.accept();
+                            ByteBuffer bb = ByteBuffer.allocate( uuid.length() );
+                            int read = channel.read( bb );
+                            assertThat( read )
+                                .isEqualTo( uuid.length() );
+                            bb.flip();
+                            assertThat( new String( bb.array(), US_ASCII ) )
+                                .isEqualTo( uuid );
+                            return true;
+                        }
+                        catch ( IOException e )
+                        {
+                            return false;
+                        }
+                    }
+                } );
+
+                Thread t = new Thread( task );
+                t.setDaemon( true );
+                t.start();
+
+                factory.connect( url );
+
+                try
+                {
+                    task.get( 10, SECONDS );
+                }
+                finally
+                {
+                    factory.close();
+                }
+            }
+        }
+    }
+
+    @Test
     public void testFlushEventChannelOnExit() throws Exception
     {
         mockStatic( ShutdownHookUtils.class );
@@ -385,4 +437,24 @@ public class ForkedBooterMockTest
         } ).when( ShutdownHookUtils.class, "addShutDownHook", any( Thread.class ) );
         invokeMethod( booter, "flushEventChannelOnExit" );
     }
+
+    @Test
+    public void shouldParseUUID() throws Exception
+    {
+        UUID uuid = UUID.randomUUID();
+        URI uri = new URI( "tcp://localhost:12345?sessionId=" + uuid );
+        String parsed = invokeMethod( SurefireMasterProcessChannelProcessorFactory.class, "extractSessionId", uri );
+        assertThat( parsed )
+            .isEqualTo( uuid.toString() );
+    }
+
+    @Test
+    public void shouldNotParseUUID() throws Exception
+    {
+        UUID uuid = UUID.randomUUID();
+        URI uri = new URI( "tcp://localhost:12345?xxx=" + uuid );
+        String parsed = invokeMethod( SurefireMasterProcessChannelProcessorFactory.class, "extractSessionId", uri );
+        assertThat( parsed )
+            .isNull();
+    }
 }
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java
index bdd0b2e..53da2bd 100644
--- a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java
+++ b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java
@@ -33,6 +33,9 @@ import java.io.File;
  */
 public interface ForkNodeArguments
 {
+    @Nonnull
+    String getSessionId();
+
     /**
      * The index of the forked JVM, from 1 to N.
      *
diff --git a/surefire-extensions-spi/src/main/java/org/apache/maven/surefire/spi/MasterProcessChannelProcessorFactory.java b/surefire-extensions-spi/src/main/java/org/apache/maven/surefire/spi/MasterProcessChannelProcessorFactory.java
index 93b61e5..8da6c11 100644
--- a/surefire-extensions-spi/src/main/java/org/apache/maven/surefire/spi/MasterProcessChannelProcessorFactory.java
+++ b/surefire-extensions-spi/src/main/java/org/apache/maven/surefire/spi/MasterProcessChannelProcessorFactory.java
@@ -41,7 +41,7 @@ public interface MasterProcessChannelProcessorFactory extends Closeable
     /**
      * Open a new connection.
      *
-     * @param channelConfig e.g. "pipe://3" or "tcp://127.0.0.1:65035"
+     * @param channelConfig e.g. "pipe://3" or "tcp://localhost:65035"
      * @throws IOException if cannot connect
      */
     void connect( String channelConfig ) throws IOException;