Android开发经验谈Android开发Android

Android 上的 Shadowsocks 源码浅析

2019-04-22  本文已影响15人  涤生_Woo

一、 代理的简单流程

  1. 应用程序使用 socket,将相应的数据包发送到真实的网络设备上。一般移动设备只有无线网卡,因此是发送到真实的WiFi设备上。
  2. Android 系统通过 iptables,使用 NAT,将所有的数据包转发到 TUN 虚拟网络设备上去,端口是 tun0。
  3. VPN 程序通过打开 /dev/tun 设备,并读取该设备上的数据,可以获得所有转发到 TUN 虚拟网络设备上的 IP 包。因为设备上的所有 IP 包都会被 NAT 转成原地址是 tun0 端口发送的,所以也就是说你的 VPN 程序可以获得进出该设备的几乎所有的数据(也有例外,不是全部,比如回环数据就无法获得)。
  4. VPN 数据可以做一些处理,然后将处理过后的数据包,通过真实的网络设备发送出去。为了防止发送的数据包再被转到 TUN 虚拟网络设备上,VPN 程序所使用的 socket 必须先被明确绑定到真实的网络设备上去。

二、利用 VpnService 截获网络层所有 IP 数据报

以下代码截取自 shadowsocks-android-java

2.1 启动 VpnService

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (LocalVpnService.IsRunning != isChecked) {
            switchProxy.setEnabled(false);
            if (isChecked) {
                Intent intent = LocalVpnService.prepare(this);
                if (intent == null) {
                    startVPNService();
                } else {
                    startActivityForResult(intent, START_VPN_SERVICE_REQUEST_CODE);
                }
            } else {
                LocalVpnService.IsRunning = false;
            }
        }
    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        if (requestCode == START_VPN_SERVICE_REQUEST_CODE) {
            if (resultCode == RESULT_OK) {
                startVPNService();
            } else {
                switchProxy.setChecked(false);
                switchProxy.setEnabled(true);
                onLogReceived("canceled.");
            }
            return;
        }
        ...
        ...
        super.onActivityResult(requestCode, resultCode, intent);
    }
    private void startVPNService() {
        String ProxyUrl = readProxyUrl();
        if (!isValidUrl(ProxyUrl)) {
            Toast.makeText(this, R.string.err_invalid_url, Toast.LENGTH_SHORT).show();
            switchProxy.post(new Runnable() {
                @Override
                public void run() {
                    switchProxy.setChecked(false);
                    switchProxy.setEnabled(true);
                }
            });
            return;
        }

        textViewLog.setText("");
        GL_HISTORY_LOGS = null;
        onLogReceived("starting...");
        LocalVpnService.ProxyUrl = ProxyUrl;
        startService(new Intent(this, LocalVpnService.class));
    }

2.2 创建并初始化虚拟网络端口

通用代码如下:

Builder builder = new Builder();
builder.setMtu(...);
builder.addAddress(...);
builder.addRoute(...);
builder.addDnsServer(...);
builder.addSearchDomain(...);
builder.setSession(...);
builder.setConfigureIntent(...);
ParcelFileDescriptor interface = builder.establish();

