Add and implement VPN preference

This patch adds a vp preference field to RTCConfig.
  DEFAULT,       // No VPN preference.
  ONLY_USE_VPN,  // only use VPN connections.
  NEVER_USE_VPN, // never use VPN connections
  PREFER_VPN,    // use a VPN connection if possible, i.e VPN connections sorts higher than all other connections.
  AVOID_VPN,     // only use VPN if there is no other connections, i.e VPN connections sorts last.

Bug: webrtc:13097
Change-Id: I3f95bdfa9134e082c7d389f803bd08facfb70262
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/229591
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Jonas Oreland <jonaso@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#34842}
diff --git a/api/peer_connection_interface.h b/api/peer_connection_interface.h
index a3c420f..a9fd5e3 100644
--- a/api/peer_connection_interface.h
+++ b/api/peer_connection_interface.h
@@ -653,6 +653,14 @@
     // The ping interval (ms) when the connection is stable and writable. This
     // parameter overrides the default value in the ICE implementation if set.
     absl::optional<int> stable_writable_connection_ping_interval_ms;
+
+    // Whether this PeerConnection will avoid VPNs (kAvoidVpn), prefer VPNs
+    // (kPreferVpn), only work over VPN (kOnlyUseVpn) or only work over non-VPN
+    // (kNeverUseVpn) interfaces. This controls which local interfaces the
+    // PeerConnection will prefer to connect over. Since VPN detection is not
+    // perfect, adherence to this preference cannot be guaranteed.
+    VpnPreference vpn_preference = VpnPreference::kDefault;
+
     //
     // Don't forget to update operator== if adding something.
     //
diff --git a/api/transport/enums.h b/api/transport/enums.h
index eb33e91..3bc8fd1 100644
--- a/api/transport/enums.h
+++ b/api/transport/enums.h
@@ -34,6 +34,16 @@
                             // on the same network.
 };
 
+enum class VpnPreference {
+  kDefault,      // No VPN preference.
+  kOnlyUseVpn,   // only use VPN connections.
+  kNeverUseVpn,  // never use VPN connections
+  kPreferVpn,    // use a VPN connection if possible,
+                 // i.e VPN connections sorts first.
+  kAvoidVpn,     // only use VPN if there is no other connections,
+                 // i.e VPN connections sorts last.
+};
+
 }  // namespace webrtc
 
 #endif  // API_TRANSPORT_ENUMS_H_
diff --git a/p2p/base/basic_ice_controller.cc b/p2p/base/basic_ice_controller.cc
index dca04ca..ac22e1d 100644
--- a/p2p/base/basic_ice_controller.cc
+++ b/p2p/base/basic_ice_controller.cc
@@ -739,6 +739,31 @@
     return compare_a_b_by_network_preference;
   }
 
+  bool a_vpn = a->network()->IsVpn();
+  bool b_vpn = b->network()->IsVpn();
+  switch (config_.vpn_preference) {
+    case webrtc::VpnPreference::kDefault:
+      break;
+    case webrtc::VpnPreference::kOnlyUseVpn:
+    case webrtc::VpnPreference::kPreferVpn:
+      if (a_vpn && !b_vpn) {
+        return a_is_better;
+      } else if (!a_vpn && b_vpn) {
+        return b_is_better;
+      }
+      break;
+    case webrtc::VpnPreference::kNeverUseVpn:
+    case webrtc::VpnPreference::kAvoidVpn:
+      if (a_vpn && !b_vpn) {
+        return b_is_better;
+      } else if (!a_vpn && b_vpn) {
+        return a_is_better;
+      }
+      break;
+    default:
+      break;
+  }
+
   uint32_t a_cost = a->ComputeNetworkCost();
   uint32_t b_cost = b->ComputeNetworkCost();
   // Prefer lower network cost.
diff --git a/p2p/base/ice_transport_internal.h b/p2p/base/ice_transport_internal.h
index 796978f..20730e1 100644
--- a/p2p/base/ice_transport_internal.h
+++ b/p2p/base/ice_transport_internal.h
@@ -175,6 +175,8 @@
 
   absl::optional<rtc::AdapterType> network_preference;
 
