我对 Android 及其服务还很陌生。我正在尝试在我的应用程序中实现本地VPN 服务(使用 Kotlin 和 Java)。
问题
我的 VPN 服务取自ToyVpn Google 示例,并结合了1、2、3中的示例中的示例在本地使用(不连接到远程服务器)不起作用。
我的应用原则
我看到了这个和这个SO 问题,但那里的答案不是很有见地,我找不到我的问题的解决方案。
所以应用程序非常简单:当用户单击主活动上的“是”按钮时,它应该转发所有数据包,当单击“否”时 - 阻止它。目的:将其用作防火墙,如下所示:
我所有的代码都是用 Kotlin 语言编写的,但并不复杂,对于 JAVA 开发人员来说非常清楚。所以我希望上面的代码很清楚,因为它取自这里(谷歌提供的 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 类与ToyVpn服务类非常相似,但必须在没有任何身份验证、握手等的情况下在本地工作,因此我使用此类设置编辑了示例:
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))
从而:
我知道我的 VPN 正在运行,因为如果我更改addRoute配置,我将无法访问 Internet。
所以我不知道我实际上做错了什么!如果我使用来自 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)
}
}
}
日志猫输出
在我的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.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)