vpn: add support for spawning VPN client programs in minijail

Spawn VPN programs in a minijail if the --jail-vpn-clients command line
flag is passed. Also update unit tests.

Also make the openvpn config dir/file world-readable so it can be created
by shill user and accessed by openvpn user. If we think this file is
sensitive and shouldn't be world-readable then we could alternatively
make shill user belong to openvpn group and have shill chgrp() on the
file to openvpn. I didn't do that here for sake of simplicity but will
make that accomodation if anyone feels strongly about this.

BUG=chromium:649417
CQ-DEPEND=CL:1086231
TEST=unit tests pass, tested as part of larger sandbox shill debug CL

Change-Id: I1037c1e3b060e53a699411c197c5961f9cbc5527
Reviewed-on: https://chromium-review.googlesource.com/1087359
Commit-Ready: Micah Morton <mortonm@chromium.org>
Tested-by: Micah Morton <mortonm@chromium.org>
Reviewed-by: Micah Morton <mortonm@chromium.org>
diff --git a/mock_manager.h b/mock_manager.h
index 0a7cc2d..80d5338 100644
--- a/mock_manager.h
+++ b/mock_manager.h
@@ -97,6 +97,7 @@
   MOCK_CONST_METHOD1(GetEnabledDeviceByLinkName,
                      DeviceRefPtr(const std::string& link_name));
   MOCK_CONST_METHOD0(GetMinimumMTU, int());
+  MOCK_CONST_METHOD0(GetJailVpnClients, bool());
   MOCK_CONST_METHOD1(ShouldAcceptHostnameFrom,
                      bool(const std::string& device_name));
   MOCK_CONST_METHOD1(IsDHCPv6EnabledForDevice,
diff --git a/vpn/l2tp_ipsec_driver.cc b/vpn/l2tp_ipsec_driver.cc
index b8cfd0e..0394ac6 100644
--- a/vpn/l2tp_ipsec_driver.cc
+++ b/vpn/l2tp_ipsec_driver.cc
@@ -240,10 +240,23 @@
   LOG(INFO) << "L2TP/IPSec VPN process options: "
             << base::JoinString(options, " ");
 
-  if (external_task_local->Start(
-          FilePath(kL2TPIPSecVPNPath), options, environment, true, error)) {
-    external_task_ = std::move(external_task_local);
-    return true;
+  if (manager()->GetJailVpnClients()) {
+    uint64_t capmask = CAP_TO_MASK(CAP_NET_ADMIN) | CAP_TO_MASK(CAP_NET_RAW) |
+                       CAP_TO_MASK(CAP_NET_BIND_SERVICE) |
+                       CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID) |
+                       CAP_TO_MASK(CAP_KILL);
+    if (external_task_local->StartInMinijail(FilePath(kL2TPIPSecVPNPath),
+                                             options, "shill", "shill", capmask,
+                                             true, error)) {
+      external_task_ = std::move(external_task_local);
+      return true;
+    }
+  } else {
+    if (external_task_local->Start(FilePath(kL2TPIPSecVPNPath), options,
+                                   environment, true, error)) {
+      external_task_ = std::move(external_task_local);
+      return true;
+    }
   }
   return false;
 }
diff --git a/vpn/l2tp_ipsec_driver.h b/vpn/l2tp_ipsec_driver.h
index d23b4fe..90674e9 100644
--- a/vpn/l2tp_ipsec_driver.h
+++ b/vpn/l2tp_ipsec_driver.h
@@ -86,6 +86,7 @@
   FRIEND_TEST(L2TPIPSecDriverTest, OnConnectionDisconnected);
   FRIEND_TEST(L2TPIPSecDriverTest, OnL2TPIPSecVPNDied);
   FRIEND_TEST(L2TPIPSecDriverTest, SpawnL2TPIPSecVPN);
+  FRIEND_TEST(L2TPIPSecDriverTest, SpawnL2TPIPSecVPNInMinijail);
 
   static const char kL2TPIPSecVPNPath[];
   static const Property kProperties[];
diff --git a/vpn/l2tp_ipsec_driver_unittest.cc b/vpn/l2tp_ipsec_driver_unittest.cc
index 47e32cd..f65df12 100644
--- a/vpn/l2tp_ipsec_driver_unittest.cc
+++ b/vpn/l2tp_ipsec_driver_unittest.cc
@@ -548,6 +548,10 @@
   static const char kHost[] = "192.168.2.254";
   SetArg(kProviderHostProperty, kHost);
 
