HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

Android VPN 实现原理介绍

  • 2016-06-19
  • 本文字数:8858 字

    阅读完需:约 29 分钟

编者按: InfoQ 开设栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自Nikolay Elenkov 所著《Android 安全架构深究》(译者为刘惠明、刘跃)一书中的“VPN 支持”。介绍了Android 系统所支持的几种VPN 安全协议、系统内置VPN 原理,以及基于应用的VPN(如OpenVPN)。

虚拟专用网络(Virtual Private Network,VPN)能够在不使用专用物理连接的情况下,将一个虚拟的网络扩展到全网,因此所有连接到VPN 中的设备可如同物理连接到同一私有网络中一样,发送并接收数据。如果个人设备使用VPN 接入目标私有网络,这种方式也叫作远程访问VPN;当VPN 用来连接两个远程网络的时候,被称为site-to-site VPN。

远程访问VPN 可以将特定设备与一个静态IP 连接,设备如同远程办公室中的一台电脑;但是对于移动设备来说,更常用的是可变网络连接和动态地址的配置方法。这样的配置通常被称为road warrior 配置,而且是Android VPN 中最常用的配置。

为了保证通过VPN 传输数据的私密性,VPN 一般会使用一个安全隧道协议以认证远程客户端并实现数据保密。由于VPN 协议需要同时在多个网络层工作而且为了兼容不同的网络配置,常常需要进行多层封装,所以非常复杂。对于VPN 的详细讨论超出了本书范围,但是在下面几节中会简单介绍主流的VPN 协议,而且会主要专注于Android 中可用的VPN 类型。

PPTP

Point-to-Point Tunneling Protocol(PPTP)使用 TCP 的信道来建立连接,并使用 Generic Routing Encapsulation(GRE)隧道协议来封装 Point-to-Point Protocol(PPP)的数据包。支持的认证方法有密码认证协议(Password Authentication Protocol,PAP)、握手问题认证协议(Challenge-Handshake Authentication Protocol,CHAP)和微软扩展 MS-CHAP v1/v2,以及 EAP-TLS。其中当前仍然被认为安全的只有 EAP-TLS。

PPP 的载荷可以使用微软点对点加密(MPPE)协议进行加密,其中使用了 RC4 流加密方法。因为 MPPE 并不支持任何密文认证,所以无法防范位翻转(bit-flipping)攻击。除此之外,RC4 加密近年来也出现过多次问题,大大减弱了 MMPE 和 PPTP 的安全性。

L2TP/IPSec

二层隧道协议(Layer 2 Tunneling Protocol,L2TP)类似于 PPTP,工作在数据链路层(OSI 模型中的第二层)。因为 L2TP 本身并不提供任何加密或者保密功能(依赖于隧道协议实现这些特性),L2TP VPN 一般使用 L2TP 和 IPSec 协议套件的组合实现,由 IPSec 完成认证,进行机密性和完整性的保证。

在 L2TP/IPSec 的配置中,首先会使用 IPSec 建立一个安全信道,然后 L2TP 隧道将会在这个安全信道之上建立。L2TP 的包会被封装到 IPSec 包中,因此保证了安全。IPSec 的连接需要建立一个安全关联(Security Association,SA),这是密钥算法和模式、加密密钥和建立安全信道所需的其他参数的组合。

SA 使用网络安全关联和密钥管理协议(ISAKMP)建立。ISAKMP 不会定义一个特殊的密钥交换方法,而是使用人工指定的预先共享的密钥或者使用网络密钥交换(IKE 和 IKEv2)协议。IKE 使用 X.509 证书进行对方身份的验证(与 SSL 类似),并使用 Diffie-Hellman 密钥交换创建一个共享密文,并使用其生成实际的会话加密密钥。

IPSec Xauth

IPSec 扩展认证(Xauth)对 IKE 进行了扩展,包含了额外的用户认证交换。这样就允许使用一个已存在的用户数据库或者 RADIUS 架构来认证远端请求访问的客户端,并且能够集成双因素认证。

