Skip navigation

If you've had occasion to use the RMI registry seriously, you may have encountered some of its shortcomings. Chief of these is that anybody on the local machine can modify the registry. There are only a few things you can do about that, of which the craziest is to reimplement enough of RMI to code your own compatible version of the registry. I did that, and write about it in a later entry, but then I discovered a simpler alternative, due to Peter Jones.

We encountered this problem when defining the JMX Remote API. The standard connector defined by this API is the RMI connector, and the obvious way for it to work would have been to interpret a URL like service:jmx:rmi://somehost:8888/bar as meaning "an RMIServer object called bar in the RMI registry at port 8888 on host somehost. We wouldn't have needed to support any other address types. The same address would work on the client and on the server.

Well, if you have a look at the documentation, you'll see that things are quite a bit more complicated than that. Addresses come in two forms, of which the first is this rather ugly one:

service:jmx:rmi:///jndi/rmi://somehost:8888/bar

That is as nothing to the ugliness of the second form, though:

service:jmx:rmi://somehost/stub/rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LnJlbW90ZS5ybWk
uUk1JU2VydmVySW1wbF9TdHViAAAAAAAAAAICAAB4cgAaamF2YS5ybWkuc2VydmVyLlJlbW90ZVN0dW
Lp/tzJi+FlGgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc5A
AtVbmljYXN0UmVmMgAADjEyOS4xNTcuMjAzLjM5AACauPDsdTz1J8m+BsXsHAAAAQ/Ow/uqgAEAeA==

Blechh.

As Spec Lead for JSR 160, I bear considerable responsibility for this ugliness, and you can believe that I didn't accept it gladly.

Why we perpetrated these address forms

There were two problems that led us to exclude the simple approach with the RMI registry. The first was that a longstanding bug in the JDK meant that you couldn't have more than one RMI registry in the same VM. So there was great potential for interference between different modules each needing to create a registry. This bug was fixed in JDK 1.5 but JSR 160 was supported all the way back to 1.3.

The second problem was the one I mentioned above, namely that the only access check on changes to the RMI registry is that the client making the change must be connecting from the same machine that the registry is running on. This means that if you store your JMX Connector Server in an RMI registry on a multiuser machine, some other unscrupulous user can come along and replace it with their own RMI object. Users would then connect to the rogue object without realising it, and send their password or other credentials to authenticate.

Securing the RMI registry

We've addressed this problem in a couple of ways. First of all, you can protect your registry using SSL client authentication. Luis-Miguel Alventosa has explained this in detail in his blog. The big advantage of using SSL is that clients can be sure they are connecting to the server they are expecting, and not a server being run by Dr Evil on the same port. But that's serverauthentication. Using SSL client authentication seems redundant given that the client can authenticate to the JMX Connector Server with a password or other credentials. You only really need the SSL part to protect the registry from rogue modifications. And setting up the necessary SSL client certificates is a non-trivial amount of work.

The second possibility is to access implementation-specificsun.* classes to define a custom registry. We did this in the JDK code that handles the JMX command-line properties,-Dcom.sun.management.jmxremote etc. When you specify those properties, we create a special read-only RMI registry with a single entry "jmxrmi". Seesun.management.jmxremote.SingleEntryRegistry in the JDK sources, which does its work by subclassingsun.rmi.registry.RegistryImpl.

We can get away with this because this code is part of the same JDK as the implementation-dependent classes it is accessing. I strongly discourage anyone from doing this sort of thing outside the JDK. The sun.* classes are subject to change without notice, and indeed as of JDK 6 the compiler will produce warnings for code that references them.

A portable (if hacky) solution

A viable alternative is described in the Work Around section ofRFE 5029435.

The access check to see if the caller is on the same machine can be faked so that it always fails, even if the caller is on the same machine. The idea is that the caller's address comes fromSocket.getInetAddress(). By imaginative use of RMI socket factories and Socket subclasses, we can arrange for this method never to return a local address.