+  EXPECT_CALL(manager_, GetJailVpnClients())
+      .Times(2)
+      .WillRepeatedly(Return(false));
+
   // TODO(quiche): Instead of setting expectations based on what
   // ExternalTask will call, mock out ExternalTask. Non-trivial,
   // though, because ExternalTask is constructed during the
@@ -562,11 +566,41 @@
   EXPECT_NE(nullptr, driver_->external_task_);
 }
 
+TEST_F(L2TPIPSecDriverTest, SpawnL2TPIPSecVPNInMinijail) {
+  Error error;
+  // Fail without sufficient arguments.
+  EXPECT_FALSE(driver_->SpawnL2TPIPSecVPN(&error));
+  EXPECT_TRUE(error.IsFailure());
+
+  // Provide the required arguments.
+  static const char kHost[] = "192.168.2.254";
+  SetArg(kProviderHostProperty, kHost);
+
+  EXPECT_CALL(manager_, GetJailVpnClients())
+      .Times(2)
+      .WillRepeatedly(Return(true));
+
+  // TODO(mortonm): Instead of setting expectations based on what
+  // ExternalTask will call, mock out ExternalTask. Non-trivial,
+  // though, because ExternalTask is constructed during the
+  // call to driver_->Connect.
+  EXPECT_CALL(process_manager_, StartProcessInMinijail(_, _, _, _, _, _, _, _))
+      .WillOnce(Return(-1))
+      .WillOnce(Return(1));
+
+  EXPECT_FALSE(driver_->SpawnL2TPIPSecVPN(&error));
+  EXPECT_FALSE(driver_->external_task_);
+  EXPECT_TRUE(driver_->SpawnL2TPIPSecVPN(&error));
+  EXPECT_NE(nullptr, driver_->external_task_);
+}
+
 TEST_F(L2TPIPSecDriverTest, Connect) {
   EXPECT_CALL(*service_, SetState(Service::kStateConfiguring));
   static const char kHost[] = "192.168.2.254";
   SetArg(kProviderHostProperty, kHost);
 
+  EXPECT_CALL(manager_, GetJailVpnClients()).WillOnce(Return(false));
+
   // TODO(quiche): Instead of setting expectations based on what
   // ExternalTask will call, mock out ExternalTask. Non-trivial,
   // though, because ExternalTask is constructed during the
diff --git a/vpn/openvpn_driver.cc b/vpn/openvpn_driver.cc
index 20dd83e..ca0a909 100644
--- a/vpn/openvpn_driver.cc
+++ b/vpn/openvpn_driver.cc
@@ -289,7 +289,14 @@
                  << openvpn_config_directory_.value();
       return false;
     }
-    if (chmod(openvpn_config_directory_.value().c_str(), S_IRWXU)) {
+    // 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::DeleteFile(openvpn_config_directory_, true);
@@ -301,7 +308,10 @@
   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())) {
+          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;
   }
@@ -313,6 +323,7 @@
 
   vector<vector<string>> options;
   Error error;
+  pid_t openvpn_pid;
   InitOptions(&options, &error);
   if (error.IsFailure()) {
     return false;
@@ -329,18 +340,29 @@
   vector<string> args = GetCommandLineArgs();
   LOG(INFO) << "OpenVPN command line args: " << base::JoinString(args, " ");
 
-  pid_t pid = process_manager_->StartProcess(
-      FROM_HERE, FilePath(kOpenVPNPath),
-      args,
-      map<string, string>(), // No env vars passed.
-      false,  // Do not terminate with parent.
-      base::Bind(&OpenVPNDriver::OnOpenVPNDied, base::Unretained(this)));
-  if (pid < 0) {
-    LOG(ERROR) << "Unable to spawn: " << kOpenVPNPath;
-    return false;
+  if (manager()->GetJailVpnClients()) {
+    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, "shill", "shill",
+        capmask, true,
+        base::Bind(&OpenVPNDriver::OnOpenVPNDied, base::Unretained(this)));
+    if (openvpn_pid == -1) {
+      LOG(ERROR) << "Minijail couldn't run our child process";
+      return false;
+    }
+  } else {
+    openvpn_pid = process_manager_->StartProcess(
+        FROM_HERE, FilePath(kOpenVPNPath), args,
+        map<string, string>(),  // No env vars passed.
+        false,                  // Do not terminate with parent.
+        base::Bind(&OpenVPNDriver::OnOpenVPNDied, base::Unretained(this)));
+    if (openvpn_pid < 0) {
+      LOG(ERROR) << "Unable to spawn: " << kOpenVPNPath;
+      return false;
+    }
   }
-
-  pid_ = pid;
+  pid_ = openvpn_pid;
   return true;
 }
 
