Java RMI — Calling Methods on Another Computer
Java RMI (Remote Method Invocation) is the networking technology that makes this entire project possible. Once you understand RMI, the rest of the architecture becomes obvious.
The Core Idea
In normal Java, when you write:
String result = someObject.doSomething("hello");
the JVM finds someObject, calls doSomething, and returns the result — all on the same machine, in the same process.
RMI makes it possible to write the exact same line of code, but someObject actually lives on a different computer (or a different JVM on the same machine). Java handles the networking transparently.
How It Works — The Five Moving Parts
1. The Interface (extends Remote)
Before any code runs, you define a Java interface that describes what methods the remote object offers:
public interface AuthServiceInterface extends Remote {
String login(UserCredentials creds) throws RemoteException;
String register(UserCredentials creds) throws RemoteException;
boolean validateToken(String token) throws RemoteException;
}
Every method must declare throws RemoteException — this is RMI's way of saying "this call might fail because the network is unreliable."
2. The Server Implementation (extends UnicastRemoteObject)
The server class implements the interface and extends UnicastRemoteObject:
public class AuthService extends UnicastRemoteObject implements AuthServiceInterface {
private final ConcurrentHashMap<String, UserRecord> users = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, String> tokens = new ConcurrentHashMap<>();
public AuthService() throws RemoteException {
super(0); // Port 0 = let the OS pick an ephemeral port
}
@Override
public String login(UserCredentials creds) throws RemoteException {
UserRecord record = users.get(creds.getUsername());
if (record == null) return "ERROR: User not found";
if (!PasswordUtils.verify(creds.getPassword(), record.salt(), record.hash())) {
return "ERROR: Invalid password";
}
String token = UUID.randomUUID().toString();
tokens.put(token, creds.getUsername());
return token;
}
// ... more methods
}
By extending UnicastRemoteObject, the object automatically becomes capable of accepting RMI calls. The super(0) call creates a server socket on a random port.
3. The RMI Registry (Phone Book)
The server must publish its object so clients can find it:
AuthService service = new AuthService();
Registry registry = LocateRegistry.createRegistry(1098);
registry.bind("AuthService", service);
The Registry is like a phone book at a well-known address (localhost:1098). Clients query the registry by name to get a reference to the object.
4. The Client Lookup (Finding the Remote Object)
The client connects to the registry and asks for the object by name:
// Secure version — over mTLS
Registry registry = LocateRegistry.getRegistry("localhost", 1098, new SslRMIClientSocketFactory());
AuthServiceInterface auth = (AuthServiceInterface) registry.lookup("AuthService");
What the client gets back is a stub — a proxy object that looks exactly like AuthServiceInterface but actually forwards every method call over the network.
5. Serialization (Marshalling Arguments)
When the client calls auth.login(creds), RMI must send the UserCredentials object over the network. It does this through Java serialization:
Client JVM Server JVM
───────── ─────────
creds (object)
↓ ObjectOutputStream
↓ → byte stream → TCP → ↓ ObjectInputStream
↓
creds (deserialized copy)
↓
login(creds) executes
↓
return token
↓ ObjectOutputStream
↓ ← byte stream ← TCP ←
↓ ObjectInputStream
token (deserialized copy)
This serialization step is also where Vulnerability 1 (Deserialization RCE) lives — more on that in the Security Analysis.
What RMI Looks Like to the Developer
// ---- Client code ----
// Step 1: Connect to the registry
Registry reg = LocateRegistry.getRegistry("localhost", 1098, csf);
AuthServiceInterface auth = (AuthServiceInterface) reg.lookup("AuthService");
// Step 2: Call methods as if the object is local
String token = auth.login(new UserCredentials("jana", "password123"));
// Step 3: Use the token for file operations
ReplicaNodeInterface node = (ReplicaNodeInterface) reg.lookup("ReplicaNode0");
OperationResult result = node.handleClientOperation(
FileOperation.upload("jana", "report.pdf", fileBytes),
token
);
// That's it. RMI handled:
// - TCP connection setup
// - Object serialization/deserialization
// - Exception propagation
// - Thread management
The mTLS Twist
In our project, RMI doesn't use plain TCP. Every connection uses mutual TLS:
// Server side — accept ONLY mTLS connections
SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(null, null, true);
// Client side — connect over mTLS
SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
// Create registry that uses these factories
Registry registry = LocateRegistry.createRegistry(port, csf, ssf);
The true parameter on the server side means "require the client to present a valid certificate." Without this, an attacker could connect without any authentication.
Why Not Just REST/gRPC?
| Aspect | Java RMI | REST | gRPC |
|---|---|---|---|
| Serialization | Java-native (binary) | JSON/XML | Protocol Buffers |
| API style | Interface-based | URL + HTTP verbs | Interface + .proto |
| Language coupling | Java only | Any language | Any language |
| Performance | Very fast (binary, no HTTP) | Slower (text, HTTP overhead) | Very fast (binary, HTTP/2) |
| Complexity | Low for Java devs | Low | Medium |
RMI is the right choice for this project because:
- It's built into the JDK — no dependencies
- It makes the distributed system feel like a local Java application
- Its serialization mechanism is exactly what we're demonstrating vulnerabilities on
Next: Lamport Clocks
Now that you understand how nodes call each other, the next page explains Lamport Logical Clocks — the mechanism we use to order events without synchronised wall clocks. → Lamport Logical Clocks