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, and L2TP/IPsec VPNs), the VPN-specific logic cannot
be contained within the VirtualDevice
instance.
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
- Built-in L2TP/IPsec VPN
- Built-in 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):
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. patchpanel creates an ARC bridge, which serves as a host-side (v4
NAT-ed) proxy for the arc0 interface on the Android-side. In addition,
patchpanel creates a corresponding arc_${IFNAME} interface for each interface
named ${IFNAME} exposed by the Shill Manager
(see patchpanel
Manager::OnShillDevicesChanged
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.