shill: OpenVPNDriver: Allow option to ignore default route

In OpenVPN, it is up to the client to decide whether or not to
accept a default route from the OpenVPN server.  If any routes
are specified by the server, the "route_vpn_gateway" option
may be provided in order to specify the default router to be
used in pushed routes.  This does not necessarily mean that
the server also wishes that the client adds a default route
through this gateway.  There is no pushed configuration from
the server that specifies what this behavior should be.

By default, ChromeOS creates a default route to the gateway.
This CL adds a configuration option to VPN services which allows
users or administrators wishing to have a "split tunel"
configuration to omit the default route option.  Supplying this
option does not override explicit routes pushed by the server, so
administrators can still effectively force a single route through
their chosen gateway.

CQ-DEPEND=CL:198900
BUG=chromium:370460
TEST=Unit tests

Change-Id: I8a3efe717b0d70742065aa9b89671ecd21f3f613
Reviewed-on: https://chromium-review.googlesource.com/198902
Reviewed-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: Paul Stewart <pstew@chromium.org>
Tested-by: Paul Stewart <pstew@chromium.org>
diff --git a/doc/service-api.txt b/doc/service-api.txt
index 889a9b4..52c4443 100644
--- a/doc/service-api.txt
+++ b/doc/service-api.txt
@@ -728,6 +728,20 @@
 			value of this property is readable in the "Provider"
 			property of this service.
 
+		string OpenVPN.IgnoreDefaultRoute [writeonly]
+
+			(VPN services of type OpenVPN only) If set, a
+			default route to the default gateway specified by
+			the server will not be configured.  This allows this
+			connection to operate as a "split tunnel" in
+			configurations where the server allows this.  Since
+			this option does not ignore routes explicitly pushed
+			from the server, this option does not allow split
+			tunnels when they are expressly forbidden in the
+			server configuration.  The current value of this
+			property is readable in the "Provider" property of
+			this service.
+
 		string OpenVPN.Key [writeonly]
 
 			(VPN services of type OpenVPN only) Specify the
diff --git a/openvpn_driver.cc b/openvpn_driver.cc
index 8939b36..cf9cf7a 100644
--- a/openvpn_driver.cc
+++ b/openvpn_driver.cc
@@ -79,6 +79,7 @@
   { kOpenVPNClientCertIdProperty, Property::kCredential },
   { kOpenVPNCompLZOProperty, 0 },
   { kOpenVPNCompNoAdaptProperty, 0 },
+  { kOpenVPNIgnoreDefaultRouteProperty, 0 },
   { kOpenVPNKeyDirectionProperty, 0 },
   { kOpenVPNNsCertTypeProperty, 0 },
   { kOpenVPNOTPProperty,
@@ -394,10 +395,9 @@
   StopConnectTimeout();
 }
 
-// static
 void OpenVPNDriver::ParseIPConfiguration(
     const map<string, string> &configuration,
-    IPConfig::Properties *properties) {
+    IPConfig::Properties *properties) const {
   ForeignOptions foreign_options;
   RouteOptions routes;
   properties->address_family = IPAddress::kFamilyIPv4;
@@ -433,7 +433,12 @@
         properties->peer_address = value;
       }
     } else if (LowerCaseEqualsASCII(key, kOpenVPNRouteVPNGateway)) {
-      properties->gateway = value;
+      if (const_args()->ContainsString(kOpenVPNIgnoreDefaultRouteProperty)) {
+        SLOG(VPN, 2) << "Ignoring default route parameter as requested by "
+                     << "configuration.";
+      } else {
+        properties->gateway = value;
+      }
     } else if (LowerCaseEqualsASCII(key, kOpenVPNTrustedIP)) {
       properties->trusted_ip = value;
     } else if (LowerCaseEqualsASCII(key, kOpenVPNTunMTU)) {
diff --git a/openvpn_driver.h b/openvpn_driver.h
index 55bccc3..a44d9ce 100644
--- a/openvpn_driver.h
+++ b/openvpn_driver.h
@@ -173,9 +173,6 @@
   static const int kReconnectOfflineTimeoutSeconds;
   static const int kReconnectTLSErrorTimeoutSeconds;
 
-  static void ParseIPConfiguration(
-      const std::map<std::string, std::string> &configuration,
-      IPConfig::Properties *properties);
   static void ParseForeignOptions(const ForeignOptions &options,
                                   IPConfig::Properties *properties);
   static void ParseForeignOption(const std::string &option,
@@ -211,6 +208,9 @@
   void InitLoggingOptions(std::vector<std::vector<std::string>> *options);
 
   void InitEnvironment(std::vector<std::string> *environment);
+  void ParseIPConfiguration(
+      const std::map<std::string, std::string> &configuration,
+      IPConfig::Properties *properties) const;
   bool ParseLSBRelease(std::map<std::string, std::string> *lsb_release);
 
   bool SpawnOpenVPN();
diff --git a/openvpn_driver_unittest.cc b/openvpn_driver_unittest.cc
index f5b1b9e..fa903e7 100644
--- a/openvpn_driver_unittest.cc
+++ b/openvpn_driver_unittest.cc
@@ -626,18 +626,18 @@
   map<string, string> config;
   IPConfig::Properties props;
 
-  OpenVPNDriver::ParseIPConfiguration(config, &props);
+  driver_->ParseIPConfiguration(config, &props);
   EXPECT_EQ(IPAddress::kFamilyIPv4, props.address_family);
   EXPECT_EQ(32, props.subnet_prefix);
 
   props.subnet_prefix = 18;
-  OpenVPNDriver::ParseIPConfiguration(config, &props);
+  driver_->ParseIPConfiguration(config, &props);
   EXPECT_EQ(18, props.subnet_prefix);
 
   // An "ifconfig_remote" parameter that looks like a netmask should be
   // applied to the subnet prefix instead of to the peer address.
   config["ifconfig_remotE"] = "255.255.0.0";
-  OpenVPNDriver::ParseIPConfiguration(config, &props);
+  driver_->ParseIPConfiguration(config, &props);
   EXPECT_EQ(16, props.subnet_prefix);
   EXPECT_EQ("", props.peer_address);
 
@@ -658,7 +658,7 @@
   config["route_gateway_2"] = kGateway2;
   config["route_gateway_1"] = kGateway1;
   config["foo"] = "bar";
-  OpenVPNDriver::ParseIPConfiguration(config, &props);
+  driver_->ParseIPConfiguration(config, &props);
   EXPECT_EQ(IPAddress::kFamilyIPv4, props.address_family);
   EXPECT_EQ("4.5.6.7", props.address);
   EXPECT_EQ("1.2.255.255", props.broadcast_address);
@@ -679,6 +679,15 @@
   EXPECT_EQ(kNetmask2, props.routes[1].netmask);
   EXPECT_EQ(kNetwork2, props.routes[1].host);
   EXPECT_FALSE(props.blackhole_ipv6);
+
+  // If the driver is configured to ignore the gateway provided, it will
+  // not set the "gateway" property for the properties, however the
+  // explicitly supplied routes should still be set.
+  SetArg(kOpenVPNIgnoreDefaultRouteProperty, "some value");
+  IPConfig::Properties props_without_gateway;
+  driver_->ParseIPConfiguration(config, &props_without_gateway);
+  EXPECT_EQ(kGateway1, props_without_gateway.routes[0].gateway);
+  EXPECT_EQ("", props_without_gateway.gateway);
 }
 
 TEST_F(OpenVPNDriverTest, InitOptionsNoHost) {