Skip to content

Commit

Permalink
Plugin implementation (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
alberto lalama authored Aug 17, 2016
1 parent 61f4539 commit 972ce0e
Show file tree
Hide file tree
Showing 18 changed files with 1,578 additions and 1,796 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ To handle DNS resoution, we have implemented a local DNS resolver that intercept

This plugin targets Android devices running Marshmellow (API 23), or higher. This requirement stems from calling `bindProcessToNetwork`, a [connectivity API](https://developer.android.com/reference/android/net/ConnectivityManager.html#bindProcessToNetwork(android.net.Network)) introduced in version 23, which allows the client application traffic to bypass the VPN.

### Javascript API

`start(socksServerAddress:string) : Promise<string>;`

Starts the VPN service, and tunnels all the traffic to the SOCKS5 server at `socksServerAddress`.

`stop(): Promise<string>;`

Stops the VPN service.

`onDisconnect(): Promise<string>;`

Sets a success callback on the returned promise, to be called if the VPN service gets revoked or disconnected.

### Code Sources

We re-use and have used as a starting point open source code from [Psiphon](https://psiphon.ca/uz@Latn/open-source.html), specifically https://github.com/mei3am/ps.
Expand Down
21 changes: 21 additions & 0 deletions build-extras.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}

defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}

packagingOptions {
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
}
}
52 changes: 52 additions & 0 deletions plugin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
id="cordova-plugin-tun2socks" version="1.0.0">
<name>Tun2Socks</name>
<description>Tun2Socks for Android</description>
<license>Apache 2.0</license>
<keywords>cordova,tun2socks,badvpn</keywords>

<engines>
<engine name="cordova-android" version=">=4.0.0-dev" />
</engines>

<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="Tun2Socks">
<param name="android-package" value="org.uproxy.tun2socks.Tun2Socks" />
<param name="onload" value="true" />
</feature>
</config-file>

<config-file target="AndroidManifest.xml" parent="/manifest">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</config-file>

<config-file target="AndroidManifest.xml" parent="/manifest/application">
<service
android:name="org.uproxy.tun2socks.TunnelVpnService"
android:exported="false"
android:label="@string/app_name"
android:permission="android.permission.BIND_VPN_SERVICE" />
<service
android:name="org.uproxy.tun2socks.DnsResolverService"
android:exported="false" />
</config-file>

<source-file
src="src/android/org/uproxy/tun2socks"
target-dir="src/org/uproxy/tun2socks" />

<framework src="src/android/org/uproxy/tun2socks/build.gradle"
custom="true" type="gradleReference" />

<resource-file src="src/badvpn" target="badvpn" />
<resource-file src="src/android/jni" target="jni" />
<resource-file src="src/android/libs" target="libs" />

<js-module src="tun2socks.js" name="tun2socks">
<clobbers target="tun2socks" />
</js-module>
</platform>
</plugin>
1 change: 1 addition & 0 deletions src/android/jni/Android.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../badvpn/Android.mk
2 changes: 2 additions & 0 deletions src/android/jni/Application.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
APP_ABI := armeabi-v7a
APP_PLATFORM := android-8
286 changes: 286 additions & 0 deletions src/android/org/uproxy/tun2socks/DnsResolverService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package org.uproxy.tun2socks;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;

import socks.Socks5Proxy;
import socks.SocksSocket;