+  webrtc::VpnPreference vpn_preference = webrtc::VpnPreference::kDefault;
+
   IceConfig();
   IceConfig(int receiving_timeout_ms,
             int backup_connection_ping_interval,
diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc
index f30783b..5587a84 100644
--- a/p2p/base/p2p_transport_channel.cc
+++ b/p2p/base/p2p_transport_channel.cc
@@ -796,6 +796,9 @@
       config_.regather_on_failed_networks_interval_or_default();
   regathering_controller_->SetConfig(regathering_config);
 
+  config_.vpn_preference = config.vpn_preference;
+  allocator_->SetVpnPreference(config_.vpn_preference);
+
   ice_controller_->SetIceConfig(config_);
 
   RTC_DCHECK(ValidateIceConfig(config_).ok());
diff --git a/p2p/base/p2p_transport_channel_unittest.cc b/p2p/base/p2p_transport_channel_unittest.cc
index e4f4fa1..0ddeb2d 100644
--- a/p2p/base/p2p_transport_channel_unittest.cc
+++ b/p2p/base/p2p_transport_channel_unittest.cc
@@ -529,9 +529,11 @@
   void AddAddress(int endpoint,
                   const SocketAddress& addr,
                   const std::string& ifname,
-                  rtc::AdapterType adapter_type) {
-    GetEndpoint(endpoint)->network_manager_.AddInterface(addr, ifname,
-                                                         adapter_type);
+                  rtc::AdapterType adapter_type,
+                  absl::optional<rtc::AdapterType> underlying_vpn_adapter_type =
+                      absl::nullopt) {
+    GetEndpoint(endpoint)->network_manager_.AddInterface(
+        addr, ifname, adapter_type, underlying_vpn_adapter_type);
   }
   void RemoveAddress(int endpoint, const SocketAddress& addr) {
     GetEndpoint(endpoint)->network_manager_.RemoveInterface(addr);
@@ -3169,6 +3171,119 @@
   DestroyChannels();
 }
 
+TEST_F(P2PTransportChannelMultihomedTest, TestVpnDefault) {
+  rtc::ScopedFakeClock clock;
+  AddAddress(0, kPublicAddrs[0], "eth0", rtc::ADAPTER_TYPE_ETHERNET);
+  AddAddress(0, kAlternateAddrs[0], "vpn0", rtc::ADAPTER_TYPE_VPN);
+  AddAddress(1, kPublicAddrs[1]);
+
+  IceConfig config;
+  CreateChannels(config, config, false);
+  EXPECT_TRUE_SIMULATED_WAIT(
+      CheckConnected(ep1_ch1(), ep2_ch1()) &&
+          !ep1_ch1()->selected_connection()->network()->IsVpn(),
+      kDefaultTimeout, clock);
+}
+
+TEST_F(P2PTransportChannelMultihomedTest, TestVpnPreferVpn) {
+  rtc::ScopedFakeClock clock;
+  AddAddress(0, kPublicAddrs[0], "eth0", rtc::ADAPTER_TYPE_ETHERNET);
+  AddAddress(0, kAlternateAddrs[0], "vpn0", rtc::ADAPTER_TYPE_VPN,
+             rtc::ADAPTER_TYPE_CELLULAR);
+  AddAddress(1, kPublicAddrs[1]);
+
+  IceConfig config;
+  config.vpn_preference = webrtc::VpnPreference::kPreferVpn;
+  RTC_LOG(LS_INFO) << "KESO: config.vpn_preference: " << config.vpn_preference;
+  CreateChannels(config, config, false);
+  EXPECT_TRUE_SIMULATED_WAIT(
+      CheckConnected(ep1_ch1(), ep2_ch1()) &&
+          ep1_ch1()->selected_connection()->network()->IsVpn(),
+      kDefaultTimeout, clock);
+
+  // Block VPN.
+  fw()->AddRule(false, rtc::FP_ANY, rtc::FD_ANY, kAlternateAddrs[0]);
+
+  // Check that it switches to non-VPN
+  EXPECT_TRUE_SIMULATED_WAIT(
+      CheckConnected(ep1_ch1(), ep2_ch1()) &&
+          !ep1_ch1()->selected_connection()->network()->IsVpn(),
+      kDefaultTimeout, clock);
+}
+
+TEST_F(P2PTransportChannelMultihomedTest, TestVpnAvoidVpn) {
+  rtc::ScopedFakeClock clock;
+  AddAddress(0, kPublicAddrs[0], "eth0", rtc::ADAPTER_TYPE_CELLULAR);
+  AddAddress(0, kAlternateAddrs[0], "vpn0", rtc::ADAPTER_TYPE_VPN,
+             rtc::ADAPTER_TYPE_ETHERNET);
+  AddAddress(1, kPublicAddrs[1]);
+
+  IceConfig config;
+  config.vpn_preference = webrtc::VpnPreference::kAvoidVpn;
+  CreateChannels(config, config, false);
+  EXPECT_TRUE_SIMULATED_WAIT(
+      CheckConnected(ep1_ch1(), ep2_ch1()) &&
+          !ep1_ch1()->selected_connection()->network()->IsVpn(),
+      kDefaultTimeout, clock);
+
+  // Block non-VPN.
+  fw()->AddRule(false, rtc::FP_ANY, rtc::FD_ANY, kPublicAddrs[0]);
+
+  // Check that it switches to VPN
+  EXPECT_TRUE_SIMULATED_WAIT(
+      CheckConnected(ep1_ch1(), ep2_ch1()) &&
+          ep1_ch1()->selected_connection()->network()->IsVpn(),
+      kDefaultTimeout, clock);
+}
+
+TEST_F(P2PTransportChannelMultihomedTest, TestVpnNeverVpn) {
+  rtc::ScopedFakeClock clock;
+  AddAddress(0, kPublicAddrs[0], "eth0", rtc::ADAPTER_TYPE_CELLULAR);
+  AddAddress(0, kAlternateAddrs[0], "vpn0", rtc::ADAPTER_TYPE_VPN,
+             rtc::ADAPTER_TYPE_ETHERNET);
+  AddAddress(1, kPublicAddrs[1]);
+
+  IceConfig config;
+  config.vpn_preference = webrtc::VpnPreference::kNeverUseVpn;
+  CreateChannels(config, config, false);
+  EXPECT_TRUE_SIMULATED_WAIT(
+      CheckConnected(ep1_ch1(), ep2_ch1()) &&
+          !ep1_ch1()->selected_connection()->network()->IsVpn(),
+      kDefaultTimeout, clock);
+
+  // Block non-VPN.
+  fw()->AddRule(false, rtc::FP_ANY, rtc::FD_ANY, kPublicAddrs[0]);
+
+  // Check that it does not switches to VPN
+  clock.AdvanceTime(webrtc::TimeDelta::Millis(kDefaultTimeout));
+  EXPECT_TRUE_SIMULATED_WAIT(!CheckConnected(ep1_ch1(), ep2_ch1()),
+                             kDefaultTimeout, clock);
+}
+
+TEST_F(P2PTransportChannelMultihomedTest, TestVpnOnlyVpn) {
+  rtc::ScopedFakeClock clock;
+  AddAddress(0, kPublicAddrs[0], "eth0", rtc::ADAPTER_TYPE_CELLULAR);
+  AddAddress(0, kAlternateAddrs[0], "vpn0", rtc::ADAPTER_TYPE_VPN,
+             rtc::ADAPTER_TYPE_ETHERNET);
+  AddAddress(1, kPublicAddrs[1]);
+
+  IceConfig config;
+  config.vpn_preference = webrtc::VpnPreference::kOnlyUseVpn;
+  CreateChannels(config, config, false);
+  EXPECT_TRUE_SIMULATED_WAIT(
+      CheckConnected(ep1_ch1(), ep2_ch1()) &&
+          ep1_ch1()->selected_connection()->network()->IsVpn(),
+      kDefaultTimeout, clock);
+
+  // Block VPN.
+  fw()->AddRule(false, rtc::FP_ANY, rtc::FD_ANY, kAlternateAddrs[0]);
+
+  // Check that it does not switch to non-VPN
+  clock.AdvanceTime(webrtc::TimeDelta::Millis(kDefaultTimeout));
+  EXPECT_TRUE_SIMULATED_WAIT(!CheckConnected(ep1_ch1(), ep2_ch1()),
+                             kDefaultTimeout, clock);
+}
+
 // A collection of tests which tests a single P2PTransportChannel by sending
 // pings.
 class P2PTransportChannelPingTest : public ::testing::Test,
diff --git a/p2p/base/port_allocator.h b/p2p/base/port_allocator.h
index 78b0c70..e9b1d1e 100644
--- a/p2p/base/port_allocator.h
+++ b/p2p/base/port_allocator.h
@@ -400,6 +400,12 @@
   // loopback interfaces.
   virtual void SetNetworkIgnoreMask(int network_ignore_mask) = 0;
 
+  // Set whether VPN connections should be preferred, avoided, mandated or
+  // blocked.
+  virtual void SetVpnPreference(webrtc::VpnPreference preference) {
+    vpn_preference_ = preference;
+  }
+
   std::unique_ptr<PortAllocatorSession> CreateSession(
       const std::string& content_name,
       int component,
@@ -638,6 +644,7 @@
   uint32_t candidate_filter_;
   std::string origin_;
   webrtc::SequenceChecker thread_checker_;
+  webrtc::VpnPreference vpn_preference_ = webrtc::VpnPreference::kDefault;
 
  private:
   ServerAddresses stun_servers_;
diff --git a/p2p/client/basic_port_allocator.cc b/p2p/client/basic_port_allocator.cc
index 748be90..0a1b996 100644
--- a/p2p/client/basic_port_allocator.cc
+++ b/p2p/client/basic_port_allocator.cc
@@ -213,6 +213,22 @@
   network_ignore_mask_ = network_ignore_mask;
 }
 
+int BasicPortAllocator::GetNetworkIgnoreMask() const {
+  CheckRunOnValidThreadIfInitialized();
+  int mask = network_ignore_mask_;
+  switch (vpn_preference_) {
+    case webrtc::VpnPreference::kOnlyUseVpn:
+      mask |= ~static_cast<int>(rtc::ADAPTER_TYPE_VPN);
+      break;
+    case webrtc::VpnPreference::kNeverUseVpn:
+      mask |= static_cast<int>(rtc::ADAPTER_TYPE_VPN);
+      break;
+    default:
+      break;
+  }
+  return mask;
+}
+
 PortAllocatorSession* BasicPortAllocator::CreateSessionInternal(
     const std::string& content_name,
     int component,
@@ -708,7 +724,7 @@
   // costly networks" flag.
   NetworkFilter ignored_filter(
       [this](rtc::Network* network) {
-        return allocator_->network_ignore_mask() & network->type();
+        return allocator_->GetNetworkIgnoreMask() & network->type();
       },
       "ignored");
   FilterNetworks(&networks, ignored_filter);
@@ -731,6 +747,7 @@
         "costly");
     FilterNetworks(&networks, costly_filter);
   }