在具体实践中,可参考下面的代码:

    private ParcelFileDescriptor establishVPN() throws Exception {
        Builder builder = new Builder();
        builder.setMtu(ProxyConfig.Instance.getMTU());
        if (ProxyConfig.IS_DEBUG)
            System.out.printf("setMtu: %d\n", ProxyConfig.Instance.getMTU());

        IPAddress ipAddress = ProxyConfig.Instance.getDefaultLocalIP();
        LOCAL_IP = CommonMethods.ipStringToInt(ipAddress.Address);
        builder.addAddress(ipAddress.Address, ipAddress.PrefixLength);
        if (ProxyConfig.IS_DEBUG)
            System.out.printf("addAddress: %s/%d\n", ipAddress.Address, ipAddress.PrefixLength);

        for (ProxyConfig.IPAddress dns : ProxyConfig.Instance.getDnsList()) {
            builder.addDnsServer(dns.Address);
            if (ProxyConfig.IS_DEBUG)
                System.out.printf("addDnsServer: %s\n", dns.Address);
        }

        if (ProxyConfig.Instance.getRouteList().size() > 0) {
            for (ProxyConfig.IPAddress routeAddress : ProxyConfig.Instance.getRouteList()) {
                builder.addRoute(routeAddress.Address, routeAddress.PrefixLength);
                if (ProxyConfig.IS_DEBUG)
                    System.out.printf("addRoute: %s/%d\n", routeAddress.Address, routeAddress.PrefixLength);
            }
            builder.addRoute(CommonMethods.ipIntToString(ProxyConfig.FAKE_NETWORK_IP), 16);

            if (ProxyConfig.IS_DEBUG)
                System.out.printf("addRoute for FAKE_NETWORK: %s/%d\n", CommonMethods.ipIntToString(ProxyConfig.FAKE_NETWORK_IP), 16);
        } else {
            builder.addRoute("0.0.0.0", 0);
            if (ProxyConfig.IS_DEBUG)
                System.out.printf("addDefaultRoute: 0.0.0.0/0\n");
        }


        Class<?> SystemProperties = Class.forName("android.os.SystemProperties");
        Method method = SystemProperties.getMethod("get", new Class[]{String.class});
        ArrayList<String> servers = new ArrayList<String>();
        for (String name : new String[]{"net.dns1", "net.dns2", "net.dns3", "net.dns4",}) {
            String value = (String) method.invoke(null, name);
            if (value != null && !"".equals(value) && !servers.contains(value)) {
                servers.add(value);
                if (value.replaceAll("\\d", "").length() == 3){//防止IPv6地址导致问题
                    builder.addRoute(value, 32);
                } else {
                    builder.addRoute(value, 128);
                }
                if (ProxyConfig.IS_DEBUG)
                    System.out.printf("%s=%s\n", name, value);
            }
        }

        if (AppProxyManager.isLollipopOrAbove){
            if (AppProxyManager.Instance.proxyAppInfo.size() == 0){
                writeLog("Proxy All Apps");
            }
            for (AppInfo app : AppProxyManager.Instance.proxyAppInfo){
                builder.addAllowedApplication("com.vm.shadowsocks");//需要把自己加入代理,不然会无法进行网络连接
                try{
                    builder.addAllowedApplication(app.getPkgName());
                    writeLog("Proxy App: " + app.getAppLabel());
                } catch (Exception e){
                    e.printStackTrace();
                    writeLog("Proxy App Fail: " + app.getAppLabel());
                }
            }
        } else {
            writeLog("No Pre-App proxy, due to low Android version.");
        }

        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
        builder.setConfigureIntent(pendingIntent);

        builder.setSession(ProxyConfig.Instance.getSessionName());
        ParcelFileDescriptor pfdDescriptor = builder.establish();
        onStatusChanged(ProxyConfig.Instance.getSessionName() + getString(R.string.vpn_connected_status), true);
        return pfdDescriptor;
    }

同样的,通用示例代码比较简单,如下:

// Packets to be sent are queued in this input stream.
FileInputStream in = new FileInputStream(interface.getFileDescriptor());
 
// Packets received need to be written to this output stream.
FileOutputStream out = new FileOutputStream(interface.getFileDescriptor());
 
// Allocate the buffer for a single packet.
ByteBuffer packet = ByteBuffer.allocate(32767);
...
// Read packets sending to this interface
int length = in.read(packet.array());
...
// Write response packets back
out.write(packet.array(), 0, length);

