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.