Servicio VPN local de Android: no se puede get respuesta

Soy bastante nuevo en Android y sus services. Estoy intentando implementar un service de VPN local en mi aplicación (con Kotlin y Java).

PREGUNTA

Mi service VPN tomado de ToyVpn Google ejemplo, combinado con ejemplos de 1 , 2 , 3 para usarlo localmente (sin connection al server remoto) NO está funcionando.


MI APLICACIÓN PRINCIPE

Vi esto y estas preguntas SO, pero las respuestas no son muy perspicaces y no puedo encontrar la solución para mi problema.

Así que la aplicación es bastante simple : debe reenviar todos los packages cuando el usuario click el button "SÍ" en la actividad principal, y cuando click "NO", bloquéelo. El propósito: usarlo como un firewall , así:

El principio de mi aplicación VPN

Todo mi código está escrito en lenguaje Kotlin, pero no es complicado y está muy claro para los desarrolladores de JAVA. Así que espero que el código anterior sea bastante claro, ya que está tomado de aquí (ejemplo de ToyVpn provisto por Google) y recién convertido a kotlin.


MI CONFIGURACIÓN Y CÓDIGO

Para activar el service VPN en mi aplicación, coloqué en mi AndroidManifest.xml en la label <application> esta configuration:

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

Mi código de MainActivity contiene:

 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); } } 

Mi class VpnFilter es bastante similar a la class de service ToyVpn , pero tiene que funcionar localmente sin ninguna authentication, apretón de manos, etc., así que he editado el ejemplo con dicha configuration:

  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(); } 

Y en mi function de ejecución acabo de configurar el túnel para conectarme a la dirección IP local:

 tunnel.connect(InetSocketAddress("127.0.0.1", 8087)) 

De este modo:

  1. la configuration de las configuraciones VPN es bastante similar a este ejemplo y a ambos ejemplos de las preguntas SO, mencionadas anteriormente, para uso local.
  2. y mi reenvío de packages está tomado del ejemplo de ToyVpn .

Sé que mi VPN se está ejecutando, porque si cambio la configuration de addRoute , no podré acceder a Internet.

¡Entonces no sé lo que estoy haciendo mal! Si utilizo el código para el reenvío de packages desde ToyVpn, la aplicación se cuelga cada vez que llega un nuevo package .

Actualizar

Lo anterior está resuelto, pero veo que los packages se están enviando lejos, pero no puedo get ninguna respuesta. No puedo entender por qué.


CÓDIGO DE JAVA COMPLETO DE MI SERVICIO VPN

 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

En mi panel de LogCat , tengo este rastro cuando la aplicación falla:

  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) 

El error registrado en 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) 

indica el problema

La documentation de onStartCommand establece que (énfasis mío):

Propósito: El Intento proporcionado a startService (Intención), como se dio. Esto puede ser nulo si el service se reinicia después de que su process haya desaparecido, y anteriormente había devuelto algo excepto START_STICKY_COMPATIBILIDAD.

Por lo tanto, debe manejar el caso null como mínimo cambiando la firma de onStartCommand en Kotlin a:

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