The access check is only made when the registry is accessed via RMI. It is not made if the Registry object returned byLocateRegistry.createRegistry is accessed directly from within the VM where it was created. So you can create the registry and set up its contents within the same VM, but others, even on the same machine, cannot modify it. Here's what it looks like in code:

    Registry r = LocateRegistry.createRegistry(
            8888, null, new NoLocalAddressServerSocketFactory());
    Foo fooObject = new Foo();
    Remote fooStub = UnicastRemoteObject.exportObject(fooObject, 0);
    r.bind("foo", fooStub);
    Bar barObject = new Bar();
    Remote barStub = UnicastRemoteObject.exportObject(barObject, 0);
    r.bind("bar", barObject);

The bind operations here will succeed because they are accessing the registry object locally, not via RMI. But any attempt to modify it via RMI, even from the same machine, will fail, if we define NoLocalAddressServerSocketFactoryas follows. (Thanks to Tom Hawtin for the idea behind this version, simpler than the one I originally posted.)

public class NoLocalAddressServerSocketFactory
        implements RMIServerSocketFactory {

    public ServerSocket createServerSocket(int port) throws IOException {
        return new NoLocalAddressServerSocket(port);
    }
    
    private static class NoLocalAddressServerSocket extends ServerSocket {
        NoLocalAddressServerSocket(int port) throws IOException {
            super(port);
        }
        
        @Override
        public Socket accept() throws IOException {
            Socket s = new NoLocalAddressSocket();
            super.implAccept(s);
            return s;
        }
    }
    
    private static class NoLocalAddressSocket extends Socket {
        @Override
        public InetAddress getInetAddress() {
            return null;
        }
    }
}

It's a bit hacky, but it does the trick!

You're not limited to rejecting all modifications. For example, suppose you wanted to allow all modifications, whether or not the caller is remote, and security be damned. Then you could simply return InetAddress.getLocalHost() instead of null. Or, if you've exported the registry through SSL (see below), then you might allow modifications only from clients that have been authenticated through SSL, but you would allow lookups from all clients. So you would return InetAddress.getLocalHost() if the client is authenticated and null otherwise.

If you are already using a socket factory

Things are a bit more complicated if you are already using an RMIServerSocketFactory, such as SslRMIServerSocketFactory. In that case, you'll need to combine subclassing with delegation, as outlined here:

public class NoLocalAddressServerSocketFactory2
        implements RMIServerSocketFactory {

    private final RMIServerSocketFactory factory;
    
    public NoLocalAddressServerSocketFactory2(RMIServerSocketFactory f) {
        if (f == null) {
            f = RMISocketFactory.getSocketFactory();
            if (f == null)
                f = RMISocketFactory.getDefaultSocketFactory();
        }
        this.factory = f;
    }

    public ServerSocket createServerSocket(int port) throws IOException {
        ServerSocket ss = factory.createServerSocket(port);
        return new NoLocalAddressServerSocket(ss);
    }
    
    private static class NoLocalAddressServerSocket extends ServerSocket {
        private final ServerSocket serverSocket;
        
        NoLocalAddressServerSocket(ServerSocket ss) throws IOException {
            this.serverSocket = ss;
        }
        
        @Override
        public Socket accept() throws IOException {
            return new NoLocalAddressSocket(serverSocket.accept());
        }

    @Override
        public void close() throws IOException {
            serverSocket.close();
        }

        ...override 13 other ServerSocket methods in the same way as close()...
    }
    
    private static class NoLocalAddressSocket extends Socket {
        private final Socket socket;
        
        NoLocalAddressSocket(Socket s) {
            this.socket = s;
        }
        
        @Override
        public InetAddress getInetAddress() {
            return null;
        }

    @Override
        public void close() throws IOException {
            socket.close();
        }
        
    ...override 38 more Socket methods in the same way as close()...
    }
}

The main drawback that I can see is that if a future version ofjava.net.Socket adds more methods, they won't be overridden by NoLocalAddressSocket, and that might cause problems if the RMI internals use the new methods. (If you're really worried about that, you might want to use cglib to produce the equivalent of NoLocalAddressSocket dynamically.)

Credit where it's due

The public bug database is anonymized but I can reveal that this idea, like so many RMI ideas, is the work of the discreet genius Peter Jones.