// This class listens on a UDP socket for incoming DNS traffic, which is proxy'd
// through SOCKS over TCP to Google's Public DNS.
public class DnsResolverService extends Service {

private static final String LOG_TAG = "DnsResolverService";
public static final String DNS_ADDRESS_BROADCAST = "dnsResolverAddressBroadcast";
public static final String DNS_ADDRESS_EXTRA = "dnsResolverAddress";
public static final String SOCKS_SERVER_ADDRESS_EXTRA = "socksServerAddress";

private final IBinder binder = new LocalBinder();
private String m_socksServerAddress;
private DnsUdpToSocksResolver dnsResolver = null;

public class LocalBinder extends Binder {
public DnsResolverService getService() {
return DnsResolverService.this;
}
}

@Override
public IBinder onBind(Intent intent) {
return binder;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(LOG_TAG, "start command");
m_socksServerAddress = intent.getStringExtra(SOCKS_SERVER_ADDRESS_EXTRA);
if (m_socksServerAddress == null) {
Log.e(LOG_TAG, "Failed to receive socks server address.");
}
return START_NOT_STICKY;
}

@Override
public void onCreate() {
Log.i(LOG_TAG, "create");
dnsResolver = new DnsUdpToSocksResolver(DnsResolverService.this);
dnsResolver.start();
}

@Override
public void onDestroy() {
Log.i(LOG_TAG, "destroy");
if (dnsResolver != null) {
dnsResolver.interrupt();
}
}

// Parses the socks server address received on start and returns it as
// a InetSocketAddress.
public InetSocketAddress getSocksServerAddress() {
if (m_socksServerAddress == null) {
return null;
}
int separatorIndex = m_socksServerAddress.indexOf(':');
String ip = m_socksServerAddress.substring(0, separatorIndex);
int port = Integer.parseInt(m_socksServerAddress.substring(separatorIndex + 1));
return new InetSocketAddress(ip, port);
}

private class DnsUdpToSocksResolver extends Thread {
private static final String LOG_TAG = "DnsUdpToSocksResolver";
private static final String DNS_RESOLVER_IP = "8.8.8.8";
// UDP and DNS over TCP theoretical max packet length is 64K.
private static final int MAX_BUFFER_SIZE = 65535;
private static final int DEFAULT_DNS_PORT = 53;

// DNS bit masks
private static final byte DNS_QR = (byte) 0x80;
private static final byte DNS_TC = (byte) 0x02;
private static final byte DNS_Z = (byte) 0x70;
// DNS header constants
private static final int DNS_HEADER_SIZE = 12;
private static final int QR_OPCODE_AA_TC_OFFSET = 2;
private static final int RA_Z_R_CODE_OFFSET = 3;
private static final int QDCOUNT_OFFSET = 4;
private static final int ANCOUNT_OFFSET = 6;
private static final int NSCOUNT_OFFSET = 8;
private static final int ARCOUNT_OFFSET = 10;

private volatile DatagramSocket udpSocket = null;
private DnsResolverService m_parentService;

public DnsUdpToSocksResolver(DnsResolverService parentService) {
m_parentService = parentService;
}

public void run() {
byte[] udpBuffer = new byte[MAX_BUFFER_SIZE];
DatagramPacket udpPacket = new DatagramPacket(udpBuffer, udpBuffer.length);
InetSocketAddress socksServerAddress =
m_parentService.getSocksServerAddress();
if (socksServerAddress == null) {
return;
}
Log.d(LOG_TAG, "SOCKS server address: " + socksServerAddress.toString());

Socks5Proxy socksProxy = null;
InetAddress dnsServerAddress = null;
try {
socksProxy = new Socks5Proxy(socksServerAddress.getAddress(),
socksServerAddress.getPort());
dnsServerAddress = InetAddress.getByName(DNS_RESOLVER_IP);
udpSocket = new DatagramSocket();
} catch (Throwable e) {
e.printStackTrace();
return;
}
broadcastUdpSocketAddress();

try {
while (!isInterrupted()) {
Log.i(LOG_TAG, "listening on " + udpSocket.getLocalSocketAddress().toString());
try {
udpSocket.receive(udpPacket);
} catch (SocketTimeoutException e) {
continue;
} catch (IOException e) {
Log.e(LOG_TAG, "Receive operation failed on udp socket: ", e);
continue;
}
Log.d(
LOG_TAG,
String.format(
"UDP: got %d bytes from %s:%d\n%s",
udpPacket.getLength(),
udpPacket.getAddress().toString(),
udpPacket.getPort(),
new String(udpBuffer, 0, udpPacket.getLength())));

if (!isValidDnsRequest(udpPacket)) {
Log.i(LOG_TAG, "Not a DNS request.");
continue;
}

Socket dnsSocket = null;
DataOutputStream dnsOutputStream = null;
DataInputStream dnsInputStream = null;
try {
dnsSocket = new SocksSocket(socksProxy, dnsServerAddress, DEFAULT_DNS_PORT);
dnsSocket.setKeepAlive(true);
dnsOutputStream = new DataOutputStream(dnsSocket.getOutputStream());
dnsInputStream = new DataInputStream(dnsSocket.getInputStream());
} catch (IOException e) {
e.printStackTrace();
continue;
}
if (!writeUdpPacketToStream(dnsOutputStream, udpPacket)) {
continue;
}

byte dnsResponse[] = readStreamPayload(dnsInputStream);
if (dnsResponse == null) {
closeSocket(dnsSocket);
continue;
}
Log.d(LOG_TAG, "Got DNS response " + dnsResponse.length);

if (!sendUdpPayload(dnsResponse, udpSocket,
udpPacket.getSocketAddress())) {
continue;
}
closeSocket(dnsSocket);
}
} finally {
if (udpSocket != null) {
udpSocket.close();
}
}
}

private boolean isValidDnsRequest(DatagramPacket packet) {
if (packet.getLength() < DNS_HEADER_SIZE) {
return false;
}
ByteBuffer buffer = ByteBuffer.wrap(packet.getData());
byte qrOpcodeAaTcRd = buffer.get(QR_OPCODE_AA_TC_OFFSET);
byte raZRcode = buffer.get(RA_Z_R_CODE_OFFSET);
short qdcount = buffer.getShort(QDCOUNT_OFFSET);
short ancount = buffer.getShort(ANCOUNT_OFFSET);
short nscount = buffer.getShort(NSCOUNT_OFFSET);

// verify DNS header is request
return (qrOpcodeAaTcRd & DNS_QR) == 0 /* query */
&& (raZRcode & DNS_Z) == 0 /* Z is Zero */
&& qdcount > 0 /* some questions */
&& nscount == 0
&& ancount == 0; /* no answers */
}

// Sends a UDP packet over TCP.
private boolean writeUdpPacketToStream(DataOutputStream outputStream,
DatagramPacket udpPacket) {
try {
outputStream.writeShort(udpPacket.getLength());
outputStream.write(udpPacket.getData());
outputStream.flush();
} catch (IOException e) {
Log.e(LOG_TAG, "Failed to send UDP packet: ", e);
return false;
}
return true;
}

// Reads the payload from a TCP stream.
private byte[] readStreamPayload(DataInputStream inputStream) {
final String errorMessage = "Failed to read TCP response: ";
byte buffer[] = null;
try {
short responseBytes = inputStream.readShort();
buffer = new byte[responseBytes];
inputStream.readFully(buffer);
} catch (SocketException e) {
Log.e(LOG_TAG, errorMessage, e);
} catch (IOException e) {
Log.e(LOG_TAG, errorMessage, e);
}
return buffer;
}

// Sends a UDP packet containing |payload| to |destAddress| via |udpSocket|.
private boolean sendUdpPayload(byte[] payload, DatagramSocket udpSocket,
SocketAddress destAddress) {
try {
DatagramPacket outPacket =
new DatagramPacket(payload, payload.length, destAddress);
udpSocket.send(outPacket);
return true;
} catch (IOException e) {
Log.d(LOG_TAG, "Failed to send UDP payload ", e);
}
return false;
}

// Utility method to close a socket.
private void closeSocket(Socket socket) {
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
Log.w("Failed to close socket ", e);
} finally {
socket = null;
}
}

private void broadcastUdpSocketAddress() {
Log.d(LOG_TAG, "Broadcasting address");
if (udpSocket == null) {
return;
}
String dnsResolverAddress = String.format("127.0.0.1:%d", udpSocket.getLocalPort());
Intent addressBroadcast = new Intent(DnsResolverService.DNS_ADDRESS_BROADCAST);
addressBroadcast.putExtra(DnsResolverService.DNS_ADDRESS_EXTRA, dnsResolverAddress);
LocalBroadcastManager.getInstance(m_parentService).sendBroadcast(addressBroadcast);
}
}
}
Loading

0 comments on commit 972ce0e

Please sign in to comment.