【问题标题】:Protect a socket in VpnService保护 VpnService 中的套接字
【发布时间】:2026-01-21 06:15:01
【问题描述】:

我正在探索 Android 的 VpnService 的功能。目前,我通过在用户空间中重建 IP 堆栈构建了一个非常基本的请求转发器:我从 VpnService 的输入流中读取 IP 数据包,解析它们,对于我不想转发的连接,我尝试重新创建那些套接字连接 VPN 连接之外。

我了解到这最后一点是由VpnService.protect() 促成的,并尝试按如下方式实现它:

Socket socket = new Socket();
vpnService.protect(socket);
socket.connect(new InetSocketAddress(
        header.getDestinationAddress(),  // From my IP datagram header
        body.getDestinationPort()));     // From the TCP datagram header

不幸的是,这种方法会导致回环到 VPN 接口。

虽然上面的代码会简单地阻塞并最终超时,但我通过从单独的线程调用Socket.connect(InetSocketAddress) 来观察环回;连接直接返回到我的 VpnService 的输入流,然后重复该过程。

不用说,这会导致循环。我觉得这是因为在创建套接字时(以及随后调用VpnService.protect(Socket)),我还没有设置目标IP和端口。

情况似乎确实如此,因为在我的 VpnService 实现中覆盖 VpnService.protect(Socket)VpnService.protect(int) 并在这两种情况下调用 supers 都会返回 false。

如何正确保护套接字连接?

【问题讨论】:

    标签: android sockets android-4.0-ice-cream-sandwich vpn packet-capture


    【解决方案1】:

    以下代码有效。

    Socket socket = SocketChannel.open().socket();
    if ((null != socket) && (null != vpnService)) {
        vpnService.protect(socket);
    }
    socket.connect(...);
    

    new Socket() 没有有效的文件描述符,因此无法保护。

    【讨论】:

    • 建立vpn接口后也要调用protect方法。
    • 此方法创建一个套接字,当在 Android 8.0 或更高版本上写入该应用程序时会导致应用崩溃。不过,Mai Quoc Hui 下面的回答很可靠。改用那个!
    【解决方案2】:

    我发现另一种解决方案是用 C/C++ 写出来。

    Java:

    public native int createSocket();
    
    public native int connectSocket(int fd);
    

    C++:

    // For sockets
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    // For error codes
    #include <errno.h>
    
    extern "C" {
    
    JNIEXPORT jint JNICALL
    Java_com_pixplicity_example_jni_VpnInterface_createSocket(
            JNIEnv * env, jobject thiz) {
        // Create the socket
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        int err = errno;
        // Return the file descriptor
        return sockfd;
    }
    
    JNIEXPORT jint JNICALL
    Java_com_pixplicity_example_jni_VpnInterface_connectSocket(
            JNIEnv * env, jobject thiz, jint sockFd) {
        // Host & port are hard-coded here
        char* host = "74.125.136.113"; // google.com
        int port = 80;
        struct sockaddr_in peerAddr;
        int ret;
        peerAddr.sin_family = AF_INET;
        peerAddr.sin_port = htons(port);
        peerAddr.sin_addr.s_addr = inet_addr(host);
        // Connect to host
        ret = connect((int) sockFd, (struct sockaddr *) &peerAddr,
                sizeof(peerAddr));
        if (ret != 0) {
            perror("connect failed");
            close(sockFd);
        }
        // Return the error code
        return ret;
    }
    
    }
    

    【讨论】:

      【解决方案3】:

      您需要在保护之前绑定您的套接字。这对我有用:

      Socket socket = new Socket();
      //bind to any address
      socket.bind(new InetSocketAddress(0));
      
      vpnService.protect(socket);
      
      socket.connect(...);
      

      【讨论】:

        【解决方案4】:

        您可以通过将您的应用程序添加到vpnService.builder.addDisallowedApplication("your package name") 来排除您的应用程序套接字(以及流经它的流量)使用 VPN

        我尝试了这个并在 vpn 隧道接口和我的传出 Internet 接口上运行 tcpdump 对其进行了测试。来自我的应用程序的数据包不会在 vpn 接口中循环,而是通过手机的前向 Internet 接口发送。

        【讨论】:

          【解决方案5】:

          我需要保护OkHttpClient 中的套接字。它会创建无法保护的未连接套接字(意味着service.protect() 返回false)并且当它们连接时显然为时已晚(例如,当我试图在networkInterceptor 中保护它们时)。经典晦涩的 Android 行为。

          无论如何,Mai Quoc Huy 的回答对我来说是正确的,整个代码看起来像这样。

              val protectedHttpClient = OkHttpClient.Builder()
                      .socketFactory(object : SocketFactory() {
          
                          override fun createSocket(): Socket = Socket().apply {
                              bind(InetSocketAddress(0))
                              val result = service?.protect(this)
                          }
          
                          override fun createSocket(host: String?, port: Int) = unsupported()
                          override fun createSocket(host: String?, port: Int, localHost: InetAddress?, localPort: Int) = unsupported()
                          override fun createSocket(host: InetAddress?, port: Int) = unsupported()
                          override fun createSocket(address: InetAddress?, port: Int, localAddress: InetAddress?, localPort: Int) = unsupported()
          
                          private fun unsupported(): Nothing = throw UnsupportedOperationException("This factory can only create unconnected sockets for OkHttp")
          
                      })
                      .build()
          

          【讨论】: