1
votes

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:

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 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:

  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.

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) 
1
Learn how to create a minimal reproducible example.nhaarman
@nhaarman thank's for the tip!heyjohnnyfunt

1 Answers

4
votes

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) {