Mode-configuration(Modecfg)是另一个 IPSec 扩展,经常被用于远程访问场景。Modecfg 可以让 VPN 服务端向客户端推送网络配置信息,比如私有 IP 地址和 DNS 服务器地址。如果同时使用 Xauth 和 Modecfg,能够生成一个纯 IPSec 的 VPN 解决方案,不需要使用任何额外协议进行认证和隧道操作。

基于 SSL 的 VPN

基于 SSL 的 VPN 使用 SSL 或者 TLS(见第 6 章)建立安全连接和网络数据传输隧道。然而并不存在一个标准来定义基于 SSL 的 VPN,所以为了建立安全信道并封装数据包,在不同实现中会使用不同的策略。

OpenVPN 是一个很流行的开源 VPN 应用,使用 SSL 进行认证和密钥交换(同样支持预先配置的共享静态密钥),并使用定制的加密协议 对数据包进行加密和认证。OpenVPN 使用多路 SSL 会话进行认证和密钥交换以及加密包的传输,而且仅仅使用单一的 UDP(或者 TCP)端口。多路协议为 SSL 在 UDP 上提供了一个可靠的传输层,但是基于 UDP 的加密数据隧道并没有一定的可靠性。可靠性通过隧道协议本身来提供,通常是 TCP。

OpenVPN 比 IPSec 的优势在于协议的简单并且可以完全在用户层实现。而 IPSec 需要内核层的支持和多个相互依赖的协议实现。此外,由于 OpenVPN 使用常见的 TCP 和 UDP 协议,而且利用一个单一的端口完成多路隧道,所以更容易穿过防火墙、NAT 和代理。

接下来的几节将会探究 Android 内置的 VPN 支持和 Android 提供给想完成额外 VPN 解决方案的 Android 应用的 API。本书将会展示 Android VPN 架构的主要组件并且介绍如何保护 VPN 的凭据。

legacy VPN

在 Android 4.0 之前,对于 VPN 的支持是内置在系统中的,无法进行扩展。对于新的 VPN 类型的支持只能通过系统更新来完成。为了将这种情况与基于应用的实现进行区分,我们称内置的 VPN 支持为 legacy VPN。

早先的 Android 版本支持基于 PPTP 和 L2TP/IPSec 的 VPN 配置,在 Android 4.0 中加入了对于使用 IPSec Xauth 的“纯 IPSec”VPN 的支持。除此之外,Android 4.0 提供了系统基类 VpnService 增加了对基于应用的 VPN 的支持。应用可以集成该基类实现一个新的 VPN 解决方案。

legacy VPN 通过系统设置应用进行控制,而且只有设备所有者用户(主用户)才可以配置。图 9-8 中展示的是添加一个新 IPSec legacy VPN 配置的对话框。

legacy VPN 的实现

legacy VPN 的实现中包含内核驱动、原生守护进程、命令和系统服务。PPTP 和 L2TP 隧道的底层实现使用了 Android 特有的 PPP 守护进程 mtpd 以及 PPPoPNS 和 PPPoLAC(仅仅在 Android 内核中可用)内核驱动。

因为对于一个设备,legacy VPN 仅仅支持单一的 VPN 连接,所以 mtpd 只能建立一个会话。IPSec VPN 的实现使用了内置的 IPSec 内核支持和修改的 racoon IKE 密钥管理守护进程(Linux 内核 IPSec 实现中的 IPSec-Tools 工具包的一部分;racoon 只支持 IKEv1)。清单 9-6 中展示了如何在 init.rc 中定义这两个守护进程。

清单 9-6:init.rc 中 racoon 和 mtpd 的定义

守护进程 racoonŒ和 mtpd�都创建控制套接字(��),只能够被系统用户访问而且不会默认启动;两个守护进程都拥有 vpn、net_admin(对应 Linux 的 CAP_NET_ADMIN 能力),并且都在额外用户组中添加了 inet(Ž‘),允许创建套接字并控制网络接口设备。在 mtpd 守护进程中还添加了 net_raw 组(对应 Linux 的 CAP_NET_RAW 能力),可以创建 GRE 数据包(被 PPTP 使用)。