而具体在实践中, 我们也是拿到 VpnService.Builder() 返回的 ParcelFileDescriptor 实例之后,对其进行进一步的操作:

    private void runVPN() throws Exception {
        this.m_VPNInterface = establishVPN();
        this.m_VPNOutputStream = new FileOutputStream(m_VPNInterface.getFileDescriptor());
        FileInputStream in = new FileInputStream(m_VPNInterface.getFileDescriptor());
        int size = 0;
        while (size != -1 && IsRunning) {
            while ((size = in.read(m_Packet)) > 0 && IsRunning) {
                if (m_DnsProxy.Stopped || m_TcpProxyServer.Stopped) {
                    in.close();
                    throw new Exception("LocalServer stopped.");
                }
                onIPPacketReceived(m_IPHeader, size);
            }
            Thread.sleep(20);
        }
        in.close();
        disconnectVPN();
    }

三、处理截获到的 IP 数据包

3.1 在 VPNService 启动时,创建并运行了线程 TcpProxyServer,类似于代理服务器。

    @Override
    public synchronized void run() {
        try {
            ...
            ...
            m_TcpProxyServer = new TcpProxyServer(0);
            m_TcpProxyServer.start();
            writeLog("LocalTcpServer started.");

            m_DnsProxy = new DnsProxy();
            m_DnsProxy.start();
            writeLog("LocalDnsProxy started.");

            while (true) {
                if (IsRunning) {
                    //加载配置文件

                    writeLog("set shadowsocks/(http proxy)");
                    try {
                        ProxyConfig.Instance.m_ProxyList.clear();
                        ProxyConfig.Instance.addProxyToList(ProxyUrl);
                        writeLog("Proxy is: %s", ProxyConfig.Instance.getDefaultProxy());
                    } catch (Exception e) {
                        ;
                        String errString = e.getMessage();
                        if (errString == null || errString.isEmpty()) {
                            errString = e.toString();
                        }
                        IsRunning = false;
                        onStatusChanged(errString, false);
                        continue;
                    }
                    String welcomeInfoString = ProxyConfig.Instance.getWelcomeInfo();
                    if (welcomeInfoString != null && !welcomeInfoString.isEmpty()) {
                        writeLog("%s", ProxyConfig.Instance.getWelcomeInfo());
                    }
                    writeLog("Global mode is " + (ProxyConfig.Instance.globalMode ? "on" : "off"));

                    runVPN();
                } else {
                    Thread.sleep(100);
                }
            }
        } catch (InterruptedException e) {
            System.out.println(e);
        } catch (Exception e) {
            e.printStackTrace();
            writeLog("Fatal error: %s", e.toString());
        } finally {
            writeLog("App terminated.");
            dispose();
        }
    }

