I'm quite new to Android and its services. I'm trying to implement a local VPN service in my app (with Kotlin and Java).
QUESTION
My VPN service taken from ToyVpn Google example, combined with examples from 1, 2, 3 to use it locally (without connection to remote server) is NOT working.
MY APP PRINCIPE
I saw this and this SO questions, but the answers there aren't very insightful and I can't find the solution for my issue.
So the app is pretty simple: it should forward all of the packets when user click "YES"-button on the main activity, and when click "NO" - block it. The purpose: to use it as a firewall, like that:
All of my code is written on Kotlin language, but it's not complicated and is very clear for JAVA developers. So I hope the code above is pretty clear as it is taken from here (ToyVpn example provided by Google) and just converted to kotlin.
MY CONFIGURATION & CODE
To enable VPN service in my app I placed in my AndroidManifest.xml into <application>
tag this setting:
<service android:name="com.example.username.wifictrl.model.VpnFilter"
android:permission="android.permission.BIND_VPN_SERVICE" >
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
My MainActivity code contains:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
... // omitted for the sake of brevity
val intent = VpnService.prepare(this);
if (intent != null) {
startActivityForResult(intent, 0);
} else {
onActivityResult(0, RESULT_OK, null);
}
... // omitted for the sake of brevity
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
val intent = Intent(this, VpnFilter::class.java);
startService(intent);
}
}
My VpnFilter class is quite similar to ToyVpn service class, but has to work locally without any authentication, handshake etc, so I've edited example with such settings:
private void configure() throws Exception {
// If the old interface has exactly the same parameters, use it!
if (mInterface != null) {
Log.i(TAG, "Using the previous interface");
return;
}
// Configure a builder while parsing the parameters.
Builder builder = new Builder();
builder.setSession(TAG)
builder.addAddress("10.0.0.2", 32).addRoute("0.0.0.0", 0)
try {
mInterface.close();
} catch (Exception e) {}
mInterface = builder.establish();
}
And in my run function I've just configured tunnel to connect to the local IP address:
tunnel.connect(InetSocketAddress("127.0.0.1", 8087))
Thereby:
- the settings of the VPN configurations are quite similar to this example and both examples from SO questions, mentioned above, for local usage.
- and my packet forwarding is taken from ToyVpn example.
I know that my VPN is running, because if I change addRoute configuration, I won't be able to access Internet.
So I don't know what I'm actually doing wrong! If I use code for packet forwarding from ToyVpn, app is crashing every time new packet comes.
Update
The above is solved, but I see, that packets are sending away, but I cannot get any response. I can't figure out why.
FULL JAVA CODE OF MY VPN SERVICE
public class VpnFilter extends VpnService implements Handler.Callback, Runnable {
private static final String TAG = "MyVpnService";
private Handler mHandler;
private Thread mThread;
private ParcelFileDescriptor mInterface;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// The handler is only used to show messages.
if (mHandler == null) {
mHandler = new Handler(this);
}
// Stop the previous session by interrupting the thread.
if (mThread != null) {
mThread.interrupt();
}
// Start a new session by creating a new thread.
mThread = new Thread(this, "ToyVpnThread");
mThread.start();
return START_STICKY;
}
@Override
public void onDestroy() {
if (mThread != null) {
mThread.interrupt();
}
}
@Override
public boolean handleMessage(Message message) {
if (message != null) {
Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
}
return true;
}
@Override
public synchronized void run() {
Log.i(TAG,"running vpnService");
try {
runVpnConnection();
} catch (Exception e) {
e.printStackTrace();
//Log.e(TAG, "Got " + e.toString());
} finally {
try {
mInterface.close();
} catch (Exception e) {
// ignore
}
mInterface = null;
mHandler.sendEmptyMessage(R.string.disconnected);
Log.i(TAG, "Exiting");
}
}
private void configure() throws Exception {
// If the old interface has exactly the same parameters, use it!
if (mInterface != null) {
Log.i(TAG, "Using the previous interface");
return;
}
// Configure a builder while parsing the parameters.
Builder builder = new Builder();
builder.setSession(TAG)
builder.addAddress("10.0.0.2", 32).addRoute("0.0.0.0", 0)
try {
mInterface.close();
} catch (Exception e) {
// ignore
}
mInterface = builder.establish();
}
private boolean runVpnConnection() throws Exception {
configure()
val in = new FileInputStream(mInterface.fileDescriptor)
// Packets received need to be written to this output stream.
val out = new FileOutputStream(mInterface.fileDescriptor)
// The UDP channel can be used to pass/get ip package to/from server
val tunnel = DatagramChannel.open()
// For simplicity, we use the same thread for both reading and
// writing. Here we put the tunnel into non-blocking mode.
tunnel.configureBlocking(false)
// Allocate the buffer for a single packet.
val packet = ByteBuffer.allocate(32767)
// Connect to the server, localhost is used for demonstration only.
tunnel.connect(InetSocketAddress("127.0.0.1", 8087))
// Protect this socket, so package send by it will not be feedback to the vpn service.
protect(tunnel.socket())
// We use a timer to determine the status of the tunnel. It
// works on both sides. A positive value means sending, and
// any other means receiving. We start with receiving.
int timer = 0
// We keep forwarding packets till something goes wrong.
while (true) {
// Assume that we did not make any progress in this iteration.
boolean idle = true
// Read the outgoing packet from the input stream.
int length = `in`.read(packet.array())
if (length > 0) {
Log.i(TAG, "************new packet")
// Write the outgoing packet to the tunnel.
packet.limit(length)
tunnel.write(packet);
packet.clear()
// There might be more outgoing packets.
idle = false
// If we were receiving, switch to sending.
if (timer < 1) {
timer = 1
}
}
length = tunnel.read(packet)
if (length > 0) {
// Ignore control messages, which start with zero.
if (packet.get(0).toInt() !== 0) {
// Write the incoming packet to the output stream.
out.write(packet.array(), 0, length)
}
packet.clear()
// There might be more incoming packets.
idle = false
// If we were sending, switch to receiving.
if (timer > 0) {
timer = 0
}
}
// If we are idle or waiting for the network, sleep for a
// fraction of time to avoid busy looping.
if (idle) {
Thread.sleep(100)
// Increase the timer. This is inaccurate but good enough,
// since everything is operated in non-blocking mode.
timer += if (timer > 0) 100 else -100
// We are receiving for a long time but not sending.
if (timer < -15000) {
// Send empty control messages.
packet.put(0.toByte()).limit(1)
for (i in 0..2) {
packet.position(0)
tunnel.write(packet)
}
packet.clear()
// Switch to sending.
timer = 1
}
// We are sending for a long time but not receiving.
if (timer > 20000) {
throw IllegalStateException("Timed out")
}
}
Thread.sleep(50)
}
}
}
LOG CAT OUTPUT
In my LogCat panel I've got this trace when app crashes:
FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start service com.example.username.wifictrl.model.VpnFilter@41ebbfb8 with null: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter intent
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2950)
at android.app.ActivityThread.access$1900(ActivityThread.java:151)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1442)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:155)
at android.app.ActivityThread.main(ActivityThread.java:5520)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1029)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:796)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter intent
at com.example.skogs.wifictrl.model.VpnFilter.onStartCommand(VpnFilter.kt)
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2916)
at android.app.ActivityThread.access$1900(ActivityThread.java:151)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1442)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:155)
at android.app.ActivityThread.main(ActivityThread.java:5520)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1029) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:796)
at dalvik.system.NativeStart.main(Native Method)