patchpanel: Add MASQUERADE rule for traffic going into namespace

DNS proxy forwards traffic into a connected namespace, in order
for that to work a SNAT / MASQUERADE rule is necessary as the
namespace is unable to reach host's physical interface. This
patch adds the MASQUERADE rule.

The scenario where SNAT / MASQUERADE rule is necessary is as follows.
When a USER does a DNS query, it will have the destination IP of the
address inside /etc/resolv.conf (which is populated with the SYSTEM
DNS proxy's IP address inside its namespace). Because of this
destination IP, the source IP of the host's part of the namespace will
be used. DNS proxy will however do a DNAT, changing the destination IP
to the USER DNS proxy's IP address. This will cause a problem as the
source IP (host part of SYSTEM namespace) is not reachable from the
destination IP (inside of USER namespace) for it to give a reply.
MASQUERADE fixes this allowing the reply to go through.

The rule is not necessary for guests (e.g. Crostini and ARC)
because their addresses are reachable from inside a namespace.

BUG=b:202356802
TEST=root, chronos, cups, ARC, and Crostini can be
     forwarded to a connected namespace.

Change-Id: I97e96cab2ffed719eb0ce88e3f8c24a4cc024275
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/3277612
Tested-by: Jason Jeremy Iman <jasongustaman@chromium.org>
Commit-Queue: Jason Jeremy Iman <jasongustaman@chromium.org>
Reviewed-by: Hugo Benichi <hugobenichi@google.com>
diff --git a/patchpanel/datapath.cc b/patchpanel/datapath.cc
index 5b34077..0696856 100644
--- a/patchpanel/datapath.cc
+++ b/patchpanel/datapath.cc
@@ -76,10 +76,13 @@
 constexpr char kIngressDefaultForwardingChain[] = "ingress_default_forwarding";
 // nat PREROUTING chains for egress traffic from downstream guests.
 constexpr char kRedirectArcDnsChain[] = "redirect_arc_dns";
-constexpr char kRedirectChromeDnsChain[] = "redirect_chrome_dns";
-// nat OUTPUT chains for egress traffic from processes running on the host.
 constexpr char kRedirectDefaultDnsChain[] = "redirect_default_dns";
+// nat OUTPUT chains for egress traffic from processes running on the host.
+constexpr char kRedirectChromeDnsChain[] = "redirect_chrome_dns";
 constexpr char kRedirectUserDnsChain[] = "redirect_user_dns";
+// nat POSTROUTING chains for egress traffic from processes running on the host.
+constexpr char kSnatChromeDnsChain[] = "snat_chrome_dns";
+constexpr char kSnatUserDnsChain[] = "snat_user_dns";
 
 // Maximum length of an iptables chain name.
 constexpr int kIptablesMaxChainLength = 28;
@@ -182,6 +185,12 @@
       {IpFamily::Dual, "nat", kRedirectDefaultDnsChain},
       {IpFamily::Dual, "nat", kRedirectUserDnsChain},
       {IpFamily::Dual, "nat", kRedirectChromeDnsChain},
+      // Set up nat chains for SNAT-ing egress DNS queries to the DNS proxy
+      // instances.
+      {IpFamily::Dual, "nat", kSnatChromeDnsChain},
+      // For the case of non-Chrome "user" DNS queries, there is already an IPv4
+      // SNAT rule with the ConnectNamespace. Only IPv6 USER SNAT is needed.
+      {IpFamily::IPv6, "nat", kSnatUserDnsChain},
       // b/178331695 Sets up a nat chain used in OUTPUT for redirecting DNS
       // queries of system services. When a VPN is connected, a query routed
       // through a physical network is redirected to the primary nameserver of
@@ -372,6 +381,18 @@
                                  true /* redirect_on_mark */)) {
     LOG(ERROR) << "Failed to add jump rule for user DNS redirection";
   }
+  if (!ModifyRedirectDnsJumpRule(
+          IpFamily::Dual, "-A", "POSTROUTING", "" /* ifname */,
+          kSnatChromeDnsChain, Fwmark::FromSource(TrafficSource::CHROME),
+          kFwmarkAllSourcesMask, true /* redirect_on_mark */)) {
+    LOG(ERROR) << "Failed to add jump rule for chrome DNS SNAT";
+  }
+  if (!ModifyRedirectDnsJumpRule(IpFamily::IPv6, "-A", "POSTROUTING",
+                                 "" /* ifname */, kSnatUserDnsChain,
+                                 kFwmarkRouteOnVpn, kFwmarkVpnMask,
+                                 true /* redirect_on_mark */)) {
+    LOG(ERROR) << "Failed to add jump rule for user DNS SNAT";
+  }
 
   // All local outgoing DNS traffic eligible to VPN routing should skip the VPN
   // routing chain and instead go through DNS proxy.