3.2 VPNService 在网络层截获所有流量,从数据包的的 IP 头和 TCP 头解析该数据包去往的 IP 地址、端口号,并将它们改成本地另一个 TCP 服务器的地址和端口,也就是TcpProxyServer。 这里就实现了一个网络层的转发,由网络层转发到传输层。

    private void runVPN() throws Exception {
        this.m_VPNInterface = establishVPN();
        this.m_VPNOutputStream = new FileOutputStream(m_VPNInterface.getFileDescriptor());
        FileInputStream in = new FileInputStream(m_VPNInterface.getFileDescriptor());
        int size = 0;
        while (size != -1 && IsRunning) {
            //读取系统的IP包,自旋等待
            while ((size = in.read(m_Packet)) > 0 && IsRunning) {
                if (m_DnsProxy.Stopped || m_TcpProxyServer.Stopped) {
                    in.close();
                    throw new Exception("LocalServer stopped.");
                }
                //处理接收到的IP包
                onIPPacketReceived(m_IPHeader, size);
            }
            Thread.sleep(20);
        }
        in.close();
        disconnectVPN();
    }
    void onIPPacketReceived(IPHeader ipHeader, int size) throws IOException {
        //首先判断接收到的包的协议,根据流中的第九位+偏移量来判断,TCP是6,UDP是17
        switch (ipHeader.getProtocol()) {
            case IPHeader.TCP:
                TCPHeader tcpHeader = m_TCPHeader;
                tcpHeader.m_Offset = ipHeader.getHeaderLength();
                if (ipHeader.getSourceIP() == LOCAL_IP) {
                    if (tcpHeader.getSourcePort() == m_TcpProxyServer.Port) {// 收到本地TCP服务器数据
                        NatSession session = NatSessionManager.getSession(tcpHeader.getDestinationPort());
                        if (session != null) {
                            ipHeader.setSourceIP(ipHeader.getDestinationIP());
                            tcpHeader.setSourcePort(session.RemotePort);
                            ipHeader.setDestinationIP(LOCAL_IP);

                            CommonMethods.ComputeTCPChecksum(ipHeader, tcpHeader);
                            m_VPNOutputStream.write(ipHeader.m_Data, ipHeader.m_Offset, size);
                            m_ReceivedBytes += size;
                        } else {
                            System.out.printf("NoSession: %s %s\n", ipHeader.toString(), tcpHeader.toString());
                        }
                    } else {
                        //这里是收到NAT的数据,向TCP服务器发送,转发到代理服务器
                        // 添加端口映射
                        int portKey = tcpHeader.getSourcePort();
                        NatSession session = NatSessionManager.getSession(portKey);
                        if (session == null || session.RemoteIP != ipHeader.getDestinationIP() || session.RemotePort != tcpHeader.getDestinationPort()) {
                            session = NatSessionManager.createSession(portKey, ipHeader.getDestinationIP(), tcpHeader.getDestinationPort());
                        }

                        session.LastNanoTime = System.nanoTime();
                        session.PacketSent++;//注意顺序

                        int tcpDataSize = ipHeader.getDataLength() - tcpHeader.getHeaderLength();
                        if (session.PacketSent == 2 && tcpDataSize == 0) {
                            return;//丢弃tcp握手的第二个ACK报文。因为客户端发数据的时候也会带上ACK,这样可以在服务器Accept之前分析出HOST信息。
                        }

                        //分析数据,找到host
                        if (session.BytesSent == 0 && tcpDataSize > 10) {
                            int dataOffset = tcpHeader.m_Offset + tcpHeader.getHeaderLength();
                            String host = HttpHostHeaderParser.parseHost(tcpHeader.m_Data, dataOffset, tcpDataSize);
                            if (host != null) {
                                session.RemoteHost = host;
                            } else {
                                System.out.printf("No host name found: %s", session.RemoteHost);
                            }
                        }

                        // 转发给本地TCP服务器
                        ipHeader.setSourceIP(ipHeader.getDestinationIP());
                        ipHeader.setDestinationIP(LOCAL_IP);
                        tcpHeader.setDestinationPort(m_TcpProxyServer.Port);

                        CommonMethods.ComputeTCPChecksum(ipHeader, tcpHeader);
                        m_VPNOutputStream.write(ipHeader.m_Data, ipHeader.m_Offset, size);
                        session.BytesSent += tcpDataSize;//注意顺序
                        m_SentBytes += size;
                    }
                }
                break;
            case IPHeader.UDP:
                // 转发DNS数据包:
                UDPHeader udpHeader = m_UDPHeader;
                udpHeader.m_Offset = ipHeader.getHeaderLength();
                if (ipHeader.getSourceIP() == LOCAL_IP && udpHeader.getDestinationPort() == 53) {
                    m_DNSBuffer.clear();
                    m_DNSBuffer.limit(ipHeader.getDataLength() - 8);
                    DnsPacket dnsPacket = DnsPacket.FromBytes(m_DNSBuffer);
                    if (dnsPacket != null && dnsPacket.Header.QuestionCount > 0) {
                        m_DnsProxy.onDnsRequestReceived(ipHeader, udpHeader, dnsPacket);
                    }
                }
                break;
        }
    }

3.3 当 TcpProxyServer 在传输层得到刚才转发过来的流量,根据代理规则向外网建立连接并建立隧道,在传输层通过 TCP Socket 进行转发。

