blob: 6d6590835da8e08192c673bb95206eda51a06380 [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 <sys/socket.h>
#include <sys/un.h>
#include <map>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/check.h>
#include <base/files/file_path_watcher.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/posix/eintr_wrapper.h>
#include <base/strings/strcat.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <chromeos/dbus/service_constants.h>
#include <re2/re2.h>
#include "shill/metrics.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 kResolvConfFilename[] = "resolv.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";
// 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 kL2TPIPsecDefaultIKEProposals[] =
"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 kL2TPIPsecDefaultESPProposals[] =
"aes128gcm16,aes128-sha256,aes128-sha1,3des-sha1,3des-md5,default";
// The default proposals used by strongSwan 5.9.2, removing 3DES and SHA1. Each
// string contains two proposals: the AEAD one and non-AEAD one, they have to be
// specified separately in IKEv2.
constexpr char kIKEv2DefaultIKEProposals[] =
// non-AEAD encryption algorithms
"aes128-aes192-aes256-camellia128-camellia192-camellia256"
// integrity algorithms
"-aesxcbc-aescmac-sha256-sha384-sha512"
// DH groups
"-ecp256-ecp384-ecp521-ecp256bp-ecp384bp-ecp512bp-curve25519-curve448-"
"modp3072-modp4096-modp6144-modp8192-modp2048,"
// AEAD encryption algorithms
"aes128gcm16-aes192gcm16-aes256gcm16-chacha20poly1305-aes128gcm12-"
"aes192gcm12-aes256gcm12-aes128gcm8-aes192gcm8-aes256gcm8"
// PRF functions
"-prfsha256-prfsha384-prfsha512-prfaesxcbc-prfaescmac"
// DH groups
"-ecp256-ecp384-ecp521-ecp256bp-ecp384bp-ecp512bp-curve25519-curve448-"
"modp3072-modp4096-modp6144-modp8192-modp2048";
constexpr char kIKEv2DefaultESPProposals[] =
"aes128gcm16-aes192gcm16-aes256gcm16," // AEAD algorithms
"aes128-aes192-aes256" // encryption algorithms
"-sha256-sha384-sha512-aesxcbc"; // integrity algorithms
constexpr char kChildSAName[] = "managed";
// The interface identifier set for the XFRM interface. This id connects the
// IPsec policies and the interface. Only used in IKEv2 connections.
constexpr int kXFRMInterfaceID = 1;
// Only used in IKEv2 connection.
constexpr char kXFRMInterfaceName[] = "xfrm0";
// The time interval between two checks for if the vici socket is connectable.
constexpr base::TimeDelta kCheckViciConnectableInterval =
base::Milliseconds(300);
// The maximum number of attempts to check if the vici socket is connectable
// before returning a failure.
constexpr int kCheckViciConnectableMaxAttempts = 10;
// The default timeout value used in `swanctl --initiate`.
constexpr base::TimeDelta kIPsecTimeout = base::Seconds(30);
// The PIN value does not have any real effects. Use the default value here.
// See platform2/chaps/README.md
constexpr char kTPMDefaultPin[] = "111111";
// 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_;
};
// Parsing the encryption algorithm output by swanctl, which may contain two
// parts: the algorithm name and an optional key size. See the following src
// files in the strongswan project for how the name is output:
// - libstrongswan/crypto/crypters/crypter.c
// - swanctl/commands/list-sas.c
Metrics::VpnIpsecEncryptionAlgorithm ParseEncryptionAlgorithm(
const std::string& input) {
// The name and the key size is concated with "-". Changes them into "_" for
// simplicity.
std::string algo_str;
base::ReplaceChars(input, "-", "_", &algo_str);
static const std::map<std::string, Metrics::VpnIpsecEncryptionAlgorithm>
str2enum = {
{"AES_CBC_128", Metrics::kVpnIpsecEncryptionAlgorithm_AES_CBC_128},
{"AES_CBC_192", Metrics::kVpnIpsecEncryptionAlgorithm_AES_CBC_192},
{"AES_CBC_256", Metrics::kVpnIpsecEncryptionAlgorithm_AES_CBC_256},
{"CAMELLIA_CBC_128",
Metrics::kVpnIpsecEncryptionAlgorithm_CAMELLIA_CBC_128},
{"CAMELLIA_CBC_192",
Metrics::kVpnIpsecEncryptionAlgorithm_CAMELLIA_CBC_192},
{"CAMELLIA_CBC_256",
Metrics::kVpnIpsecEncryptionAlgorithm_CAMELLIA_CBC_256},
{"3DES_CBC", Metrics::kVpnIpsecEncryptionAlgorithm_3DES_CBC},
{"AES_GCM_16_128",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_16_128},
{"AES_GCM_16_192",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_16_192},
{"AES_GCM_16_256",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_16_256},
{"AES_GCM_12_128",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_12_128},
{"AES_GCM_12_192",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_12_192},
{"AES_GCM_12_256",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_12_256},
{"AES_GCM_8_128",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_8_128},
{"AES_GCM_8_192",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_8_192},
{"AES_GCM_8_256",
Metrics::kVpnIpsecEncryptionAlgorithm_AES_GCM_8_256},
};
const auto it = str2enum.find(algo_str);
if (it == str2enum.end()) {
return Metrics::kVpnIpsecEncryptionAlgorithmUnknown;
}
return it->second;
}
// Parsing the integrity algorithm output by swanctl, which may contain two
// parts: the algorithm name and an optional key size. See the following src
// files in the strongswan project for how the name is output:
// - libstrongswan/crypto/signers/signer.c
// - swanctl/commands/list-sas.c
Metrics::VpnIpsecIntegrityAlgorithm ParseIntegrityAlgorithm(
const std::string& input) {
// The name and the key size is concated with "-". Changes them into "_" for
// simplicity.
std::string algo_str;
base::ReplaceChars(input, "-", "_", &algo_str);
static const std::map<std::string, Metrics::VpnIpsecIntegrityAlgorithm>
str2enum = {
{"HMAC_SHA2_256_128",
Metrics::kVpnIpsecIntegrityAlgorithm_HMAC_SHA2_256_128},
{"HMAC_SHA2_384_192",
Metrics::kVpnIpsecIntegrityAlgorithm_HMAC_SHA2_384_192},
{"HMAC_SHA2_512_256",
Metrics::kVpnIpsecIntegrityAlgorithm_HMAC_SHA2_512_256},
{"HMAC_SHA1_96", Metrics::kVpnIpsecIntegrityAlgorithm_HMAC_SHA1_96},
{"AES_XCBC_96", Metrics::kVpnIpsecIntegrityAlgorithm_AES_XCBC_96},
{"AES_CMAC_96", Metrics::kVpnIpsecIntegrityAlgorithm_AES_CMAC_96},
};
const auto it = str2enum.find(algo_str);
if (it == str2enum.end()) {
return Metrics::kVpnIpsecIntegrityAlgorithmUnknown;
}
return it->second;
}
// Parsing the DH group output by swanctl. See the following src files in the
// strongswan project for the names:
// - libstrongswan/crypto/diffie_hellman.c
Metrics::VpnIpsecDHGroup ParseDHGroup(const std::string& input) {
static const std::map<std::string, Metrics::VpnIpsecDHGroup> str2enum = {
{"ECP_256", Metrics::kVpnIpsecDHGroup_ECP_256},
{"ECP_384", Metrics::kVpnIpsecDHGroup_ECP_384},
{"ECP_521", Metrics::kVpnIpsecDHGroup_ECP_521},
{"ECP_256_BP", Metrics::kVpnIpsecDHGroup_ECP_256_BP},
{"ECP_384_BP", Metrics::kVpnIpsecDHGroup_ECP_384_BP},
{"ECP_512_BP", Metrics::kVpnIpsecDHGroup_ECP_512_BP},
{"CURVE_25519", Metrics::kVpnIpsecDHGroup_CURVE_25519},
{"CURVE_448", Metrics::kVpnIpsecDHGroup_CURVE_448},
{"MODP_1024", Metrics::kVpnIpsecDHGroup_MODP_1024},
{"MODP_1536", Metrics::kVpnIpsecDHGroup_MODP_1536},
{"MODP_2048", Metrics::kVpnIpsecDHGroup_MODP_2048},
{"MODP_3072", Metrics::kVpnIpsecDHGroup_MODP_3072},
{"MODP_4096", Metrics::kVpnIpsecDHGroup_MODP_4096},
{"MODP_6144", Metrics::kVpnIpsecDHGroup_MODP_6144},
{"MODP_8192", Metrics::kVpnIpsecDHGroup_MODP_8192},
};
const auto it = str2enum.find(input);
if (it == str2enum.end()) {
return Metrics::kVpnIpsecDHGroupUnknown;
}
return it->second;
}
// Returns whether the pathname UNIX socket pointed by |path| is connect()-able.
bool TestUnixSocketConnectable(const base::FilePath& path) {
base::ScopedFD fd(socket(AF_UNIX, SOCK_STREAM, 0));
if (!fd.is_valid()) {
PLOG(ERROR) << "Failed to open UNIX socket";
return false;
}
struct sockaddr_un addr = {0};
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path.value().c_str());
if (HANDLE_EINTR(connect(fd.get(), (struct sockaddr*)&addr, sizeof(addr))) <
0) {
PLOG(WARNING) << "Failed to connect to UNIX socket file: " << path;
return false;
}
return true;
}
} // namespace
// static
IPsecConnection::CipherSuite IPsecConnection::ParseCipherSuite(
const std::string& input) {
constexpr auto kInvalidResults =
std::make_tuple(Metrics::kVpnIpsecEncryptionAlgorithmUnknown,
Metrics::kVpnIpsecIntegrityAlgorithmUnknown,
Metrics::kVpnIpsecDHGroupUnknown);
auto [encryption_algo, integrity_algo, dh_group] = kInvalidResults;
const std::vector<std::string> names = base::SplitString(
input, "/", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& name : names) {
// Tries parsing the name as an encryption algorithm.
auto parsed_encryption_algo = ParseEncryptionAlgorithm(name);
if (parsed_encryption_algo !=
Metrics::kVpnIpsecEncryptionAlgorithmUnknown) {
if (encryption_algo != Metrics::kVpnIpsecEncryptionAlgorithmUnknown) {
// This means |input| contains algorithm names with a certain type
// multiple times. This is not expected, discards the results.
LOG(ERROR) << "The input contains multiple encryption algorithm: "
<< input;
return kInvalidResults;
}
encryption_algo = parsed_encryption_algo;
continue;
}
// Tries parsing the name as an integrity algorithm.
auto parsed_integrity_algo = ParseIntegrityAlgorithm(name);
if (parsed_integrity_algo != Metrics::kVpnIpsecIntegrityAlgorithmUnknown) {
if (integrity_algo != Metrics::kVpnIpsecIntegrityAlgorithmUnknown) {
LOG(ERROR) << "The input contains multiple integrity algorithm: "
<< input;
return kInvalidResults;
}
integrity_algo = parsed_integrity_algo;
continue;
}
// Tries parsing the name as a DH group.
auto parsed_dh_group = ParseDHGroup(name);
if (parsed_dh_group != Metrics::kVpnIpsecDHGroupUnknown) {
if (dh_group != Metrics::kVpnIpsecDHGroupUnknown) {
LOG(ERROR) << "The input contains multiple DH group: " << input;
return kInvalidResults;
}
dh_group = parsed_dh_group;
continue;
}
}
return {encryption_algo, integrity_algo, dh_group};
}
IPsecConnection::IPsecConnection(std::unique_ptr<Config> config,
std::unique_ptr<Callbacks> callbacks,
std::unique_ptr<VPNConnection> l2tp_connection,
DeviceInfo* device_info,
EventDispatcher* dispatcher,
ProcessManager* process_manager)
: VPNConnection(std::move(callbacks), dispatcher),
config_(std::move(config)),
l2tp_connection_(std::move(l2tp_connection)),
vici_socket_path_(kViciSocketPath),
device_info_(device_info),
process_manager_(process_manager),
vpn_util_(VPNUtil::New()) {
if (l2tp_connection_) {
CHECK(config_->ike_version == Config::IKEVersion::kV1);
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 {
CHECK(config_->ike_version == Config::IKEVersion::kV2);
}
}
IPsecConnection::~IPsecConnection() {
if (state() == State::kIdle || state() == State::kStopped) {
return;
}
// This is unexpected but cannot be fully avoided, e.g., shill stops or
// restarts (or the corresponding service is removed) while the
// IPsecConnection is still connected or connecting (this is very likely to
// happen in the tast tests). Call StopCharon() to make sure that we at least
// send a SIGTERM to the charon process.
LOG(WARNING) << "Destructor called but the current state is " << state();
StopCharon();
}
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:
SwanctlListSAs();
return;
case ConnectStep::kIPsecStatusRead:
if (l2tp_connection_) {
l2tp_connection_->Connect();
} else {
ParseDNSServers();
CreateXFRMInterface();
}
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},
" }",
" }",
" }",
" resolve {",
" file = " + StrongSwanResolvConfPath().value(),
" }",
" }",
"}",
};
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);
const std::string kIfIdString = base::NumberToString(kXFRMInterfaceID);
switch (config_->ike_version) {
case Config::IKEVersion::kV1:
vpn_section->AddKeyValue("version", "1"); // IKEv1
vpn_section->AddKeyValue("proposals", kL2TPIPsecDefaultIKEProposals);
break;
case Config::IKEVersion::kV2:
vpn_section->AddKeyValue("version", "2"); // IKEv2
vpn_section->AddKeyValue("proposals", kIKEv2DefaultIKEProposals);
vpn_section->AddKeyValue("vips", "0.0.0.0");
vpn_section->AddKeyValue("if_id_in", kIfIdString);
vpn_section->AddKeyValue("if_id_out", kIfIdString);
break;
default:
NOTREACHED();
}
// Fields for PSK.
if (config_->psk.has_value()) {
Section* local = vpn_section->AddSection("local-psk");
Section* remote = vpn_section->AddSection("remote-psk");
local->AddKeyValue("auth", "psk");
remote->AddKeyValue("auth", "psk");
auto* psk_section = secrets_section.AddSection("ike-1");
psk_section->AddKeyValue("secret", config_->psk.value());
if (config_->local_id.has_value()) {
local->AddKeyValue("id", config_->local_id.value());
}
if (config_->remote_id.has_value()) {
remote->AddKeyValue("id", config_->remote_id.value());
}
// TODO(b/165170125): This part is untested.
if (config_->tunnel_group.has_value()) {
// Aggressive mode is insecure but required by the legacy Cisco VPN here.
// See https://crbug.com/199004 .
vpn_section->AddKeyValue("aggressive", "yes");
// Sets local id.
const std::string tunnel_group = config_->tunnel_group.value();
const std::string hex_tunnel_id =
base::HexEncode(tunnel_group.c_str(), tunnel_group.length());
const std::string local_id =
base::StringPrintf("@#%s", hex_tunnel_id.c_str());
local->AddKeyValue("id", local_id);
}
}
// Fields for local pubkey.
if (config_->client_cert_id.has_value()) {
if (!config_->ca_cert_pem_strings.has_value() ||
!config_->client_cert_slot.has_value()) {
NotifyFailure(Service::kFailureInternal,
"Expect cert auth but some required fields are empty");
return;
}
Section* local_cert = vpn_section->AddSection("local-pubkey");
local_cert->AddKeyValue("auth", "pubkey");
if (config_->local_id.has_value()) {
local_cert->AddKeyValue("id", config_->local_id.value());
}
Section* cert = local_cert->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", kTPMDefaultPin);
}
// Fields for remote pubkey.
if (config_->ca_cert_pem_strings) {
Section* remote = vpn_section->AddSection("remote-pubkey");
remote->AddKeyValue("auth", "pubkey");
if (config_->remote_id.has_value()) {
remote->AddKeyValue("id", config_->remote_id.value());
}
// 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());
remote->AddKeyValue("cacerts", server_ca_path_.value());
}
// Fields for Xauth/EAP-MSCHAPv2. This will be used as the second round in
// L2TP/IPsec VPN or the first round in IKEv2 VPN.
if (config_->xauth_user.has_value() || config_->xauth_password.has_value()) {
if (!config_->xauth_user.has_value()) {
NotifyFailure(Service::kFailureInternal, "Only Xauth password is set");
return;
}
if (!config_->xauth_password.has_value()) {
NotifyFailure(Service::kFailureInternal, "Only Xauth user is set");
return;
}
Section* local = vpn_section->AddSection("local-xauth");
if (config_->local_id.has_value()) {
local->AddKeyValue("id", config_->local_id.value());
}
switch (config_->ike_version) {
case Config::IKEVersion::kV1:
local->AddKeyValue("auth", "xauth");
local->AddKeyValue("xauth_id", config_->xauth_user.value());
break;
case Config::IKEVersion::kV2:
local->AddKeyValue("auth", "eap-mschapv2");
local->AddKeyValue("eap_id", config_->xauth_user.value());
break;
default:
NOTREACHED();
}
Section* xauth_section = secrets_section.AddSection("xauth-1");
xauth_section->AddKeyValue("id", config_->xauth_user.value());
xauth_section->AddKeyValue("secret", config_->xauth_password.value());
}
// Fields for CHILD_SA.
Section* children_section = vpn_section->AddSection("children");
Section* child_section = children_section->AddSection(kChildSAName);
switch (config_->ike_version) {
case Config::IKEVersion::kV1:
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",
kL2TPIPsecDefaultESPProposals);
child_section->AddKeyValue("mode", "transport");
break;
case Config::IKEVersion::kV2:
child_section->AddKeyValue("local_ts", "dynamic");
child_section->AddKeyValue("remote_ts", "0.0.0.0/0");
child_section->AddKeyValue("esp_proposals", kIKEv2DefaultESPProposals);
child_section->AddKeyValue("mode", "tunnel");
break;
default:
NOTREACHED();
}
// 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).
std::vector<std::string> args = {};
std::map<std::string, std::string> env = {
{"STRONGSWAN_CONF", strongswan_conf_path_.value()},
};
constexpr uint64_t kCapMask = CAP_TO_MASK(CAP_NET_ADMIN) |
CAP_TO_MASK(CAP_NET_BIND_SERVICE) |
CAP_TO_MASK(CAP_NET_RAW);
auto minijail_options = VPNUtil::BuildMinijailOptions(kCapMask);
// Charon can have a quite large VmSize/VmPeak despite not using much resident
// memory. This can be partially reduced by lowering charon.threads, but in
// any case, Charon cannot rely on inheriting shill's RLIMIT_AS. See
// crbug/961519.
minijail_options.rlimit_as_soft = 750'000'000; // 750MB
charon_pid_ = process_manager_->StartProcessInMinijail(
FROM_HERE, base::FilePath(kCharonPath), args, env, minijail_options,
base::BindOnce(&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(),
kCheckViciConnectableMaxAttempts);
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::SwanctlNextStep,
weak_factory_.GetWeakPtr(),
ConnectStep::kSwanctlConfigLoaded),
Service::kFailureInternal, "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::string timeout_str =
base::NumberToString(kIPsecTimeout.InSeconds());
const std::vector<std::string> args = {"--initiate", "-c", kChildSAName,
"--timeout", timeout_str};
RunSwanctl(
args,
base::BindOnce(&IPsecConnection::SwanctlNextStep,
weak_factory_.GetWeakPtr(), ConnectStep::kIPsecConnected),
Service::kFailureConnect, "Failed to initiate IPsec connection");
}
void IPsecConnection::SwanctlListSAs() {
const std::vector<std::string> args = {"--list-sas"};
RunSwanctl(args,
base::BindOnce(&IPsecConnection::OnSwanctlListSAsDone,
weak_factory_.GetWeakPtr()),
Service::kFailureInternal, "Failed to get SA information");
}
void IPsecConnection::CreateXFRMInterface() {
// We use the lo interface as the underlying interface of the created
// xfrm interface. This field is mandatory but does not really matter in
// our use case: it does matter if the outbound interface of the IPsec
// policies is configured. See the following link for more details:
// https://wiki.strongswan.org/projects/strongswan/wiki/RouteBasedVPN
int lo_index = device_info_->GetIndex("lo");
if (lo_index == -1) {
NotifyFailure(Service::kFailureInternal, "Failed to get index of lo");
return;
}
const std::string err_msg = "Failed to create XFRM interface";
if (!device_info_->CreateXFRMInterface(
kXFRMInterfaceName, lo_index, kXFRMInterfaceID,
base::BindOnce(&IPsecConnection::OnXFRMInterfaceReady,
weak_factory_.GetWeakPtr()),
base::BindOnce(&IPsecConnection::NotifyFailure,
weak_factory_.GetWeakPtr(), Service::kFailureInternal,
err_msg))) {
NotifyFailure(Service::kFailureInternal, err_msg);
}
return;
}
void IPsecConnection::OnViciSocketPathEvent(int remaining_attempts,
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;
}
vici_socket_watcher_ = nullptr;
if (remaining_attempts <= 0) {
NotifyFailure(Service::kFailureInternal,
"Failed to wait for vici socket ready.");
return;
}
if (!TestUnixSocketConnectable(vici_socket_path_)) {
LOG(WARNING) << "vici socket is not connectable";
dispatcher()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&IPsecConnection::OnViciSocketPathEvent,
weak_factory_.GetWeakPtr(), remaining_attempts - 1,
vici_socket_path_, false),
kCheckViciConnectableInterval);
return;
}
LOG(INFO) << "vici socket is ready";
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::OnSwanctlListSAsDone(const std::string& stdout_str) {
// Note that any failure in parsing the cipher suite is unexpected but will
// not block the connection. We only leave a log for such failures.
const std::vector<std::string> lines = base::SplitString(
stdout_str, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (!l2tp_connection_) {
ParseLocalVirtualIP(lines);
if (local_virtual_ip_.empty()) {
NotifyFailure(Service::kFailureInternal,
"Failed to get local virtual IP");
return;
}
}
ParseIKECipherSuite(lines);
ParseESPCipherSuite(lines);
ScheduleConnectTask(ConnectStep::kIPsecStatusRead);
}
void IPsecConnection::RunSwanctl(const std::vector<std::string>& args,
SwanctlCallback on_success,
Service::ConnectFailure reason_on_failure,
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_->StartProcessInMinijailWithStdout(
FROM_HERE, base::FilePath(kSwanctlPath), args, env,
VPNUtil::BuildMinijailOptions(kCapMask),
base::BindOnce(&IPsecConnection::OnSwanctlExited,
weak_factory_.GetWeakPtr(), std::move(on_success),
reason_on_failure, message_on_failure));
if (pid == -1) {
NotifyFailure(Service::kFailureInternal,
message_on_failure + ": failed to run swanctl in minijail");
}
}
void IPsecConnection::OnSwanctlExited(SwanctlCallback on_success,
Service::ConnectFailure reason_on_failure,
const std::string& message_on_failure,
int exit_code,
const std::string& stdout_str) {
if (exit_code == 0) {
std::move(on_success).Run(stdout_str);
} else {
NotifyFailure(reason_on_failure,
base::StringPrintf("%s, exit_code=%d",
message_on_failure.c_str(), exit_code));
}
}
void IPsecConnection::SwanctlNextStep(ConnectStep step, const std::string&) {
ScheduleConnectTask(step);
}
void IPsecConnection::ParseLocalVirtualIP(
const std::vector<std::string>& swanctl_output) {
local_virtual_ip_ = "";
// The index of the line which contains the virtual IP information in
// |swanctl_output|.
constexpr int kVIPLineNumber = 1;
if (swanctl_output.size() <= kVIPLineNumber) {
LOG(ERROR) << "Failed to parse the virtual IP, output only contains "
<< swanctl_output.size() << " lines";
return;
}
// Example: local '192.168.1.245' @ 192.168.1.245[4500] [10.10.10.2]
// We need to match the IP address in the last bracket ("[10.10.10.2]").
static constexpr LazyRE2 kVIPLine = {
R"(\s*local.*@.*\s+\[(\d*\.\d*\.\d*\.\d*)\]\s*$)"};
const std::string& line = swanctl_output[kVIPLineNumber];
std::string matched_part;
if (!RE2::FullMatch(line, *kVIPLine, &matched_part)) {
LOG(ERROR) << "Failed to parse the virtual IP, the line is: " << line;
return;
}
local_virtual_ip_ = matched_part;
}
void IPsecConnection::ParseIKECipherSuite(
const std::vector<std::string>& swanctl_output) {
ike_encryption_algo_ = Metrics::kVpnIpsecEncryptionAlgorithmUnknown;
ike_integrity_algo_ = Metrics::kVpnIpsecIntegrityAlgorithmUnknown;
ike_dh_group_ = Metrics::kVpnIpsecDHGroupUnknown;
// The index of the line which contains the cipher suite information for IKE
// in |swanctl_output|.
constexpr int kIKECipherSuiteLineNumber = 3;
if (swanctl_output.size() <= kIKECipherSuiteLineNumber) {
LOG(ERROR) << "Failed to parse the IKE cipher suite, the number of line is "
<< swanctl_output.size();
return;
}
// Example: AES_CBC-128/HMAC_SHA2_256_128/PRF_HMAC_SHA2_256/MODP_3072
// See `swanctl/commands/list-sas.c:ike_sa()` in the strongswan project for
// the format.
static constexpr LazyRE2 kIKECipherSuiteLine = {
R"(^\s*((?:[^/\s]+)(?:/[^/\s]+)*)\s*$)"};
const std::string& line = swanctl_output[kIKECipherSuiteLineNumber];
std::string matched_part;
if (!RE2::FullMatch(line, *kIKECipherSuiteLine, &matched_part)) {
LOG(ERROR) << "Failed to parse the IKE cipher suite, the line is: " << line;
return;
}
std::tie(ike_encryption_algo_, ike_integrity_algo_, ike_dh_group_) =
ParseCipherSuite(matched_part);
if (ike_encryption_algo_ == Metrics::kVpnIpsecEncryptionAlgorithmUnknown ||
ike_integrity_algo_ == Metrics::kVpnIpsecIntegrityAlgorithmUnknown ||
ike_dh_group_ == Metrics::kVpnIpsecDHGroupUnknown) {
LOG(ERROR) << "The output does not contain a valid cipher suite for IKE: "
<< matched_part;
}
}
void IPsecConnection::ParseESPCipherSuite(
const std::vector<std::string>& swanctl_output) {
esp_encryption_algo_ = Metrics::kVpnIpsecEncryptionAlgorithmUnknown;
esp_integrity_algo_ = Metrics::kVpnIpsecIntegrityAlgorithmUnknown;
// The index of the line which contains the cipher suite information for ESP
// in |swanctl_output|.
constexpr int kESPCipherSuiteLineNumber = 5;
if (swanctl_output.size() <= kESPCipherSuiteLineNumber) {
LOG(ERROR) << "Failed to parse the ESP cipher suite, the number of line is "
<< swanctl_output.size();
return;
}
// This line does not only contains the cipher suite for ESP. Example:
// managed: #1, reqid 1, INSTALLED, TUNNEL, ESP:AES_CBC-128/HMAC_SHA2_256_128
// See `swanctl/commands/list-sas.c:child_sas()` in the strongswan project
// for the format.
static constexpr LazyRE2 kESPCipherSuiteLine = {
R"(^.*ESP:((?:[^/\s]+)(?:/[^/\s]+)*)\s*$)"};
const std::string& line = swanctl_output[kESPCipherSuiteLineNumber];
std::string matched_part;
if (!RE2::FullMatch(line, *kESPCipherSuiteLine, &matched_part)) {
LOG(ERROR) << "Failed to parse the ESP cipher suite, the line is: " << line;
return;
}
const auto parsed_results = ParseCipherSuite(matched_part);
esp_encryption_algo_ = std::get<0>(parsed_results);
esp_integrity_algo_ = std::get<1>(parsed_results);
if (esp_encryption_algo_ == Metrics::kVpnIpsecEncryptionAlgorithmUnknown ||
esp_integrity_algo_ == Metrics::kVpnIpsecIntegrityAlgorithmUnknown) {
LOG(ERROR) << "The output does not contain a valid cipher suite for ESP: "
<< matched_part;
}
}
// The file to be parsed is in resolv.conf format. Example of its contents:
// nameserver 1.2.3.4 # by strongSwan
// nameserver 1.2.3.5 # by strongSwan
// TODO(b/229918180): Add a fuzzer test for this function.
void IPsecConnection::ParseDNSServers() {
dns_servers_.clear();
const base::FilePath path = StrongSwanResolvConfPath();
if (!base::PathExists(path)) {
LOG(INFO) << "No DNS servers found";
return;
}
std::string contents;
if (!base::ReadFileToString(path, &contents)) {
LOG(ERROR) << "Failed to read " << path.value();
return;
}
// TODO(jiejiang): Support IPv6 name servers.
static constexpr LazyRE2 kNameServerLine = {
R"(^nameserver\s+(\d+\.\d+\.\d+\.\d+).*)"};
const std::vector<std::string> lines = base::SplitString(
contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& line : lines) {
std::string matched_part;
if (RE2::FullMatch(line, *kNameServerLine, &matched_part)) {
dns_servers_.push_back(matched_part);
}
}
LOG(INFO) << "Received " << dns_servers_.size() << " DNS server entries";
return;
}
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:
StopCharon();
return;
case State::kConnecting:
case State::kConnected:
l2tp_connection_->Disconnect();
return;
case State::kDisconnecting:
// StopCharon() called in the stopped callback.
return;
case State::kStopped:
// If |l2tp_connection_| is in stopped state but has not been destroyed,
// the stopped callback must be in the queue, so StopCharon() will be
// called later.
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::OnXFRMInterfaceReady(const std::string& ifname,
int ifindex) {
xfrm_interface_index_ = ifindex;
IPConfig::Properties props;
props.address = local_virtual_ip_;
props.subnet_prefix = 32;
props.dns_servers = dns_servers_;
props.blackhole_ipv6 = true;
// This is a point-to-point link, gateway does not make sense here. Set it
// default to skip RTA_GATEWAY when installing routes.
props.gateway = "0.0.0.0";
props.mtu = IPConfig::kMinIPv6MTU;
props.method = kTypeVPN;
NotifyConnected(ifname, ifindex, props);
}
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";
}
// Removes the XFRM interface if it has been created.
if (xfrm_interface_index_.has_value()) {
device_info_->DeleteInterface(xfrm_interface_index_.value());
xfrm_interface_index_.reset();
}
// 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();
}
}
base::FilePath IPsecConnection::StrongSwanResolvConfPath() const {
return temp_dir_.GetPath().Append(kResolvConfFilename);
}
} // namespace shill