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

hey*_*unt 2 java vpn android kotlin kotlin-android-extensions

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

\n\n

问题

\n\n

我的 VPN 服务取自ToyVpn Google 示例,并结合 1、2、3 中的示例在本地使用无需连接到远程服务器),但无法正常工作。

\n\n
\n\n

我的应用程序原理

\n\n

我看到了这个这个问题,但是那里的答案不是很有见地,我找不到我的问题的解决方案。

\n\n

所以该应用程序非常简单:当用户单击主活动上的“是”按钮时,它应该转发所有数据包,当单击“否”时,它应该阻止它。目的:将其用作防火墙,如下所示:

\n\n

我的VPN应用程序的原理

\n\n

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

\n\n
\n\n

我的配置和代码

\n\n

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

\n\n
<service android:name="com.example.username.wifictrl.model.VpnFilter"\n         android:permission="android.permission.BIND_VPN_SERVICE" >\n    <intent-filter>\n        <action android:name="android.net.VpnService" />\n    </intent-filter>\n</service>\n
Run Code Online (Sandbox Code Playgroud)\n\n

我的MainActivity代码包含:

\n\n
override fun onCreate(savedInstanceState: Bundle?) {\n\n        super.onCreate(savedInstanceState)\n\n        ... // omitted for the sake of brevity\n\n        val intent = VpnService.prepare(this);\n        if (intent != null) {\n            startActivityForResult(intent, 0);\n        } else {\n            onActivityResult(0, RESULT_OK, null);\n        }\n\n        ... // omitted for the sake of brevity\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        if (resultCode == RESULT_OK) {\n            val intent = Intent(this, VpnFilter::class.java);\n            startService(intent);\n        }\n    }\n
Run Code Online (Sandbox Code Playgroud)\n\n

我的VpnFilter 类与ToyVpn服务类非常相似,但必须在本地工作,无需任何身份验证、握手等,因此我使用以下设置编辑了示例:

\n\n
 private void configure() throws Exception {\n    // If the old interface has exactly the same parameters, use it!\n    if (mInterface != null) {\n        Log.i(TAG, "Using the previous interface");\n        return;\n    }\n\n    // Configure a builder while parsing the parameters.\n    Builder builder = new Builder();\n    builder.setSession(TAG)\n    builder.addAddress("10.0.0.2", 32).addRoute("0.0.0.0", 0)\n    try {\n        mInterface.close();\n    } catch (Exception e) {}\n\n    mInterface = builder.establish();\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
tunnel.connect(InetSocketAddress("127.0.0.1", 8087))\n
Run Code Online (Sandbox Code Playgroud)\n\n

从而:

\n\n
    \n
  1. VPN配置的设置示例非常相似,并且上述两个示例都来自 SO 问题,供本地使用。
  2. \n
  3. 我的数据包转发取自ToyVpn示例。
  4. \n
\n\n

我知道我的 VPN 正在运行,因为如果我更改addRoute配置,我将无法访问 Internet。

\n\n

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

\n\n

更新

\n\n

上面的问题已解决,但我看到数据包正在发送,但我无法得到任何响应。我不明白为什么。

\n\n
\n\n

我的 VPN 服务的完整 JAVA 代码

\n\n
public class VpnFilter extends VpnService implements Handler.Callback, Runnable {\n    private static final String TAG = "MyVpnService";\n\n    private Handler mHandler;\n    private Thread mThread;\n\n    private ParcelFileDescriptor mInterface;\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        // The handler is only used to show messages.\n        if (mHandler == null) {\n            mHandler = new Handler(this);\n        }\n\n        // Stop the previous session by interrupting the thread.\n        if (mThread != null) {\n            mThread.interrupt();\n        }\n\n        // Start a new session by creating a new thread.\n        mThread = new Thread(this, "ToyVpnThread");\n        mThread.start();\n        return START_STICKY;\n    }\n\n    @Override\n    public void onDestroy() {\n        if (mThread != null) {\n            mThread.interrupt();\n        }\n    }\n\n    @Override\n    public boolean handleMessage(Message message) {\n        if (message != null) {\n            Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();\n        }\n        return true;\n    }\n\n    @Override\n    public synchronized void run() {\n        Log.i(TAG,"running vpnService");\n        try {\n            runVpnConnection();\n        } catch (Exception e) {\n            e.printStackTrace();\n            //Log.e(TAG, "Got " + e.toString());\n        } finally {\n            try {\n                mInterface.close();\n            } catch (Exception e) {\n                // ignore\n            }\n            mInterface = null;\n\n            mHandler.sendEmptyMessage(R.string.disconnected);\n            Log.i(TAG, "Exiting");\n        }\n    }\n\n    private void configure() throws Exception {\n        // If the old interface has exactly the same parameters, use it!\n        if (mInterface != null) {\n            Log.i(TAG, "Using the previous interface");\n            return;\n        }\n\n        // Configure a builder while parsing the parameters.\n        Builder builder = new Builder();\n        builder.setSession(TAG)\n        builder.addAddress("10.0.0.2", 32).addRoute("0.0.0.0", 0)\n        try {\n            mInterface.close();\n        } catch (Exception e) {\n            // ignore\n        }\n\n        mInterface = builder.establish();\n    }\n\n    private boolean runVpnConnection() throws Exception {\n\n        configure()\n\n        val in = new FileInputStream(mInterface.fileDescriptor)\n\n        // Packets received need to be written to this output stream.\n        val out = new FileOutputStream(mInterface.fileDescriptor)\n\n        // The UDP channel can be used to pass/get ip package to/from server\n        val tunnel = DatagramChannel.open()\n\n        // For simplicity, we use the same thread for both reading and\n        // writing. Here we put the tunnel into non-blocking mode.\n        tunnel.configureBlocking(false)\n\n        // Allocate the buffer for a single packet.\n        val packet = ByteBuffer.allocate(32767)\n\n        // Connect to the server, localhost is used for demonstration only.\n        tunnel.connect(InetSocketAddress("127.0.0.1", 8087))\n\n        // Protect this socket, so package send by it will not be feedback to the vpn service.\n        protect(tunnel.socket())\n\n        // We use a timer to determine the status of the tunnel. It\n        // works on both sides. A positive value means sending, and\n        // any other means receiving. We start with receiving.\n        int timer = 0\n\n        // We keep forwarding packets till something goes wrong.\n        while (true) {\n            // Assume that we did not make any progress in this iteration.\n            boolean idle = true\n\n            // Read the outgoing packet from the input stream.\n            int length = `in`.read(packet.array())\n\n            if (length > 0) {\n\n                Log.i(TAG, "************new packet")\n\n                // Write the outgoing packet to the tunnel.\n                packet.limit(length)\n                tunnel.write(packet);\n                packet.clear()\n                // There might be more outgoing packets.\n                idle = false\n                // If we were receiving, switch to sending.\n                if (timer < 1) {\n                    timer = 1\n                }\n\n            }\n\n            length = tunnel.read(packet)\n\n            if (length > 0) {\n                // Ignore control messages, which start with zero.\n                if (packet.get(0).toInt() !== 0) {\n                    // Write the incoming packet to the output stream.\n                    out.write(packet.array(), 0, length)\n                }\n                packet.clear()\n                // There might be more incoming packets.\n                idle = false\n                // If we were sending, switch to receiving.\n                if (timer > 0) {\n                    timer = 0\n                }\n            }\n            // If we are idle or waiting for the network, sleep for a\n            // fraction of time to avoid busy looping.\n            if (idle) {\n                Thread.sleep(100)\n                // Increase the timer. This is inaccurate but good enough,\n                // since everything is operated in non-blocking mode.\n                timer += if (timer > 0) 100 else -100\n                // We are receiving for a long time but not sending.\n                if (timer < -15000) {\n                    // Send empty control messages.\n                    packet.put(0.toByte()).limit(1)\n                    for (i in 0..2) {\n                        packet.position(0)\n                        tunnel.write(packet)\n                    }\n                    packet.clear()\n                    // Switch to sending.\n                    timer = 1\n                }\n                // We are sending for a long time but not receiving.\n                if (timer > 20000) {\n                    throw IllegalStateException("Timed out")\n                }\n            }\n            Thread.sleep(50)\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

记录猫输出

\n\n

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

\n\n
   FATAL EXCEPTION: main\n    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\n          at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2950)\n          at android.app.ActivityThread.access$1900(ActivityThread.java:151)\n          at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1442)\n          at android.os.Handler.dispatchMessage(Handler.java:99)\n          at android.os.Looper.loop(Looper.java:155)\n          at android.app.ActivityThread.main(ActivityThread.java:5520)\n          at java.lang.reflect.Method.invokeNative(Native Method)\n          at java.lang.reflect.Method.invoke(Method.java:511)                                                                                   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1029)\n          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:796)\n          at dalvik.system.NativeStart.main(Native Method)\n    Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter intent\n              at com.example.skogs.wifictrl.model.VpnFilter.onStartCommand(VpnFilter.kt)\n              at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2916)\n              at android.app.ActivityThread.access$1900(ActivityThread.java:151)\xc2\xa0\n              at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1442)\xc2\xa0\n              at android.os.Handler.dispatchMessage(Handler.java:99)\xc2\xa0\n              at android.os.Looper.loop(Looper.java:155)\xc2\xa0\n              at android.app.ActivityThread.main(ActivityThread.java:5520)\xc2\xa0\n              at java.lang.reflect.Method.invokeNative(Native Method)\xc2\xa0\n              at java.lang.reflect.Method.invoke(Method.java:511)\xc2\xa0\n              at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1029)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:796)\xc2\xa0\n              at dalvik.system.NativeStart.main(Native Method)\xc2\xa0\n
Run Code Online (Sandbox Code Playgroud)\n

mie*_*sol 5

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)
Run Code Online (Sandbox Code Playgroud)

表明问题所在。

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

Intent:提供给 startService(Intent) 的 Intent,如给定。如果服务在其进程消失后重新启动,并且之前已返回除 START_STICKY_COMPATIBILITY 之外的任何内容,则该值可能为 null 。

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

override fun onStartCommand(intent:Intent?, flags:Int, startId:Int) {
Run Code Online (Sandbox Code Playgroud)

  • @heyjohnnyfunt 更新后的问题似乎是一个具有不同根本原因的问题 - 我回答的问题与语言使用有关 - 而您现在遇到的问题似乎与 Android API 使用和网络配置有关。我想用网络节点的图像(带有地址)和您想要实现的目标的描述创建一个单独的问题是有意义的。 (3认同)