JMX: Connecting Through Firewalls Using RMI Over SSL And a Single Port (Part III)

This post explains how you can configure your Java application to export a single port using JMX RMI Connector Server over SSL. This is particularly useful when your application is located behind a firewall, because you will only need to let through a single port. However, using a single port when SSL is enabled requires a little care, because it can only work if the same RMI Socket Factories are used everywhere: indeed the same port cannot be shared by two different RMI Socket Factories.

This post is built as a follow-up of some previous entries:

The code posted here combines these threes techniques, and shows the additional configuration needed to use RMI over SSL.

Update: see also my post on Building a Remotely Stoppable Connector.

How does it work?

The pre-main method in the CustomAgent class is almost identical than that shown in my previous post, except that it now creates a JMX RMI Connector Server which uses RMI over SSL. This is done in three steps:

The pre-main method also creates a CleanThread daemon, which tries to detect the end of the application by waiting for the termination of any non daemon threads (except those that are kept alive by the presence of a started JMX RMI Connector Server). The CleanThread daemon is the same than that shown in my previous post - there are no changes here.

The important lines with regard to the SSL configuration are these ones:

        // We create a couple of SslRMIClientSocketFactory and 
        // SslRMIServerSocketFactory. We will use the same factories to export
        // the RMI Registry and the JMX RMI Connector Server objects. This 
        // will allow us to use the same port for all the exported objects.
        // If we didn't use the same factories everywhere, we would have to
        // use at least two ports, because two different RMI Socket Factories
        // cannot share the same port.
        //
        final SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
        final SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory();
        // Create the RMI Registry using the SSL socket factories above.
        // In order to use a single port, we must use these factories 
        // everywhere, or nowhere. Since we want to use them in the JMX
        // RMI Connector server, we must also use them in the RMI Registry.
        // Otherwise, we wouldn't be able to use a single port.
        //
        LocateRegistry.createRegistry(port, csf, ssf);
        // Now specify the SSL Socket Factories - use the same factories
        // everywhere!
        //
        // For the client side (remote)
        //
        env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,csf);
        
        // For the server side (local)
        //
        env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ssf);
        
        // For binding the JMX RMI Connector Server with the registry 
        // created above:
        //
        env.put("com.sun.jndi.rmi.factory.socket", csf);

Full Code Of The Example

The full code is here:

/\*
 \* CustomAgent.java
 \*
 \* Copyright 2007 Sun Microsystems, Inc.  All Rights Reserved.
 \*
 \* Redistribution and use in source and binary forms, with or without
 \* modification, are permitted provided that the following conditions
 \* are met:
 \*
 \*   - Redistributions of source code must retain the above copyright
 \*     notice, this list of conditions and the following disclaimer.
 \*
 \*   - Redistributions in binary form must reproduce the above copyright
 \*     notice, this list of conditions and the following disclaimer in the
 \*     documentation and/or other materials provided with the distribution.
 \*
 \*   - Neither the name of Sun Microsystems nor the names of its
 \*     contributors may be used to endorse or promote products derived
 \*     from this software without specific prior written permission.
 \*
 \* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 \* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 \* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 \* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 \* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 \* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 \* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 \* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 \* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 \* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 \* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 \* 
 \* Created on Jul 25, 2007, 11:42:49 AM
 \* 
 \*/

package example.rmi.agent;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.rmi.registry.LocateRegistry;
import java.util.HashMap;
import javax.management.MBeanServer;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;

/\*\*
 \* This CustomAgent will start an RMI Connector Server using only
 \* port "example.rmi.agent.port". 
 \*
 \* @author dfuchs
 \*/
public class CustomAgent {

    private CustomAgent() { }
    
    /\*\*
     \* The CleanThread daemon thread will wait until all non-daemon threads
     \* are terminated, excluding those non-daemon threads that are kept alive
     \* by the presence of a started JMX RMI Connector Server. When no other
     \* non-daemon threads remain, it stops the JMX RMI Connector Server,
     \* allowing the application to terminate gracefully.
     \*\*/
    public static class CleanThread extends Thread {
        private final JMXConnectorServer cs;
        public CleanThread(JMXConnectorServer cs) {
            super("JMX Agent Cleaner");
            this.cs = cs;
            setDaemon(true);
        }
        
