VPN

Overview

From the perspective of Shill's architecture, VPN is inherently different from physical connections because the corresponding Device (in this case representing a virtual interface) may not exist when a Connect is requested. Therefore the standard means of a Service passing Connect requests over to its corresponding Device does not work. Also since the VirtualDevice type is not unique to a particular VPN solution (PPPDevices, for example, are used for cellular dongles, PPPoE, and L2TP/IPSec VPNs), the VPN-specific logic cannot be contained within the VirtualDevice instance. Note that PPPoE is in a similar position but solves this by piggybacking off of the Ethernet instance that will carry the PPPoE traffic, moving the complexity of PPPoE configuration into Ethernet code, which creates a PPPoEService rather than EthernetService when PPPoE is used.

For VPN, this is solved through the use of VPNDrivers. A VPNDriver takes care of attaining a proper VirtualDevice, communicating with processes outside of Shill which implement some part of the VPN functionality, and setting up routes and routing rules for the corresponding VirtualDevice. Thus a VPNService passes Connect and Disconnect requests to its corresponding VPNDriver. Note that VPNDriver D-Bus properties are exposed through the owning VPNService; VPNDrivers are an implementation detail that is not exposed to D-Bus clients.

ChromeOS supports 4 types of VPN solutions:

  • Android 3rd-party VPN apps in ARC
  • Native L2TP/IPSec VPN
  • Native OpenVPN
  • Chrome Extension VPN App