当系统设置应用启动 VPN 之后,Android 启动 racoon 和 mtpd 守护进程并且通过本地套接字向它们发送控制命令,以建立配置的连接。这两个守护进程将会创建请求的 VPN 隧道,并使用收到的 IP 地址和网络掩码创建并配置隧道网络接口。其中,mtpd 守护进程会完成接口配置,而 racoon 使用 ip-up-vpn 帮助命令打开隧道接口——通常为 tun0。

为了将通信的参数回传给系统,VPN 守护进程将会在 /data/misc/vpn 目录下写入一个 state 文件,如清单 9-7 所示。

清单 9-7:VPN 状态文件的内容

这个文件中包含了隧道接口的名称Œ,IP 地址和掩码�,配置的路由Ž,DNS 服务器�和搜索域名�,每项占一行。

VPN 守护进程开始运行后,系统会处理 state 文件,然后调用系统的 ConnectivityService 为新建立的 VPN 连接配置路由、DNS 服务器,以及搜索域名。ConnectivityService 会向 netd 守护进程的本地套接字中发送控制命令,随后以 root 权限运行的 netd 将会修改内核的包过滤和路由表。通过添加匹配应用 UID 的防火墙规则和路由表,所有设备所有者或特定配置启动的所有应用流量将会被通过 VPN 接口进行路由。(本章后面“多用户支持”一节中讨论了各应用路由规则和多用户支持。)

配置和凭据存储

每个由设置应用创建的 VPN 配置被称为 VPN 配置文件(profile),在本地硬盘加密存储。加密的过程由 Android 的凭据存储守护进程 keystore 完成,并且需要一个与设备相关的密钥。

VPN 配置文件的所有属性以 NUL 字符(\0)分隔连接到一起组成序列化的单个配置字符串,作为二进制大对象类型存储到系统密钥库中。VPN 配置文件的文件名为 VPN 前缀加上以毫秒为单位的当前时间(十六进制格式)。例如,清单 9-8 中显示了用户的密钥库目录,其中包含三个 VPN 配置文件(未显示时间戳)。

清单 9-8:配置 VPN 之后 keystore 目录中的内容

三个 VPN 配置文件被保存在 1000_VPN_144965b85a6�、1000_VPN_ 145635c88c8‘和 1000_VPN_14569512c80“文件中。1000_ 前缀代表了文件所有者用户,也就是 system(UID 1000)。因为 VPN 配置文件的所有者是 system,所以只有系统应用可以获取并解密配置文件的内容。

清单 9-9 中展示了三个 VPN 配置文件解密之后的明文内容(为了可读性,NUL 字符使用竖分割线 [|] 代替)。

清单 9-9:VPN 配置文件的内容

配置文件中包含了 VPN 配置编辑对话框(参见图 9-8)中的所有域,如果未指定则由一个空字符串代表。开头的 5 个域分别代表了 VPN 的名称、类型、网关主机、用户名和密码。在清单 9-9 中,第一个 VPN 配置文件Œ是针对一个使用预先共享密钥方式(type1)运作的 L2TP/IPSec VPN;第二个配置文件�对应的是 PPTP VPN(type 0),而最后一个配置文件Ž针对的是使用证书和 Xauth 认证的 IPSec VPN(type 4)。

除了用户名和密码,VPN 配置文件中还包含了其他建立连接需要的凭据。在清单 9-9 中的第一个 VPN 配置Œ中,额外的凭据是预分享的建立 IPSec 安全连接所需的密钥(在本例中为 PSK 字符串)。而对于第三个配置Ž,额外的凭据是用户的私钥和证书。然而,正如在清单中看到的那样,密钥和证书并不会完全包含在配置文件中;配置文件中仅仅包含密钥和证书的别名(两者相同,均为 vpnclient)。密钥和证书被保存在系统凭据库中,VPN 配置中的别名仅仅充当一个标识符,用来获取密钥和证书。