        public void run() {
            boolean loop = true;
            try {
                while (loop) {
                    final Thread[] all = new Thread[Thread.activeCount()+100];
                    final int count = Thread.enumerate(all);
                    loop = false;
                    for (int i=0;i<count;i++) {
                        final Thread t = all[i];
                        // daemon: skip it.
                        if (t.isDaemon()) continue;
                        // RMI Reaper: skip it.
                        if (t.getName().startsWith("RMI Reaper")) continue;
                        if (t.getName().startsWith("DestroyJavaVM")) continue;
                        // Non daemon, non RMI Reaper: join it, break the for 
                        // loop, continue in the while loop (loop=true)
                        loop = true;
                        try {
                            // Found a non-daemon thread. Wait for it.
                            System.out.println("Waiting on "+t.getName()+
                                    " [id="+t.getId()+"]");
                            t.join();
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                        break;
                    }
                }
                // We went through a whole for-loop without finding any thread
                // to join. We can close cs.
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                try {
                    // if we reach here it means the only non-daemon threads
                    // that remain are reaper threads - or that we got an 
                    // unexpected exception/error.
                    //
                    cs.stop();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
    
    public static void premain(String agentArgs) 
        throws IOException {

        // Ensure cryptographically strong random number generator used
        // to choose the object number - see java.rmi.server.ObjID
        //
        System.setProperty("java.rmi.server.randomIDs", "true");

        // Start an RMI registry on port specified by example.rmi.agent.port
        // (default 3000).
        //
        final int port= Integer.parseInt(
                System.getProperty("example.rmi.agent.port","3412"));
        System.out.println("Create RMI registry on port "+port);
        
        // We create a couple of SslRMIClientSocketFactory and 
        // SslRMIServerSocketFactory. We will use the same factories to export
        // the RMI Registry and the JMX RMI Connector Server objects. This 
        // will allow us to use the same port for all the exported objects.
        // If we didn't use the same factories everywhere, we would have to
        // use at least two ports, because two different RMI Socket Factories
        // cannot share the same port.
        //
        final SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
        final SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory();
        
        // Create the RMI Registry using the SSL socket factories above.
        // In order to use a single port, we must use these factories 
        // everywhere, or nowhere. Since we want to use them in the JMX
        // RMI Connector server, we must also use them in the RMI Registry.
        // Otherwise, we wouldn't be able to use a single port.
        //
        LocateRegistry.createRegistry(port, csf, ssf);

        // Retrieve the PlatformMBeanServer.
        //
        System.out.println("Get the platform's MBean server");
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

        // Environment map.
        //
        System.out.println("Initialize the environment map");
        HashMap<String,Object> env = new HashMap<String,Object>();
        
        // Now specify the SSL Socket Factories:
        //
        // For the client side (remote)
        //
        env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,csf);
        
        // For the server side (local)
        //
        env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ssf);
        
        // For binding the JMX RMI Connector Server with the registry 
        // created above:
        //
        env.put("com.sun.jndi.rmi.factory.socket", csf);

        // Create an RMI connector server.
        //
        // As specified in the JMXServiceURL the RMIServer stub will be
        // registered in the RMI registry running in the local host on
        // port 3000 with the name "jmxrmi". This is the same name the
        // out-of-the-box management agent uses to register the RMIServer
        // stub too.
        //
        // The port specified in "service:jmx:rmi://"+hostname+":"+port
        // is the second port, where RMI connection objects will be exported.
        // Here we use the same port as that we choose for the RMI registry. 
        // The port for the RMI registry is specified in the second part
        // of the URL, in "rmi://"+hostname+":"+port
        //
        System.out.println("Create an RMI connector server");
        final String hostname = InetAddress.getLocalHost().getHostName();
        JMXServiceURL url =
            new JMXServiceURL("service:jmx:rmi://"+hostname+
            ":"+port+"/jndi/rmi://"+hostname+":"+port+"/jmxrmi");
        
        
        System.out.println("creating server with URL: "+url);
        
        // Now create the server from the JMXServiceURL
        //
        final JMXConnectorServer cs =
            JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);

        // Start the RMI connector server.
        //
        System.out.println("Start the RMI connector server on port "+port);
        cs.start();
        System.out.println("Server started at: "+cs.getAddress());

        // Start the CleanThread daemon...
        //
        final Thread clean = new CleanThread(cs);
        clean.start();
    }
}

Creating a Premain Agent Jar

With the NetBeans IDE you can the use the ant target shown here in order to generate the agent jar: don't forget to specify the Premain-Class attribute in the jar manifest [more info].

Starting your application

Since you are now using SSL, you will need a valid keystore and trustore. The Monitoring and Management guide explain how to create them. For the sake of the example, you might want to use the dummy keystore and trustore provide in the JDK 6 JMX example - they are located under <JDK_HOME>/sample/jmx/jmx-scandir/. But don't use these in production mode! They confer no security at all since anybody can get these dummy certificates.

When starting your application, you will need to add the following flags on the Java command line:

  # add these flags to load our pre-main agent and start the connector.
  -Dexample.rmi.agent.port=<port> -javaagent:<agent.jar>
  # add these flags to configure the default SSL keystore and truststore:
  -Djavax.net.ssl.keyStore=<keystore> \\
  -Djavax.net.ssl.keyStorePassword=<password> \\
  -Djavax.net.ssl.trustStore=<truststore> \\
  -Djavax.net.ssl.trustStorePassword=<trustword>

If you don'have an application yet, the you could use this one:

    public class Test { 
        public static void main(String[] args) throws Exception {
            System.out.println("Strike Enter to exit:");
            System.in.read();
        }
    }

Now you should be seing something like this (assuming port=5945):

    Create RMI registry on port 5945
    Get the platform's MBean server
    Initialize the environment map
    Create an RMI connector server
    creating server with URL: service:jmx:rmi://host:5945/jndi/rmi://host:5945/jmxrmi
    Start the RMI connector server on port 5945
    Server started at: service:jmx:rmi://host:5945/jndi/rmi://host:5945/jmxrmi
    Waiting on main [id=1]

How To Connect with JConsole

Start jconsole with the following flags:

  jconsole -J-Djavax.net.ssl.keyStore=<keystore> \\
    -J-Djavax.net.ssl.keyStorePassword=<password> \\
    -J-Djavax.net.ssl.trustStore=<truststore> \\
    -J-Djavax.net.ssl.trustStorePassword=<trustword>

Then in the JConsole connection window select Remote Connection and simply type <host>:<port> in the entry field - and that's it! JConsole will connect to your application through your JMX RMI SSL Connector.

If for some reason, JConsole does not connect, then try to enable the JMX traces by adding a -J-Djava.util.logging.config.file=<logging.properties> as shown in this post.

Hope you enjoyed the tour!
-- daniel

Comments:

Thanks Daniel, great post!

I do have one issue with JConsole though: I keep getting the following exception:
java.io.IOException: Failed to retrieve RMIServer stub: javax.naming.CommunicationException [Root exception is java.rmi.ConnectIOException: non-JRMP server at remote endpoint]

After a lot of trial and error I was able to connect a custom client by adding the SSL client socket factory to the environment map with key "com.sun.jndi.rmi.factory.socket" (in the client code as well as in the server code). For example:

// begin client code
SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory();
HashMap<String, Object> env = new HashMap<String, Object>();
env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf);

// Needed to avoid "non-JRMP server at remote endpoint" error
env.put("com.sun.jndi.rmi.factory.socket", csf);

JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://localhost:5945/jndi/rmi://localhost:5945/jmxrmi");
JMXConnector jmxc = JMXConnectorFactory.connect(url, env);
System.out.println("Client connected ok to " + url);

Any idea how to avoid the "non-JRMP server at remote endpoint" error with JConsole?
--Remko

Posted by Remko Popma on January 05, 2008 at 07:29 AM CET #

Hi Remko,

You will need to use JConsole for JDK 6 for this: I believe JConsole depends on
http://bugs.sun.com/view_bug.do?bug_id=5107423
which was fixed in JDK 6. You do not need to specify the com.sun.jndi.rmi.factory.socket property with JConsole - but I believe JDK 6 JConsole use it internally when establishing the secure connection.

Let me know if it still doesn't work.

-- daniel

Posted by daniel on January 07, 2008 at 02:44 AM CET #

Hi Daniel, thanks for getting back to me.

I was using JDK 6, but no joy...

My thinking is that this is a bug in JConsole: it looks like sun.tools.jconsole.ProxyClient only uses LocateRegistry.getRegistry(host, port, sslRMIClientSocketFactory) when connecting to a "host:port" address, not when connecting to a "service:jmx:" URL.

When connecting to a "service:jmx:" URL, eventually JMXConnectorFactory.connect(jmxUrl, envMap) is called with a Map that only contains credentials, without any socket factories...

That would explain the "non-JRMP server at remote endpoint" exception I keep running into.
What do you think? Did you get JConsole to connect successfully to a SSL-protected registry using a "service:jmx:" URL?

Posted by Remko Popma on January 07, 2008 at 06:46 AM CET #

Hi Remko,

It does work for me. However you do have to use the proper jmx service URL on the server side (that is bind the RMIServer object to the name "jmxrmi") and you do have to use the short remote form <hostname>:<port> when connecting with JConsole.
If you use the long service:jmx:rmi:.... form it won't work.

JConsole will only do the appropriate magic if it thinks it is connecting to the out-of-the-box agent - that's why it only works if you simply give <hostname>:<port> in the remote connection field.

Also make sure that you do give all the appropriate -J-Djavax.net.ssl... (don't forget the -J) on the JConsole command line.

-- daniel

Posted by daniel on January 07, 2008 at 07:19 AM CET #

Hi Daniel,

I was able to connect successfully using the short <hostname>:<port> form. Thanks a lot for your help!

Best regards,
-Remko

Posted by Remko Popma on January 08, 2008 at 08:24 PM CET #

Hi Daniel,

Do you know if connecting through firewalls using RMI over SSL and a single port works with Java 5?

I keep getting this exception when starting the JMXConnectorServer (error happens on the server, not on the client):
Cannot bind to URL [rmi://myhost:4545/jmxrmi]: javax.naming.CommunicationException [Root exception is java.rmi.ConnectIOException: non-JRMP server at remote endpoint]

Your example works with Java 6, but I'm having trouble with Java 5... (Different server though, still trying stuff, but losing hope...)
Any ideas?
Best regards, -Remko

Posted by Remko Popma on June 04, 2008 at 01:31 AM CEST #

Is there a way to make this work under Java 5? I am getting the same error.

Posted by Dana P'Simer on July 28, 2008 at 02:45 PM CEST #

Hi guys,

For making this work on Java 5 please see here:
http://blogs.sun.com/jmxetc/entry/java_5_premain_rmi_connectors

Of course the simplest way would be to switch to Java 6 ;-)

Hope this helps,

-- daniel

Posted by daniel on July 29, 2008 at 07:42 AM CEST #

I have a few questions on JMX with SSL. I have to create client with 2 SSL connections to different hosts (with different truststore/keystore). I can't modify servers but I now that they use secure JMX and I nknow all credentials (keystore/trustore/uisername/passwords). How can I do it? Usage of System.setProperty(...) doesn't workk for the second connection. Unfortunately, creation of my own factory (jmx.remote.tls.socket.factory) doesn't connect even to one server. DOes anybody know how can I perform my task?
Thanks

Posted by Andrey on July 02, 2009 at 06:53 AM CEST #

Hi Daniel,

I'm using one of your examples to create a premain agent and I'm struggling with understanding how to enable security. Can you give a brief description of the different ways? It seems like a password file is different from SSL, but I'm confused as what the strengths of each is. The application we would like to remotely monitor will not be publicly accessible, so I don't think SSL is really necessary - it seems like a hassle to get a signed certificate and all that. Would a self-signed certificate be possible? I'm just trying to understand what's needed, etc, and I'm getting lost trying to follow the different documentation and blog postings around the net...

Can you shed some light on this aspect?

Thanks and much appreciated.
-- KaJun

Posted by KaJun on November 25, 2009 at 06:30 PM CET #

thank u

Posted by Manivannan.R on January 29, 2010 at 09:06 AM CET #

Hi, everybody!

When I connect via jconsole from the local machine using

jconsole -J-Djavax.net.ssl.keyStore=mykeystore \\
-J-Djavax.net.ssl.keyStorePassword=<password> \\
-J-Djavax.net.ssl.trustStore=mykeystore \\
-J-Djavax.net.ssl.trustStorePassword=<trustword>

it's OK!

But when I try to do it from remote machine jconsole says

Connection failed: non-JRMP server at remote endpoint

I use the same and single keystore and truststore file on both machines. It's generated by command:

keytool -genkeypair -keystore mykeystore

Posted by Timur on May 12, 2010 at 05:56 AM CEST #

Hi Timur,

Please have a look at http://blogs.sun.com/jmxetc/entry/troubleshooting_connection_problems_in_jconsole

Also make sure to follow the link that point to Dustin Marx's article.

This should help you diagnose what's wrong!

-- daniel

Posted by Daniel Fuchs on May 12, 2010 at 07:31 AM CEST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Daniel Fuchs blogs on Scene Builder, JMX, SNMP, Java, etc...

The views expressed on this blog are those of the author and do not necessarily reflect the views of Oracle.

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today