Each of these types has a corresponding VPNDriver child which contains the functionality needed on the Shill-side to support that VPN solution (note that Shill's involvement varies between different types of VPNs):

VPNDriver Inheritance

When a VPNService is created by VPNProvider (whether from a Manager ConfigureService D-Bus call or from a Profile containing an already-configured VPNService), the “Provider.Type” Service property is used specify what type of VPNDriver that VPNService should use. Note that “Provider.Type” is only valid for Services whose “Type” property is of value “vpn”. See VPNProvider::CreateServiceInner for more details.

VPN Types

Android 3rd-party VPN in ARC

Android 3rd-party VPNs (implemented using the Android VpnService API in ARC are the VPN type requiring the least amount of functionality within Shill, where the majority of the ArcVpnDriver functionality is just setting up routing properly. arc-networkd creates an ARC bridge, which serves as a host-side (v4 NAT-ed) proxy for the arc0 interface on the Android-side. In addition, arc-networkd creates a corresponding arc_${IFNAME} interface for each interface named ${IFNAME} exposed by the Shill Manager (see arc-networkd DeviceManager::OnDevicesChanged for more detail). This allows traffic from the Android-side to have a specific host-side interface that will carry it.

Traffic that needs to pass through the VPN gets sent to the ARC bridge rather than out of a physical interface. VPN-tunnelled traffic will then be sent out of Android to arc_${IFNAME} interfaces to actually send the traffic out of the system.

Internally, Chrome's ArcNetHostImpl and the ARC ArcNetworkBridge communicate between each other to create the appropriate behavior as specified by the ARC net.mojom interface. For example, the ARC VpnTracker will trigger ArcNetworkBridge.androidVpnConnected when an Android VPN connects. This triggers ArcNetHostImpl::AndroidVpnConnected on the Chrome-side, which will Connect the appropriate VpnService in Shill, first configuring a new VpnService in Shill if needed.

Native L2TP/IPSec VPN

The native L2TP/IPSec VPN is implemented with multiple projects, the two Chrome OS components being the Shill L2TPIPSecDriver and the vpn-manager project. The vpn-manager project (in particular the l2tpipsec_vpn binary) serves to create the L2TP/IPSec nested tunnels that define the L2TP/IPSec VPN. l2tpipsec_vpn creates the outer IPSec tunnel using strongSwan, while the inner L2TP tunnel is created by xl2tpd (which itself uses pppd).

Note: There are actually two distinct L2TP standards (distinguished as L2TPv2 and L2TPv3). RFC 2661 defines L2TPv2, which is a protocol specifically designed for the tunnelling of PPP traffic. RFC 3931 generalizes L2TPv2 such that the assumption of the L2 protocol being PPP is removed. L2TP/IPSec is described in RFC 3193, which--as the RFC numbers might suggest--is based on L2TPv2. In particular, xl2tpd is an implementation of RFC 2661, and all references to L2TP in Shill and vpn-manager are specifically referencing L2TPv2.

Upon a Connect request, L2TPIPSecDriver spawns an l2tpipsec_vpn process, passing the proper configuration flags and options given the configuration of relevant Service properties. One important configuration option set is the “--pppd_plugin” option, which is used so that L2TPIPSecDriver can get updates from the pppd process created by xl2tpd, which passes messages to L2TPIPSecDriver::Notify. One use of the Notify method is to get information of the PPP interface created by pppd when the PPP connection is established, which is used to create the corresponding PPPDevice instance. The Notify method is also used to get information about a disconnection, although L2TPIPSecDriver::OnL2TPIPSecVPNDied also serves that purpose but receives the exit status from l2tpipsec_vpn rather than pppd.

Native OpenVPN

The native OpenVPN implementation consists primarily of the open-source OpenVPN project, and of Shill's OpenVPNDriver and OpenVPNManagementServer. Upon a Connect request, OpenVPNDriver creates a TUN interface and spawns an openvpn process, passing a set of command-line options including the interface name of the created TUN interface (using the OpenVPN “dev” option). Shill interacts with the spawned openvpn process in two distinct ways.

One interaction is between openvpn and OpenVPNDriver::Notify. The OpenVPN “up” and “up-restart” options are set so that the shill openvpn_script is called when openvpn first opens the TUN interface and whenever openvpn restarts. This script leads to OpenVPNDriver::Notify being invoked (through OpenVPNDriver::rpc_task_), which will process environment variables passed by openvpn in order to populate an IPConfig::Properties instance appropriately.

Note: From the OpenVPN documentation:

On restart, OpenVPN will not pass the full set of environment variables to the script. Namely, everything related to routing and gateways will not be passed, as nothing needs to be done anyway – all the routing setup is already in place.

The other interaction is between openvpn and OpenVPNManagementServer. OpenVPN provides the concept of a management server, which is an entity external to the openvpn process which provides administrative control. Communication between the openvpn and management server processes occurs either over TCP or unix domain sockets. In this case, OpenVPNManagementServer uses a TCP socket over 127.0.0.1 to communicate with the OpenVPN client. This allows for Shill to control openvpn behavior like holds (keeping openvpn hibernated until the hold is released) and restarts (triggered by sending “signal SIGUSR1” over the socket), but also allows for openvpn to send information like state changes and failure events back over to Shill (see OpenVPNManagementServer::OnInput). To clarify, the communication between OpenVPNManagementServer and openvpn is an out-of-band control channel; since openvpn already has the TUN interface opened, the Shill-side is not involved with processing data packets themselves.

Chrome Extension VPN App

ThirdPartyVpnDriver exposes the Shill ThirdPartyVpn API through ThirdPartyVpnDBusAdaptor, which Chrome VpnService instances use, such that Chrome VPN App information can be passed between Shill and Chrome. Chrome's VpnService is wrapped by VpnThreadExtensionFunction children to create the Chrome vpnProvider API for Chrome Apps.

When the Shill VpnService receives a Connect call, the ThirdPartyVpnDriver will create a TUN interface where packets received on the interface reach ThirdPartyVpnDriver::OnInput as a vector of bytes. Within OnInput, IPv4 packets are sent using the OnPacketReceived D-Bus signal, which Chrome's VpnService will forward to the Chrome VPN App. In the other direction, Chrome VPN Apps use the SendPacket vpnProvider function to cause its Chrome VpnService to call the SendPacket D-Bus method on the corresponding ThirdPartyVpnDriver in Shill, which causes the driver to send that packet to the TUN interface. Understandably, the performance of Chrome App VPNs is not optimal, but the performance drawbacks of this design are embedded in the ThirdPartyVpn and Chrome vpnProvider APIs, as opposed to being hidden implementation details. One can contrast this with how native OpenVPN above works, where the TUN interface is passed to openvpn so that the Shill <-> external VPN entity communication is exclusively a control channel rather than both a control and data channel.