patchpanel: mark connections with fwmark routing tags

This patch enables per-network tracking of connections using
CONNMARK conntrack for storing the fwmark routing tag associated with
the output interface selected by routing. This is achieved with a mangle
POSTROUTING rule using the CONNMARK target.

A follow-up patch in shill will add the final piece and add ip rules
using these fwmark routing tags to complete the per-network connection
routing and fwmark routing system.

BUG=b:161507671
BUG=b:161508179
TEST=Unit tests. Manual tests: verified with /proc/net/ip_conntrack that
conntrack entries get marked with the fwmark routing tag of the network
through which they are routed.

Change-Id: I9f147179283246c1aafda87ad716e209ce387736
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2377337
Tested-by: Hugo Benichi <hugobenichi@google.com>
Tested-by: Mitu Aktar <mitu64649@gmail.com>
Commit-Queue: Hugo Benichi <hugobenichi@google.com>
Reviewed-by: Taoyu Li <taoyl@chromium.org>
diff --git a/patchpanel/datapath.cc b/patchpanel/datapath.cc
index 73075bd..6155749 100644
--- a/patchpanel/datapath.cc
+++ b/patchpanel/datapath.cc
@@ -571,6 +571,56 @@
   process_runner_->ip6("addr", "del", {ipv6_addr, "dev", ifname});
 }
 
+void Datapath::StartConnectionPinning(const std::string& ext_ifname) {
+  if (!ModifyConnmarkSetPostrouting(IpFamily::Dual, "-A", ext_ifname))
+    LOG(ERROR) << "Could not start connection pinning on " << ext_ifname;
+}
+
+void Datapath::StopConnectionPinning(const std::string& ext_ifname) {
+  if (!ModifyConnmarkSetPostrouting(IpFamily::Dual, "-D", ext_ifname))
+    LOG(ERROR) << "Could not stop connection pinning on " << ext_ifname;
+}
+
+bool Datapath::ModifyConnmarkSetPostrouting(IpFamily family,
+                                            const std::string& op,
+                                            const std::string& oif) {
+  if (oif.empty()) {
+    LOG(ERROR) << "Cannot change POSTROUTING CONNMARK set-mark with no "
+                  "interface specified";
+    return false;
+  }
+
+  if (!IsValidIpFamily(family)) {
+    LOG(ERROR) << "Cannot change POSTROUTING CONNMARK set-mark for " << oif
+               << ": incorrect IP family " << family;
+    return false;
+  }
+
+  int ifindex = FindIfIndex(oif);
+  if (ifindex == 0) {
+    PLOG(ERROR) << "if_nametoindex(" << oif << ") failed";
+    return false;
+  }
+
+  std::vector<std::string> args = {op,
+                                   "POSTROUTING",
+                                   "-o",
+                                   oif,
+                                   "-j",
+                                   "CONNMARK",
+                                   "--set-mark",
+                                   Fwmark::FromIfIndex(ifindex).ToString() +
+                                       "/" + kFwmarkRoutingMask.ToString(),
+                                   "-w"};
+
+  bool success = true;
+  if (family & IPv4)
+    success &= process_runner_->iptables("mangle", args) == 0;
+  if (family & IPv6)
+    success &= process_runner_->ip6tables("mangle", args) == 0;
+  return false;
+}
+
 bool Datapath::ModifyConnmarkRestore(IpFamily family,
                                      const std::string& chain,
                                      const std::string& op,
diff --git a/patchpanel/datapath.h b/patchpanel/datapath.h
index c2dcbfd..ea7ae28 100644
--- a/patchpanel/datapath.h
+++ b/patchpanel/datapath.h
@@ -144,6 +144,13 @@
                                  uint32_t int_ipv4_addr,
                                  TrafficSource source);
 
+  // Starts or stops marking conntrack entries routed to |ext_ifname| with its
+  // associated fwmark routing tag. Once a conntrack entry is marked with the
+  // fwmark routing tag of a external device, the connection will be pinned
+  // to that deviced if conntrack fwmark restore is set for the source.
+  virtual void StartConnectionPinning(const std::string& ext_ifname);
+  virtual void StopConnectionPinning(const std::string& ext_ifname);
+
   // Create (or delete) pre-routing rules allowing direct ingress on |ifname|
   // to guest desintation |ipv4_addr|.
   virtual bool AddInboundIPv4DNAT(const std::string& ifname,
@@ -242,6 +249,9 @@
   MinijailedProcessRunner& runner() const;
 
  private:
+  bool ModifyConnmarkSetPostrouting(IpFamily family,
+                                    const std::string& op,
+                                    const std::string& oif);
   bool ModifyConnmarkRestore(IpFamily family,
                              const std::string& chain,
                              const std::string& op,
diff --git a/patchpanel/datapath_test.cc b/patchpanel/datapath_test.cc
index 4df4342..f69e7d6 100644
--- a/patchpanel/datapath_test.cc
+++ b/patchpanel/datapath_test.cc
@@ -591,6 +591,35 @@
   }
 }
 
