blob: 1e18aab0f5eb5d8f852f123bb1329f23dd3b3d31 [file] [log] [blame]
// Copyright 2021 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/l2tp_connection.h"
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <base/callback.h>
#include <base/check.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/strcat.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <chromeos/dbus/service_constants.h>
#include <libpasswordprovider/password_provider.h>
#include "shill/ppp_daemon.h"
#include "shill/ppp_device.h"
#include "shill/vpn/vpn_util.h"
namespace shill {
namespace {
// TODO(b/165170125): Consider using /run/xl2tpd folder.
constexpr char kRunDir[] = "/run/l2tpipsec_vpn";
constexpr char kXl2tpdPath[] = "/usr/sbin/xl2tpd";
constexpr char kL2TPDConfigFileName[] = "l2tpd.conf";
constexpr char kL2TPDControlFileName[] = "l2tpd.control";
constexpr char kPPPDConfigFileName[] = "pppd.conf";
// Environment variable available to ppp plugin to know the resolved address
// of the L2TP server.
const char kLnsAddress[] = "LNS_ADDRESS";
// Constants used in the config file for xl2tpd.
const char kL2TPConnectionName[] = "managed";
const char kBpsParameter[] = "1000000";
const char kRedialTimeoutParameter[] = "2";
const char kMaxRedialsParameter[] = "30";
// xl2tpd (1.3.12 at the time of writing) uses fgets with a size 1024 buffer to
// get configuration lines. If a configuration line was longer than that and
// didn't contain the comment delimiter ';', it could be used to populate
// multiple configuration options.
constexpr size_t kXl2tpdMaxConfigurationLength = 1023;
} // namespace
L2TPConnection::L2TPConnection(std::unique_ptr<Config> config,
std::unique_ptr<Callbacks> callbacks,
ControlInterface* control_interface,
DeviceInfo* device_info,
EventDispatcher* dispatcher,
ProcessManager* process_manager)
: VPNConnection(std::move(callbacks), dispatcher),
config_(std::move(config)),
control_interface_(control_interface),
device_info_(device_info),
password_provider_(
std::make_unique<password_provider::PasswordProvider>()),
process_manager_(process_manager),
vpn_util_(VPNUtil::New()) {}
L2TPConnection::~L2TPConnection() {
if (state() == State::kIdle || state() == State::kStopped) {
return;
}
// This is unexpected but cannot be fully avoided. Call OnDisconnect() to make
// sure resources are released.
LOG(WARNING) << "Destructor called but the current state is " << state();
OnDisconnect();
}
void L2TPConnection::OnConnect() {
temp_dir_ = vpn_util_->CreateScopedTempDir(base::FilePath(kRunDir));
if (!WritePPPDConfig()) {
NotifyFailure(Service::kFailureInternal,
"Failed to write pppd config file");
return;
}
if (!WriteL2TPDConfig()) {
NotifyFailure(Service::kFailureInternal,
"Failed to write xl2tpd config file");
return;
}
StartXl2tpd();
}
void L2TPConnection::GetLogin(std::string* user, std::string* password) {
LOG(INFO) << "Login requested.";
if (config_->user.empty()) {
LOG(ERROR) << "User not set.";
return;
}
std::string password_local = config_->password;
if (config_->use_login_password) {
std::unique_ptr<password_provider::Password> login_password =
password_provider_->GetPassword();
if (login_password == nullptr || login_password->size() == 0) {
LOG(ERROR) << "Unable to retrieve user password";
return;
}
password_local =
std::string(login_password->GetRaw(), login_password->size());
} else if (password_local.empty()) {
LOG(ERROR) << "Password not set.";
return;
}
*user = config_->user;
*password = password_local;
}
void L2TPConnection::Notify(const std::string& reason,
const std::map<std::string, std::string>& dict) {
// TODO(b/165170125): On failure, check the reason (e.g., if it is an
// authentication failure).
if (reason == kPPPReasonAuthenticating || reason == kPPPReasonAuthenticated) {
// These are uninteresting intermediate states that do not indicate failure.
return;
}
if (reason != kPPPReasonConnect) {
if (!IsConnectingOrConnected()) {
// We have notified the upper layer, or the disconnect is triggered by the
// upper layer. In both cases, we don't need call NotifyFailure().
LOG(INFO) << "pppd notifies us of " << reason << ", the current state is "
<< state();
return;
}
NotifyFailure(Service::kFailureInternal, "pppd disconnected");
return;
}
// The message is kPPPReasonConnect. Checks if we are in the connecting state
// at first.
if (state() != State::kConnecting) {
LOG(WARNING) << "pppd notifies us of " << reason
<< ", the current state is " << state();
return;
}
std::string interface_name = PPPDevice::GetInterfaceName(dict);
IPConfig::Properties ip_properties = PPPDevice::ParseIPConfiguration(dict);
// There is no IPv6 support for L2TP/IPsec VPN at this moment, so create a
// blackhole route for IPv6 traffic after establishing a IPv4 VPN.
ip_properties.blackhole_ipv6 = true;
// Reduce MTU to the minimum viable for IPv6, since the IPsec layer consumes
// some variable portion of the payload. Although this system does not yet
// support IPv6, it is a reasonable value to start with, since the minimum
// IPv6 packet size will plausibly be a size any gateway would support, and
// is also larger than the IPv4 minimum size.
ip_properties.mtu = IPConfig::kMinIPv6MTU;
ip_properties.method = kTypeVPN;
// Notify() could be invoked either before or after the creation of the ppp
// interface. We need to make sure that the interface is ready (by checking
// DeviceInfo) before invoking the connected callback here.
int interface_index = device_info_->GetIndex(interface_name);
if (interface_index != -1) {
NotifyConnected(interface_name, interface_index, ip_properties);
} else {
device_info_->AddVirtualInterfaceReadyCallback(
interface_name,
base::BindOnce(&L2TPConnection::OnLinkReady, weak_factory_.GetWeakPtr(),
ip_properties));
}
}
void L2TPConnection::OnDisconnect() {
// TODO(b/165170125): Terminate the connection before stopping xl2tpd.
external_task_ = nullptr;
if (state() == State::kDisconnecting) {
NotifyStopped();
}
}
bool L2TPConnection::WritePPPDConfig() {
pppd_config_path_ = temp_dir_.GetPath().Append(kPPPDConfigFileName);
// TODO(b/200636771): Use proper mtu and mru.
std::vector<std::string> lines = {
"ipcp-accept-local",
"ipcp-accept-remote",
"refuse-eap",
"noccp",
"noauth",
"crtscts",
"mtu 1410",
"mru 1410",
"lock",
"connect-delay 5000",
"nodefaultroute",
"nosystemconfig",
"usepeerdns",
};
if (config_->lcp_echo) {
lines.push_back("lcp-echo-failure 4");
lines.push_back("lcp-echo-interval 30");
}
lines.push_back(base::StrCat({"plugin ", PPPDaemon::kShimPluginPath}));
std::string contents = base::JoinString(lines, "\n");
return vpn_util_->WriteConfigFile(pppd_config_path_, contents);
}
bool L2TPConnection::WriteL2TPDConfig() {
CHECK(!pppd_config_path_.empty());
// b/187984628: When UseLoginPassword is enabled, PAP must be refused to
// prevent potential password leak to a malicious server.
if (config_->use_login_password) {
config_->refuse_pap = true;
}
l2tpd_config_path_ = temp_dir_.GetPath().Append(kL2TPDConfigFileName);
std::vector<std::string> lines;
lines.push_back(base::StringPrintf("[lac %s]", kL2TPConnectionName));
// Fills in bool properties.
auto bool_property = [](const std::string& key, bool value) -> std::string {
return base::StrCat({key, " = ", value ? "yes" : "no"});
};
lines.push_back(bool_property("require chap", config_->require_chap));
lines.push_back(bool_property("refuse pap", config_->refuse_pap));
lines.push_back(
bool_property("require authentication", config_->require_auth));
lines.push_back(bool_property("length bit", config_->length_bit));
lines.push_back(bool_property("redial", true));
lines.push_back(bool_property("autodial", true));
// Fills in string properties. Note that some values are input by users, we
// need to check them to ensure that the generated config file will not be
// polluted. See https://crbug.com/1077754. Note that the ordering of
// properties in the config file does not matter, we use a vector instead of
// map just for the ease of unit tests.
std::vector<std::pair<std::string, std::string>> string_properties = {
{"lns", config_->remote_ip},
{"name", config_->user},
{"bps", kBpsParameter},
{"redial timeout", kRedialTimeoutParameter},
{"max redials", kMaxRedialsParameter},
{"pppoptfile", pppd_config_path_.value()},
};
for (const auto& [key, value] : string_properties) {
if (value.find('\n') != value.npos) {
LOG(ERROR) << "The value for " << key << " contains newline characters";
return false;
}
const auto line = base::StrCat({key, " = ", value});
if (line.size() > kXl2tpdMaxConfigurationLength) {
LOG(ERROR) << "Line length for " << key << " exceeds "
<< kXl2tpdMaxConfigurationLength;
return false;
}
lines.push_back(line);
}
std::string contents = base::JoinString(lines, "\n");
return vpn_util_->WriteConfigFile(l2tpd_config_path_, contents);
}
void L2TPConnection::StartXl2tpd() {
const base::FilePath l2tpd_control_path =
temp_dir_.GetPath().Append(kL2TPDControlFileName);
std::vector<std::string> args = {
"-c", l2tpd_config_path_.value(), "-C", l2tpd_control_path.value(),
"-D" // prevents xl2tpd from detaching from the terminal and daemonizing
};
std::map<std::string, std::string> env = {
{kLnsAddress, config_->remote_ip},
};
auto external_task_local = std::make_unique<ExternalTask>(
control_interface_, process_manager_, weak_factory_.GetWeakPtr(),
base::BindRepeating(&L2TPConnection::OnXl2tpdExitedUnexpectedly,
weak_factory_.GetWeakPtr()));
Error error;
constexpr uint64_t kCapMask = CAP_TO_MASK(CAP_NET_ADMIN);
if (!external_task_local->StartInMinijail(
base::FilePath(kXl2tpdPath), &args, env, VPNUtil::kVPNUser,
VPNUtil::kVPNGroup, kCapMask, /*inherit_supplementary_groups=*/true,
/*close_nonstd_fds=*/true, &error)) {
NotifyFailure(Service::kFailureInternal,
base::StrCat({"Failed to start xl2tpd: ", error.message()}));
return;
}
external_task_ = std::move(external_task_local);
}
void L2TPConnection::OnLinkReady(const IPConfig::Properties& ip_properties,
const std::string& if_name,
int if_index) {
if (state() != State::kConnecting) {
// Needs to do nothing here. The ppp interface is managed by the pppd
// process so we don't need to remove it here.
LOG(WARNING) << "OnLinkReady() called but the current state is " << state();
return;
}
NotifyConnected(if_name, if_index, ip_properties);
}
void L2TPConnection::OnXl2tpdExitedUnexpectedly(pid_t pid, int exit_code) {
const std::string message =
base::StringPrintf("xl2tpd exited unexpectedly with code=%d", exit_code);
if (!IsConnectingOrConnected()) {
LOG(WARNING) << message;
return;
}
NotifyFailure(Service::kFailureInternal, message);
}
} // namespace shill