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:
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):
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.
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.
The built-in 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.
The built-in 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.
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 built-in 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.