Android VpnService的防火墙

33

我正在尝试使用VpnService为BS项目实现Android的简单防火墙。我选择VpnService,因为它可以在非root设备上运行。它将记录连接并让您过滤连接(基于IP)。

有一个应用程序可以完成此操作,所以这是可能的。

Google play应用商店

我做了一些研究,发现VpnService创建了一个Tun接口。没有VPN实现,只有隧道。它允许您为此接口提供地址并添加路由。它返回一个文件描述符。您可以读取出站包并写入入站包。

我创建了一个派生自VpnService的类,并启动了服务。我可以使用VpnService.Builder类配置tun0。当我查看mobiwol的连接时,通过adb shell netcfg,它会创建一个带有10.2.3.4/32地址的tun0接口。它将所有数据包路由到此私有网络并发送到互联网。我也在尝试同样的事情。创建了一个带有10.0.0.2/32地址的接口。使用addRoute函数添加了一个路由。0.0.0.0/0,因此我可以捕获来自所有网络的所有数据包,就我所知。(我对这个主题还很陌生,仍在学习中。我从互联网上找到了一些片段,所以我不太确定。如果我错了,请纠正我。)

我在服务中创建了2个线程。一个从文件描述符中读取并使用受保护的套接字将其写入127.0.0.1。(我不确定是否应该读/写127.0.0.1。也许这是问题所在。)

我分析了从文件描述符中读取的数据包。例如:

01000101    byte:69     //ipv4 20byte header
00000000    byte:0      //TOS
00000000    byte:0      //Total Length
00111100    byte:60     //Total Length
11111100    byte:-4     //ID
11011011    byte:-37    //ID
01000000    byte:64     //fragment
00000000    byte:0      //"
01000000    byte:64     //TTL
00000110    byte:6      //Protocol 6 -> TCP
01011110    byte:94     //Header checksum
11001111    byte:-49    //Header checksum
00001010    byte:10     //10.0.0.2
00000000    byte:0
00000000    byte:0
00000010    byte:2
10101101    byte:-83    //173.194.39.78 //google
00111110    byte:-62
00100111    byte:39
********    byte:78

10110100    byte:-76    // IP option
01100101    byte:101
00000001    byte:1
10111011    byte:-69
                //20byte IP haeder
01101101    byte:109
.       .       //40byte data (i couldnt parse TCP header, 
                    I think its not needed when I route this in IP layer)
.       .
.       .
00000110    byte:6

我在数据剩余的部分中没有找到其他IP头。我认为应该有一个封装在10.0.0.2网络与本地网络(192.168.2.1)和互联网之间。但我不确定。

我的真正问题是我卡在了传入包线程上。我什么也读不到。没有响应。如您在截图中所见,没有传入数据:

截图

我正在尝试从相同的连接中读取数据,该连接用于写入到受保护的套接字127.0.0.1。

Android <-> Tun Interface (tun0) <-> Internet connection

所有的包 <-> 10.0.0.2 <-> 127.0.0.1? <-> 192.168.2.1 <-> Internet?

关于VpnService,我找不到任何有用的信息。(ToyVPN示例是无用的)我阅读了关于Linux Tun/Tap的文档,但它是关于主机和远程之间的隧道。我想要的是主机和远程在同一设备上。而不是像隧道一样。

我该怎么办?

编辑:代码请求。 这是一个VpnService派生类,处于非常早期的阶段。在服务线程中创建了2个线程(读取和写入)。

package com.git.firewall;

public class GITVpnService extends VpnService implements Handler.Callback, Runnable {
    private static final String TAG = "GITVpnService";