@@ -482,6 +503,8 @@
       {IpFamily::Dual, "nat", kRedirectArcDnsChain, true},
       {IpFamily::Dual, "nat", kRedirectChromeDnsChain, true},
       {IpFamily::Dual, "nat", kRedirectUserDnsChain, true},
+      {IpFamily::Dual, "nat", kSnatChromeDnsChain, true},
+      {IpFamily::IPv6, "nat", kSnatUserDnsChain, true},
       {IpFamily::IPv4, "nat", kRedirectDnsChain, true},
       {IpFamily::IPv4, "nat", kIngressDefaultForwardingChain, true},
   };
@@ -933,14 +956,9 @@
         success = false;
       }
     }
-    if (!ModifyIptables(family, "nat",
-                        {op, "POSTROUTING", "-p", protocol, "--dport",
-                         kDefaultDnsPort, "-m", "mark", "--mark",
-                         Fwmark::FromSource(TrafficSource::CHROME).ToString() +
-                             "/" + kFwmarkAllSourcesMask.ToString(),
-                         "-j", "MASQUERADE", "-w"})) {
-      success = false;
-    }
+  }
+  if (!ModifyDnsProxyMasquerade(family, op, kSnatChromeDnsChain)) {
+    success = false;
   }
   return success;
 }
@@ -972,6 +990,21 @@
   return success;
 }
 
+bool Datapath::ModifyDnsProxyMasquerade(IpFamily family,
+                                        const std::string& op,
+                                        const std::string& chain) {
+  bool success = true;
+  for (const auto& protocol : {"udp", "tcp"}) {
+    std::vector<std::string> args = {op,       chain,        "-p",
+                                     protocol, "--dport",    kDefaultDnsPort,
+                                     "-j",     "MASQUERADE", "-w"};
+    if (!ModifyIptables(family, "nat", args)) {
+      success = false;
+    }
+  }
+  return success;
+}
+
 bool Datapath::StartDnsRedirection(const DnsRedirectionRule& rule) {
   IpFamily family;
   sa_family_t sa_family = GetIpFamily(rule.proxy_address);
@@ -1023,6 +1056,13 @@
         LOG(ERROR) << "Failed to add user DNS DNAT rule";
         return false;
       }
+
+      // Add MASQUERADE rule for user traffic.
+      if (family == IpFamily::IPv6 &&
+          !ModifyDnsProxyMasquerade(family, "-A", kSnatUserDnsChain)) {
+        LOG(ERROR) << "Failed to add user DNS MASQUERADE rule";
+        return false;
+      }
       return true;
     }
     default:
@@ -1065,6 +1105,9 @@
       ModifyDnsProxyDNAT(family, rule, "-D", "" /* ifname */,
                          kRedirectUserDnsChain);
       ModifyDnsRedirectionSkipVpnRule(family, "-D");
+      if (family == IpFamily::IPv6) {
+        ModifyDnsProxyMasquerade(family, "-D", kSnatUserDnsChain);
+      }
       break;
     }
     default:
diff --git a/patchpanel/datapath.h b/patchpanel/datapath.h
index 8abcc0b..3543ea7 100644
--- a/patchpanel/datapath.h
+++ b/patchpanel/datapath.h
@@ -389,6 +389,9 @@
                                  const std::string& protocol,
                                  const std::string& ifname,
                                  const std::string& dns_ipv4_addr);