A multihomed computer is one that has more than one network interface. Problems arise when you export an RMI object from such a computer. Here's why, and some ways you can work around the problem.

A typical example of a multihomed computer is a laptop with both Ethernet and WiFi interfaces. If you look closely at such a computer, you'll see that each interface has its own IP address. Use ifconfig -a on Unix or ipconfig/a on MS Windows to see these addresses. The picture here shows my laptop, which is connected through an Ethernet cable to the company intranet and through WiFi to a wireless access point.

My laptop connected to two networks 

There's an important point here, which is that an IP address is the address of a network adaptor, not the address of a computer.

Now let's look at some details of how RMI works. To interact with a remote object, you need to get a stub for it. Typically you connect to an RMI registry to get a remote object to start from. Methods on that object may return references to other remote objects, which are also stubs.

For example, one way to use the JMX Remote API is to pick up a stub for a remote RMIServer object. Then you call newClient() on that object, which returns you a stub for a newRMIConnection object. The picture here shows the first step, where you pick up the stub from the RMI registry.

Looking up a stub in the RMI registry 

What do these stubs look like? The stub object implements the appropriate Remoteinterface, for example RMIServer or RMIConnection or Spume. The implementation of these methods in the stub forwards the method to the remote object. So when you call stub.newClient(), for example, the call is forwarded over the network to the real RMIServer object on the remote machine. This is just basic RMI and should be no surprise.

Stubs are serializable. This is how you can get them from a remote RMI registry, of course. But it also means that you can send the stub to someone else. A stub doesn't care where you get it from - it just connects to its remote object and does its stuff.

So what's inside a stub?

What's inside a stub? 
  • The host where the remote object lives. This is a string, and we will have much more to say about it later.
  • The port on that host where RMI is listening for requests for that object and possibly other objects.
  • The object id that allows RMI to know which object you are talking to.
  • The socket factory, an instance of RMIClientSocketFactory that controls how the connection to the given host and port is made.

The first item we're interested in here is the host. This is a string, and by default it is the result of InetAddress.getLocalHost(). getHostAddress(). In other words it is a numeric IP address like 10.0.10.58. We can immediately see why this default behaviour is going to be a problem for my multihomed laptop. This address is the WiFi address. If a client from within the other network (the company intranet) wants to connect using this stub, it can't.

The simple solution that RMI provides for this situation is a system property, "java.rmi.server.hostname". If I only ever want connections from clients within the intranet, I just set this to the intranet address of my laptop, 129.157.209.250, and I'm done.

But what if I want connections both from the intranet and from the wireless network? That's where things get a bit more complicated.

Domain Name Service

First, if my laptop has a DNS name such as eamonnslaptop.france.sun.com then I might be able to arrange for that name to resolve to more than one IP address. (Look at the DNS lookup for google.com for example.) So I could set java.rmi.server.hostname to the DNS name and wait for RFE 5052134 to be implemented.

Apart from the fact that I might not be able to wait, this solution is unsatisfactory because it's highly unlikely that such a DNS name exists in my case. The two IP addresses come from two different DHCPservers, one for the local intranet and one for the local wireless network. They will probably be different if I come back tomorrow and connect up my laptop again. There are plenty of other scenarios, for example involving "floating IP addresses", that will also fail.

Client socket factory solutions

All is not lost, though. Remember that, in addition to a host and port, the stub also contains a client socket factorywhich controls exactly how a connection is made to the host and port. By supplying our own socket factory, we can try to do the right thing for all clients, regardless of where they are connecting from.

Before we see what that might look like, a note about what it implies. The client socket factory is part of the RMI stub, which means, paradoxically, that it is defined by the server. So if I want my clients to be able to do some magic to choose the right address to connect to, I'll need to export my remote object appropriately. Clients will not be able to choose to use the magic factory on their own, or indeed not to use it if I have defined one. (Well, using reflection or serialization games, they might, but it will almost certainly not be portable.)

Another implication is that my clients will need to have the client socket factory class in their classpath. (Or they could set up code downloading if they are very brave.)

Given that, what might the client socket factory look like?

ThreadLocal client socket factory