3.4 当 TcpProxyServer 获得外网回来的流量后,会转发给系统内部一个不存在的 IP 地址,TUN 虚拟网卡截获到去往这个不存在的 IP 地址数据报后,用同样的方法将它们修改回原本的地址和端口号,并将它们写入到虚拟网卡中,交由系统的 TCP/IP 协议栈去处理。

public TcpProxyServer(int port) throws IOException {
    m_Selector = Selector.open();
    m_ServerSocketChannel = ServerSocketChannel.open();
    m_ServerSocketChannel.configureBlocking(false);
   //这里外部是传入0的,端口号 0 是一种由系统指定动态生成的端口。
    m_ServerSocketChannel.socket().bind(new InetSocketAddress(port));
  //Selector的用法可以自行百度下,是NIO下的一个类,一个单独的线程可以管理多个channel,从而管理多个网络连接
    m_ServerSocketChannel.register(m_Selector, SelectionKey.OP_ACCEPT);
    this.Port = (short) m_ServerSocketChannel.socket().getLocalPort();
    System.out.printf("AsyncTcpServer listen on %d success.\n", this.Port & 0xFFFF);
}
    @Override
    public void run() {
        try {
            while (true) {
                m_Selector.select();
                Iterator<SelectionKey> keyIterator = m_Selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    if (key.isValid()) {
                        try {
                            if (key.isReadable()) {
                                ((Tunnel) key.attachment()).onReadable(key);
                            } else if (key.isWritable()) {
                                ((Tunnel) key.attachment()).onWritable(key);
                            } else if (key.isConnectable()) {
                                ((Tunnel) key.attachment()).onConnectable();
                            } else if (key.isAcceptable()) {
                                onAccepted(key);
                            }
                        } catch (Exception e) {
                            System.out.println(e.toString());
                        }
                    }
                    keyIterator.remove();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            this.stop();
            System.out.println("TcpServer thread exited.");
        }
    }
    private SocketChannel m_InnerChannel;
    private ByteBuffer m_SendRemainBuffer;
    private Selector m_Selector;
    private Tunnel m_BrotherTunnel;
    private boolean m_Disposed;
    private InetSocketAddress m_ServerEP;
    protected InetSocketAddress m_DestAddress;

按照顺序,先来看 onAccepted() 方法:

    void onAccepted(SelectionKey key) {
        Tunnel localTunnel = null;
        try {
            //首先用accept获取本地连接TCPProxy的channel
            SocketChannel localChannel = m_ServerSocketChannel.accept();
            //把channel包装成这里自定义的Tunnel
            localTunnel = TunnelFactory.wrap(localChannel, m_Selector);

            //获取代理服务器的地址
            InetSocketAddress destAddress = getDestAddress(localChannel);
            if (destAddress != null) {
                //这里我们用的SS协议,所以创建的是一个ShadowsocksTunnel
                Tunnel remoteTunnel = TunnelFactory.createTunnelByConfig(destAddress, m_Selector);
                remoteTunnel.setBrotherTunnel(localTunnel);//关联兄弟
                localTunnel.setBrotherTunnel(remoteTunnel);//关联兄弟
                remoteTunnel.connect(destAddress);//开始连接
            } else {
                LocalVpnService.Instance.writeLog("Error: socket(%s:%d) target host is null.", localChannel.socket().getInetAddress().toString(), localChannel.socket().getPort());
                localTunnel.dispose();
            }
        } catch (Exception e) {
            e.printStackTrace();
            LocalVpnService.Instance.writeLog("Error: remote socket create failed: %s", e.toString());
            if (localTunnel != null) {
                localTunnel.dispose();
            }
        }
    }

我们注意到上面的这行代码 remoteTunnel.connect(destAddress);//开始连接 ,看下面:

    public void connect(InetSocketAddress destAddress) throws Exception {
        if (LocalVpnService.Instance.protect(m_InnerChannel.socket())) {//保护socket不走vpn
            m_DestAddress = destAddress;
            m_InnerChannel.register(m_Selector, SelectionKey.OP_CONNECT, this);//注册连接事件
            m_InnerChannel.connect(m_ServerEP);//连接目标
        } else {
            throw new Exception("VPN protect socket failed.");
        }
    }

上面的 LocalVpnService.Instance.protect(m_InnerChannel.socket())) ,一般的应用程序,在获得这些 IP 数据包后,会将它们再通过 socket 发送出去。但是,这样做会有问题,你的程序建立的 socket 和别的程序建立的 socket 其实没有区别,发送出去后,还是会被转发到 tun0 接口,再回到你的程序,这样就是一个死循环了。为了解决这个问题,VpnService 类提供了一个叫 protect() 的函数,在 VPN 程序自己建立 socket 之后,必须要对其进行保护。

    @SuppressLint("DefaultLocale")
    public void onConnectable() {
        try {
            if (m_InnerChannel.finishConnect()) {//连接成功
                onConnected(GL_BUFFER);//通知子类TCP已连接,子类可以根据协议实现握手等。
            } else {//连接失败
                LocalVpnService.Instance.writeLog("Error: connect to %s failed.", m_ServerEP);
                this.dispose();
            }
        } catch (Exception e) {
            LocalVpnService.Instance.writeLog("Error: connect to %s failed: %s", m_ServerEP, e);
            this.dispose();
        }
    }
    @Override
    protected void onConnected(ByteBuffer buffer) throws Exception {

        buffer.clear();
        // https://shadowsocks.org/en/spec/protocol.html

        buffer.put((byte) 0x03);//domain
        byte[] domainBytes = m_DestAddress.getHostName().getBytes();
        buffer.put((byte) domainBytes.length);//domain length;
        buffer.put(domainBytes);
        buffer.putShort((short) m_DestAddress.getPort());
        buffer.flip();
        byte[] _header = new byte[buffer.limit()];
        buffer.get(_header);

        buffer.clear();
        buffer.put(m_Encryptor.encrypt(_header));
        buffer.flip();

        if (write(buffer, true)) {
            m_TunnelEstablished = true;
            onTunnelEstablished();
        } else {
            m_TunnelEstablished = true;
            this.beginReceive();
        }
    }
    public void onReadable(SelectionKey key) {
        try {
            ByteBuffer buffer = GL_BUFFER;
            buffer.clear();
            int bytesRead = m_InnerChannel.read(buffer);
            if (bytesRead > 0) {
                buffer.flip();
                afterReceived(buffer);//先让子类处理,例如解密数据。
                if (isTunnelEstablished() && buffer.hasRemaining()) {//将读到的数据,转发给兄弟。
                    m_BrotherTunnel.beforeSend(buffer);//发送之前,先让子类处理,例如做加密等。
                    if (!m_BrotherTunnel.write(buffer, true)) {
                        key.cancel();//兄弟吃不消,就取消读取事件。
                        if (ProxyConfig.IS_DEBUG)
                            System.out.printf("%s can not read more.\n", m_ServerEP);
                    }
                }
            } else if (bytesRead < 0) {
                this.dispose();//连接已关闭,释放资源。
            }
        } catch (Exception e) {
            e.printStackTrace();
            this.dispose();
        }
    }

    public void onWritable(SelectionKey key) {
        try {
            this.beforeSend(m_SendRemainBuffer);//发送之前,先让子类处理,例如做加密等。
            if (this.write(m_SendRemainBuffer, false)) {//如果剩余数据已经发送完毕
                key.cancel();//取消写事件。
                if (isTunnelEstablished()) {
                    m_BrotherTunnel.beginReceive();//这边数据发送完毕,通知兄弟可以收数据了。
                } else {
                    this.beginReceive();//开始接收代理服务器响应数据
                }
            }
        } catch (Exception e) {
            this.dispose();
        }
    }

四、小结

1、首先,都要借助 TUN 虚拟网络设备得到数据。
2、根据协议和实现需要,对数据报进行处理。
3、最后,通过真实的网卡将数据发送出去。
4、在远程服务器端也需要相应的配置。

上一篇下一篇

猜你喜欢

热点阅读