    private String mServerAddress = "127.0.0.1";
    private int mServerPort = 55555;
    private PendingIntent mConfigureIntent;

    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, "VpnThread");
        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, (String)message.obj, Toast.LENGTH_SHORT).show();
        }
        return true;
    }

    @Override
    public synchronized void run() {
        try {
            Log.i(TAG, "Starting");
            InetSocketAddress server = new InetSocketAddress(
                    mServerAddress, mServerPort);

            run(server);

              } catch (Exception e) {
            Log.e(TAG, "Got " + e.toString());
            try {
                mInterface.close();
            } catch (Exception e2) {
                // ignore
            }
            Message msgObj = mHandler.obtainMessage();
            msgObj.obj = "Disconnected";
            mHandler.sendMessage(msgObj);

        } finally {

        }
    }

    DatagramChannel mTunnel = null;


    private boolean run(InetSocketAddress server) throws Exception {
        boolean connected = false;

        android.os.Debug.waitForDebugger();

        // Create a DatagramChannel as the VPN tunnel.
        mTunnel = DatagramChannel.open();

        // Protect the tunnel before connecting to avoid loopback.
        if (!protect(mTunnel.socket())) {
            throw new IllegalStateException("Cannot protect the tunnel");
        }

        // Connect to the server.
        mTunnel.connect(server);

        // For simplicity, we use the same thread for both reading and
        // writing. Here we put the tunnel into non-blocking mode.
        mTunnel.configureBlocking(false);

        // Authenticate and configure the virtual network interface.
        handshake();

        // Now we are connected. Set the flag and show the message.
        connected = true;
        Message msgObj = mHandler.obtainMessage();
        msgObj.obj = "Connected";
        mHandler.sendMessage(msgObj);

        new Thread ()
        {
            public void run ()
                {
                    // Packets to be sent are queued in this input stream.
                    FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
                    // Allocate the buffer for a single packet.
                    ByteBuffer packet = ByteBuffer.allocate(32767);
                    int length;
                    try
                    {
                        while (true)
                        {
                            while ((length = in.read(packet.array())) > 0) {
                                    // Write the outgoing packet to the tunnel.
                                    packet.limit(length);
                                    debugPacket(packet);    // Packet size, Protocol, source, destination
                                    mTunnel.write(packet);
                                    packet.clear();

                                }
                            }
                    }
                    catch (IOException e)
                    {
                            e.printStackTrace();
                    }

            }
        }.start();

        new Thread ()
        {

            public void run ()
            {
                    DatagramChannel tunnel = mTunnel;
                    // Allocate the buffer for a single packet.
                    ByteBuffer packet = ByteBuffer.allocate(8096);
                    // Packets received need to be written to this output stream.
                    FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());

                    while (true)
                    {
                        try
                        {
                            // Read the incoming packet from the tunnel.
                            int length;
                            while ((length = tunnel.read(packet)) > 0)
                            {
                                    // Write the incoming packet to the output stream.
                                out.write(packet.array(), 0, length);

                                packet.clear();

                            }
                        }
                        catch (IOException ioe)
                        {
                                ioe.printStackTrace();
                        }
                    }
            }
        }.start();

        return connected;
    }

    private void handshake() throws Exception {

        if (mInterface == null)
        {
            Builder builder = new Builder();

            builder.setMtu(1500);
            builder.addAddress("10.0.0.2",32);
            builder.addRoute("0.0.0.0", 0);
            //builder.addRoute("192.168.2.0",24);
            //builder.addDnsServer("8.8.8.8");

            // Close the old interface since the parameters have been changed.
            try {
                mInterface.close();
            } catch (Exception e) {
                // ignore
            }


            // Create a new interface using the builder and save the parameters.
            mInterface = builder.setSession("GIT VPN")
                    .setConfigureIntent(mConfigureIntent)
                    .establish();
        }
    }

    private void debugPacket(ByteBuffer packet)
    {
        /*
        for(int i = 0; i < length; ++i)
        {
            byte buffer = packet.get();

            Log.d(TAG, "byte:"+buffer);
        }*/



        int buffer = packet.get();
        int version;
        int headerlength;
        version = buffer >> 4;
        headerlength = buffer & 0x0F;
        headerlength *= 4;
        Log.d(TAG, "IP Version:"+version);
        Log.d(TAG, "Header Length:"+headerlength);

        String status = "";
        status += "Header Length:"+headerlength;

        buffer = packet.get();      //DSCP + EN
        buffer = packet.getChar();  //Total Length

        Log.d(TAG, "Total Length:"+buffer);

        buffer = packet.getChar();  //Identification
        buffer = packet.getChar();  //Flags + Fragment Offset
        buffer = packet.get();      //Time to Live
        buffer = packet.get();      //Protocol

        Log.d(TAG, "Protocol:"+buffer);

        status += "  Protocol:"+buffer;

        buffer = packet.getChar();  //Header checksum

        String sourceIP  = "";
        buffer = packet.get();  //Source IP 1st Octet
        sourceIP += buffer;
        sourceIP += ".";

        buffer = packet.get();  //Source IP 2nd Octet
        sourceIP += buffer;
        sourceIP += ".";

        buffer = packet.get();  //Source IP 3rd Octet
        sourceIP += buffer;
        sourceIP += ".";

        buffer = packet.get();  //Source IP 4th Octet
        sourceIP += buffer;

        Log.d(TAG, "Source IP:"+sourceIP);

        status += "   Source IP:"+sourceIP;

        String destIP  = "";
        buffer = packet.get();  //Destination IP 1st Octet
        destIP += buffer;
        destIP += ".";

        buffer = packet.get();  //Destination IP 2nd Octet
        destIP += buffer;
        destIP += ".";

        buffer = packet.get();  //Destination IP 3rd Octet
        destIP += buffer;
        destIP += ".";

        buffer = packet.get();  //Destination IP 4th Octet
        destIP += buffer;

        Log.d(TAG, "Destination IP:"+destIP);

        status += "   Destination IP:"+destIP;
        /*
        msgObj = mHandler.obtainMessage();
        msgObj.obj = status;
        mHandler.sendMessage(msgObj);
        */

        //Log.d(TAG, "version:"+packet.getInt());
        //Log.d(TAG, "version:"+packet.getInt());
        //Log.d(TAG, "version:"+packet.getInt());

    }

}