+TEST(DatapathTest, StartStopConnectionPinning) {
+  MockProcessRunner runner;
+  MockFirewall firewall;
+  EXPECT_CALL(runner, iptables(StrEq("mangle"),
+                               ElementsAre("-A", "POSTROUTING", "-o", "eth0",
+                                           "-j", "CONNMARK", "--set-mark",
+                                           "0x03eb0000/0xffff0000", "-w"),
+                               true, nullptr));
+  EXPECT_CALL(runner, iptables(StrEq("mangle"),
+                               ElementsAre("-D", "POSTROUTING", "-o", "eth0",
+                                           "-j", "CONNMARK", "--set-mark",
+                                           "0x03eb0000/0xffff0000", "-w"),
+                               true, nullptr));
+  EXPECT_CALL(runner, ip6tables(StrEq("mangle"),
+                                ElementsAre("-A", "POSTROUTING", "-o", "eth0",
+                                            "-j", "CONNMARK", "--set-mark",
+                                            "0x03eb0000/0xffff0000", "-w"),
+                                true, nullptr));
+  EXPECT_CALL(runner, ip6tables(StrEq("mangle"),
+                                ElementsAre("-D", "POSTROUTING", "-o", "eth0",
+                                            "-j", "CONNMARK", "--set-mark",
+                                            "0x03eb0000/0xffff0000", "-w"),
+                                true, nullptr));
+  Datapath datapath(&runner, &firewall);
+  datapath.SetIfnameIndex("eth0", 3);
+  datapath.StartConnectionPinning("eth0");
+  datapath.StopConnectionPinning("eth0");
+}
+
 TEST(DatapathTest, AddInboundIPv4DNAT) {
   MockProcessRunner runner;
   MockFirewall firewall;
diff --git a/patchpanel/manager.cc b/patchpanel/manager.cc
index 370df2e..841526c 100644
--- a/patchpanel/manager.cc
+++ b/patchpanel/manager.cc
@@ -185,6 +185,8 @@
                << " object";
   }
 
+  shill_client_ = std::make_unique<ShillClient>(bus_);
+
   using ServiceMethod =
       std::unique_ptr<dbus::Response> (Manager::*)(dbus::MethodCall*);
   const std::map<const char*, ServiceMethod> kServiceMethods = {
@@ -225,7 +227,6 @@
   nd_proxy_->RegisterNDProxyMessageHandler(
       base::Bind(&Manager::OnNDProxyMessage, weak_factory_.GetWeakPtr()));
 
-  shill_client_ = std::make_unique<ShillClient>(bus_);
   auto* const forwarder = static_cast<TrafficForwarder*>(this);
 
   GuestMessage::GuestType arc_guest =
@@ -319,7 +320,7 @@
     LOG(ERROR) << "Failed to limit local port range. Some Android features or"
                << " apps may not work correctly.";
   }
-  // Enable IPv6 packet forarding
+  // Enable IPv6 packet forwarding
   if (runner.sysctl_w("net.ipv6.conf.all.forwarding", "1") != 0) {
     LOG(ERROR) << "Failed to update net.ipv6.conf.all.forwarding."
                << " IPv6 functionality may be broken.";
@@ -349,6 +350,9 @@
     LOG(ERROR) << "Failed to set up NAT for TAP devices."
                << " Guest connectivity may be broken.";
   }
+
+  shill_client_->RegisterDevicesChangedHandler(base::BindRepeating(
+      &Manager::OnDevicesChanged, weak_factory_.GetWeakPtr()));
 }
 
 void Manager::StopDatapath() {
@@ -377,6 +381,15 @@
   }
 }
 
+void Manager::OnDevicesChanged(const std::set<std::string>& added,
+                               const std::set<std::string>& removed) {
+  for (const std::string& ifname : removed)
+    datapath_->StopConnectionPinning(ifname);
+
+  for (const std::string& ifname : added)
+    datapath_->StartConnectionPinning(ifname);
+}
+
 bool Manager::StartArc(pid_t pid) {
   if (!arc_svc_->Start(pid))
     return false;
diff --git a/patchpanel/manager.h b/patchpanel/manager.h
index d3977c9..3f9d5e7 100644
--- a/patchpanel/manager.h
+++ b/patchpanel/manager.h
@@ -85,6 +85,8 @@
   int OnInit() override;
 
  private:
+  void OnDevicesChanged(const std::set<std::string>& added,
+                        const std::set<std::string>& removed);
   void InitialSetup();
 
   // Start and stop the Datapath, creating or destroying the initial iptables