获取凭据

原本使用 PEM 文件中密钥和证书的 racoon 守护进程,在 Android 中被修改为使用密钥库(keystore)的 OpenSSL 引擎。正如第 7 章中讨论过的,keystore 引擎是系统凭据库的入口,如果设备支持,甚至可以使用硬件凭据库。当 keystore 引擎处理一个密钥别名的时候,不需要从密钥库提取出密钥就能够使用对应的私钥对认证包进行签名。

清单 9-9 中的 VPN 配置文件Ž中也包含 CA 证书的别名(cacert),该证书在验证服务器的证书时会被作为信任锚点。在运行时,系统从密钥库中获取到客户端证书(清单 9-8 中的Ž)和 CA 证书(清单 9-8 中的Œ),然后通过控制套接字将它们和其他连接参数一起传送给 racoon。私钥 blob(清单 9-8 中的�)永远不会被直接发送给 racoon 守护进程,而仅仅发送其别名(vpnclient)。

NOTE

虽然设备上的私钥使用硬件密钥库(hardware-backed keystore)保存,但 VPN 配置中预共享的密钥和密码却不是。造成这种情况的原因是:在撰写本书时,Android 不支持在硬件密钥库中导入对称密 钥,而仅仅支持非对称密钥(RSA、DSA 和 EC)。所以,使用预共享密钥的 VPN 的凭据被直接明文保存在 VPN 配置文件中,这样就导致当配置文件被解密 到内存中后,可被 root 用户提取出来。

始终在线的 VPN

Android 4.2 和之后的版本中支持始终在线的 VPN 配置:在与指定的 VPN 建立连接前会屏蔽应用发起的所有网络连接。这样就防止了应用使用非加密的信道,例如公开 Wi-Fi 来发送数据。

设置一个始终在线的 VPN 需要将 VPN 的网关设置为 IP 地址,并且明确指定 DNS 服务器的 IP。这种明确的配置是为了保证 DNS 流量不会被发送给本地配置的 DNS 服务器,其在始终在线的 VPN 状态下是被屏蔽的。VPN 配置文件选择对话框如图 9-9 所示。

用户选择的配置会以“其他 VPN 设置”的类型被加密保存到文件

LOCKDOWN_VPN 中(清单 9-8 中的�);该文件中仅包含了选择的配置的名字—本例中为 144965b85a6。如果存在 LOCKDOWN_ VPN 文件,系统在启动的时候就会自动连接指定的 VPN。如果下层网络连接发生了重连或者改变(比如切换了 Wi-Fi 热点),VPN 也将会自动重启。

始终在线的 VPN 通过加入防火墙规则屏蔽了所有不经过 VPN 接口的所有数据包,保证了所有的流量都会通过 VPN 进行发送。系统使用 LockdownVpnTracker 类(始终在线 VPN 在 Android 源码中被称为 lockdown VPN)来添加防火墙规则:监视 VPN 的状态,并向 netd 守护进程发送命令来动态调整当前的防火墙状态。而 netd 守护进程使用 iptables 工具修改内核的包过滤表。比如,当一个始终在线的 L2TP/IPSec VPN 以 11.22.33.44 的 IP 地址连接到了 VPN 服务器,并且以 10.1.1.1 的 IP 地址创建了一个隧道接口 tun0,添加的防火墙规则(通过使用 iptables 列出,为了简洁省略了一些列)的情况可能如清单 9-10 所示。

清单 9-10:始终在线的 VPN 防火墙规则

如清单 9-10 所示,所有来自和发往 VPN 网络的流量都被允许(Œ�),隧道接口也同样(�‘)。而且仅仅允许 IPSec 端口(500 和 4500)和 L2TP 的端口(1701)上的来自 / 发往 VPN 服务端的流量(Ž’)。其他所有收到的流量都会被丢弃�,所有向外的流量都会被拒绝“。

