I'm exploring the capabilities of Android's VpnService. Presently, I've built a very rudimentary request forwarder by essentially rebuilding the IP stack in user space: I read IP packets from the VpnService's input stream, parse them, and for connections I don't want to forward, I attempt to recreate those socket connections outside the VPN connection.
I've understood that this last bit is facilitated by VpnService.protect()
and have tried implementing it as follows:
Socket socket = new Socket();
vpnService.protect(socket);
socket.connect(new InetSocketAddress(
header.getDestinationAddress(), // From my IP datagram header
body.getDestinationPort())); // From the TCP datagram header
Unfortunately, this approach is causing a loopback into the VPN interface.
Whereas the above code will simply block and eventually time out, I observe the loopback by calling Socket.connect(InetSocketAddress)
from a separate thread; the connection comes straight back into my VpnService's input stream and the process repeats.
Needless to say, this causes a loop. I get the feeling that the reason for this is that at the time of socket creation (and subsequently, the call to VpnService.protect(Socket)
), I haven't set the destination IP & port yet.
This seems to indeed be the case, as the by overriding VpnService.protect(Socket)
and VpnService.protect(int)
in my VpnService implementation and calling the supers in both cases returns false.
How can I properly protect a socket connection?
I needed to protect sockets in OkHttpClient
. It creates unconnected sockets which cannot be protected yet (meaning that service.protect()
returns false
) and when they are connected it apparently is too late (e.g. when I tried to protect them inside networkInterceptor
). Classic obscure Android behaviors.
Anyway, Mai Quoc Huy's answer was right for me and the whole code looks like this.
val protectedHttpClient = OkHttpClient.Builder()
.socketFactory(object : SocketFactory() {
override fun createSocket(): Socket = Socket().apply {
bind(InetSocketAddress(0))
val result = service?.protect(this)
}
override fun createSocket(host: String?, port: Int) = unsupported()
override fun createSocket(host: String?, port: Int, localHost: InetAddress?, localPort: Int) = unsupported()
override fun createSocket(host: InetAddress?, port: Int) = unsupported()
override fun createSocket(address: InetAddress?, port: Int, localAddress: InetAddress?, localPort: Int) = unsupported()
private fun unsupported(): Nothing = throw UnsupportedOperationException("This factory can only create unconnected sockets for OkHttp")
})
.build()
You need to bind your socket before protecting. This works for me:
Socket socket = new Socket();
//bind to any address
socket.bind(new InetSocketAddress(0));
vpnService.protect(socket);
socket.connect(...);
The following code works.
Socket socket = SocketChannel.open().socket();
if ((null != socket) && (null != vpnService)) {
vpnService.protect(socket);
}
socket.connect(...);
new Socket() doesn't have a valid file descriptor, so it cannot be protected.
I found that an alternative solution was to write it out in C/C++.
Java:
public native int createSocket();
public native int connectSocket(int fd);
C++:
// For sockets
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// For error codes
#include <errno.h>
extern "C" {
JNIEXPORT jint JNICALL
Java_com_pixplicity_example_jni_VpnInterface_createSocket(
JNIEnv * env, jobject thiz) {
// Create the socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int err = errno;
// Return the file descriptor
return sockfd;
}
JNIEXPORT jint JNICALL
Java_com_pixplicity_example_jni_VpnInterface_connectSocket(
JNIEnv * env, jobject thiz, jint sockFd) {
// Host & port are hard-coded here
char* host = "74.125.136.113"; // google.com
int port = 80;
struct sockaddr_in peerAddr;
int ret;
peerAddr.sin_family = AF_INET;
peerAddr.sin_port = htons(port);
peerAddr.sin_addr.s_addr = inet_addr(host);
// Connect to host
ret = connect((int) sockFd, (struct sockaddr *) &peerAddr,
sizeof(peerAddr));
if (ret != 0) {
perror("connect failed");
close(sockFd);
}
// Return the error code
return ret;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With