Android 本地 VPN 服务:无法得到响应

Android local VPN Service: can't get response

我对 Android 及其服务还很陌生。我正在尝试在我的应用程序中实现 local VPN 服务(使用 Kotlin 和 Java)。

问题

我从 ToyVpn Google example, combined with examples from 1, 2, 3 获取的 VPN 服务无法在本地使用(没有连接到远程服务器)。


我的应用程序原则

我看到 this and this SO 问题,但那里的答案不是很有见地,我找不到解决我的问题的方法。

所以这个应用程序非常简单:当用户点击主activity上的"YES"按钮时,它应该转发所有的数据包,当单击 "NO" - 阻止它。目的:将它用作防火墙,像那样:

我所有的代码都是用 Kotlin 语言编写的,但它并不复杂,对于 JAVA 开发人员来说非常清晰。所以我希望上面的代码非常清楚,因为它取自 here(Google 提供的 ToyVpn 示例)并且只是转换为 kotlin。


我的配置和代码

为了在我的应用程序中启用 VPN 服务,我将 AndroidManifest.xml 放入 <application> 标记此设置:

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

我的 MainActivity 代码包含:

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

我的 VpnFilter classToyVpn 服务 class 非常相似,但必须在本地工作而无需任何身份验证、握手等,所以我用这样的设置编辑了示例:

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

并且在我的 运行 函数中,我刚刚配置隧道以连接到本地 IP 地址:

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

因此:

  1. VPN 配置的 设置 this 示例和上述 SO 问题中的两个示例非常相似,供本地使用。
  2. 我的数据包转发取自ToyVpn示例。

我知道我的 VPN 是 运行ning,因为如果我更改 addRoute 配置,我将无法访问互联网。

所以我不知道我到底做错了什么!如果我使用来自 ToyVpn 的数据包转发代码,每次 新数据包到来 时,应用程序都会 崩溃

更新

上面已经解决了,但是我看到,数据包正在发送,但是我没有得到任何响应。我不知道为什么。


我的 VPN 服务的完整 JAVA 代码

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

记录 CAT 输出

在我的 LogCat 面板中,当应用程序崩溃时我得到了这个跟踪:

   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.access00(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.access00(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) 

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)

表示问题。

onStartCommand 的文档指出(强调我的):

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.

因此,您至少应该通过将 Kotlin 中 onStartCommand 的签名更改为:

来相应地处理 null 案例
override fun onStartCommand(intent:Intent?, flags:Int, startId:Int) {