基于应用的 VPN

Android 4.0 中增加了一个 VpnService 公开 API ,允许第三方应用实现 VPN 解决方案,而且此应用不需要被植入系统镜像或者拥有系统权限。VpnService 和相关的 Builder 类允许应用指定网络参数,比如接口 IP 地址和路由,随后系统使用这些参数创建并配置一个虚拟网络接口。应用将会接收到一个该网络接口对应的文件描述符,之后就可以通过写入或者读取该描述符进行网络隧道通信。
每次读取都会获取一个出去的 IP 包,而且每次写入会注入一个进入的 IP 包。因为对于网络包有效的直接访问能够允许基于应用的 VPN 完成对网络包的注入和修改,所以这种 VPN 不能自动启动,而且总是需要用户操作。此外,当 VPN 成功连接,将会显示一个运行状态的通告。对基于应用的 VPN 连接的警告框如图 9-10 所示。

图 9-10:基于应用的 VPN 连接警告框

VPN 声明

基于应用的 VPN 创建了一个继承 VpnService 基类的服务,然后将其注册到应用 manifest 中,如清单 9-11 所示。

清单 9-11:在应用 manifest 中注册一个 VPN 服务

服务中必须包含一个可以匹配 android.net.VpnService 动作�的 intent filter,以允许系统绑定并控制这个服务。除此之外,服务还要求绑定者拥有 BIND_VPN_SERVICE 系统签名权限Œ,保证只有系统应用才能够绑定。

VPN 准备

为了在系统中注册一个新 VPN 连接,应用需要首先调用 VpnService.prepare() 方法获取运行所需要的权限,然后调用 establish() 方法创建一个网络隧道(在下一节中讨论)。其中,prepare() 方法返回一个用于启动图 9-10 所示警告框的 intent。这个对话框用来获取用户的许可并且保证在任何情况下,一个设备上只运行一个 VPN 连接。如果 prepare() 被调用的同时,存在另一个应用创建的 VPN 连接,那么该连接将会被中断。同时 prepare() 方法也会保存调用者应用的包名,如果没有被再次调用之前,只有该应用可以启动一个 VPN 连接;或者系统将这个 VPN 连接终止(例如 VPN 应用进程崩溃)。如果 VPN 连接因为任何情况被关闭,系统将会调用当前 VPN 应用的 VpnService 实现中的 onRevoke() 方法。

建立 VPN 连接

在 VPN 应用被准备完成并且获取到了执行所需要的权限之后即可启动 VpnService 组件,创建一个指向 VPN 网关的隧道,然后协商 VPN 连接需要的网络参数。接下来,VpnService 使用协商的参数创建 VpnService.Builder 类,然后调用 VpnService.establish() 方法获取读写数据包用到的文件描述符。其中,establish() 方法将会:首先检查调用者应用和得到当前建立 VPN 连接权限应用的 UID 是否匹配,保证两者相同。然后检查当前 Android 用户是否拥有建立 VPN 连接的权限,并且验证该服务的绑定需要 BIND_VPN_SERVICE 权限;如果服务中没有对这个权限进行要求,就会被认为是不安全的,并且抛出 SecurityException 异常。之后使用原生代码创建并配置一个隧道接口,然后设置路由和 DNS 服务器。

将 VPN 连接状态通知用户

建立 VPN 连接的最后一步是显示一个运行状态通知,告知用户网络流量通过 VPN 隧道进行发送。而且通过相关的控制对话框,用户可以对连接进行监视和控制。OpenVPN Android 应用相关的对话框如图 9-11 所示。

该对话框是专用包 com.android.vpndialogs 的一部分,这个包是非系统用户管理基于应用的 VPN 连接唯一可用的包。这样保证了 VPN 连接只能够通过系统授权的 UI 进行启动和管理。