One idea (due to Laurent Farcy) is to have a ThreadLocalvariable that the client sets explicitly around code that might use a stub. The outline might be something like this...

            String oldHostName = ThreadLocalRMIClientSocketFactory.getHostName();
            try {
                ThreadLocalRMIClientSocketFactory.setHostName("129.157.209.250");
                ...operations using the stub...
            } finally {
                ThreadLocalRMIClientSocketFactory.setHostName(oldHostName);
            }
       

...with a socket factory that looks something like this (untested code)...

            public class ThreadLocalRMIClientSocketFactory
                    implements RMIClientSocketFactory {
                private static final ThreadLocal<String> hostName =
                    new ThreadLocal<String>();
                
                public Socket createSocket(String host, int port)
                        throws IOException {
                    String hostOverride = getHostName();
                    if (hostOverride != null)
                        host = hostOverride;
                    return new Socket(host, port);
                }

                public static String getHostName() {
                    return hostName.get();
                }

                public static void setHostName(String name) {
                    hostName.set(name);
                }
            }
       

Basically, the socket factory ignores the address contained in the stub and forces the address to be the one you set with the ThreadLocal instead. This is workable if (a) you know the address that your stub is supposed to connect to, and (b) you can be sure that the host name is set appropriately around every use of the stub. (RMI can close idle connections at any time and will create a new one the next time you use the stub.) These conditions can be satisfied for certain uses of the JMX Remote API and you could encapsulate them in a custom JMXConnectorProvider. For example, you could arrange for connections to the JMXServiceURLservice:jmx:mrmi://129.157.209.250:8888/jmxrmito pick up an RMIServer from the RMI registry at that address and to set the ThreadLocal to 129.157.209.250 around the call to RMIServer.newClient() and around all calls to the resultant RMIConnection. It's heavy going, but you can do it.

Choice of IP addresses

Another possibility is to set the java.rmi.server.hostname property to a list of all local IP addresses. Then you can define a client socket factory that picks the right IP address for the client.

The tricky part is in that last sentence. How do we know which IP address is appropriate? If I have the list [129.157.209.250, 10.0.10.58], how do I know which address is the one for me?

One answer is that I could simply try to connect to all of them and pick whichever one works. I can make all the connection attempts in parallel using a NIO Selector. Or, if I'm on at least version 5.0 of the Java platform, I could use isReachable() on each address, perhaps in parallel in different threads. (The remarks in the isReachable() documentation might discourage me from doing that, however.)

Either way, though, I have to face up to an uncomfortable truth, which is that not all IP addresses are unique. The 10.0.10.58 address of my WiFi interface is an example. The 10.* IP addresses are specifically reserved for private networks. A client on the same wireless network as my laptop can contact it using that address. A client on the company intranet can't, but it's possible that the same address is being used for something else on that network. Such a client might end up connecting to some totally different machine thinking it was my laptop.

For any given network configuration, you can probably find appropriate logic to pick the right IP address out of the list. But you probably can't write logic that will work everywhere.

With that caveat, let's look at the details. First, we need to set the java.rmi.server.hostname property appropriately. In my example, it will be set to "129.157.209.250!10.0.10.58". Notice that if there is only one network interface, the property will be set to the same value that RMI would have used anyway. But if there is more than one, it will be set to a string that only our magic client socket factory can understand. Because system properties are global to the Java Virtual Machine, this means that all RMI objects in the JVM had better be exported with the socket factory. Given that default RMI doesn't work splendidly with multihomed machines this is not necessarily a big drawback. You could also try to set the property just at the point where you export your objects, though it will be hard to coordinate with other possible RMI exporters in the JVM.

So here's the code to set the property.

        System.setProperty("java.rmi.server.hostname",
            addressString(localAddresses()));
        ...
            
    private static Set<InetAddress> localAddresses() throws SocketException {
        Set<InetAddress> localAddrs = new HashSet<InetAddress>();
        Enumeration<NetworkInterface> ifaces =
                NetworkInterface.getNetworkInterfaces();
        while (ifaces.hasMoreElements()) {
            NetworkInterface iface = ifaces.nextElement();
            Enumeration<InetAddress> addrs = iface.getInetAddresses();
            while (addrs.hasMoreElements())
                localAddrs.add(addrs.nextElement());
        }
        return localAddrs;
    }

    private static String addressString(Collection<InetAddress> addrs) {
        String s = "";
        for (InetAddress addr : addrs) {
            if (addr.isLoopbackAddress())
                continue;
            if (s.length() > 0)
                s += "!";
            s += addr.getHostAddress();
        }
        return s;
    }
       