+  bool ModifyDnsProxyMasquerade(IpFamily family,
+                                const std::string& op,
+                                const std::string& chain);
   bool ModifyRedirectDnsJumpRule(IpFamily family,
                                  const std::string& op,
                                  const std::string& chain,
diff --git a/patchpanel/datapath_test.cc b/patchpanel/datapath_test.cc
index 4904ef3..3e20648 100644
--- a/patchpanel/datapath_test.cc
+++ b/patchpanel/datapath_test.cc
@@ -323,6 +323,12 @@
       {Dual, "nat -L redirect_user_dns -w"},
       {Dual, "nat -F redirect_user_dns -w"},
       {Dual, "nat -X redirect_user_dns -w"},
+      {Dual, "nat -L snat_chrome_dns -w"},
+      {Dual, "nat -F snat_chrome_dns -w"},
+      {Dual, "nat -X snat_chrome_dns -w"},
+      {IPv6, "nat -L snat_user_dns -w"},
+      {IPv6, "nat -F snat_user_dns -w"},
+      {IPv6, "nat -X snat_user_dns -w"},
       {IPv4, "nat -F POSTROUTING -w"},
       {Dual, "nat -F OUTPUT -w"},
       // Asserts for SNAT rules of traffic forwarded from downstream interfaces.
@@ -460,6 +466,14 @@
       {Dual,
        "nat -A OUTPUT -m mark --mark 0x00008000/0x0000c000 -j "
        "redirect_user_dns -w"},
+      {Dual, "nat -N snat_chrome_dns -w"},
+      {IPv6, "nat -N snat_user_dns -w"},
+      {Dual,
+       "nat -A POSTROUTING -m mark --mark 0x00000100/0x00003f00 -j "
+       "snat_chrome_dns -w"},
+      {IPv6,
+       "nat -A POSTROUTING -m mark --mark 0x00008000/0x0000c000 -j "
+       "snat_user_dns -w"},
       // Asserts for egress and ingress port firewall chains
       {Dual, "filter -N ingress_port_firewall -w"},
       {Dual, "filter -A INPUT -j ingress_port_firewall -w"},
@@ -544,6 +558,12 @@
       {Dual, "nat -L redirect_user_dns -w"},
       {Dual, "nat -F redirect_user_dns -w"},
       {Dual, "nat -X redirect_user_dns -w"},
+      {Dual, "nat -L snat_chrome_dns -w"},
+      {Dual, "nat -F snat_chrome_dns -w"},
+      {Dual, "nat -X snat_chrome_dns -w"},
+      {IPv6, "nat -L snat_user_dns -w"},
+      {IPv6, "nat -F snat_user_dns -w"},
+      {IPv6, "nat -X snat_user_dns -w"},
       {IPv4, "nat -F POSTROUTING -w"},
       {Dual, "nat -F OUTPUT -w"},
   };
@@ -1471,11 +1491,11 @@
       "--uid-owner chronos -m statistic --mode nth --every 3 --packet "
       "0 -j DNAT --to-destination 1.1.1.1 -w");
   Verify_iptables(*runner, IPv4,
-                  "nat -I POSTROUTING -p udp --dport 53 -m mark --mark "
-                  "0x00000100/0x00003f00 -j MASQUERADE -w");
+                  "nat -I snat_chrome_dns -p udp --dport 53 -j "
+                  "MASQUERADE -w");
   Verify_iptables(*runner, IPv4,
-                  "nat -I POSTROUTING -p tcp --dport 53 -m mark --mark "
-                  "0x00000100/0x00003f00 -j MASQUERADE -w");
+                  "nat -I snat_chrome_dns -p tcp --dport 53 -j "
+                  "MASQUERADE -w");
   Verify_iptables(*runner, IPv4,
                   "nat -A redirect_user_dns -p udp --dport 53 -j DNAT "
                   "--to-destination 100.115.92.130 -w");
@@ -1576,11 +1596,11 @@
       "--uid-owner chronos -m statistic --mode nth --every 3 --packet "
       "0 -j DNAT --to-destination 1.1.1.1 -w");
   Verify_iptables(*runner, IPv4,
-                  "nat -D POSTROUTING -p udp --dport 53 -m mark --mark "
-                  "0x00000100/0x00003f00 -j MASQUERADE -w");
+                  "nat -D snat_chrome_dns -p udp --dport 53 -j "
+                  "MASQUERADE -w");
   Verify_iptables(*runner, IPv4,
-                  "nat -D POSTROUTING -p tcp --dport 53 -m mark --mark "
-                  "0x00000100/0x00003f00 -j MASQUERADE -w");
+                  "nat -D snat_chrome_dns -p tcp --dport 53 -j "
+                  "MASQUERADE -w");
   Verify_iptables(*runner, IPv4,
                   "nat -D redirect_user_dns -p udp --dport 53 -j DNAT "
                   "--to-destination 100.115.92.130 -w");