利用基于应用的 VPN 架构,应用可以自行实现网络隧道并且使用任何认证和加密方法。因为设备发送或接收的所有数据包都会通过 VPN 应用,所以 VPN 应用不仅可以用来建立网络隧道,而且可以用来进行流量记录、过滤和修改(比如屏蔽广告)。

NOTE

若想获取一个可用的、使用 Android 凭据库管理认证密钥和证书的、基于应用的 VPN 工具的实现,请查看 OpenVPN for Android 的源码。该应用实现了一个完全兼容 OpenVPN 服务器的 SSL VPN 客户端。

多用户支持

正如上文提到的,在多用户设备上,只有设备所有者用户才能够控制 legacy VPN。然而,在 Android 4.2 和之后版本中增加了多用户的支持,允许所有次级用户(除了在受限设置,即 restricted profile 的情况下,所有次级用户必须共享主用户的 VPN 连接)启动基于应用的 VPN。虽然这样允许每个用户自行启动各自的 VPN,但是只能同时启动一个基于应用的 VPN,所有设备用户的流量都会通过当前激活的 VPN,启动这个 VPN 的用户是谁并不会造成影响。在 Android 4.4 中引入了对于多用户 VPN 的完整支持:增加了 per-user VPN,允许每个用户使用各自的 VPN 连接,因此实现了多用户之间的隔离。

Linux 高级路由

Android 使用了多个 Linux 内核的高级包过滤和路由特性来实现 per-user VPN。这些特性(通过 netfilter 内核框架实现)包含了 Linux 的 iptables 工具的 owner 模块,能够使用数据包生成进程的 UID、GID 或者 PID 对数据包进行匹配。例如,清单 9-12 中的Œ命令创建了一个丢弃所有 UID 为 1234 的用户发送的数据包的包过滤规则。

清单 9-12:使用 iptables 进行拥有者匹配和数据包标记

此外,netfilter 的另外一个重要特性是能够为特定数据包打上特定的数字标签(mark)。例如,�的规则将所有目标端口为 80(通常是网站服务器)的数据包标记上 0x1。之后这个标记可以被用来进行过滤和路由。比如,通过添加将标记的包发送给预定义的路由表(本例中为 webŽ)的路由规则,将标记的包通过特定的接口进行发送。最后添加一条路由�,将匹配 web 路由表的数据包发送到 em3 接口。

多用户 VPN 的实现

Android 使用之前提到的包过滤和路由特性,将特定 Android 用户的所有应用产生的数据,全部发送给该用户启动的 VPN 应用所创建的隧道。当设备所有者用户启动 VPN 的情况下,所有受限用户无法自行启动 VPN,而是必须共享所有者所创建的 VPN 连接。

这种隔离路由的方法通过系统层 NetworkManagementService 实现,并且提供了管理 UID 或 UID 粒度的包匹配和路由的 API。NetworkManagementService 通过向以 root 运行的原生 netd 守护进程发送命令来实现这些 API,因此可以修改内核的包过滤和路由表。而 netd 通过调用 iptables 和 ip 用户态工具进行内核层包过滤和路由的配置。

之后我们通过一个例子讲解 Android 的 per-user VPN 路由,如清单 9-13 所示。主用户(用户 ID 为 0)和次级用户(用户 ID 为 10)各自启动了一个基于应用的 VPN。主用户的隧道接口为 tun0,次级用户的为 tun1。设备中也包含了一个受限用户,其用户 ID 为 13。清单 9-13 中显示了当这两个 VPN 都被连接成功之后的内核包过滤表状态(忽略了一些不重要的细节)。

清单 9-13:两个不同的设备用户启动 VPN 之后的包匹配规则

向外的数据包将会首先被发送给 st_mangle_OUTPUT 链,其负责对数据包进行匹配和标记。不需要进行 per-user 路由(已经被标记上 0x1Œ)和来自于 legacy VPN(UID 1016�,表示内置 vpn 用户,负责启动 mtd 和 racoon 守护进程)的数据包不被修改直接通过。

