ORIGINAL DRAFT
The Internet is a near-ubiquitous mechanism for transmitting virtually any type of information between computers. There are security concerns that need to be addressed, of course, but often the information is meant to be publicly available, as in a web page, or convenience is more important, such as in email. If you need to transfer more sensitive information, you need a secure transport protocol, such as SSL.
In this article, we’ll develop a simple, multi-threaded proxy service that can forward requests and return responses transparently. You can use it with the Java SSL (Secure Socket Layer) implementation to create a kind of virtual private network for specific services. To do this, you would set up a proxy on the client side which listens on a clear port and forwards traffic across an SSL connection to another proxy, typically at a remote location. The remote proxy listens for SSL connections and re-forwards traffic to a service on the other side of a firewall.
This process actually tunnels through the firewall on the SSL port, but the connection is completely secure and you can make services available across corporate divisions without risking strategic information.
Figure 1 shows the architecture for a simple secure networking scenario. The diagram shows that clients can point their browsers to a secure proxy and access a web server remotely, even though it’s behind a firewall. Each transaction is handled through the Secure Socket Layer, protecting your data from prying eyes.
Because of the overhead associated with SSL connections, this is not necessarily the best example application, though it certainly works. Applications which maintain persistent, long term connections will benefit more than short connection, stateless protocols such as HTTP. Picked web services as an example because they are so wide spread and easy to understand.
Building Blocks
A proxy, at it’s most fundamental level is little more than a server which opens a target client socket connection and passes all input to the connecting client and all output from the target client back to the connecting client. In practice, we need to support multithreading to do this without blocking on each client connection. If you approach this correctly, its actually very simple.
Figure 2 shows how two classes can provide an elegant solution to a potentially complex problem. The Client Socket and Server Socket boxes represent instances of a Java Socket associated with a given role. From a Socket you can get an InputStream and an OutputStream. Since these are all standard Java classes, I’ve shown them in rounded rectangles.
The trick is to connect streams together in separate threads, so that input never blocks for output and vice versa. If you don’t do this, you can end up with a deadlocked proxy as the output stream waits for the input stream to complete or the output stream waits for the input stream to complete. Since you can’t predict the nature of any protocol that talks through the proxy, you have to avoid this kind of coupling at all costs.
Listing 1 shows the StreamCoupler code. The StreamCoupler is designed so that you can specify an InputStream and an OutputStream to be coupled, and the rest is handled for you. Because the StreamCoupler extends the Thread class, all you need to do is call the start method to get things going. The run method will take any incoming data and pass it directly to the output stream.
The beauty of this solution is how reusable this is. You can use the StreamCoupler to copy files by opening a FileInputStream and a FileOutputStream, calling the start method in the StreamCoupler thread. You can use it to copy files to a Socket connection or back again, or you can use it to copy data across threads with pipe streams or any other stream you can think of.
Listing 2 shows the SocketCoupler code. Where the StreamCoupler connects two streams, the SocketCoupler connects two Socket objects. All we need to do is connect respective input and output streams from the sockets together. SocketCoupler extends the Thread class and starts the two StreamCoupler threads automatically when the run method executes. You can get things going by calling the SocketCoupler’s start method, which will exit when the two StreamCoupler threads complete execution or encounter an exception. We use the Thread join method to ensure both threads are done before exiting the SocketCoupler thread.
The ProxyServer is not very complicated, given all the multithreading work handled for us by the previous two classes. Listing 3 shows the ProxyServer class, which lets you define a sourcePort to listen on, as well as a targetHost and targetPort to forward traffic to. You can also specify whether you are acting as a client or server proxy and whether you want the connection to be secure. When you start the server, the main clause creates an instance of the ProxyServer class, using the settings provided on the command line.
To avoid complications associated with the SSL implementation, we set providers in the main method. The alternative is to edit your java.security file but that’s harder to manage and could complicate installations. You’ll need use Java 1.4 or make sure the SSL jar files are accessible on the class path. The easiest way to do this is to drop them into the Java 1.2 or 1.3 jre/lib/ext directory. The SSL classes assume you are running the Java 2 platform, Java 1.2 or higher.
To further simplify access to SSL sockets, the ProxyServer class gets the SSLSocketFactory and SSLServerSocketFactory in the ProxyServer constructor. This helps performance to some extent, since we can avoid future lookups on each client connection.
Finally, we implement a createClientSocket and a createServerSocket method to actually create the Socket and ServerSocket instances we will need. Because it’s so much faster to work without SSL sockets, we can test without it until we are sure everything works as expected.
My own testing was fairly simple. I started a ProxyServer on a free source port and targeted another machine on which I ran another ProxyServer, forwarding requests to a web server on that machine. By using my browser to access the local ProxyServer, I could reliably navigate content from the web server on the other machine, though the local proxy address.
It’s worth noting that you’ll probably run into frequent exceptions with HTTP, all of which can be ignored. They are typically caused by socket disconnections by either the client or the host, depending on your activities. This is fairly common during web browsing and has no negative impact on the proxy. In a more sophisticated implementation, you might catch these exceptions and log them as warnings instead.
Authentication
Secure transmissions are a good idea, but you also need to know that the parties at each end of the connection are who they claim to be. The best way to do this is to use both server and client authentication in SSL. When SSL is used on the web, only the server typically authenticates itself, but the protocol is capable of doing this both way.
We can use X.509 certificates to ensure the client and server can identify each other. All we have to do is generate both a client and server certificate, placing them in a client and server keystore, respectively. We’ll use the same keystores, reversing the roles, as trusted certificate stores. That is to say, the server will consider the client a trusted source because it will have a trusted keystore with the client certificate in it, and vice versa. You can add more clients or servers to your trusted keystores, but we’ll keep things simple by supporting direct connections between only one client proxy and one server proxy in this case.
To make it easy to generate a certificate, I’ve put this small Windows batch file together.
set KEYSTORE=client.store
set STOREPASS=password
set KEYPASS=password
set ALIAS=client
set CERTFILE=client.cer
set KEYALG=RSA
set DNAME=
"cn=Secure Proxy Client,
ou=Programming,
o=JavaPro,
c=US";
del %KEYSTORE
keytool -genkey
-keyalg %KEYALG
-keystore %KEYSTORE
-storepass %STOREPASS
-keypass %KEYPASS
-alias %ALIAS
-dname %DNAME
keytool -list
-keystore %KEYSTORE
-storepass %STOREPASS
keytool -export
-alias %ALIAS
-file %CERTFILE
-keystore %KEYSTORE
-storepass %STOREPASS
-keypass %KEYPASS
It’s easy to port this to UNIX and UNIX afficionados are more apt to port this from Windows than the other way around. I’ve used environment variables for each of the fields because they’re repeated in different invocations of the keytool utility. The indented line breaks are there for clarity and should be removed in a runnable batch file. You’ll find a runnable version of this batch file, along with the rest of the source code at www.javapro.com.
After deleting any previously existing keystore, we generates a new certificate. The next call lists the content of the keystore for verification purposes. You can remove this if you like. The last call export the certificate. For our purposes, this isn’t necessary because we’ll reuse the same keystore, but you might need to import the certificate elsewhere, so it’s good to have it accessible in a separate file.
Under Windows, the “.cer” file extension is normally mapped to a viewer that you can use by double clicking on the file. You’ll see something like Figure 3. Notice that there is no Certificate Authority (CA) associated with this certificate because it was self-signed, so Windows does not consider it trusted. You could get a certificate generated by Verisign or another CA, but that’ll cost you and we don’t really care about that in this application since both our client and server code will point to its own trusted keystores.
Getting back to Listing 3, you’ll see that the main clause does some setup work by calling the static addProvider method in the Security class, in order to set up the standard Sun providers. We also use the getProperties static method in the System class to set up property settings for the keystore, and the keystore password, as well as the trusted store.
The trusted store doesn’t need a password because it contains only certificates (one in this case) and no private keys. It is not considered sensitive data by the Java security system. You’ll also see that we call the setNeedClientAuth method on the SSLServerSocket, to enable client authentication, whenever we’re setting up a secure server proxy.
SSL supports a number of cipher suites that define the algorithms used for key exchange and authentication, data encryption and message digest authentication. To show how you can control the cipher suite, I’ve set it explicitly to SSL_RSA_WITH_RC4_128_MD5, which uses RSA for key exchange and authentication, RC4 with a 128 bit key for encryption, and MD5 hashing for the message digest. if you use this code elsewhere, you’ll may want to allow the software to select the best available algorithm on its own.
The ProxyServer can give you access to resources that are otherwise unreachable for legitimate security reasons. Clearly, this can’t be done without the cooperation of resource owners on both sides of the equation, but objections are easily addressed by the secure nature of each connection. With proper configuration, this is a vehicle for access that provides secure authentication and encrypted data transmission. It is, given the power it provides, surprisingly simple to implement.
Listing 1
import java.io.*;
public class StreamCoupler extends Thread
{
protected InputStream in;
protected OutputStream out;
protected int bufferSize;
public StreamCoupler(
InputStream in, OutputStream out)
{
this(in, out, 4096);
}
public StreamCoupler(InputStream in,
OutputStream out, int bufferSize)
{
this.in = in;
this.out = out;
this.bufferSize = bufferSize;
}
public void run()
{
try
{
int count;
byte[] buffer = new byte[bufferSize];
while ((count = in.read(buffer)) > -1)
{
out.write(buffer, 0, count);
out.flush();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
Listing 2
import java.io.*;
import java.net.*;
public class SocketCoupler extends Thread
{
protected Socket client, server;
protected StreamCoupler clientToServer;
protected StreamCoupler serverToClient;
public SocketCoupler(
Socket client, Socket server)
throws IOException
{
this.client = client;
this.server = server;
clientToServer = new StreamCoupler(
server.getInputStream(),
client.getOutputStream());
serverToClient = new StreamCoupler(
client.getInputStream(),
server.getOutputStream());
}
public void run()
{
clientToServer.start();
serverToClient.start();
try
{
clientToServer.join();
serverToClient.join();
client.close();
server.close();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
Listing 3
import java.io.*;
import java.net.*;
import java.util.*;
import java.security.*;
import javax.net.*;
import javax.net.ssl.*;
public class ProxyServer
{
protected int sourcePort;
protected String targetHost;
protected int targetPort;
protected SSLSocketFactory sslClientFactory;
protected SSLServerSocketFactory sslServerFactory;
protected static final String[] CIPHERS =
{"SSL_RSA_WITH_RC4_128_MD5"};
public ProxyServer(int sourcePort,
String targetHost, int targetPort)
{
this.sourcePort = sourcePort;
this.targetHost = targetHost;
this.targetPort = targetPort;
sslClientFactory = (SSLSocketFactory)
SSLSocketFactory.getDefault();
sslServerFactory = (SSLServerSocketFactory)
SSLServerSocketFactory.getDefault();
}
public Socket createClientSocket(boolean isSecure)
throws IOException
{
if (isSecure)
{
SSLSocket socket =
(SSLSocket)sslClientFactory.
createSocket(targetHost, targetPort);
socket.setEnabledCipherSuites(CIPHERS);
return socket;
}
return new Socket(targetHost, targetPort);
}
public ServerSocket createServerSocket(boolean isSecure)
throws IOException
{
if (isSecure)
{
SSLServerSocket socket =
(SSLServerSocket)sslServerFactory.
createServerSocket(sourcePort);
socket.setEnabledCipherSuites(CIPHERS);
return socket;
}
return new ServerSocket(sourcePort);
}
public static void main(String[] args)
throws IOException
{
Security.addProvider(new sun.security.provider.Sun());
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
if (args.length != 5)
{
System.out.print("Usage: java ProxyServer");
System.out.print(" sourcePort targetHost targetPort");
System.out.print(" client|server normal|secure");
System.out.println();
System.exit(0);
}
int sourcePort = Integer.parseInt(args[0]);
String targetHost = args[1];
int targetPort = Integer.parseInt(args[2]);
boolean isClient = args[3].equalsIgnoreCase("client");
boolean isSecure = args[4].equalsIgnoreCase("secure");
String keyStore = isClient ?
"client.store" : "server.store";
String trustStore = isClient ?
"server.store" : "client.store";
Properties props = System.getProperties();
props.setProperty(
"javax.net.ssl.keyStore", keyStore);
props.setProperty(
"javax.net.ssl.keyStorePassword", "password");
props.setProperty(
"javax.net.ssl.trustStore", trustStore);
ProxyServer proxy = new ProxyServer(
sourcePort, targetHost, targetPort);
ServerSocket server = proxy.createServerSocket(
!isClient && isSecure);
if (!isClient && isSecure)
((SSLServerSocket)server).setNeedClientAuth(true);
System.out.println("Ready...");
while (true)
{
Socket source = server.accept();
System.out.println("Connected to " +
source.getLocalAddress() + ":" +
source.getLocalPort());
Socket target = proxy.createClientSocket(
isClient && isSecure);
SocketCoupler coupler = new SocketCoupler(source, target);
coupler.start();
}
}
}