(This code, and the remainder of the code, should work unchanged on versions 5.0 or 6 of the Java platform, and should work on 1.4 if you remove the generics.)

Now here's the magic client socket factory. As I described, it uses NIO to connect to all the addresses in parallel, and picks whichever address works first. Again, be aware that this may not work if there are network-private IP addresses in the picture. You may want to modify the logic to work appropriately for your network environment.

import java.io.IOException;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMISocketFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class MultihomeRMIClientSocketFactory
        implements RMIClientSocketFactory, Serializable {
    private static final long serialVersionUID = 7033753601964541325L;
    
    private final RMIClientSocketFactory factory;
    
    public MultihomeRMIClientSocketFactory(RMIClientSocketFactory wrapped) {
        this.factory = wrapped;
    }
    
    public Socket createSocket(String hostString, int port) throws IOException {
        final String[] hosts = hostString.split("!");
        final int nhosts = hosts.length;
        if (hosts.length < 2)
            return factory().createSocket(hostString, port);

        List<IOException> exceptions = new ArrayList<IOException>();
        Selector selector = Selector.open();
        for (String host : hosts) {
            SocketChannel channel = SocketChannel.open();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_CONNECT);
            SocketAddress addr = new InetSocketAddress(host, port);
            channel.connect(addr);
        }
        SocketChannel connectedChannel = null;
        
        connect:
        while (true) {
            if (selector.keys().isEmpty()) {
                throw new IOException("Connection failed for " + hostString +
                        ": " + exceptions);
            }
            selector.select();  // you can add a timeout parameter in millseconds
            Set<SelectionKey> keys = selector.selectedKeys();
            if (keys.isEmpty()) {
                throw new IOException("Selection keys unexpectedly empty for " +
                        hostString + "[exceptions: " + exceptions + "]");
            }
            for (SelectionKey key : keys) {
                SocketChannel channel = (SocketChannel) key.channel();
                key.cancel();
                try {
                    channel.configureBlocking(true);
                    channel.finishConnect();
                    connectedChannel = channel;
                    break connect;
                } catch (IOException e) {
                    exceptions.add(e);
                }
            }
        }
        
        assert connectedChannel != null;
        
        // Close the channels that didn't connect
        for (SelectionKey key : selector.keys()) {
            Channel channel = key.channel();
            if (channel != connectedChannel)
                channel.close();
        }
        
        final Socket socket = connectedChannel.socket();
        if (factory == null && RMISocketFactory.getSocketFactory() == null)
            return socket;
        
        // We've determined that we can connect to this host but we didn't use
        // the right factory so we have to reconnect with the factory.
        String host = socket.getInetAddress().getHostAddress();
        socket.close();
        return factory().createSocket(host, port);
    }
    
    private RMIClientSocketFactory factory() {
        if (factory != null)
            return factory;
        RMIClientSocketFactory f = RMISocketFactory.getSocketFactory();
        if (f != null)
            return f;
        return RMISocketFactory.getDefaultSocketFactory();
    }

    // Thanks to "km" for the reminder that I need these:
    public boolean equals(Object x) {
        if (x.getClass() != this.getClass())
            return false;
        MultihomeRMIClientSocketFactory f = (MultihomeRMIClientSocketFactory) x;
        return ((factory == null) ?
                (f.factory == null) :
                (factory.equals(f.factory)));
    }

    public int hashCode() {
        int h = getClass().hashCode();
        if (factory != null)
            h += factory.hashCode();
        return h;
    }
}
   

The MultihomeRMIClientSocketFactory constructor takes an RMIClientSocketFactory parameter which can be null or another factory to be used once the address to connect to has been determined. For example, it could be an SslRMIClientSocketFactory.