@@ -1054,7 +1076,7 @@
 vector<string> OpenVPNDriver::GetCommandLineArgs() {
   SLOG(this, 2) << __func__ << "(" << lsb_release_file_.value() << ")";
   vector<string> args =
-                 vector<string>{"--config", openvpn_config_file_.value()};
+      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: "
diff --git a/vpn/openvpn_driver.h b/vpn/openvpn_driver.h
index 5ae9829..a727552 100644
--- a/vpn/openvpn_driver.h
+++ b/vpn/openvpn_driver.h
@@ -159,6 +159,7 @@
   FRIEND_TEST(OpenVPNDriverTest, ParseRouteOption);
   FRIEND_TEST(OpenVPNDriverTest, SetRoutes);
   FRIEND_TEST(OpenVPNDriverTest, SpawnOpenVPN);
+  FRIEND_TEST(OpenVPNDriverTest, SpawnOpenVPNInMinijail);
   FRIEND_TEST(OpenVPNDriverTest, SplitPortFromHost);
   FRIEND_TEST(OpenVPNDriverTest, WriteConfigFile);
 
diff --git a/vpn/openvpn_driver_unittest.cc b/vpn/openvpn_driver_unittest.cc
index a045d3b..78d1175 100644
--- a/vpn/openvpn_driver_unittest.cc
+++ b/vpn/openvpn_driver_unittest.cc
@@ -1112,6 +1112,7 @@
   SetArg(kProviderHostProperty, kHost);
   EXPECT_CALL(*management_server_, Start(_, _, _)).WillOnce(Return(true));
   EXPECT_CALL(manager_, IsConnected()).WillOnce(Return(false));
+  EXPECT_CALL(manager_, GetJailVpnClients()).WillOnce(Return(false));
   EXPECT_CALL(
       process_manager_,
       StartProcess(_, _, _, _, false /* Don't exit with parent */, _))
@@ -1200,6 +1201,10 @@
       .WillRepeatedly(Return(true));
   EXPECT_CALL(manager_, IsConnected()).Times(2).WillRepeatedly(Return(false));
 
+  EXPECT_CALL(manager_, GetJailVpnClients())
+      .Times(2)
+      .WillRepeatedly(Return(false));
+
   const int kPID = 234678;
   const map<string, string> expected_env; // No env vars passed.
   EXPECT_CALL(process_manager_, StartProcess(_, _, _, expected_env, _, _))
@@ -1210,6 +1215,33 @@
   EXPECT_EQ(kPID, driver_->pid_);
 }
 
+TEST_F(OpenVPNDriverTest, SpawnOpenVPNInMinijail) {
+  SetupLSBRelease();
+
+  EXPECT_FALSE(driver_->SpawnOpenVPN());
+
+  static const char kHost[] = "192.168.2.254";
+  SetArg(kProviderHostProperty, kHost);
+  driver_->tunnel_interface_ = "tun0";
+  driver_->rpc_task_.reset(new RPCTask(&control_, this));
+  EXPECT_CALL(*management_server_, Start(_, _, _))
+      .Times(2)
+      .WillRepeatedly(Return(true));
+  EXPECT_CALL(manager_, IsConnected()).Times(2).WillRepeatedly(Return(false));
+
+  EXPECT_CALL(manager_, GetJailVpnClients())
+      .Times(2)
+      .WillRepeatedly(Return(true));
+
+  const int kPID = 234678;
+  EXPECT_CALL(process_manager_, StartProcessInMinijail(_, _, _, _, _, _, _, _))
+      .WillOnce(Return(-1))
+      .WillOnce(Return(kPID));
+  EXPECT_FALSE(driver_->SpawnOpenVPN());
+  EXPECT_TRUE(driver_->SpawnOpenVPN());
+  EXPECT_EQ(kPID, driver_->pid_);
+}
+
 TEST_F(OpenVPNDriverTest, OnOpenVPNDied) {
   const int kPID = 99999;
   driver_->device_ = device_;