接下来,由 UID 在 0 和 99999 之间的进程(由主用户启动的应用进程,详见第 4 章)产生的数据包会被匹配,并被发送给 st_mangle_tun0_ OUTPUT 链Ž。而由受限用户(用户 ID 为 13)启动的进程(UID 在 1300000~1399999 之间)产生的数据包也会被发送到同一个链�。因此,主用户和受限用户的流量会被以同样的方式进行处理。而由第一次级用户(用户 ID 为 10,UID 为 1000000~1099999)产生的数据包会被发送给一个不同的链,st_mangle_ tun1_OUTPUT�。目标链本身非常简单:st_mangle_tun0_OUTPUT 首先会清除包原来的标记并标记上 0x3c‘;st_mangle_tun1_OUTPUT 会做相同的处理,但使用的是标签 0x3d’。在数据包被标记之后,这些标记会被用于实现不同的路由规则,如清单 9-14 所示。

清单 9-14:两个不同的设备用户启动 VPN 之后的路由规则

可以注意到,清单中的两条规则针对每个标记创建,而且关联不同的路由表。拥有 0x3c 标签的数据包会被发送给路由表 60(十六进制 0x3cŒ),而拥有 0x3d 标签的数据包会被发送给路由表 61(十六进制 0x3d�)。路由表 60 将所有流量发送给主用户创建的 tun0 隧道接口Ž,而路由表 61 将所有流量发送给次级用户创建的 tun1 接口�。

NOTE

虽然 Android 4.4 中引入的 VPN 路由方法提供了更多灵活性,而且允许隔离不同用户的流量,但是在撰写本书时,这个实现还有很多问题,特别是在不同接口之间进行切换的 场景下,比如移动网络和 Wi-Fi 之间的相互切换。这些问题在之后的 Android 版本中可能会被改进,可能会修改包过滤链与接口的关联方式,但是基本策略的实现方法应该会保持不变。

">

2016-06-19 23:1056979

评论

发布
暂无评论
发现更多内容

周练习 11

何毅曦

训练营第十一周作业 1

仲夏

第十一周作业

alpha

极客大学架构师训练营

第七周作业

晴空万里

极客大学架构师训练营

架构师训练营 Week 13 总结

Wancho

架构师训练营 Week 14 总结

Wancho

架构师训练营第十一周课后作业

Gosling

极客大学架构师训练营

架构师训练营第十一周作业

Shunyi

极客大学架构师训练营

第十一周总结

alpha

极客大学架构师训练营

区块链系统面临的风险和防范

CECBC

区块链 系统

苏州派发数字人民币红包:挺进线上消费场景,“双离线”功能首次曝光

CECBC

数字红包

作业-第7周 性能优化一

arcyao

水滴互助上链:利用区块链技术打造透明安全互助业务

CECBC

区块链

架构师训练营 Week 8 总结

Wancho

第十一周作业

极客大学架构师训练营

训练营第十一周作业2

仲夏

沉默的性能杀手 - false sharing

helbing

Go 语言

架构师训练营 第七周作业

文江

第七周-作业

ray-arch

11周作业

橘子皮嚼着不脆

架构师训练营 week7 学习总结

花果山

极客大学架构师训练营

架构师训练营第二期 Week 7 作业

bigxiang

极客大学架构师训练营

第十一周作业

Geek_ce484f

极客大学架构师训练营

week11作业

龙卷风

架构师一期

架构师训练营第七周学习笔记

李日盛

笔记

架构师训练营第十一周学习总结

Gosling

极客大学架构师训练营

架构师训练营 week7 课后作业

花果山

极客大学架构师训练营

第十一周总结

Geek_ce484f

极客大学架构师训练营

面试官:说说你对【注解】的理解

田维常

架构师训练营第二期 Week 7 总结

bigxiang

极客大学架构师训练营

ShardingSphere RAW JDBC 分布式事务 Atomikos XA 代码示例

Java MySQL 数据库 分布式事务 ShardingSphere

Android VPN实现原理介绍_Android/iOS_Nikolay Elenkov_InfoQ精选文章