It would be nice if RMI came with something like this by default, but I'm not sure given the design of RMI stubs that there is a good general solution. At a minimum, it would be good if RMI allowed you to specify the java.rmi.server.hostname locally for a given export, and if it allowed you to inspect and change the socket factory inside a stub.

I'm writing this in what I used to think was the world's nastiest airport, where I have a five-hour stopover. I'm somewhat revising my opinion of the airport because I discovered a "Quiet Seating Area" with real seats and real quiet. A bit like a business-class lounge but for the plebs. So I'm sitting there typing this, with my laptop on a table and the charger plugged in and everything.

As I mentioned last time, I was at The Spring Experience 2006 in Hollywood, Florida where I was speaking about JMX stuff.

I had been surprised at how close the conference hotel was to the sea. In France, the infamous loi littoraleforbids construction within a certain distance of the sea, unless you are a member of the Saudi royal family that the French state doesn't want to displease. The idea is of course avoid the sort of concrete nastiness to be seen in certain neighbouring countries.

Anyway, some local friends set me straight. When the hotel was built, the sea was further away! That doesn't happen much with the dull old Mediterranean, but over there in hurricaneland it's a different story. A constant battle with a shifting shoreline, in fact.

But back to your regular scheduled geekery. Here are some of the talks I attended.

Rapid Web App Development (Rob Harrop)

This was mostly about Spring Web Flow, so I'll leave detailed comments to the Spring experts. I came away with a few items of general interest:

  • Good tools are essential. Rob recommended having a good IDE (IDEA, Eclipse WTP, NetBeans). He was surely preaching to the converted here, but maybe there were one or two diehard Emacs users in the audience.

    [I like Emacs a lot, and in fact I'm typing this using it. But I wouldn't think of using it for serious Java programming. There have been valiant attempts at producing IDEs in Emacs, but even I am not mad enough to use them.]

    Rob also recommended the Jetty web server, at least for development. It starts in about two seconds and has reliable hot deploy, apparently unlike its rivals.

    Finally, Rob recommended Maven, notably because of its good integration with Jetty.

  • For serious work, you need to be able to attach to your app server with a debugger. It might take a bit of investigation to figure out how to do this, but it is a worthwhile investment.

    I use NetBeans and Glassfish, and this is trivially easy. Right-click on the app server in the Runtime tab and select "Start in debug mode". Then, with your web app as your main project, do "debug main project" (F5). NetBeans deploys the app, launches it in your browser, and attaches the debugger.

  • Rob demonstrated the use of JRuby for parts of the web app that may change often, such as the formatting within a page. Spring can be set up so that it continually polls the file containing the Ruby script for changes and reloads the script if it finds any. The result is that you can change the script and immediately see the result in your browser without any redeploy step. Requests that were in progress before the change continue with the old version, and new requests use the new. Nifty.

Spring and dynamic languages (Rod Johnson, Guillaume LaForge)

Rod presented the general Spring support for dynamic languages, such as Groovy, BeanShell, and JRuby. Then Guillaume, who is the current holder of the Groovy torch, talked about that language in particular. Script languages lend themselves well to interactive demos, with the presenter typing in some stuff and showing what it does. Several speakers showed this in fact, with varying degrees of success. Guillaume's demo went off well, except when he was smitten by the demo gods. His computer went spontaneously into hibernation in the middle, which caused Guillaume to emit a bad word.

I'm not sure what to think of Groovy, myself. On the plus side, it is very similar to Java, so easy to learn and read, and it has some powerful features. On the minus side, it smells a bit like Perl, with these individually-nifty features slapped together without a coherent philosophy.

JRuby (Rob Harrop)

There was some overlap with the Groovy talk here. Rob walked through some examples of Ruby programming, stressing the interactivity and the access to the Java platform classes. One thing I picked up on is that Ruby (and Groovy) can make the JMX API's Open Types much more palatable. You can set things up so that instead of accessing a field in a CompositeData using cd.get("maxTime")you can use cd.maxTime. In fact Open Types are an excellent fit for dynamic languages and I think we should revisit the JavaScript library in the script plugin for JConsole to take that into account.

Code organization for large systems (J