欢迎来到Stackoverflow。好的,问题描述得很详细。请展示一下你目前为止已经尝试的代码内容。 - LuigiEdlCarno
我发布了代码。我尝试了不同的服务器地址、路由、tun 接口地址。我尝试在单线程中进行读写操作,但并没有什么改变。我认为我需要更多的网络知识(如何在 internet-tun/tap 之间进行路由)。 - fatihdurmus
Android没有TAP接口。我想要将wlan0与tap桥接,但这是不可能的。 - fatihdurmus
你想暂时禁用应用程序或永久拒绝其访问互联网吗? - Farshid Shekari
如果您已经解决了问题,请向我们展示您的解决方案。 - ospider
显示剩余2条评论
2个回答

18

几个月前有一个类似的问题被提出,虽然那里的回答不是很有见地,但被接受的答案中的评论提供了一些关于可能出错的信息。

你应该记住你的逻辑属于 OSI 模型 中的哪一层:

  • VpnService 的输入和输出流处于网络层; 你正在接收(并且应当转发)原始 IP 数据包,正如你在问题中所描述的那样。

    在你的示例字节流中,你可以看到传入字节流是一个 IPv4 数据报,因为前四位是 0100 (4)。参考这个数据包结构规范了解 IPv4 的详细信息。

  • 在转发请求时,你处于应用程序层; 你应该使用 DatagramSocket 或 Socket 分别仅传输 UDP or TCP 负载的内容(即仅传输数据,而不是头本身)。

    要注意这会跳过传输层,因为这些实现会负责构建 UDP 头(对于 DatagramSocket)和 TCP 头和选项(对于 Socket)。

你的应用程序需要能够解释和构建 IPv4 和 IPv6 头和选项,以及 IP 负载、UDP 头和 TCP 头和选项。


我们将项目改为Linux防火墙(基于netfilter钩子和conntrack)。但我可以说这是正确的答案。目前我无法尝试,但我会在学期假期尝试作为业余项目完成。感谢您的帮助。 - fatihdurmus
@Paul - 你的意思是说我们需要剥离接收到的数据包头部(从Android VpnService中提取数据包的数据,该服务在执行vpnservice.builder.establish时获取了隧道文件描述符),添加一个新的标头,其中包含修改后的发件地址(收件地址将是我们收到的包的地址),然后发送出去。在收到回复后,我们需要维护一个映射器,并粘贴回旧的头部(其中的发件人和收件人互换),然后写回隧道套接字(我们从VpnService.Builder.establish()收到的套接字)。 - Suman
当您转发请求并收到回复时,如何知道要将其发送回哪个客户端?您可以使用转发的数据包中的16位标识符标志吗?在接收到的数据包中,这个标识符是否相同? - Simon Langhoff
当在Android应用程序中启动vpnService时,我想禁止应用程序访问互联网,并且当禁用此应用程序时,其他应用程序是否使用vpn进行连接? - Farshid Shekari
@Farshid 如果你的VPN接口没有转发请求(并排除自己的数据包从接口中回路返回),那么其他应用程序的所有请求都会被忽略。这些应用程序将仅在其请求超时时失效。 - Paul Lammertsma
显示剩余6条评论

0
也许最好去寻找像OpenVpn这样的开源项目。它在API级别14+(冰淇淋三明治)上运行,无需Root访问权限。

1
我看了一下他们的代码,似乎是关于VPN客户端-服务器数据包发送的。我猜互联网路由是在服务器端进行的。 - fatihdurmus
正如您所知,我们在伊朗使用此Android程序绕过过滤。这不仅涉及数据包的传输。 - Ali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接