heyjohnnyfunt heyjohnnyfunt - 5 months ago 119
Android Question

Android local VPN Service: can't get response

StackOverflowers.

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, nevermind).

Firsly, I should write the question, as its body is too long.




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:
The principle of my VPN app
All of my code is written on Kotlin language, but it's not complicated and is very clear for JAVA lovers & developrs. 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 CONFIGURATIONS & CODES



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:


  1. the settings of the VPN configurations are quite similar to this example and both examples from SO questions, mentioned above, for local usage.

  2. 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.

PREVIOUS QUESTION (solved by @miensol): 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.

NEW QUESTION: I see, that packets are sending away, but I can not 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) 

Answer

The error logged in logcat:

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)

indicates the problem.

The documentation of onStartCommand states that (emphasis mine):

Intent: The Intent supplied to startService(Intent), as given. This may be null if the service is being restarted after its process has gone away, and it had previously returned anything except START_STICKY_COMPATIBILITY.

Hence you should handle the null case accordingly at the very least by changing the signature of onStartCommand in Kotlin to:

override fun onStartCommand(intent:Intent?, flags:Int, startId:Int) {
Comments