Proof of concept:
- Terminal 1:
- SSH to remote host
- Start a Java process with JMX registry port 50004, RMI callback
port 50005, and RMI hostname pinned to localhost:
java -Dcom.sun.management.jmxremote.port=50004 -Dcom.sun.management.jmxremote.rmi.port=50005 -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -cp /some/jar/file main.class
- Terminal 2:
- Port-forward local 50004 and 50005 to the host:
ssh remote-host -L 50004:localhost:50004 -L 50005:localhost:50005
- Port-forward local 50004 and 50005 to the host:
- Terminal 3:
- Make JMX call against
localhost:50004
- Make JMX call against
Explanation:
(First note that I'm waving my hands somewhat, as I don't really know JMX. This is just my working understanding at time of posting.)
When JMX is initialized, it listens on the specified registry port
(here specified as -Dcom.sun.management.jmxremote.port=50004
) for
commands, but it also listens on a random port for RMI
callbacks. (Here I'll use 4217 to indicate that random port.) When a
command is issued, JMX responds saying "OK, but you need to talk to
addr-1-2-3-4:4217
to get the response". Since it sends a hostname
and port as data over the connection, there's no guarantee the caller
(who is probably on the other side of at least one SSH tunnel and at
least one NAT) can talk to the port, let alone resolve the hostname!
There are two parts to the solution:
- Pin the RMI callback port so that it can be predictably forwarded:
-Dcom.sun.management.jmxremote.rmi.port=50005
- Pin the declared hostname to
localhost
to ensure the connection doesn't go haring off into the void:-Djava.rmi.server.hostname=localhost
One downside is that since RMI forces the client to look at a specific
host and port, and the port is what it listens on, we also have to
forward that exact port locally. This means that if you have the same
JMX settings for your app locally and in deployment, they can't both
be running (and serving JMX) at the time you want to do some
debugging. One might be able to use an alternative loopback IP address
for the RMI hostname (e.g. 127.0.0.43
), but this would again have to
differ between local and deployed configurations.
Sources: