blob: f5a2b47d581c0d1c411479d9ef48c8d74c287d26 [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/ipsec_connection.h"
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <base/files/file_path_watcher.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 "shill/process_manager.h"
#include "shill/vpn/vpn_util.h"
namespace shill {
namespace {
constexpr char kBaseRunDir[] = "/run/ipsec";
constexpr char kStrongSwanConfFileName[] = "strongswan.conf";
constexpr char kSwanctlConfFileName[] = "swanctl.conf";
constexpr char kSwanctlPath[] = "/usr/sbin/swanctl";
constexpr char kCharonPath[] = "/usr/libexec/ipsec/charon";
constexpr char kViciSocketPath[] = "/run/ipsec/charon.vici";
constexpr char kSmartcardModuleName[] = "crypto_module";
// TODO(b/197839464): Consider adding metrics for the final selected value.
// aes128-sha256-modp3072: new strongSwan default
// aes128-sha1-modp2048: old strongSwan default
// 3des-sha1-modp1536: strongSwan fallback
// 3des-sha1-modp1024: for compatibility with Windows RRAS, which requires
// using the modp1024 dh-group
constexpr char kDefaultIKEProposals[] =
"aes128-sha256-modp3072,aes128-sha1-modp2048,3des-sha1-modp1536,3des-sha1-"
"modp1024,default";
// Cisco ASA L2TP/IPsec setup instructions indicate using md5 for authentication
// for the IPsec SA. Default StrongS/WAN setup is to only propose SHA1.
constexpr char kDefaultESPProposals[] =
"aes128gcm16,aes128-sha256,aes128-sha1,3des-sha1,3des-md5,default";
constexpr char kChildSAName[] = "managed";
// Represents a section in the format used by strongswan.conf and swanctl.conf.
// We use this class only for formatting swanctl.conf since the contents of
// strongswan.conf generated by this class are fixed. The basic syntax is:
// section := name { settings }
// settings := (section|keyvalue)*
// keyvalue := key = value\n
// Also see the following link for more details.
// https://wiki.strongswan.org/projects/strongswan/wiki/Strongswanconf
class StrongSwanConfSection {
public:
explicit StrongSwanConfSection(const std::string& name) : name_(name) {}
StrongSwanConfSection* AddSection(const std::string& name) {
auto section = new StrongSwanConfSection(name);
sections_.emplace_back(section);
return section;
}
void AddKeyValue(const std::string& key, const std::string& value) {
key_values_[key] = value;
}
std::string Format(int indent_base = 0) const {
std::vector<std::string> lines;
const std::string indent_str(indent_base, ' ');
lines.push_back(base::StrCat({indent_str, name_, " {"}));
for (const auto& [k, v] : key_values_) {
lines.push_back(
base::StrCat({indent_str, " ", k, " = ", FormatValue(v)}));
}
for (const auto& section : sections_) {
lines.push_back(section->Format(indent_base + 2));
}
lines.push_back(base::StrCat({indent_str, "}"}));
return base::JoinString(lines, "\n");
}
private:
// Wraps the value in quotation marks and encodes control chars to make sure
// the whole value will be read as a single string.
static std::string FormatValue(const std::string& input) {
std::string output;
output.reserve(input.size() + 2);
output.append("\"");
for (char c : input) {
switch (c) {
case '\b':
output.append("\\b");
break;
case '\f':
output.append("\\f");
break;
case '\n':
output.append("\\n");
break;
case '\r':
output.append("\\r");
break;
case '\t':
output.append("\\t");
break;
case '"':
output.append("\\\"");
break;
case '\\':
output.append("\\\\");
break;
default:
output.push_back(c);
break;
}
}
output.append("\"");
return output;
}
std::string name_;
std::vector<std::unique_ptr<StrongSwanConfSection>> sections_;
std::map<std::string, std::string> key_values_;
};
} // namespace
IPsecConnection::IPsecConnection(std::unique_ptr<Config> config,
std::unique_ptr<Callbacks> callbacks,
std::unique_ptr<VPNConnection> l2tp_connection,
EventDispatcher* dispatcher,
ProcessManager* process_manager)
: VPNConnection(std::move(callbacks), dispatcher),
config_(std::move(config)),
l2tp_connection_(std::move(l2tp_connection)),
vici_socket_path_(kViciSocketPath),
process_manager_(process_manager),
vpn_util_(VPNUtil::New()) {
if (l2tp_connection_) {
l2tp_connection_->ResetCallbacks(std::make_unique<VPNConnection::Callbacks>(
base::BindRepeating(&IPsecConnection::OnL2TPConnected,
weak_factory_.GetWeakPtr()),
base::BindOnce(&IPsecConnection::OnL2TPFailure,
weak_factory_.GetWeakPtr()),
base::BindOnce(&IPsecConnection::OnL2TPStopped,
weak_factory_.GetWeakPtr())));
} else {
NOTREACHED(); // Reserved for IKEv2 VPN
}
}
IPsecConnection::~IPsecConnection() {
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 IPsecConnection::OnConnect() {
temp_dir_ = vpn_util_->CreateScopedTempDir(base::FilePath(kBaseRunDir));
if (!temp_dir_.IsValid()) {
NotifyFailure(Service::kFailureInternal,
"Failed to create temp dir for IPsec");
return;
}
ScheduleConnectTask(ConnectStep::kStart);
}
void IPsecConnection::ScheduleConnectTask(ConnectStep step) {
switch (step) {
case ConnectStep::kStart:
WriteStrongSwanConfig();
return;
case ConnectStep::kStrongSwanConfigWritten:
StartCharon();
return;
case ConnectStep::kCharonStarted:
WriteSwanctlConfig();
return;
case ConnectStep::kSwanctlConfigWritten:
SwanctlLoadConfig();
return;
case ConnectStep::kSwanctlConfigLoaded:
SwanctlInitiateConnection();
return;
case ConnectStep::kIPsecConnected:
if (l2tp_connection_) {
l2tp_connection_->Connect();
} else {
NOTREACHED(); // Reserved for IKEv2 VPN
}
return;
default:
NOTREACHED();
}
}
void IPsecConnection::WriteStrongSwanConfig() {
strongswan_conf_path_ = temp_dir_.GetPath().Append(kStrongSwanConfFileName);
// See the following link for the format and descriptions for each field:
// https://wiki.strongswan.org/projects/strongswan/wiki/strongswanconf
// TODO(b/165170125): Check if routing_table is still required.
std::vector<std::string> lines = {
"charon {",
" accept_unencrypted_mainmode_messages = yes",
" ignore_routing_tables = 0",
" install_routes = no",
" routing_table = 0",
" syslog {",
" daemon {",
" ike = 2", // Logs some traffic selector info.
" cfg = 2", // Logs algorithm proposals.
" knl = 2", // Logs high-level xfrm crypto parameters.
" }",
" }",
" plugins {",
" pkcs11 {",
" modules {",
base::StringPrintf(" %s {", kSmartcardModuleName),
" path = " + std::string{PKCS11_LIB},
" }",
" }",
" }",
" }",
"}",
};
std::string contents = base::JoinString(lines, "\n");
if (!vpn_util_->WriteConfigFile(strongswan_conf_path_, contents)) {
NotifyFailure(Service::kFailureInternal,
base::StrCat({"Failed to write ", kStrongSwanConfFileName}));
return;
}
ScheduleConnectTask(ConnectStep::kStrongSwanConfigWritten);
}
// The swanctl.conf which we generate here will look like:
// connections {
// vpn { // A connection named "vpn".
// ... // Parameters used in the IKE phase.
// local-1 { ... } // First round of authentication in local or remote.
// remote-1 { ... }
// local-2 { ... } // Second round of authentication (if exists).
// remote-2 { ... }
// managed { // A CHILD_SA named "managed".
// ... // Parameters for SA negotiation.
// }
// }
// }
// secrets {
// ... // secrets used in IKE (e.g., PSK).
// }
// For the detailed meanings of each field, see
// https://wiki.strongswan.org/projects/strongswan/wiki/Swanctlconf
void IPsecConnection::WriteSwanctlConfig() {
swanctl_conf_path_ = temp_dir_.GetPath().Append(kSwanctlConfFileName);
using Section = StrongSwanConfSection;
Section connections_section("connections");
Section secrets_section("secrets");
Section* vpn_section = connections_section.AddSection("vpn");
vpn_section->AddKeyValue("local_addrs", "0.0.0.0/0,::/0");
vpn_section->AddKeyValue("remote_addrs", config_->remote);
vpn_section->AddKeyValue("proposals", kDefaultIKEProposals);
vpn_section->AddKeyValue("version", "1"); // IKEv1
// Fields for authentication.
Section* local1 = vpn_section->AddSection("local-1");
Section* remote1 = vpn_section->AddSection("remote-1");
if (config_->psk.has_value()) {
local1->AddKeyValue("auth", "psk");
remote1->AddKeyValue("auth", "psk");
auto* psk_section = secrets_section.AddSection("ike-1");
psk_section->AddKeyValue("secret", config_->psk.value());
} else {
if (!config_->ca_cert_pem_strings.has_value() ||
!config_->client_cert_id.has_value() ||
!config_->client_cert_pin.has_value() ||
!config_->client_cert_slot.has_value()) {
NotifyFailure(Service::kFailureInternal,
"Expect cert auth but some required fields are empty");
return;
}
local1->AddKeyValue("auth", "pubkey");
remote1->AddKeyValue("auth", "pubkey");
// Writes server CA to a file and references this file in the config.
server_ca_.set_root_directory(temp_dir_.GetPath());
server_ca_path_ =
server_ca_.CreatePEMFromStrings(config_->ca_cert_pem_strings.value());
remote1->AddKeyValue("cacerts", server_ca_path_.value());
Section* cert = local1->AddSection("cert");
cert->AddKeyValue("handle", config_->client_cert_id.value());
cert->AddKeyValue("slot", config_->client_cert_slot.value());
cert->AddKeyValue("module", kSmartcardModuleName);
Section* token = secrets_section.AddSection("token-1");
token->AddKeyValue("module", kSmartcardModuleName);
token->AddKeyValue("handle", config_->client_cert_id.value());
token->AddKeyValue("slot", config_->client_cert_slot.value());
token->AddKeyValue("pin", config_->client_cert_pin.value());
}
// Fields for CHILD_SA.
Section* children_section = vpn_section->AddSection("children");
Section* child_section = children_section->AddSection(kChildSAName);
child_section->AddKeyValue(
"local_ts", base::StrCat({"dynamic[", config_->local_proto_port, "]"}));
child_section->AddKeyValue(
"remote_ts", base::StrCat({"dynamic[", config_->remote_proto_port, "]"}));
child_section->AddKeyValue("esp_proposals", kDefaultESPProposals);
// L2TP/IPsec always uses transport mode.
child_section->AddKeyValue("mode", "transport");
// Writes to file.
const std::string contents = base::StrCat(
{connections_section.Format(), "\n", secrets_section.Format()});
if (!vpn_util_->WriteConfigFile(swanctl_conf_path_, contents)) {
NotifyFailure(
Service::kFailureInternal,
base::StrCat({"Failed to write swanctl.conf", kSwanctlConfFileName}));
return;
}
ScheduleConnectTask(ConnectStep::kSwanctlConfigWritten);
}
void IPsecConnection::StartCharon() {
// We should make sure there is no socket file before starting charon, since
// we rely on its existence to know if charon is ready.
if (base::PathExists(vici_socket_path_)) {
// This could happen if something unexpected happened in the previous run,
// e.g., shill crashed.
LOG(WARNING) << "vici socket exists before starting charon";
if (!base::DeleteFile(vici_socket_path_)) {
const std::string reason = "Failed to delete vici socket file";
PLOG(ERROR) << reason;
NotifyFailure(Service::kFailureInternal, reason);
return;
}
}
// TODO(b/165170125): Check the behavior when shill crashes (if charon is
// still running).
// TODO(b/165170125): May need to increase RLIMIT_AS to run charon. See
// https://crrev.com/c/1757203.
std::vector<std::string> args = {};
std::map<std::string, std::string> env = {
{"STRONGSWAN_CONF", strongswan_conf_path_.value()},
};
// TODO(b/197199752): Consider removing CAP_SETGID.
constexpr uint64_t kCapMask =
CAP_TO_MASK(CAP_NET_ADMIN) | CAP_TO_MASK(CAP_NET_BIND_SERVICE) |
CAP_TO_MASK(CAP_NET_RAW) | CAP_TO_MASK(CAP_SETGID);
charon_pid_ = process_manager_->StartProcessInMinijail(
FROM_HERE, base::FilePath(kCharonPath), args, env, VPNUtil::kVPNUser,
VPNUtil::kVPNGroup, kCapMask,
/*inherit_supplementary_groups=*/true, /*close_nonstd_fds*/ true,
base::BindRepeating(&IPsecConnection::OnCharonExitedUnexpectedly,
weak_factory_.GetWeakPtr()));
if (charon_pid_ == -1) {
NotifyFailure(Service::kFailureInternal, "Failed to start charon");
return;
}
LOG(INFO) << "charon started";
if (!base::PathExists(vici_socket_path_)) {
vici_socket_watcher_ = std::make_unique<base::FilePathWatcher>();
auto callback = base::BindRepeating(&IPsecConnection::OnViciSocketPathEvent,
weak_factory_.GetWeakPtr());
if (!vici_socket_watcher_->Watch(vici_socket_path_,
base::FilePathWatcher::Type::kNonRecursive,
callback)) {
NotifyFailure(Service::kFailureInternal,
"Failed to set up FilePathWatcher for the vici socket");
return;
}
} else {
LOG(INFO) << "vici socket is already here";
ScheduleConnectTask(ConnectStep::kCharonStarted);
}
}
void IPsecConnection::SwanctlLoadConfig() {
const std::vector<std::string> args = {"--load-all", "--file",
swanctl_conf_path_.value()};
RunSwanctl(args,
base::BindOnce(&IPsecConnection::ScheduleConnectTask,
weak_factory_.GetWeakPtr(),
ConnectStep::kSwanctlConfigLoaded),
"Failed to load swanctl.conf");
}
void IPsecConnection::SwanctlInitiateConnection() {
// This is a blocking call: if the execution returns with 0, then it means the
// IPsec connection has been established.
const std::vector<std::string> args = {"--initiate", "-c", kChildSAName};
RunSwanctl(
args,
base::BindOnce(&IPsecConnection::ScheduleConnectTask,
weak_factory_.GetWeakPtr(), ConnectStep::kIPsecConnected),
"Failed to initiate IPsec connection");
}
void IPsecConnection::OnViciSocketPathEvent(const base::FilePath& /*path*/,
bool error) {
if (state() != State::kConnecting) {
LOG(WARNING) << "OnViciSocketPathEvent triggered on state " << state();
return;
}
if (error) {
NotifyFailure(Service::kFailureInternal,
"FilePathWatcher error for the vici socket");
return;
}
if (!base::PathExists(vici_socket_path_)) {
// This is kind of unexpected, since the first event should be the creation
// of this file. Waits for the next event.
LOG(WARNING) << "vici socket is still not ready";
return;
}
LOG(INFO) << "vici socket is ready";
vici_socket_watcher_ = nullptr;
ScheduleConnectTask(ConnectStep::kCharonStarted);
}
void IPsecConnection::OnCharonExitedUnexpectedly(int exit_code) {
charon_pid_ = -1;
NotifyFailure(Service::kFailureInternal,
base::StringPrintf(
"charon exited unexpectedly with exit code %d", exit_code));
return;
}
void IPsecConnection::RunSwanctl(const std::vector<std::string>& args,
base::OnceClosure on_success,
const std::string& message_on_failure) {
std::map<std::string, std::string> env = {
{"STRONGSWAN_CONF", strongswan_conf_path_.value()},
};
constexpr uint64_t kCapMask = 0;
pid_t pid = process_manager_->StartProcessInMinijail(
FROM_HERE, base::FilePath(kSwanctlPath), args, env, VPNUtil::kVPNUser,
VPNUtil::kVPNGroup, kCapMask,
/*inherit_supplementary_groups=*/true, /*close_nonstd_fds*/ true,
base::BindRepeating(
&IPsecConnection::OnSwanctlExited, weak_factory_.GetWeakPtr(),
base::AdaptCallbackForRepeating(std::move(on_success)),
message_on_failure));
if (pid == -1) {
NotifyFailure(Service::kFailureInternal, message_on_failure);
}
}
void IPsecConnection::OnSwanctlExited(base::OnceClosure on_success,
const std::string& message_on_failure,
int exit_code) {
if (exit_code == 0) {
std::move(on_success).Run();
} else {
NotifyFailure(Service::kFailureInternal,
base::StringPrintf("%s, exit_code=%d",
message_on_failure.c_str(), exit_code));
}
}
void IPsecConnection::OnL2TPConnected(const std::string& interface_name,
int interface_index,
const IPConfig::Properties& properties) {
if (state() != State::kConnecting) {
// This is possible, e.g., the upper layer called Disconnect() right before
// this callback is triggered.
LOG(WARNING) << "OnL2TPConnected() called but the IPsec layer is "
<< state();
return;
}
NotifyConnected(interface_name, interface_index, properties);
}
void IPsecConnection::OnDisconnect() {
if (!l2tp_connection_) {
StopCharon();
return;
}
switch (l2tp_connection_->state()) {
case State::kIdle:
case State::kStopped:
StopCharon();
return;
case State::kConnecting:
case State::kConnected:
l2tp_connection_->Disconnect();
return;
case State::kDisconnecting:
// StopCharon() called in the stopped callback.
return;
default:
NOTREACHED();
}
}
void IPsecConnection::OnL2TPFailure(Service::ConnectFailure reason) {
switch (state()) {
case State::kDisconnecting:
// If the IPsec layer is disconnecting, it could mean the failure happens
// in the IPsec layer, and the failure must have been propagated to the
// upper layer.
return;
case State::kConnecting:
case State::kConnected:
NotifyFailure(reason, "L2TP layer failure");
return;
default:
// Other states are unexpected.
LOG(DFATAL) << "OnL2TPFailure() called but the IPsec layer is "
<< state();
}
}
void IPsecConnection::OnL2TPStopped() {
l2tp_connection_ = nullptr;
if (state() != State::kDisconnecting) {
LOG(DFATAL) << "OnL2TPStopped() called but the IPsec layer is " << state();
// Does the cleanup anyway.
}
StopCharon();
}
void IPsecConnection::StopCharon() {
if (charon_pid_ != -1) {
process_manager_->StopProcess(charon_pid_);
charon_pid_ = -1;
}
// Removes the vici socket file, since the charon process will not do that by
// itself. Note that base::DeleteFile() will return true if the file does not
// exist.
if (!base::DeleteFile(vici_socket_path_)) {
PLOG(ERROR) << "Failed to delete vici socket file";
}
// This function can be called directly from the destructor, and in that case
// the state may not be kDisconnecting.
if (state() == State::kDisconnecting) {
// Currently we do not wait for charon fully stopped to send out this
// signal.
NotifyStopped();
}
}
} // namespace shill