blob: 15baf715ff3bdf6f3882bf8796ff5205fc5847d8 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shill/vpn/openvpn_driver.h"
#include <arpa/inet.h>
#include <limits>
#include <base/files/file_util.h>
#include <base/stl_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <chromeos/dbus/service_constants.h>
#include "shill/certificate_file.h"
#include "shill/connection.h"
#include "shill/device_info.h"
#include "shill/error.h"
#include "shill/ipconfig.h"
#include "shill/logging.h"
#include "shill/manager.h"
#include "shill/net/sockets.h"
#include "shill/process_manager.h"
#include "shill/rpc_task.h"
#include "shill/virtual_device.h"
#include "shill/vpn/openvpn_management_server.h"
#include "shill/vpn/vpn_service.h"
using base::FilePath;
using base::SplitString;
using base::Unretained;
using std::map;
using std::string;
using std::vector;
namespace shill {
namespace Logging {
static auto kModuleLogScope = ScopeLogger::kVPN;
static string ObjectID(const OpenVPNDriver*) {
return "(openvpn_driver)";
}
} // namespace Logging
namespace {
const char kChromeOSReleaseName[] = "CHROMEOS_RELEASE_NAME";
const char kChromeOSReleaseVersion[] = "CHROMEOS_RELEASE_VERSION";
const char kOpenVPNForeignOptionPrefix[] = "foreign_option_";
const char kOpenVPNIfconfigBroadcast[] = "ifconfig_broadcast";
const char kOpenVPNIfconfigLocal[] = "ifconfig_local";
const char kOpenVPNIfconfigNetmask[] = "ifconfig_netmask";
const char kOpenVPNIfconfigRemote[] = "ifconfig_remote";
const char kOpenVPNRedirectGateway[] = "redirect_gateway";
const char kOpenVPNRouteOptionPrefix[] = "route_";
const char kOpenVPNRouteNetGateway[] = "route_net_gateway";
const char kOpenVPNRouteVPNGateway[] = "route_vpn_gateway";
const char kOpenVPNTrustedIP[] = "trusted_ip";
const char kOpenVPNTunMTU[] = "tun_mtu";
// Typically OpenVPN will set environment variables like:
// route_net_gateway=<existing default LAN gateway>
// route_vpn_gateway=10.8.0.1
// route_gateway_1=10.8.0.1
// route_netmask_1=255.255.255.0
// route_network_1=192.168.10.0
// This example shows a split include route of 192.168.10.0/24, and
// 10.8.0.1 is the ifconfig_remote (remote peer) address.
const char kOpenVPNRouteNetworkPrefix[] = "network_";
const char kOpenVPNRouteNetmaskPrefix[] = "netmask_";
const char kOpenVPNRouteGatewayPrefix[] = "gateway_";
const char kDefaultPKCS11Provider[] = "libchaps.so";
// Some configurations pass the netmask in the ifconfig_remote property.
// This is due to some servers not explicitly indicating that they are using
// a "broadcast mode" network instead of peer-to-peer. See
// http://crbug.com/241264 for an example of this issue.
const char kSuspectedNetmaskPrefix[] = "255.";
void DoNothingWithExitStatus(int exit_status) {}
} // namespace
// static
const char OpenVPNDriver::kDefaultCACertificates[] =
"/etc/ssl/certs/ca-certificates.crt";
// static
const char OpenVPNDriver::kOpenVPNPath[] = "/usr/sbin/openvpn";
// static
const char OpenVPNDriver::kOpenVPNScript[] = SHIMDIR "/openvpn-script";
// static
const VPNDriver::Property OpenVPNDriver::kProperties[] = {
{kOpenVPNAuthNoCacheProperty, 0},
{kOpenVPNAuthProperty, 0},
{kOpenVPNAuthRetryProperty, 0},
{kOpenVPNAuthUserPassProperty, 0},
{kOpenVPNCipherProperty, 0},
{kOpenVPNClientCertIdProperty, Property::kCredential},
{kOpenVPNCompLZOProperty, 0},
{kOpenVPNCompNoAdaptProperty, 0},
{kOpenVPNCompressProperty, 0},
{kOpenVPNExtraHostsProperty, Property::kArray},
{kOpenVPNIgnoreDefaultRouteProperty, 0},
{kOpenVPNKeyDirectionProperty, 0},
{kOpenVPNNsCertTypeProperty, 0},
{kOpenVPNOTPProperty,
Property::kEphemeral | Property::kCredential | Property::kWriteOnly},
{kOpenVPNPasswordProperty, Property::kCredential | Property::kWriteOnly},
{kOpenVPNPinProperty, Property::kCredential},
{kOpenVPNPortProperty, 0},
{kOpenVPNProtoProperty, 0},
{kOpenVPNPushPeerInfoProperty, 0},
{kOpenVPNRemoteCertEKUProperty, 0},
{kOpenVPNRemoteCertKUProperty, 0},
{kOpenVPNRemoteCertTLSProperty, 0},
{kOpenVPNRenegSecProperty, 0},
{kOpenVPNServerPollTimeoutProperty, 0},
{kOpenVPNShaperProperty, 0},
{kOpenVPNStaticChallengeProperty, 0},
{kOpenVPNTLSAuthContentsProperty, 0},
{kOpenVPNTLSRemoteProperty, 0},
{kOpenVPNTLSVersionMinProperty, 0},
{kOpenVPNTokenProperty,
Property::kEphemeral | Property::kCredential | Property::kWriteOnly},
{kOpenVPNUserProperty, 0},
{kProviderHostProperty, 0},
{kProviderTypeProperty, 0},
{kOpenVPNCaCertPemProperty, Property::kArray},
{kOpenVPNExtraCertPemProperty, Property::kArray},
{kOpenVPNPingExitProperty, 0},
{kOpenVPNPingProperty, 0},
{kOpenVPNPingRestartProperty, 0},
{kOpenVPNTLSAuthProperty, 0},
{kOpenVPNVerbProperty, 0},
{kOpenVPNVerifyHashProperty, 0},
{kOpenVPNVerifyX509NameProperty, 0},
{kOpenVPNVerifyX509TypeProperty, 0},
{kVPNMTUProperty, 0},
};
const char OpenVPNDriver::kLSBReleaseFile[] = "/etc/lsb-release";
// Directory where OpenVPN configuration files are exported while the
// process is running.
const char OpenVPNDriver::kDefaultOpenVPNConfigurationDirectory[] =
RUNDIR "/openvpn_config";
const int OpenVPNDriver::kConnectTimeoutSeconds = 2 * 60;
const int OpenVPNDriver::kReconnectOfflineTimeoutSeconds = 2 * 60;
const int OpenVPNDriver::kReconnectTLSErrorTimeoutSeconds = 20;
OpenVPNDriver::OpenVPNDriver(Manager* manager, ProcessManager* process_manager)
: VPNDriver(manager, process_manager, kProperties, base::size(kProperties)),
management_server_(new OpenVPNManagementServer(this)),
certificate_file_(new CertificateFile()),
extra_certificates_file_(new CertificateFile()),
lsb_release_file_(kLSBReleaseFile),
openvpn_config_directory_(kDefaultOpenVPNConfigurationDirectory),
pid_(0) {}
OpenVPNDriver::~OpenVPNDriver() {
Cleanup();
}
void OpenVPNDriver::FailService(Service::ConnectFailure failure,
const string& error_details) {
SLOG(this, 2) << __func__ << "(" << error_details << ")";
Cleanup();
if (service_callback_) {
service_callback_.Run(VPNService::kEventDriverFailure, failure,
error_details);
service_callback_.Reset();
}
}
void OpenVPNDriver::Cleanup() {
StopConnectTimeout();
// Disconnecting the management interface will terminate the openvpn
// process. Ensure this is handled robustly by first unregistering
// the callback for OnOpenVPNDied, and then terminating and reaping
// the process with StopProcess().
if (pid_) {
process_manager()->UpdateExitCallback(pid_,
base::Bind(DoNothingWithExitStatus));
}
management_server_->Stop();
if (!tls_auth_file_.empty()) {
base::DeleteFile(tls_auth_file_);
tls_auth_file_.clear();
}
if (!openvpn_config_file_.empty()) {
base::DeleteFile(openvpn_config_file_);
openvpn_config_file_.clear();
}
rpc_task_.reset();
ip_properties_ = IPConfig::Properties();
if (pid_) {
process_manager()->StopProcessAndBlock(pid_);
pid_ = 0;
}
interface_name_.clear();
}
// static
string OpenVPNDriver::JoinOptions(const vector<vector<string>>& options,
char separator) {
vector<string> option_strings;
for (const auto& option : options) {
vector<string> quoted_option;
for (const auto& argument : option) {
if (argument.find(' ') != string::npos ||
argument.find('\t') != string::npos ||
argument.find('"') != string::npos ||
argument.find(separator) != string::npos) {
string quoted_argument(argument);
const char separator_chars[] = {separator, '\0'};
base::ReplaceChars(argument, separator_chars, " ", &quoted_argument);
base::ReplaceChars(quoted_argument, "\\", "\\\\", &quoted_argument);
base::ReplaceChars(quoted_argument, "\"", "\\\"", &quoted_argument);
quoted_option.push_back("\"" + quoted_argument + "\"");
} else {
quoted_option.push_back(argument);
}
}
option_strings.push_back(base::JoinString(quoted_option, " "));
}
return base::JoinString(option_strings, string{separator});
}
bool OpenVPNDriver::WriteConfigFile(const vector<vector<string>>& options,
FilePath* config_file) {
if (!base::DirectoryExists(openvpn_config_directory_)) {
if (!base::CreateDirectory(openvpn_config_directory_)) {
LOG(ERROR) << "Unable to create configuration directory "
<< openvpn_config_directory_.value();
return false;
}
// OpenVPN running as user 'openvpn' needs access to the config directory,
// and openvpn user is not member of shill group so make the dir
// world-readable. We'd rather not have openvpn belong to shill group since
// shill is more privileged than openvpn, hence the idea of 'dropping'
// UID/GID from shill to openvpn. Moreover since shill no longer runs with
// CAP_CHOWN, we can't chown the dir to shill:openvpn.
if (chmod(openvpn_config_directory_.value().c_str(),
S_IRWXU | S_IRWXG | S_IROTH)) {
LOG(ERROR) << "Failed to set permissions on "
<< openvpn_config_directory_.value();
base::DeletePathRecursively(openvpn_config_directory_);
return false;
}
}
string contents = JoinOptions(options, '\n');
contents.push_back('\n');
if (!base::CreateTemporaryFileInDir(openvpn_config_directory_, config_file) ||
base::WriteFile(*config_file, contents.data(), contents.size()) !=
static_cast<int>(contents.size()) ||
chmod(config_file->value().c_str(), S_IRWXU | S_IRWXG | S_IROTH)) {
// Make the config file world-readable. Same rationale as listed above for
// the config directory.
LOG(ERROR) << "Unable to setup OpenVPN config file.";
return false;
}
return true;
}
bool OpenVPNDriver::SpawnOpenVPN() {
SLOG(this, 2) << __func__ << "(" << interface_name_ << ")";
vector<vector<string>> options;
Error error;
pid_t openvpn_pid;
InitOptions(&options, &error);
if (error.IsFailure()) {
return false;
}
LOG(INFO) << "OpenVPN process options: " << JoinOptions(options, ',');
if (!WriteConfigFile(options, &openvpn_config_file_)) {
return false;
}
// TODO(quiche): This should be migrated to use ExternalTask.
// (crbug.com/246263).
CHECK(!pid_);
vector<string> args = GetCommandLineArgs();
LOG(INFO) << "OpenVPN command line args: " << base::JoinString(args, " ");
// OpenSSL compatibility settings.
// TODO(crbug.com/1047146): Drop these stop-gaps after addressing the
// underlying problems described in the bug.
const map<string, string> kEnv = {
{"OPENSSL_CONF", "/etc/ssl/openssl.cnf.compat"},
{"OPENSSL_CHROMIUM_SKIP_TRUSTED_PURPOSE_CHECK", "1"},
{"OPENSSL_CHROMIUM_GENERATE_METRICS", "1"},
};
uint64_t capmask = CAP_TO_MASK(CAP_NET_ADMIN) | CAP_TO_MASK(CAP_NET_RAW) |
CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID);
openvpn_pid = process_manager()->StartProcessInMinijail(
FROM_HERE, base::FilePath(kOpenVPNPath), args, kEnv, "shill", "shill",
capmask, true, true,
base::Bind(&OpenVPNDriver::OnOpenVPNDied, base::Unretained(this)));
if (openvpn_pid == -1) {
LOG(ERROR) << "Minijail couldn't run our child process";
return false;
}
pid_ = openvpn_pid;
return true;
}
void OpenVPNDriver::OnOpenVPNDied(int exit_status) {
SLOG(nullptr, 2) << __func__ << "(" << pid_ << ", " << exit_status << ")";
pid_ = 0;
FailService(Service::kFailureInternal, Service::kErrorDetailsNone);
// TODO(petkov): Figure if we need to restart the connection.
}
void OpenVPNDriver::GetLogin(string* /*user*/, string* /*password*/) {
NOTREACHED();
}
void OpenVPNDriver::Notify(const string& reason,
const map<string, string>& dict) {
LOG(INFO) << "IP configuration received: " << reason;
// We only registered "--up" script so this should be the only
// reason we get notified here. Note that "--up-restart" is set
// so we will get notification also upon reconnection.
if (reason != "up") {
LOG(DFATAL) << "Unexpected notification reason";
return;
}
// On restart/reconnect, update the existing IP configuration.
ParseIPConfiguration(dict, &ip_properties_);
ReportConnectionMetrics();
StopConnectTimeout();
if (service_callback_) {
service_callback_.Run(VPNService::kEventConnectionSuccess,
Service::kFailureNone, Service::kErrorDetailsNone);
} else {
LOG(DFATAL) << "Missing service callback";
}
}
IPConfig::Properties OpenVPNDriver::GetIPProperties() const {
return ip_properties_;
}
void OpenVPNDriver::ParseIPConfiguration(
const map<string, string>& configuration,
IPConfig::Properties* properties) const {
ForeignOptions foreign_options;
RouteOptions routes;
bool redirect_gateway = false;
properties->address_family = IPAddress::kFamilyIPv4;
if (!properties->subnet_prefix) {
properties->subnet_prefix =
IPAddress::GetMaxPrefixLength(properties->address_family);
}
for (const auto& configuration_map : configuration) {
const string& key = configuration_map.first;
const string& value = configuration_map.second;
SLOG(this, 2) << "Processing: " << key << " -> " << value;
if (base::LowerCaseEqualsASCII(key, kOpenVPNIfconfigLocal)) {
properties->address = value;
} else if (base::LowerCaseEqualsASCII(key, kOpenVPNIfconfigBroadcast)) {
properties->broadcast_address = value;
} else if (base::LowerCaseEqualsASCII(key, kOpenVPNIfconfigNetmask)) {
properties->subnet_prefix =
IPAddress::GetPrefixLengthFromMask(properties->address_family, value);
} else if (base::LowerCaseEqualsASCII(key, kOpenVPNIfconfigRemote)) {
if (base::StartsWith(value, kSuspectedNetmaskPrefix,
base::CompareCase::INSENSITIVE_ASCII)) {
LOG(WARNING) << "Option " << key << " value " << value
<< " looks more like a netmask than a peer address; "
<< "assuming it is the former.";
// In this situation, the "peer_address" value will be left
// unset and Connection::UpdateFromIPConfig() will treat the
// interface as if it were a broadcast-style network. The
// kernel will, automatically set the peer address equal to
// the local address.
properties->subnet_prefix = IPAddress::GetPrefixLengthFromMask(
properties->address_family, value);
} else {
// This creates an explicit route to the peer address in SetRoutes().
properties->peer_address = value;
}
} else if (base::LowerCaseEqualsASCII(key, kOpenVPNRedirectGateway)) {
redirect_gateway = true;
} else if (base::LowerCaseEqualsASCII(key, kOpenVPNTrustedIP)) {
size_t prefix = IPAddress::GetMaxPrefixLength(properties->address_family);
properties->exclusion_list.push_back(value + "/" +
base::NumberToString(prefix));
} else if (base::LowerCaseEqualsASCII(key, kOpenVPNTunMTU)) {
int mtu = 0;
if (base::StringToInt(value, &mtu) && mtu >= IPConfig::kMinIPv4MTU) {
properties->mtu = mtu;
} else {
LOG(ERROR) << "MTU " << value << " ignored.";
}
} else if (base::StartsWith(key, kOpenVPNForeignOptionPrefix,
base::CompareCase::INSENSITIVE_ASCII)) {
const string suffix = key.substr(strlen(kOpenVPNForeignOptionPrefix));
int order = 0;
if (base::StringToInt(suffix, &order)) {
foreign_options[order] = value;
} else {
LOG(ERROR) << "Ignored unexpected foreign option suffix: " << suffix;
}
} else if (base::LowerCaseEqualsASCII(key, kOpenVPNRouteNetGateway) ||
base::LowerCaseEqualsASCII(key, kOpenVPNRouteVPNGateway)) {
// These options are unused. Catch them here so that they don't
// get passed to ParseRouteOption().
} else if (base::StartsWith(key, kOpenVPNRouteOptionPrefix,
base::CompareCase::INSENSITIVE_ASCII)) {
ParseRouteOption(key.substr(strlen(kOpenVPNRouteOptionPrefix)), value,
&routes);
} else {
SLOG(this, 2) << "Key ignored.";
}
}
ParseForeignOptions(foreign_options, properties);
// Since we use persist-tun, we expect that a reconnection will use the same
// routes *and* that OpenVPN will not re-provide us with all the needed
// routing information. Simply re-use the routing information we attained from
// the initial connection.
if (!properties->routes.empty())
return;
// Ignore the route_vpn_gateway parameter as VPNs don't need gateway IPs.
// This guarantees that we will pass the various sanity checks in
// connection.cc.
properties->gateway = properties->address;
if (redirect_gateway &&
const_args()->Contains<string>(kOpenVPNIgnoreDefaultRouteProperty)) {
LOG(INFO) << "Ignoring default route parameter as requested by "
<< "configuration.";
redirect_gateway = false;
}
properties->default_route = properties->blackhole_ipv6 = redirect_gateway;
SetRoutes(routes, properties);
}
// static
void OpenVPNDriver::ParseForeignOptions(const ForeignOptions& options,
IPConfig::Properties* properties) {
vector<string> domain_search;
vector<string> dns_servers;
for (const auto& option_map : options) {
ParseForeignOption(option_map.second, &domain_search, &dns_servers);
}
if (!domain_search.empty()) {
properties->domain_search.swap(domain_search);
}
LOG_IF(INFO, properties->domain_search.empty())
<< "No search domains provided.";
if (!dns_servers.empty()) {
properties->dns_servers.swap(dns_servers);
}
LOG_IF(WARNING, properties->dns_servers.empty())
<< "No DNS servers provided.";
}
// static
void OpenVPNDriver::ParseForeignOption(const string& option,
vector<string>* domain_search,
vector<string>* dns_servers) {
SLOG(nullptr, 2) << __func__ << "(" << option << ")";
vector<string> tokens =
SplitString(option, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (tokens.size() != 3 ||
!base::LowerCaseEqualsASCII(tokens[0], "dhcp-option")) {
return;
}
if (base::LowerCaseEqualsASCII(tokens[1], "domain")) {
domain_search->push_back(tokens[2]);
} else if (base::LowerCaseEqualsASCII(tokens[1], "dns")) {
dns_servers->push_back(tokens[2]);
}
}
// static
IPConfig::Route* OpenVPNDriver::GetRouteOptionEntry(const string& prefix,
const string& key,
RouteOptions* routes) {
int order = 0;
if (!base::StartsWith(key, prefix, base::CompareCase::INSENSITIVE_ASCII) ||
!base::StringToInt(key.substr(prefix.size()), &order)) {
return nullptr;
}
return &(*routes)[order];
}
// static
void OpenVPNDriver::ParseRouteOption(const string& key,
const string& value,
RouteOptions* routes) {
// IPv4 uses route_{network,netmask,gateway}_<index>
// IPv6 uses route_ipv6_{network,gateway}_<index>
IPConfig::Route* route =
GetRouteOptionEntry(kOpenVPNRouteNetworkPrefix, key, routes);
if (route) {
route->host = value;
return;
}
route = GetRouteOptionEntry(kOpenVPNRouteNetmaskPrefix, key, routes);
if (route) {
route->prefix =
IPAddress::GetPrefixLengthFromMask(IPAddress::kFamilyIPv4, value);
return;
}
route = GetRouteOptionEntry(kOpenVPNRouteGatewayPrefix, key, routes);
if (route) {
route->gateway = value;
return;
}
LOG(WARNING) << "Unknown route option ignored: " << key;
}
// static
void OpenVPNDriver::SetRoutes(const RouteOptions& routes,
IPConfig::Properties* properties) {
vector<IPConfig::Route> new_routes;
int32_t max_prefix =
IPAddress::GetMaxPrefixLength(properties->address_family);
if (!properties->peer_address.empty()) {
// --topology net30 or p2p will set ifconfig_remote
// Setting a point-to-point address in the kernel will create a route
// in RT_TABLE_MAIN instead of our per-device table. To avoid this,
// create an explicit host route here, and clear
// |properties->peer_address.|
IPConfig::Route route(properties->peer_address, max_prefix,
properties->address);
new_routes.push_back(route);
properties->peer_address.clear();
} else if (properties->subnet_prefix != max_prefix) {
// --topology subnet will set ifconfig_netmask instead
IPAddress network_addr(properties->address);
if (network_addr.family() != properties->address_family) {
LOG(WARNING) << "Error obtaining network address for "
<< properties->address;
} else {
network_addr.set_prefix(properties->subnet_prefix);
IPConfig::Route route(network_addr.GetNetworkPart().ToString(),
properties->subnet_prefix, properties->address);
new_routes.push_back(route);
}
}
// Ignore |route.gateway|. If it's wrong, it can cause the kernel to
// refuse to add the route. If it's correct, it has no effect anyway.
for (const auto& route_map : routes) {
const IPConfig::Route& route = route_map.second;
if (route.host.empty() || route.gateway.empty()) {
LOG(WARNING) << "Ignoring incomplete route: " << route_map.first;
continue;
}
IPConfig::Route new_route(route.host, route.prefix, properties->address);
new_routes.push_back(new_route);
}
if (!new_routes.empty()) {
properties->routes.swap(new_routes);
} else if (!properties->default_route) {
LOG(WARNING) << "No routes provided.";
}
}
// static
bool OpenVPNDriver::SplitPortFromHost(const string& host,
string* name,
string* port) {
vector<string> tokens =
SplitString(host, ":", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
int port_number = 0;
if (tokens.size() != 2 || tokens[0].empty() || tokens[1].empty() ||
!base::IsAsciiDigit(tokens[1][0]) ||
!base::StringToInt(tokens[1], &port_number) ||
port_number > std::numeric_limits<uint16_t>::max()) {
return false;
}
*name = tokens[0];
*port = tokens[1];
return true;
}
void OpenVPNDriver::ConnectAsync(
const VPNService::DriverEventCallback& callback) {
service_callback_ = callback;
if (interface_name_.empty()) {
LOG(DFATAL) << "Tunnel interface name needs to be set before connecting.";
FailService(Service::kFailureInternal, "Invalid tunnel interface");
return;
}
StartConnectTimeout(kConnectTimeoutSeconds);
rpc_task_.reset(new RpcTask(control_interface(), this));
if (!SpawnOpenVPN()) {
FailService(Service::kFailureInternal, Service::kErrorDetailsNone);
}
}
void OpenVPNDriver::InitOptions(vector<vector<string>>* options, Error* error) {
string vpnhost = args()->Lookup<string>(kProviderHostProperty, "");
if (vpnhost.empty()) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"VPN host not specified.");
return;
}
AppendOption("client", options);
AppendOption("tls-client", options);
AppendRemoteOption(vpnhost, options);
if (args()->Contains<vector<string>>(kOpenVPNExtraHostsProperty)) {
for (const auto& host :
args()->Get<vector<string>>(kOpenVPNExtraHostsProperty)) {
AppendRemoteOption(host, options);
}
}
AppendOption("mark", "1280", options); // 0x500: source type = 5 (Native VPN)
AppendOption("nobind", options);
AppendOption("persist-key", options);
AppendOption("persist-tun", options);
if (interface_name_.empty()) {
LOG(DFATAL) << "Tunnel interface name needs to be set before connecting.";
Error::PopulateAndLog(FROM_HERE, error, Error::kInternalError,
"Invalid tunnel interface");
return;
}
AppendOption("dev", interface_name_, options);
AppendOption("dev-type", "tun", options);
InitLoggingOptions(options);
AppendValueOption(kVPNMTUProperty, "mtu", options);
AppendValueOption(kOpenVPNProtoProperty, "proto", options);
AppendValueOption(kOpenVPNPortProperty, "port", options);
AppendValueOption(kOpenVPNTLSAuthProperty, "tls-auth", options);
{
string contents =
args()->Lookup<string>(kOpenVPNTLSAuthContentsProperty, "");
if (!contents.empty()) {
if (!base::CreateTemporaryFile(&tls_auth_file_) ||
base::WriteFile(tls_auth_file_, contents.data(), contents.size()) !=
static_cast<int>(contents.size())) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInternalError,
"Unable to setup tls-auth file.");
return;
}
AppendOption("tls-auth", tls_auth_file_.value(), options);
}
}
if (args()->Contains<string>(kOpenVPNTLSVersionMinProperty)) {
AppendOption("tls-version-min",
args()->Get<string>(kOpenVPNTLSVersionMinProperty), options);
}
string tls_remote = args()->Lookup<string>(kOpenVPNTLSRemoteProperty, "");
if (!tls_remote.empty()) {
AppendOption("verify-x509-name", tls_remote, "name-prefix", options);
}
AppendValueOption(kOpenVPNCipherProperty, "cipher", options);
AppendValueOption(kOpenVPNAuthProperty, "auth", options);
AppendFlag(kOpenVPNAuthNoCacheProperty, "auth-nocache", options);
AppendValueOption(kOpenVPNAuthRetryProperty, "auth-retry", options);
AppendFlag(kOpenVPNCompLZOProperty, "comp-lzo", options);
AppendFlag(kOpenVPNCompNoAdaptProperty, "comp-noadapt", options);
AppendValueOption(kOpenVPNCompressProperty, "compress", options);
AppendFlag(kOpenVPNPushPeerInfoProperty, "push-peer-info", options);
AppendValueOption(kOpenVPNRenegSecProperty, "reneg-sec", options);
AppendValueOption(kOpenVPNShaperProperty, "shaper", options);
AppendValueOption(kOpenVPNServerPollTimeoutProperty, "server-poll-timeout",
options);
if (!InitCAOptions(options, error)) {
return;
}
// Additional remote certificate verification options.
InitCertificateVerifyOptions(options);
if (!InitExtraCertOptions(options, error)) {
return;
}
// Client-side ping support.
AppendValueOption(kOpenVPNPingProperty, "ping", options);
AppendValueOption(kOpenVPNPingExitProperty, "ping-exit", options);
AppendValueOption(kOpenVPNPingRestartProperty, "ping-restart", options);
AppendValueOption(kOpenVPNNsCertTypeProperty, "ns-cert-type", options);
InitClientAuthOptions(options);
InitPKCS11Options(options);
// TLS support.
string remote_cert_tls =
args()->Lookup<string>(kOpenVPNRemoteCertTLSProperty, "");
if (remote_cert_tls.empty()) {
remote_cert_tls = "server";
}
if (remote_cert_tls != "none") {
AppendOption("remote-cert-tls", remote_cert_tls, options);
}
AppendValueOption(kOpenVPNKeyDirectionProperty, "key-direction", options);
AppendValueOption(kOpenVPNRemoteCertEKUProperty, "remote-cert-eku", options);
AppendDelimitedValueOption(kOpenVPNRemoteCertKUProperty, "remote-cert-ku",
' ', options);
if (!InitManagementChannelOptions(options, error)) {
return;
}
// Setup openvpn-script options and RPC information required to send back
// Layer 3 configuration.
AppendOption("setenv", kRpcTaskServiceVariable,
rpc_task_->GetRpcConnectionIdentifier().value(), options);
AppendOption("setenv", kRpcTaskPathVariable,
rpc_task_->GetRpcIdentifier().value(), options);
AppendOption("script-security", "2", options);
AppendOption("up", kOpenVPNScript, options);
AppendOption("up-restart", options);
// Disable openvpn handling since we do route+ifconfig work.
AppendOption("route-noexec", options);
AppendOption("ifconfig-noexec", options);
// Drop root privileges on connection and enable callback scripts to send
// notify messages.
AppendOption("user", "openvpn", options);
AppendOption("group", "openvpn", options);
}
bool OpenVPNDriver::InitCAOptions(vector<vector<string>>* options,
Error* error) {
vector<string> ca_cert_pem;
if (args()->Contains<vector<string>>(kOpenVPNCaCertPemProperty)) {
ca_cert_pem = args()->Get<vector<string>>(kOpenVPNCaCertPemProperty);
}
if (ca_cert_pem.empty()) {
// Use default CAs if no CA certificate is provided.
AppendOption("ca", kDefaultCACertificates, options);
return true;
}
FilePath certfile = certificate_file_->CreatePEMFromStrings(ca_cert_pem);
if (certfile.empty()) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"Unable to extract PEM CA certificates.");
return false;
}
AppendOption("ca", certfile.value(), options);
return true;
}
void OpenVPNDriver::InitCertificateVerifyOptions(
std::vector<std::vector<std::string>>* options) {
AppendValueOption(kOpenVPNVerifyHashProperty, "verify-hash", options);
string x509_name = args()->Lookup<string>(kOpenVPNVerifyX509NameProperty, "");
if (!x509_name.empty()) {
string x509_type =
args()->Lookup<string>(kOpenVPNVerifyX509TypeProperty, "");
if (x509_type.empty()) {
AppendOption("verify-x509-name", x509_name, options);
} else {
AppendOption("verify-x509-name", x509_name, x509_type, options);
}
}
}
bool OpenVPNDriver::InitExtraCertOptions(vector<vector<string>>* options,
Error* error) {
if (!args()->Contains<vector<string>>(kOpenVPNExtraCertPemProperty)) {
// It's okay for this parameter to be unspecified.
return true;
}
vector<string> extra_certs =
args()->Get<vector<string>>(kOpenVPNExtraCertPemProperty);
if (extra_certs.empty()) {
// It's okay for this parameter to be empty.
return true;
}
FilePath certfile =
extra_certificates_file_->CreatePEMFromStrings(extra_certs);
if (certfile.empty()) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"Unable to extract extra PEM CA certificates.");
return false;
}
AppendOption("extra-certs", certfile.value(), options);
return true;
}
void OpenVPNDriver::InitPKCS11Options(vector<vector<string>>* options) {
string id = args()->Lookup<string>(kOpenVPNClientCertIdProperty, "");
if (!id.empty()) {
AppendOption("pkcs11-providers", kDefaultPKCS11Provider, options);
AppendOption("pkcs11-id", id, options);
}
}
void OpenVPNDriver::InitClientAuthOptions(vector<vector<string>>* options) {
// If the AuthUserPass property is set, or the User property is non-empty, or
// a client cert was not provided, specify user-password client
// authentication.
if (args()->Contains<string>(kOpenVPNAuthUserPassProperty) ||
!args()->Lookup<string>(kOpenVPNUserProperty, "").empty() ||
args()->Lookup<string>(kOpenVPNClientCertIdProperty, "").empty()) {
AppendOption("auth-user-pass", options);
}
}
bool OpenVPNDriver::InitManagementChannelOptions(
vector<vector<string>>* options, Error* error) {
if (!management_server_->Start(&sockets_, options)) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInternalError,
"Unable to setup management channel.");
return false;
}
// If there's a connected default service already, allow the openvpn client to
// establish connection as soon as it's started. Otherwise, hold the client
// until an underlying service connects and OnDefaultServiceChanged is
// invoked.
if (manager()->IsConnected()) {
management_server_->ReleaseHold();
}
return true;
}
void OpenVPNDriver::InitLoggingOptions(vector<vector<string>>* options) {
AppendOption("syslog", options);
string verb = args()->Lookup<string>(kOpenVPNVerbProperty, "");
if (!verb.empty()) {
AppendOption("verb", verb, options);
return;
}
if (SLOG_IS_ON(VPN, 6)) {
// Maximum output:
// --verb 9 enables PKCS11 debug, TCP stream, link read/write
// --verb 8 enables event waits, scheduler, tls_session
AppendOption("verb", "9", options);
} else if (SLOG_IS_ON(VPN, 5)) {
// --verb 7 enables data channel encryption keys, routing,
// pkcs11 actions, pings, push/pull debug
AppendOption("verb", "7", options);
} else if (SLOG_IS_ON(VPN, 4)) {
// --verb 6 enables tcp/udp reads/writes (short), tun/tap reads/writes
// --verb 5 enables printing 'R' or 'W' per packet to stdout
AppendOption("verb", "6", options);
} else if (SLOG_IS_ON(VPN, 3)) {
// --verb 4 enables logging packet drops, options
AppendOption("verb", "4", options);
} else if (SLOG_IS_ON(VPN, 0)) {
// --verb 3 is the old default for `ff_debug +vpn`
AppendOption("verb", "3", options);
}
}
void OpenVPNDriver::AppendOption(const string& option,
vector<vector<string>>* options) {
options->push_back(vector<string>{option});
}
void OpenVPNDriver::AppendOption(const string& option,
const string& value,
vector<vector<string>>* options) {
options->push_back(vector<string>{option, value});
}
void OpenVPNDriver::AppendOption(const string& option,
const string& value0,
const string& value1,
vector<vector<string>>* options) {
options->push_back(vector<string>{option, value0, value1});
}
void OpenVPNDriver::AppendRemoteOption(const string& host,
vector<vector<string>>* options) {
string host_name, host_port;
if (SplitPortFromHost(host, &host_name, &host_port)) {
DCHECK(!host_name.empty());
DCHECK(!host_port.empty());
AppendOption("remote", host_name, host_port, options);
} else {
AppendOption("remote", host, options);
}
}
bool OpenVPNDriver::AppendValueOption(const string& property,
const string& option,
vector<vector<string>>* options) {
string value = args()->Lookup<string>(property, "");
if (!value.empty()) {
AppendOption(option, value, options);
return true;
}
return false;
}
bool OpenVPNDriver::AppendDelimitedValueOption(
const string& property,
const string& option,
char delimiter,
vector<vector<string>>* options) {
string value = args()->Lookup<string>(property, "");
if (!value.empty()) {
vector<string> parts =
SplitString(value, std::string{delimiter}, base::TRIM_WHITESPACE,
base::SPLIT_WANT_ALL);
parts.insert(parts.begin(), option);
options->push_back(parts);
return true;
}
return false;
}
bool OpenVPNDriver::AppendFlag(const string& property,
const string& option,
vector<vector<string>>* options) {
if (args()->Contains<string>(property)) {
AppendOption(option, options);
return true;
}
return false;
}
void OpenVPNDriver::Disconnect() {
SLOG(this, 2) << __func__;
Cleanup();
service_callback_.Reset();
}
void OpenVPNDriver::OnConnectTimeout() {
VPNDriver::OnConnectTimeout();
Service::ConnectFailure failure =
management_server_->state() == OpenVPNManagementServer::kStateResolve
? Service::kFailureDNSLookup
: Service::kFailureConnect;
FailService(failure, Service::kErrorDetailsNone);
}
void OpenVPNDriver::OnReconnecting(ReconnectReason reason) {
LOG(INFO) << __func__ << "(" << reason << ")";
int timeout_seconds = GetReconnectTimeoutSeconds(reason);
if (reason == kReconnectReasonTLSError &&
timeout_seconds < connect_timeout_seconds()) {
// Reconnect due to TLS error happens during connect so we need to cancel
// the original connect timeout first and then reduce the time limit.
StopConnectTimeout();
}
StartConnectTimeout(timeout_seconds);
// On restart/reconnect, drop the VPN connection, if any. The openvpn client
// might be in hold state if the VPN connection was previously established
// successfully. The hold will be released by OnDefaultServiceChanged when a
// new default service connects. This ensures that the client will use a fully
// functional underlying connection to reconnect.
if (service_callback_) {
service_callback_.Run(VPNService::kEventDriverReconnecting,
Service::kFailureNone, Service::kErrorDetailsNone);
}
}
// static
int OpenVPNDriver::GetReconnectTimeoutSeconds(ReconnectReason reason) {
switch (reason) {
case kReconnectReasonOffline:
return kReconnectOfflineTimeoutSeconds;
case kReconnectReasonTLSError:
return kReconnectTLSErrorTimeoutSeconds;
default:
break;
}
return kConnectTimeoutSeconds;
}
string OpenVPNDriver::GetProviderType() const {
return kProviderOpenVpn;
}
VPNDriver::IfType OpenVPNDriver::GetIfType() const {
return kTunnel;
}
KeyValueStore OpenVPNDriver::GetProvider(Error* error) {
SLOG(this, 2) << __func__;
KeyValueStore props = VPNDriver::GetProvider(error);
props.Set<bool>(
kPassphraseRequiredProperty,
args()->Lookup<string>(kOpenVPNPasswordProperty, "").empty() &&
args()->Lookup<string>(kOpenVPNTokenProperty, "").empty());
return props;
}
vector<string> OpenVPNDriver::GetCommandLineArgs() {
SLOG(this, 2) << __func__ << "(" << lsb_release_file_.value() << ")";
vector<string> args =
vector<string>{"--config", openvpn_config_file_.value()};
string contents;
if (!base::ReadFileToString(lsb_release_file_, &contents)) {
LOG(ERROR) << "Unable to read the lsb-release file: "
<< lsb_release_file_.value();
return args;
}
vector<string> lines =
SplitString(contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
for (const auto& line : lines) {
const size_t assign = line.find('=');
if (assign == string::npos) {
continue;
}
const string key = line.substr(0, assign);
const string value = line.substr(assign + 1);
if (key == kChromeOSReleaseName) {
args.push_back("--setenv");
args.push_back("UV_PLAT");
args.push_back(value);
} else if (key == kChromeOSReleaseVersion) {
args.push_back("--setenv");
args.push_back("UV_PLAT_REL");
args.push_back(value);
}
// Other LSB release values are irrelevant.
}
return args;
}
void OpenVPNDriver::OnDefaultPhysicalServiceEvent(
DefaultPhysicalServiceEvent event) {
if (!service_callback_)
return;
if (event == kDefaultPhysicalServiceDown ||
event == kDefaultPhysicalServiceChanged) {
// Inform the user that the VPN is reconnecting.
service_callback_.Run(VPNService::kEventDriverReconnecting,
Service::kFailureNone, Service::kErrorDetailsNone);
StopConnectTimeout();
}
switch (event) {
case kDefaultPhysicalServiceUp:
management_server_->ReleaseHold();
StartConnectTimeout(GetReconnectTimeoutSeconds(kReconnectReasonOffline));
break;
case kDefaultPhysicalServiceDown:
management_server_->Hold();
management_server_->Restart();
break;
case kDefaultPhysicalServiceChanged:
// Ask the management server to reconnect immediately.
management_server_->ReleaseHold();
management_server_->Restart();
StartConnectTimeout(GetReconnectTimeoutSeconds(kReconnectReasonOffline));
break;
default:
NOTREACHED();
}
}
void OpenVPNDriver::ReportConnectionMetrics() {
metrics()->SendEnumToUMA(Metrics::kMetricVpnDriver,
Metrics::kVpnDriverOpenVpn,
Metrics::kMetricVpnDriverMax);
if (args()->Contains<vector<string>>(kOpenVPNCaCertPemProperty) &&
!args()->Get<vector<string>>(kOpenVPNCaCertPemProperty).empty()) {
metrics()->SendEnumToUMA(
Metrics::kMetricVpnRemoteAuthenticationType,
Metrics::kVpnRemoteAuthenticationTypeOpenVpnCertificate,
Metrics::kMetricVpnRemoteAuthenticationTypeMax);
} else {
metrics()->SendEnumToUMA(
Metrics::kMetricVpnRemoteAuthenticationType,
Metrics::kVpnRemoteAuthenticationTypeOpenVpnDefault,
Metrics::kMetricVpnRemoteAuthenticationTypeMax);
}
bool has_user_authentication = false;
if (args()->Lookup<string>(kOpenVPNTokenProperty, "") != "") {
metrics()->SendEnumToUMA(
Metrics::kMetricVpnUserAuthenticationType,
Metrics::kVpnUserAuthenticationTypeOpenVpnUsernameToken,
Metrics::kMetricVpnUserAuthenticationTypeMax);
has_user_authentication = true;
}
if (args()->Lookup<string>(kOpenVPNOTPProperty, "") != "") {
metrics()->SendEnumToUMA(
Metrics::kMetricVpnUserAuthenticationType,
Metrics::kVpnUserAuthenticationTypeOpenVpnUsernamePasswordOtp,
Metrics::kMetricVpnUserAuthenticationTypeMax);
has_user_authentication = true;
}
if (args()->Lookup<string>(kOpenVPNAuthUserPassProperty, "") != "" ||
args()->Lookup<string>(kOpenVPNUserProperty, "") != "") {
metrics()->SendEnumToUMA(
Metrics::kMetricVpnUserAuthenticationType,
Metrics::kVpnUserAuthenticationTypeOpenVpnUsernamePassword,
Metrics::kMetricVpnUserAuthenticationTypeMax);
has_user_authentication = true;
}
if (args()->Lookup<string>(kOpenVPNClientCertIdProperty, "") != "") {
metrics()->SendEnumToUMA(
Metrics::kMetricVpnUserAuthenticationType,
Metrics::kVpnUserAuthenticationTypeOpenVpnCertificate,
Metrics::kMetricVpnUserAuthenticationTypeMax);
has_user_authentication = true;
}
if (!has_user_authentication) {
metrics()->SendEnumToUMA(Metrics::kMetricVpnUserAuthenticationType,
Metrics::kVpnUserAuthenticationTypeOpenVpnNone,
Metrics::kMetricVpnUserAuthenticationTypeMax);
}
}
} // namespace shill