+
   // Lastly, if we have a limit for the number of IPv6 network interfaces (by
   // default, it's 5), remove networks to ensure that limit is satisfied.
   //
diff --git a/p2p/client/basic_port_allocator.h b/p2p/client/basic_port_allocator.h
index 3fca9be..c3858f7 100644
--- a/p2p/client/basic_port_allocator.h
+++ b/p2p/client/basic_port_allocator.h
@@ -46,10 +46,7 @@
 
   // Set to kDefaultNetworkIgnoreMask by default.
   void SetNetworkIgnoreMask(int network_ignore_mask) override;
-  int network_ignore_mask() const {
-    CheckRunOnValidThreadIfInitialized();
-    return network_ignore_mask_;
-  }
+  int GetNetworkIgnoreMask() const;
 
   rtc::NetworkManager* network_manager() const {
     CheckRunOnValidThreadIfInitialized();
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index 5bcd940..5d10107 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -336,6 +336,7 @@
     absl::optional<bool> allow_codec_switching;
     absl::optional<int> report_usage_pattern_delay_ms;
     absl::optional<int> stable_writable_connection_ping_interval_ms;
+    webrtc::VpnPreference vpn_preference;
   };
   static_assert(sizeof(stuff_being_tested_for_equality) == sizeof(*this),
                 "Did you add something to RTCConfiguration and forget to "
@@ -397,7 +398,8 @@
          allow_codec_switching == o.allow_codec_switching &&
          report_usage_pattern_delay_ms == o.report_usage_pattern_delay_ms &&
          stable_writable_connection_ping_interval_ms ==
-             o.stable_writable_connection_ping_interval_ms;
+             o.stable_writable_connection_ping_interval_ms &&
+         vpn_preference == o.vpn_preference;
 }
 
 bool PeerConnectionInterface::RTCConfiguration::operator!=(
diff --git a/rtc_base/fake_network.h b/rtc_base/fake_network.h
index 1bbdd46..53664cb 100644
--- a/rtc_base/fake_network.h
+++ b/rtc_base/fake_network.h
@@ -36,7 +36,12 @@
  public:
   FakeNetworkManager() {}
 
-  typedef std::vector<std::pair<SocketAddress, AdapterType>> IfaceList;
+  struct Iface {
+    SocketAddress socket_address;
+    AdapterType adapter_type;
+    absl::optional<AdapterType> underlying_vpn_adapter_type;
+  };
+  typedef std::vector<Iface> IfaceList;
 
   void AddInterface(const SocketAddress& iface) {
     // Ensure a unique name for the interface if its name is not given.
@@ -47,18 +52,20 @@
     AddInterface(iface, if_name, ADAPTER_TYPE_UNKNOWN);
   }
 
-  void AddInterface(const SocketAddress& iface,
-                    const std::string& if_name,
-                    AdapterType type) {
+  void AddInterface(
+      const SocketAddress& iface,
+      const std::string& if_name,
+      AdapterType type,
+      absl::optional<AdapterType> underlying_vpn_adapter_type = absl::nullopt) {
     SocketAddress address(if_name, 0);
     address.SetResolvedIP(iface.ipaddr());
-    ifaces_.push_back(std::make_pair(address, type));
+    ifaces_.push_back({address, type, underlying_vpn_adapter_type});
     DoUpdateNetworks();
   }
 
   void RemoveInterface(const SocketAddress& iface) {
     for (IfaceList::iterator it = ifaces_.begin(); it != ifaces_.end(); ++it) {
-      if (it->first.EqualIPs(iface)) {
+      if (it->socket_address.EqualIPs(iface)) {
         ifaces_.erase(it);
         break;
       }
@@ -112,17 +119,20 @@
     std::vector<Network*> networks;
     for (IfaceList::iterator it = ifaces_.begin(); it != ifaces_.end(); ++it) {
       int prefix_length = 0;
-      if (it->first.ipaddr().family() == AF_INET) {
+      if (it->socket_address.ipaddr().family() == AF_INET) {
         prefix_length = kFakeIPv4NetworkPrefixLength;
-      } else if (it->first.ipaddr().family() == AF_INET6) {
+      } else if (it->socket_address.ipaddr().family() == AF_INET6) {
         prefix_length = kFakeIPv6NetworkPrefixLength;
       }
-      IPAddress prefix = TruncateIP(it->first.ipaddr(), prefix_length);
-      std::unique_ptr<Network> net(new Network(it->first.hostname(),
-                                               it->first.hostname(), prefix,
-                                               prefix_length, it->second));
+      IPAddress prefix = TruncateIP(it->socket_address.ipaddr(), prefix_length);
+      std::unique_ptr<Network> net(new Network(
+          it->socket_address.hostname(), it->socket_address.hostname(), prefix,
+          prefix_length, it->adapter_type));
+      if (it->underlying_vpn_adapter_type.has_value()) {
+        net->set_underlying_type_for_vpn(*it->underlying_vpn_adapter_type);
+      }
       net->set_default_local_address_provider(this);
-      net->AddIP(it->first.ipaddr());
+      net->AddIP(it->socket_address.ipaddr());
       networks.push_back(net.release());
     }
     bool changed;