| // Copyright 2019 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "patchpanel/datapath.h" | |
| #include <arpa/inet.h> | |
| #include <fcntl.h> | |
| #include <linux/if_tun.h> | |
| #include <linux/sockios.h> | |
| #include <net/if_arp.h> | |
| #include <netinet/in.h> | |
| #include <string.h> | |
| #include <sys/ioctl.h> | |
| #include <sys/socket.h> | |
| #include <iostream> | |
| #include <optional> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <base/check.h> | |
| #include <base/files/file_util.h> | |
| #include <base/files/scoped_file.h> | |
| #include <base/logging.h> | |
| #include <base/posix/eintr_wrapper.h> | |
| #include <base/strings/string_number_conversions.h> | |
| #include <base/strings/string_split.h> | |
| #include <base/strings/string_util.h> | |
| #include <base/strings/stringprintf.h> | |
| #include <brillo/userdb_utils.h> | |
| #include <chromeos/net-base/mac_address.h> | |
| #include "patchpanel/adb_proxy.h" | |
| #include "patchpanel/arc_service.h" | |
| #include "patchpanel/bpf/constants.h" | |
| #include "patchpanel/iptables.h" | |
| #include "patchpanel/net_util.h" | |
| #include "patchpanel/routing_service.h" | |
| #include "patchpanel/scoped_ns.h" | |
| namespace patchpanel { | |
| namespace { | |
| using net_base::IPv4Address; | |
| using net_base::IPv4CIDR; | |
| using net_base::IPv6Address; | |
| using net_base::IPv6CIDR; | |
| // TODO(hugobenichi) Consolidate this constant definition in a single place. | |
| constexpr pid_t kTestPID = -2; | |
| constexpr char kDefaultIfname[] = "vmtap%d"; | |
| constexpr net_base::IPv4Address kArcAddr(100, 115, 92, 2); | |
| constexpr net_base::IPv4Address kLocalhostAddr(127, 0, 0, 1); | |
| constexpr char kDefaultDnsPort[] = "53"; | |
| constexpr uint16_t kAdbServerPort = 5555; | |
| constexpr std::string_view kIptablesStartScriptPath = | |
| "/etc/patchpanel/iptables.start"; | |
| constexpr std::string_view kIp6tablesStartScriptPath = | |
| "/etc/patchpanel/ip6tables.start"; | |
| // Chains for tagging egress traffic in the OUTPUT and PREROUTING chains of the | |
| // mangle table. Note that these value needs to be consistent with those in the | |
| // static iptables-start scripts. | |
| constexpr char kSkipApplyVpnMarkChain[] = "skip_apply_vpn_mark"; | |
| constexpr char kApplyVpnMarkChain[] = "apply_vpn_mark"; | |
| // Egress filter chain to allow traffic to DNS proxy. | |
| constexpr char kAcceptEgressToDnsProxyChain[] = "accept_egress_to_dns_proxy"; | |
| // Egress filter chain for dropping in the OUTPUT chain any local traffic | |
| // incorrectly bound to a static IPv4 address used for ARC or Crostini. | |
| constexpr char kDropGuestIpv4PrefixChain[] = "drop_guest_ipv4_prefix"; | |
| // Egress nat chain for redirecting DNS queries from system services. | |
| // TODO(b/162788331) Remove once dns-proxy has become fully operational. | |
| constexpr char kRedirectDnsChain[] = "redirect_dns"; | |
| // OUTPUT filter chain to enforce source IP on egress IPv6 packets. | |
| constexpr char kEnforceSourcePrefixChain[] = "enforce_ipv6_src_prefix"; | |
| // VPN egress filter chains for the filter OUTPUT and FORWARD chains. | |
| constexpr char kVpnAcceptChain[] = "vpn_accept"; | |
| constexpr char kVpnLockdownChain[] = "vpn_lockdown"; | |
| // FORWARD filter chain to: | |
| // - accept any tethering traffic forwarded between the upstream and downstream | |
| // network interfaces. | |
| // - and drop any forwarded traffic between the downstream network interface | |
| // and some other network interface unrelated to tethering. | |
| // Note that the upstream network interface may be used for other forwarded | |
| // traffic legitimately. | |
| constexpr char kForwardTetheringChain[] = "forward_tethering"; | |
| // OUPUT filter chain to accept egress traffic sent by ChromeOS on the | |
| // downstream network interface of a tethered connection. | |
| constexpr char kEgressTetheringChain[] = "egress_tethering"; | |
| // INPUT filter chain to accept ingress traffic received by ChromeOS on the | |
| // downstream network interface of a tethered connection by a tethering client. | |
| constexpr char kIngressTetheringChain[] = "ingress_tethering"; | |
| // FORWARD filter chain to stop any forwarded traffic between a downstream | |
| // network interface and any other physical network, VPN, or guest virtual | |
| // network. | |
| constexpr char kForwardLocalOnlyChain[] = "forward_localonly"; | |
| // OUPUT filter chain to accept egress traffic sent by ChromeOS on the | |
| // downstream network interface of a local only network managed by patchpanel. | |
| constexpr char kEgressLocalOnlyChain[] = "egress_localonly"; | |
| // INPUT filter chain to accept ingress traffic received by ChromeOS on the | |
| // downstream network interface of a local only network managed by patchpanel. | |
| constexpr char kIngressLocalOnlyChain[] = "ingress_localonly"; | |
| // INPUT filter chain to jump to the specialized ingress chains for tethering or | |
| // local only networks. This static chain ensures the correct the traversal | |
| // orders of other INPUT rules and must be after "ingress_port_firewall". | |
| constexpr char kIngressDownstreamNetworkChain[] = "ingress_downstream_network"; | |
| // OUTPUT filter chain to drop host-initiated connection to Bruschetta and | |
| // FORWARD filter chain to drop external- and other-vm-initiated connection. | |
| constexpr char kDropOutputToBruschettaChain[] = "drop_output_to_bruschetta"; | |
| constexpr char kDropForwardToBruschettaChain[] = "drop_forward_to_bruschetta"; | |
| // IPv4 nat PREROUTING chains for forwarding ingress traffic to different types | |
| // of hosted guests with the corresponding hierarchy. | |
| constexpr char kApplyAutoDNATToArcChain[] = "apply_auto_dnat_to_arc"; | |
| constexpr char kApplyAutoDNATToCrostiniChain[] = "apply_auto_dnat_to_crostini"; | |
| constexpr char kApplyAutoDNATToParallelsChain[] = | |
| "apply_auto_dnat_to_parallels"; | |
| // nat PREROUTING chain for egress traffic from downstream guests. | |
| constexpr char kRedirectDefaultDnsChain[] = "redirect_default_dns"; | |
| // nat OUTPUT chain for egress traffic from processes running on the host. | |
| constexpr char kRedirectUserDnsChain[] = "redirect_user_dns"; | |
| // nat POSTROUTING chain for egress traffic from processes running on the host. | |
| constexpr char kSNATUserDnsChain[] = "snat_user_dns"; | |
| // Chains for QoS. | |
| // mangle OUTPUT and PREROUTING chains for applying fwmarks on both ingress and | |
| // egress traffic for QoS. | |
| // - qos_detect: Hold the rules for applying fwmarks. Jump rule to this chain is | |
| // in qos_detect_static and installed dynamically. | |
| // - qos_detect_static: Hold only the jump rule to qos_detect. Jump rules to | |
| // this chain are in OUTPUT and PREROUTING chains and installed statically. | |
| // The main purpose to have these two layers is for having static order of rules | |
| // in the mangle table. Also see b/301566901. | |
| constexpr char kQoSDetectChain[] = "qos_detect"; | |
| constexpr char kQoSDetectStaticChain[] = "qos_detect_static"; | |
| // mangle chain for holding the dynamic matching rules for DoH. Referenced in | |
| // the qos_detect chain. | |
| constexpr char kQoSDetectDoHChain[] = "qos_detect_doh"; | |
| // mangle chain for holding the dynamic matching rules for Borealis. Referenced | |
| // in the qos_detect chain. | |
| constexpr char kQoSDetectBorealisChain[] = "qos_detect_borealis"; | |
| // mangle POSTROUTING chain for applying DSCP fields based on fwmarks for egress | |
| // traffic. | |
| constexpr char kQoSApplyDSCPChain[] = "qos_apply_dscp"; | |
| // Maximum length of an iptables chain name. | |
| constexpr int kIptablesMaxChainLength = 28; | |
| IpFamily ConvertIpFamily(net_base::IPFamily family) { | |
| return (family == net_base::IPFamily::kIPv4) ? IpFamily::kIPv4 | |
| : IpFamily::kIPv6; | |
| } | |
| std::string AutoDNATTargetChainName(AutoDNATTarget auto_dnat_target) { | |
| switch (auto_dnat_target) { | |
| case AutoDNATTarget::kArc: | |
| return kApplyAutoDNATToArcChain; | |
| case AutoDNATTarget::kCrostini: | |
| return kApplyAutoDNATToCrostiniChain; | |
| case AutoDNATTarget::kParallels: | |
| return kApplyAutoDNATToParallelsChain; | |
| } | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const DeviceMode tun_tap) { | |
| switch (tun_tap) { | |
| case DeviceMode::kTun: | |
| return stream << "tun"; | |
| case DeviceMode::kTap: | |
| return stream << "tap"; | |
| } | |
| } | |
| // Returns the conventional name for the PREROUTING mangle subchain | |
| // pertaining to the downstream interface |int_ifname|. | |
| std::string PreroutingSubChainName(std::string_view int_ifname) { | |
| return base::StrCat({"PREROUTING_", int_ifname}); | |
| } | |
| std::string EgressSubChainName(std::string_view ext_ifname) { | |
| return base::StrCat({"egress_", ext_ifname}); | |
| } | |
| // Helper enum for controlling what type of FORWARD firewall rules are | |
| // configured for a given network interface. | |
| enum class ForwardFirewallRuleType { | |
| // Rules for the downstream interface used for tethering. It is assumed that: | |
| // - The downstream interface is not used for anything other traffic. | |
| // - There is at most a single unique tethering setup on the device. | |
| kTethering, | |
| // Rules for a downstream interface used for a local-only network associated | |
| // with a WiFi hotspot or a WiFi Direct Group Owner link. At the moment, no | |
| // traffic forwarding is allowed. | |
| kLocalOnly, | |
| // Rules for a virtual interface used for an isolated guest VM like Bruschetta | |
| // with strict ingress firewall rules. Only traffic originated from the VM is | |
| // allowed. | |
| kIsolatedGuest, | |
| // Rules for any other virtual interface or downstream network interface. | |
| kOpen, | |
| }; | |
| ForwardFirewallRuleType GetForwardFirewallRuleType(TrafficSource source) { | |
| switch (source) { | |
| case TrafficSource::kTetherDownstream: | |
| return ForwardFirewallRuleType::kTethering; | |
| case TrafficSource::kWiFiLOHS: | |
| case TrafficSource::kWiFiDirect: | |
| return ForwardFirewallRuleType::kLocalOnly; | |
| case TrafficSource::kBruschettaVM: | |
| return ForwardFirewallRuleType::kIsolatedGuest; | |
| default: | |
| return ForwardFirewallRuleType::kOpen; | |
| } | |
| } | |
| std::string_view GetEgressFilterChainName(DownstreamNetworkTopology topology) { | |
| switch (topology) { | |
| case DownstreamNetworkTopology::kLocalOnly: | |
| return kEgressLocalOnlyChain; | |
| case DownstreamNetworkTopology::kTethering: | |
| return kEgressTetheringChain; | |
| } | |
| } | |
| std::string_view GetIngressFilterChainName(DownstreamNetworkTopology topology) { | |
| switch (topology) { | |
| case DownstreamNetworkTopology::kLocalOnly: | |
| return kIngressLocalOnlyChain; | |
| case DownstreamNetworkTopology::kTethering: | |
| return kIngressTetheringChain; | |
| } | |
| } | |
| bool IsDownstreamNetworkForwardFirewallRule(ForwardFirewallRuleType rule) { | |
| return rule == ForwardFirewallRuleType::kTethering || | |
| rule == ForwardFirewallRuleType::kLocalOnly; | |
| } | |
| } // namespace | |
| Datapath::Datapath(System* system) | |
| : Datapath(MinijailedProcessRunner::GetInstance(), new Firewall(), system) { | |
| } | |
| Datapath::Datapath(MinijailedProcessRunner* process_runner, | |
| Firewall* firewall, | |
| System* system) | |
| : process_runner_(process_runner), system_(system) { | |
| firewall_.reset(firewall); | |
| } | |
| void Datapath::Start() { | |
| // Enable IPv4 packet forwarding | |
| if (!system_->SysNetSet(System::SysNet::kIPv4Forward, "1")) { | |
| LOG(ERROR) << "Failed to update net.ipv4.ip_forward." | |
| << " Guest connectivity will not work correctly."; | |
| } | |
| // Enable IPv6 packet forwarding and cross-interface proxying | |
| if (!system_->SysNetSet(System::SysNet::kIPv6Forward, "1")) { | |
| LOG(ERROR) << "Failed to update net.ipv6.conf.all.forwarding." | |
| << " IPv6 functionality may be broken."; | |
| } | |
| if (!system_->SysNetSet(System::SysNet::kIPv6ProxyNDP, "1")) { | |
| LOG(ERROR) << "Failed to update net.ipv6.conf.all.proxy_ndp." | |
| << " IPv6 functionality may be broken."; | |
| } | |
| if (process_runner_->iptables_restore(kIptablesStartScriptPath) != 0) { | |
| LOG(ERROR) << "Failed to call iptables_restore"; | |
| } | |
| if (process_runner_->ip6tables_restore(kIp6tablesStartScriptPath) != 0) { | |
| LOG(ERROR) << "Failed to call ip6tables_restore"; | |
| } | |
| // Rules for WebRTC detection. Notes: | |
| // - In short, this is implemented by detecting the client hello packet in a | |
| // WebRTC connection, and mark the whole connection on success. See the | |
| // WebRTC detection section in go/cros-wifi-qos-dd for more details. | |
| // - The BPF program will only be installed on 5.8+ kernels, where CAP_BPF is | |
| // available. We check if the existence of the program instead of the kernel | |
| // version here. That's why only WebRTC detection is done here dynamically | |
| // while other QoS detection rules are in the static iptables-start script. | |
| // - Marking the whole connection is implemented by saving the mark into | |
| // connmark. To avoid saving the mark for unrelated connections, we check if | |
| // the packet already have a mark at first. | |
| if (system_->IsEbpfEnabled()) { | |
| auto run_iptables_in_batch = process_runner_->AcquireIptablesBatchMode(); | |
| const auto install_rule = | |
| [this](IpFamily family, const std::vector<std::string_view>& args) { | |
| ModifyIptables(family, Iptables::Table::kMangle, | |
| Iptables::Command::kA, kQoSDetectChain, args); | |
| }; | |
| const std::string qos_mask = kFwmarkQoSCategoryMask.ToString(); | |
| const std::string default_mark = QoSFwmarkWithMask(QoSCategory::kDefault); | |
| const std::string multimedia_conferencing_mark = | |
| QoSFwmarkWithMask(QoSCategory::kMultimediaConferencing); | |
| install_rule(IpFamily::kDual, {"-m", "mark", "!", "--mark", default_mark, | |
| "-j", "RETURN", "-w"}); | |
| install_rule(IpFamily::kDual, | |
| {"-m", "bpf", "--object-pinned", kWebRTCMatcherPinPath, "-j", | |
| "MARK", "--set-xmark", multimedia_conferencing_mark, "-w"}); | |
| install_rule(IpFamily::kDual, {"-j", "CONNMARK", "--save-mark", "--nfmask", | |
| qos_mask, "--ctmask", qos_mask, "-w"}); | |
| } | |
| } | |
| void Datapath::Stop() { | |
| // Disable packet forwarding | |
| if (!system_->SysNetSet(System::SysNet::kIPv6Forward, "0")) { | |
| LOG(ERROR) << "Failed to restore net.ipv6.conf.all.forwarding."; | |
| } | |
| if (!system_->SysNetSet(System::SysNet::kIPv4Forward, "0")) { | |
| LOG(ERROR) << "Failed to restore net.ipv4.ip_forward."; | |
| } | |
| } | |
| bool Datapath::NetnsAttachName(std::string_view netns_name, pid_t netns_pid) { | |
| // Try first to delete any netns with name |netns_name| in case patchpanel | |
| // did not exit cleanly. | |
| if (process_runner_->ip_netns_delete(netns_name, | |
| /*log_failures=*/false) == 0) | |
| LOG(INFO) << "Deleted left over network namespace name " << netns_name; | |
| if (netns_pid == ConnectedNamespace::kNewNetnsPid) | |
| return process_runner_->ip_netns_add(netns_name) == 0; | |
| else | |
| return process_runner_->ip_netns_attach(netns_name, netns_pid) == 0; | |
| } | |
| bool Datapath::NetnsDeleteName(std::string_view netns_name) { | |
| return process_runner_->ip_netns_delete(netns_name) == 0; | |
| } | |
| bool Datapath::AddBridge(std::string_view ifname, const IPv4CIDR& cidr) { | |
| base::ScopedFD control_fd(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)); | |
| if (!control_fd.is_valid() || | |
| system_->Ioctl(control_fd.get(), SIOCBRADDBR, | |
| std::string(ifname).c_str()) != 0) { | |
| LOG(ERROR) << "Failed to create bridge " << ifname; | |
| return false; | |
| } | |
| // Configure the persistent Chrome OS bridge interface with static IP. | |
| if (process_runner_->ip("addr", "add", | |
| {cidr.ToString(), "brd", | |
| cidr.GetBroadcast().ToString(), "dev", ifname}) != | |
| 0) { | |
| RemoveBridge(ifname); | |
| return false; | |
| } | |
| if (process_runner_->ip("link", "set", {ifname, "up"}) != 0) { | |
| RemoveBridge(ifname); | |
| return false; | |
| } | |
| return true; | |
| } | |
| void Datapath::RemoveBridge(std::string_view ifname) { | |
| process_runner_->ip("link", "set", {ifname, "down"}); | |
| base::ScopedFD control_fd(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)); | |
| if (!control_fd.is_valid() || | |
| system_->Ioctl(control_fd.get(), SIOCBRDELBR, | |
| std::string(ifname).c_str()) != 0) { | |
| LOG(ERROR) << "Failed to destroy bridge " << ifname; | |
| } | |
| } | |
| bool Datapath::AddToBridge(std::string_view br_ifname, | |
| std::string_view ifname) { | |
| struct ifreq ifr; | |
| FillInterfaceRequest(br_ifname, &ifr); | |
| ifr.ifr_ifindex = system_->IfNametoindex(ifname); | |
| base::ScopedFD control_fd(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)); | |
| if (!control_fd.is_valid() || | |
| system_->Ioctl(control_fd.get(), SIOCBRADDIF, &ifr) != 0) { | |
| LOG(ERROR) << "Failed to add " << ifname << " to bridge " << br_ifname; | |
| return false; | |
| } | |
| return true; | |
| } | |
| std::string Datapath::AddTunTap( | |
| std::string_view name, | |
| const std::optional<net_base::MacAddress>& mac_addr, | |
| const std::optional<net_base::IPv4CIDR>& ipv4_cidr, | |
| std::string_view user, | |
| DeviceMode dev_mode) { | |
| base::ScopedFD dev = system_->OpenTunDev(); | |
| if (!dev.is_valid()) { | |
| PLOG(ERROR) << "Failed to open tun device"; | |
| return ""; | |
| } | |
| struct ifreq ifr; | |
| FillInterfaceRequest(name.empty() ? kDefaultIfname : name, &ifr); | |
| switch (dev_mode) { | |
| case DeviceMode::kTun: | |
| ifr.ifr_flags = IFF_TUN | IFF_NO_PI; | |
| break; | |
| case DeviceMode::kTap: | |
| ifr.ifr_flags = IFF_TAP | IFF_NO_PI; | |
| break; | |
| } | |
| // If a template was given as the name, ifr_name will be updated with the | |
| // actual interface name. | |
| if (system_->Ioctl(dev.get(), TUNSETIFF, &ifr) != 0) { | |
| PLOG(ERROR) << "Failed to create " << dev_mode << " interface " << name; | |
| return ""; | |
| } | |
| const char* ifname = ifr.ifr_name; | |
| if (system_->Ioctl(dev.get(), TUNSETPERSIST, 1) != 0) { | |
| PLOG(ERROR) << "Failed to persist " << dev_mode << " interface " << ifname; | |
| return ""; | |
| } | |
| if (!user.empty()) { | |
| uid_t uid = 0; | |
| if (!brillo::userdb::GetUserInfo(std::string(user), &uid, nullptr)) { | |
| PLOG(ERROR) << "Unable to look up UID for " << user; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| if (system_->Ioctl(dev.get(), TUNSETOWNER, uid) != 0) { | |
| PLOG(ERROR) << "Failed to set owner " << uid << " of " << dev_mode | |
| << " interface " << ifname; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| } | |
| // Create control socket for configuring the interface. | |
| base::ScopedFD sock(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)); | |
| if (!sock.is_valid()) { | |
| PLOG(ERROR) << "Failed to create control socket for " << dev_mode | |
| << " interface " << ifname; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| if (ipv4_cidr) { | |
| struct sockaddr_in* addr = | |
| reinterpret_cast<struct sockaddr_in*>(&ifr.ifr_addr); | |
| addr->sin_family = AF_INET; | |
| addr->sin_addr = ipv4_cidr->address().ToInAddr(); | |
| if (system_->Ioctl(sock.get(), SIOCSIFADDR, &ifr) != 0) { | |
| PLOG(ERROR) << "Failed to set ip address for " << dev_mode | |
| << " interface " << ifname << " {" << ipv4_cidr->ToString() | |
| << "}"; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| struct sockaddr_in* netmask = | |
| reinterpret_cast<struct sockaddr_in*>(&ifr.ifr_netmask); | |
| netmask->sin_family = AF_INET; | |
| netmask->sin_addr = ipv4_cidr->ToNetmask().ToInAddr(); | |
| if (system_->Ioctl(sock.get(), SIOCSIFNETMASK, &ifr) != 0) { | |
| PLOG(ERROR) << "Failed to set netmask for " << dev_mode << " interface " | |
| << ifname << " {" << ipv4_cidr->ToString() << "}"; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| } | |
| if (mac_addr) { | |
| struct sockaddr* hwaddr = &ifr.ifr_hwaddr; | |
| hwaddr->sa_family = ARPHRD_ETHER; | |
| memcpy(&hwaddr->sa_data, mac_addr->data(), | |
| net_base::MacAddress::kAddressLength); | |
| if (system_->Ioctl(sock.get(), SIOCSIFHWADDR, &ifr) != 0) { | |
| PLOG(ERROR) << "Failed to set mac address for " << dev_mode | |
| << " interface " << ifname << " {" << mac_addr->ToString() | |
| << "}"; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| } | |
| if (system_->Ioctl(sock.get(), SIOCGIFFLAGS, &ifr) != 0) { | |
| PLOG(ERROR) << "Failed to get flags for " << dev_mode << " interface " | |
| << ifname; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); | |
| if (system_->Ioctl(sock.get(), SIOCSIFFLAGS, &ifr) != 0) { | |
| PLOG(ERROR) << "Failed to enable " << dev_mode << " interface " << ifname; | |
| RemoveTunTap(ifname, dev_mode); | |
| return ""; | |
| } | |
| return ifname; | |
| } | |
| void Datapath::RemoveTunTap(std::string_view ifname, DeviceMode dev_mode) { | |
| std::string_view dev_mode_str = | |
| (dev_mode == DeviceMode::kTun) ? "tun" : "tap"; | |
| process_runner_->ip("tuntap", "del", {ifname, "mode", dev_mode_str}, | |
| /*as_patchpanel_user=*/true); | |
| } | |
| bool Datapath::ConnectVethPair(pid_t netns_pid, | |
| std::string_view netns_name, | |
| std::string_view veth_ifname, | |
| std::string_view peer_ifname, | |
| net_base::MacAddress remote_mac_addr, | |
| const IPv4CIDR& remote_ipv4_cidr, | |
| const std::optional<IPv6CIDR>& remote_ipv6_cidr, | |
| bool remote_multicast_flag) { | |
| // Set up the virtual pair across the current namespace and |netns_name|. | |
| if (!AddVirtualInterfacePair(netns_name, veth_ifname, peer_ifname)) { | |
| LOG(ERROR) << "Failed to create veth pair " << veth_ifname << "," | |
| << peer_ifname; | |
| return false; | |
| } | |
| // Configure the remote veth in namespace |netns_name|. | |
| { | |
| auto ns = ScopedNS::EnterNetworkNS(netns_name); | |
| if (!ns && netns_pid != kTestPID) { | |
| LOG(ERROR) | |
| << "Cannot create virtual link -- invalid container namespace?"; | |
| return false; | |
| } | |
| if (!ConfigureInterface(peer_ifname, remote_mac_addr, remote_ipv4_cidr, | |
| remote_ipv6_cidr, /*up=*/true, | |
| remote_multicast_flag)) { | |
| LOG(ERROR) << "Failed to configure interface " << peer_ifname; | |
| RemoveInterface(peer_ifname); | |
| return false; | |
| } | |
| } | |
| if (!ToggleInterface(veth_ifname, /*up=*/true)) { | |
| LOG(ERROR) << "Failed to bring up interface " << veth_ifname; | |
| RemoveInterface(veth_ifname); | |
| return false; | |
| } | |
| return true; | |
| } | |
| void Datapath::RestartIPv6() { | |
| if (!system_->SysNetSet(System::SysNet::kIPv6Disable, "1")) { | |
| LOG(ERROR) << "Failed to disable IPv6"; | |
| } | |
| if (!system_->SysNetSet(System::SysNet::kIPv6Disable, "0")) { | |
| LOG(ERROR) << "Failed to re-enable IPv6"; | |
| } | |
| } | |
| bool Datapath::AddVirtualInterfacePair(std::string_view netns_name, | |
| std::string_view veth_ifname, | |
| std::string_view peer_ifname) { | |
| return process_runner_->ip("link", "add", | |
| {veth_ifname, "type", "veth", "peer", "name", | |
| peer_ifname, "netns", netns_name}) == 0; | |
| } | |
| bool Datapath::ToggleInterface(std::string_view ifname, bool up) { | |
| std::string_view link = up ? "up" : "down"; | |
| return process_runner_->ip("link", "set", {ifname, link}) == 0; | |
| } | |
| bool Datapath::ConfigureInterface(std::string_view ifname, | |
| std::optional<net_base::MacAddress> mac_addr, | |
| const IPv4CIDR& ipv4_cidr, | |
| const std::optional<IPv6CIDR>& ipv6_cidr, | |
| bool up, | |
| bool enable_multicast) { | |
| if (process_runner_->ip( | |
| "addr", "add", | |
| {ipv4_cidr.ToString(), "brd", ipv4_cidr.GetBroadcast().ToString(), | |
| "dev", ifname}) != 0) { | |
| return false; | |
| } | |
| if (ipv6_cidr && | |
| process_runner_->ip("addr", "add", | |
| {ipv6_cidr->ToString(), "dev", ifname}) != 0) { | |
| return false; | |
| } | |
| std::vector<std::string> iplink_args{ | |
| "dev", | |
| std::string(ifname), | |
| up ? "up" : "down", | |
| }; | |
| if (mac_addr) { | |
| iplink_args.insert(iplink_args.end(), {"addr", mac_addr->ToString()}); | |
| } | |
| iplink_args.insert(iplink_args.end(), | |
| {"multicast", enable_multicast ? "on" : "off"}); | |
| return process_runner_->ip("link", "set", iplink_args) == 0; | |
| } | |
| void Datapath::RemoveInterface(std::string_view ifname) { | |
| process_runner_->ip("link", "delete", {ifname}, | |
| /*as_patchpanel_user=*/false, | |
| /*log_failures=*/false); | |
| } | |
| bool Datapath::AddSourceIPv4DropRule(std::string_view oif, | |
| std::string_view src_ip) { | |
| return process_runner_->iptables( | |
| Iptables::Table::kFilter, Iptables::Command::kI, | |
| kDropGuestIpv4PrefixChain, | |
| {"-o", oif, "-s", src_ip, "-j", "DROP", "-w"}) == 0; | |
| } | |
| bool Datapath::StartRoutingNamespace(const ConnectedNamespace& nsinfo) { | |
| // Veth interface configuration and client routing configuration: | |
| // - attach a name to the client namespace (or create a new named namespace | |
| // if no client is specified). | |
| // - create veth pair across the current namespace and the client namespace. | |
| // - configure IPv4 address on remote veth inside client namespace. | |
| // - configure IPv4 address on local veth inside host namespace. | |
| // - add a default IPv4 /0 route sending traffic to that remote veth. | |
| if (!NetnsAttachName(nsinfo.netns_name, nsinfo.pid)) { | |
| LOG(ERROR) << "Failed to attach name " << nsinfo.netns_name | |
| << " to namespace pid " << nsinfo.pid; | |
| return false; | |
| } | |
| if (!ConnectVethPair( | |
| nsinfo.pid, nsinfo.netns_name, nsinfo.host_ifname, nsinfo.peer_ifname, | |
| nsinfo.peer_mac_addr, nsinfo.peer_ipv4_cidr, | |
| nsinfo.static_ipv6_config | |
| ? std::make_optional(nsinfo.static_ipv6_config->peer_cidr) | |
| : std::nullopt, | |
| /*enable_multicast=*/false)) { | |
| LOG(ERROR) << "Failed to create veth pair for" | |
| " namespace pid " | |
| << nsinfo.pid; | |
| NetnsDeleteName(nsinfo.netns_name); | |
| return false; | |
| } | |
| if (!ConfigureInterface( | |
| nsinfo.host_ifname, nsinfo.host_mac_addr, nsinfo.host_ipv4_cidr, | |
| nsinfo.static_ipv6_config | |
| ? std::make_optional(nsinfo.static_ipv6_config->host_cidr) | |
| : std::nullopt, | |
| /*up=*/true, | |
| /*enable_multicast=*/false)) { | |
| LOG(ERROR) << "Cannot configure host interface " << nsinfo.host_ifname; | |
| RemoveInterface(nsinfo.host_ifname); | |
| NetnsDeleteName(nsinfo.netns_name); | |
| return false; | |
| } | |
| { | |
| auto ns = ScopedNS::EnterNetworkNS(nsinfo.netns_name); | |
| if (!ns && nsinfo.pid != kTestPID) { | |
| LOG(ERROR) << "Invalid namespace pid " << nsinfo.pid; | |
| RemoveInterface(nsinfo.host_ifname); | |
| NetnsDeleteName(nsinfo.netns_name); | |
| return false; | |
| } | |
| if (!AddIPv4Route(nsinfo.host_ipv4_cidr.address(), /*subnet_cidr=*/{})) { | |
| LOG(ERROR) << "Failed to add default /0 route to " << nsinfo.host_ifname | |
| << " inside namespace pid " << nsinfo.pid; | |
| RemoveInterface(nsinfo.host_ifname); | |
| NetnsDeleteName(nsinfo.netns_name); | |
| return false; | |
| } | |
| if (nsinfo.static_ipv6_config && | |
| !AddIPv6Route(nsinfo.static_ipv6_config->host_cidr.address(), | |
| /*subnet_cidr=*/{})) { | |
| LOG(ERROR) << "Failed to add IPv6 default /0 route to " | |
| << nsinfo.host_ifname << " inside namespace pid " | |
| << nsinfo.pid; | |
| RemoveInterface(nsinfo.host_ifname); | |
| NetnsDeleteName(nsinfo.netns_name); | |
| return false; | |
| } | |
| } | |
| // Host namespace routing configuration | |
| // - ingress: route added automatically by kernel when adding the device's | |
| // address with prefix. | |
| // - egress: - allow forwarding for traffic outgoing |host_ifname|. | |
| // - add SNAT mark 0x1/0x1 for traffic outgoing |host_ifname|. | |
| // Note that by default unsolicited ingress traffic is not forwarded to the | |
| // client namespace unless the client specifically set port forwarding | |
| // through permission_broker DBus APIs. | |
| if (!nsinfo.outbound_ifname.empty()) { | |
| if (!nsinfo.current_outbound_device) { | |
| LOG(ERROR) << __func__ << ": No shill Device known for ConnectNamespace " | |
| << nsinfo; | |
| return false; | |
| } | |
| StartRoutingDevice(*nsinfo.current_outbound_device, nsinfo.host_ifname, | |
| nsinfo.source, nsinfo.static_ipv6_config.has_value()); | |
| } else if (!nsinfo.route_on_vpn) { | |
| StartRoutingDeviceAsSystem(nsinfo.host_ifname, nsinfo.source, | |
| nsinfo.static_ipv6_config.has_value()); | |
| } else { | |
| StartRoutingDeviceAsUser( | |
| nsinfo.host_ifname, nsinfo.source, nsinfo.host_ipv4_cidr.address(), | |
| nsinfo.peer_ipv4_cidr.address(), | |
| nsinfo.static_ipv6_config | |
| ? std::make_optional(nsinfo.static_ipv6_config->host_cidr.address()) | |
| : std::nullopt, | |
| nsinfo.static_ipv6_config | |
| ? std::make_optional(nsinfo.static_ipv6_config->peer_cidr.address()) | |
| : std::nullopt); | |
| } | |
| return true; | |
| } | |
| void Datapath::StopRoutingNamespace(const ConnectedNamespace& nsinfo) { | |
| StopRoutingDevice(nsinfo.host_ifname, nsinfo.source); | |
| RemoveInterface(nsinfo.host_ifname); | |
| NetnsDeleteName(nsinfo.netns_name); | |
| } | |
| bool Datapath::ModifyDnsProxyDNAT(IpFamily family, | |
| const DnsRedirectionRule& rule, | |
| Iptables::Command op, | |
| std::string_view ifname, | |
| std::string_view chain) { | |
| bool success = true; | |
| for (const auto& protocol : {"udp", "tcp"}) { | |
| std::vector<std::string_view> args; | |
| if (!ifname.empty()) { | |
| args.insert(args.end(), {"-i", ifname}); | |
| } | |
| std::string proxy_addr = rule.proxy_address.ToString(); | |
| args.insert(args.end(), {"-p", protocol, "--dport", kDefaultDnsPort, "-j", | |
| "DNAT", "--to-destination", proxy_addr, "-w"}); | |
| if (!ModifyIptables(family, Iptables::Table::kNat, op, chain, args)) { | |
| success = false; | |
| } | |
| } | |
| return success; | |
| } | |
| bool Datapath::ModifyDnsProxyMasquerade(IpFamily family, | |
| Iptables::Command op, | |
| std::string_view chain) { | |
| bool success = true; | |
| for (const auto& protocol : {"udp", "tcp"}) { | |
| std::vector<std::string_view> args = { | |
| "-p", protocol, "--dport", kDefaultDnsPort, "-j", "MASQUERADE", "-w"}; | |
| if (!ModifyIptables(family, Iptables::Table::kNat, op, chain, args)) { | |
| success = false; | |
| } | |
| } | |
| return success; | |
| } | |
| bool Datapath::StartDnsRedirection(const DnsRedirectionRule& rule) { | |
| auto batch_mode = process_runner_->AcquireIptablesBatchMode(); | |
| const IpFamily family = ConvertIpFamily(rule.proxy_address.GetFamily()); | |
| switch (rule.type) { | |
| case patchpanel::SetDnsRedirectionRuleRequest::DEFAULT: { | |
| if (!ModifyDnsProxyDNAT(family, rule, Iptables::Command::kA, | |
| rule.input_ifname, kRedirectDefaultDnsChain)) { | |
| LOG(ERROR) << "Failed to add DNS DNAT rule for " << rule.input_ifname; | |
| return false; | |
| } | |
| break; | |
| } | |
| case patchpanel::SetDnsRedirectionRuleRequest::ARC: { | |
| break; | |
| } | |
| case patchpanel::SetDnsRedirectionRuleRequest::USER: { | |
| // Start protecting DNS traffic from VPN fwmark tagging. | |
| if (!ModifyDnsRedirectionSkipVpnRule(family, Iptables::Command::kA)) { | |
| LOG(ERROR) << "Failed to add VPN skip rule for DNS proxy"; | |
| return false; | |
| } | |
| // Add DNS redirect rule for user (including Chrome) traffic. | |
| if (!ModifyDnsProxyDNAT(family, rule, Iptables::Command::kA, | |
| /*ifname=*/"", kRedirectUserDnsChain)) { | |
| LOG(ERROR) << "Failed to add user DNS DNAT rule"; | |
| return false; | |
| } | |
| // Add MASQUERADE rule for user (including Chrome) traffic. | |
| if (family == IpFamily::kIPv6 && | |
| !ModifyDnsProxyMasquerade(family, Iptables::Command::kA, | |
| kSNATUserDnsChain)) { | |
| LOG(ERROR) << "Failed to add user DNS MASQUERADE rule"; | |
| return false; | |
| } | |
| // Allows user (including Chrome) traffic to go to user DNS proxy's | |
| // address. | |
| if (!ModifyDnsProxyAcceptRule(family, rule, Iptables::Command::kA)) { | |
| LOG(ERROR) << "Failed to add DNS proxy accept rule for " | |
| << rule.host_ifname; | |
| return false; | |
| } | |
| break; | |
| } | |
| case patchpanel::SetDnsRedirectionRuleRequest::EXCLUDE_DESTINATION: { | |
| if (!ModifyDnsExcludeDestinationRule(family, rule, Iptables::Command::kI, | |
| kRedirectUserDnsChain)) { | |
| LOG(ERROR) << "Failed to add user DNS exclude rule"; | |
| return false; | |
| } | |
| if (!ModifyDnsProxyAcceptRule(family, rule, Iptables::Command::kA)) { | |
| LOG(ERROR) << "Failed to add DNS proxy accept rule for " | |
| << rule.host_ifname; | |
| return false; | |
| } | |
| break; | |
| } | |
| default: | |
| LOG(ERROR) << "Invalid DNS proxy type " << rule; | |
| return false; | |
| } | |
| if (batch_mode) { | |
| return process_runner_->CommitIptablesRules(std::move(batch_mode)); | |
| } else { | |
| // This means that the caller of this function already acquired the batch | |
| // mode. The execution will be done there. | |
| return true; | |
| } | |
| } | |
| void Datapath::StopDnsRedirection(const DnsRedirectionRule& rule) { | |
| auto batch_mode = process_runner_->AcquireIptablesBatchMode(); | |
| const IpFamily family = ConvertIpFamily(rule.proxy_address.GetFamily()); | |
| // Whenever the client that requested the rule closes the fd, the requested | |
| // rule will be deleted. There is a delay between fd closing time and rule | |
| // removal time. This prevents deletion of the rules by flushing the chains. | |
| switch (rule.type) { | |
| case patchpanel::SetDnsRedirectionRuleRequest::DEFAULT: { | |
| ModifyDnsProxyDNAT(family, rule, Iptables::Command::kD, rule.input_ifname, | |
| kRedirectDefaultDnsChain); | |
| break; | |
| } | |
| case patchpanel::SetDnsRedirectionRuleRequest::ARC: { | |
| break; | |
| } | |
| case patchpanel::SetDnsRedirectionRuleRequest::USER: { | |
| ModifyDnsProxyDNAT(family, rule, Iptables::Command::kD, /*ifname=*/"", | |
| kRedirectUserDnsChain); | |
| ModifyDnsRedirectionSkipVpnRule(family, Iptables::Command::kD); | |
| if (family == IpFamily::kIPv6) { | |
| ModifyDnsProxyMasquerade(family, Iptables::Command::kD, | |
| kSNATUserDnsChain); | |
| } | |
| ModifyDnsProxyAcceptRule(family, rule, Iptables::Command::kD); | |
| break; | |
| } | |
| case patchpanel::SetDnsRedirectionRuleRequest::EXCLUDE_DESTINATION: { | |
| ModifyDnsExcludeDestinationRule(family, rule, Iptables::Command::kD, | |
| kRedirectUserDnsChain); | |
| ModifyDnsProxyAcceptRule(family, rule, Iptables::Command::kD); | |
| break; | |
| } | |
| default: | |
| LOG(ERROR) << "Invalid DNS proxy type " << rule; | |
| } | |
| } | |
| void Datapath::AddDownstreamInterfaceRules( | |
| std::optional<ShillClient::Device> upstream_device, | |
| std::string_view int_ifname, | |
| TrafficSource source, | |
| bool static_ipv6) { | |
| auto forward_firewall_rule_type = GetForwardFirewallRuleType(source); | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kTethering) { | |
| // Explicitly accept any traffic forwarded between the upstream and | |
| // downstream network interfaces. | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kForwardTetheringChain, "ACCEPT", | |
| upstream_device->ifname, int_ifname); | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kForwardTetheringChain, "ACCEPT", | |
| int_ifname, upstream_device->ifname); | |
| // Then, reject any other forwarded traffic between the downstream network | |
| // interface and any other interface. | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kForwardTetheringChain, "DROP", | |
| /*iif=*/"", int_ifname); | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kForwardTetheringChain, "DROP", | |
| int_ifname, /*oif=*/""); | |
| // If the upstream network is shared with the device (e.g. non-cellular | |
| // upstream, DEFAULT PDN used as upstream, DUN PDN upstream also used as | |
| // DEFAULT), it is not possible to drop any other forwarded traffic in or | |
| // out of the upstream network interface. | |
| // TODO(b/273749806): Make patchpanel aware of whether the upstream network | |
| // is exclusively used for tethering or not, and add the additional DROP | |
| // rules if that is the case. | |
| } | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kLocalOnly) { | |
| // Reject any forwarded traffic between the downstream network interface | |
| // and any other interface. | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kForwardLocalOnlyChain, "DROP", | |
| /*iif=*/"", int_ifname); | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kForwardLocalOnlyChain, "DROP", | |
| int_ifname, /*oif=*/""); | |
| } | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kOpen) { | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, "FORWARD", "ACCEPT", /*iif=*/"", | |
| int_ifname); | |
| } | |
| if (!IsDownstreamNetworkForwardFirewallRule(forward_firewall_rule_type)) { | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, "FORWARD", "ACCEPT", int_ifname, | |
| /*oif=*/""); | |
| } | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kIsolatedGuest) { | |
| ModifyIsolatedGuestDropRule(Iptables::Command::kA, int_ifname); | |
| } | |
| std::string subchain = PreroutingSubChainName(int_ifname); | |
| // This can fail if patchpanel did not stopped correctly or failed to cleanup | |
| // the chain when |int_ifname| was previously deleted. | |
| if (!AddChain(IpFamily::kDual, Iptables::Table::kMangle, subchain)) | |
| LOG(ERROR) << "Failed to create mangle chain " << subchain; | |
| // Make sure the chain is empty if patchpanel did not cleaned correctly that | |
| // chain before. | |
| if (!FlushChain(IpFamily::kDual, Iptables::Table::kMangle, subchain)) { | |
| LOG(ERROR) << "Could not flush " << subchain; | |
| } | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kA, "PREROUTING", subchain, int_ifname, | |
| /*oif=*/""); | |
| // IPv4 traffic from all downstream interfaces should be tagged to go through | |
| // SNAT. | |
| if (!ModifyFwmark(IpFamily::kIPv4, subchain, Iptables::Command::kA, "", "", 0, | |
| kFwmarkLegacySNAT, kFwmarkLegacySNAT)) { | |
| LOG(ERROR) << "Failed to add fwmark SNAT tagging rule for " << int_ifname; | |
| } | |
| // IPv6 traffic from all downstream interfaces should be tagged to go through | |
| // SNAT if NAT66 is used (see ConnectNamespace |static_ipv6|). | |
| if (static_ipv6 && | |
| !ModifyFwmark(IpFamily::kIPv6, subchain, Iptables::Command::kA, "", "", 0, | |
| kFwmarkLegacySNAT, kFwmarkLegacySNAT)) { | |
| LOG(ERROR) << "Failed to add fwmark SNAT tagging rule for " << int_ifname; | |
| } | |
| if (!ModifyFwmarkSourceTag(subchain, Iptables::Command::kA, source)) { | |
| LOG(ERROR) << "Failed to add fwmark tagging rule for source " << source | |
| << " in " << subchain; | |
| } | |
| } | |
| void Datapath::StartRoutingDevice(const ShillClient::Device& shill_device, | |
| std::string_view int_ifname, | |
| TrafficSource source, | |
| bool static_ipv6) { | |
| auto batch_mode = process_runner_->AcquireIptablesBatchMode(); | |
| std::string_view ext_ifname = shill_device.ifname; | |
| AddDownstreamInterfaceRules(shill_device, int_ifname, source, static_ipv6); | |
| // If |ext_ifname| is not null, mark egress traffic with the | |
| // fwmark routing tag corresponding to |ext_ifname|. | |
| int ifindex = system_->IfNametoindex(ext_ifname); | |
| if (ifindex == 0) { | |
| LOG(ERROR) << "Failed to retrieve interface index of " << ext_ifname; | |
| return; | |
| } | |
| auto routing_mark = Fwmark::FromIfIndex(ifindex); | |
| if (!routing_mark.has_value()) { | |
| LOG(ERROR) << "Failed to compute fwmark value of interface " << ext_ifname | |
| << " with index " << ifindex; | |
| return; | |
| } | |
| std::string subchain = PreroutingSubChainName(int_ifname); | |
| if (!ModifyFwmarkRoutingTag(subchain, Iptables::Command::kA, | |
| routing_mark.value())) { | |
| LOG(ERROR) << "Failed to add fwmark routing tag for " << ext_ifname << "<-" | |
| << int_ifname << " in " << subchain; | |
| } | |
| // Restores the source bits from the conntrack mark to the fwmark of a | |
| // packet. The source information specific to a particular connection in | |
| // conntrack table is always preferred over the default source value. | |
| // Such source tag overrides can be injected in conntrack with | |
| // ConntrackMonitor. | |
| if (!ModifyConnmarkRestore(IpFamily::kDual, subchain, Iptables::Command::kA, | |
| /*iif=*/"", kFwmarkAllSourcesMask)) { | |
| LOG(ERROR) << "Failed to add CONNMARK restore rule in " << subchain; | |
| } | |
| // If the source bit from the connmark has already been restored to a known | |
| // traffic source mark, return. Otherwise mark with traffic source specified | |
| // in the args. | |
| std::string mark = SourceFwmarkWithMask(TrafficSource::kUnknown); | |
| ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kA, subchain, | |
| {"-m", "mark", "!", "--mark", mark, "-j", "RETURN", "-w"}); | |
| if (!ModifyFwmarkSourceTag(subchain, Iptables::Command::kA, source)) { | |
| LOG(ERROR) << "Failed to add source fwmark tagging rule for source " | |
| << source << " in " << subchain; | |
| } | |
| } | |
| void Datapath::StartRoutingDeviceAsSystem(std::string_view int_ifname, | |
| TrafficSource source, | |
| bool static_ipv6) { | |
| auto batch_mode = process_runner_->AcquireIptablesBatchMode(); | |
| AddDownstreamInterfaceRules(std::nullopt, int_ifname, source, static_ipv6); | |
| // Set up a CONNMARK restore rule in PREROUTING to apply any fwmark routing | |
| // tag saved for the current connection, and rely on implicit routing to the | |
| // default physical network otherwise. | |
| std::string subchain = PreroutingSubChainName(int_ifname); | |
| if (!ModifyConnmarkRestore(IpFamily::kDual, subchain, Iptables::Command::kA, | |
| /*iif=*/"", kFwmarkRoutingMask)) { | |
| LOG(ERROR) << "Failed to add CONNMARK restore rule in " << subchain; | |
| } | |
| } | |
| void Datapath::StartRoutingDeviceAsUser( | |
| std::string_view int_ifname, | |
| TrafficSource source, | |
| const IPv4Address& int_ipv4_addr, | |
| std::optional<net_base::IPv4Address> peer_ipv4_addr, | |
| std::optional<IPv6Address> int_ipv6_addr, | |
| std::optional<net_base::IPv6Address> peer_ipv6_addr) { | |
| auto batch_mode = process_runner_->AcquireIptablesBatchMode(); | |
| AddDownstreamInterfaceRules(std::nullopt, int_ifname, source, | |
| peer_ipv6_addr.has_value()); | |
| // Set up a CONNMARK restore rule in PREROUTING to apply any fwmark routing | |
| // tag saved for the current connection, and rely on implicit routing to the | |
| // default logical network otherwise. | |
| std::string subchain = PreroutingSubChainName(int_ifname); | |
| if (!ModifyConnmarkRestore(IpFamily::kDual, subchain, Iptables::Command::kA, | |
| /*iif=*/"", kFwmarkRoutingMask)) { | |
| LOG(ERROR) << "Failed to add CONNMARK restore rule in " << subchain; | |
| } | |
| // Explicitly bypass VPN fwmark tagging rules on returning traffic of a | |
| // connected namespace. This allows the return traffic to reach the local | |
| // source. Connected namespace interface can be identified by checking if | |
| // the value of |peer_ipv4_addr| or |peer_ipv6_addr| is not empty. | |
| if (peer_ipv4_addr && | |
| process_runner_->iptables( | |
| Iptables::Table::kMangle, Iptables::Command::kA, subchain, | |
| {"-s", peer_ipv4_addr->ToString(), "-d", int_ipv4_addr.ToString(), | |
| "-j", "ACCEPT", "-w"}) != 0) { | |
| LOG(ERROR) << "Failed to add connected namespace IPv4 VPN bypass rule"; | |
| } | |
| if (peer_ipv6_addr && int_ipv6_addr && | |
| process_runner_->ip6tables( | |
| Iptables::Table::kMangle, Iptables::Command::kA, subchain, | |
| {"-s", peer_ipv6_addr->ToString(), "-d", int_ipv6_addr->ToString(), | |
| "-j", "ACCEPT", "-w"}) != 0) { | |
| LOG(ERROR) << "Failed to add connected namespace IPv6 VPN bypass rule"; | |
| } | |
| // The jump rule below should not be applied for traffic from a | |
| // ConnectNamespace traffic that needs DNS to go to the VPN | |
| // (ConnectNamespace of the DNS default instance). | |
| if (!peer_ipv4_addr) { | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kA, subchain, kSkipApplyVpnMarkChain, | |
| /*iif=*/"", /*oif=*/""); | |
| } | |
| // Forwarded traffic from downstream interfaces routed to the logical | |
| // default network is eligible to be routed through a VPN. | |
| if (!ModifyFwmarkVpnJumpRule(subchain, Iptables::Command::kA, {}, {})) | |
| LOG(ERROR) << "Failed to add jump rule to VPN chain for " << int_ifname; | |
| } | |
| void Datapath::StopRoutingDevice(std::string_view int_ifname, | |
| TrafficSource source) { | |
| auto batch_mode = process_runner_->AcquireIptablesBatchMode(); | |
| auto forward_firewall_rule_type = GetForwardFirewallRuleType(source); | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kTethering) { | |
| // Assume there is a single and unique tethering setup across the device. | |
| // Therefore, the tethering accept and drop rules can be removed by flushing | |
| // the relevant chain. | |
| FlushChain(IpFamily::kDual, Iptables::Table::kFilter, | |
| kForwardTetheringChain); | |
| } | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kLocalOnly) { | |
| // Assume there is a single and unique local only downstream network setup | |
| // across the device. Therefore, the tethering accept and drop rules can be | |
| // removed by flushing the relevant chain. | |
| FlushChain(IpFamily::kDual, Iptables::Table::kFilter, | |
| kForwardLocalOnlyChain); | |
| } | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kOpen) { | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kD, "FORWARD", "ACCEPT", /*iif=*/"", | |
| int_ifname); | |
| } | |
| if (!IsDownstreamNetworkForwardFirewallRule(forward_firewall_rule_type)) { | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kD, "FORWARD", "ACCEPT", int_ifname, | |
| /*oif=*/""); | |
| } | |
| if (forward_firewall_rule_type == ForwardFirewallRuleType::kIsolatedGuest) { | |
| ModifyIsolatedGuestDropRule(Iptables::Command::kD, int_ifname); | |
| } | |
| std::string subchain = PreroutingSubChainName(int_ifname); | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kD, "PREROUTING", subchain, int_ifname, | |
| /*oif=*/""); | |
| FlushChain(IpFamily::kDual, Iptables::Table::kMangle, subchain); | |
| RemoveChain(IpFamily::kDual, Iptables::Table::kMangle, subchain); | |
| } | |
| void Datapath::AddInboundIPv4DNAT(AutoDNATTarget auto_dnat_target, | |
| const ShillClient::Device& shill_device, | |
| const IPv4Address& ipv4_addr) { | |
| const std::string ipv4_addr_str = ipv4_addr.ToString(); | |
| const std::string chain = AutoDNATTargetChainName(auto_dnat_target); | |
| // Direct ingress IP traffic to existing sockets. | |
| bool success = true; | |
| if (process_runner_->iptables(Iptables::Table::kNat, Iptables::Command::kA, | |
| chain, | |
| {"-i", shill_device.ifname, "-m", "socket", | |
| "--nowildcard", "-j", "ACCEPT", "-w"}) != 0) { | |
| success = false; | |
| } | |
| // Direct ingress TCP & UDP traffic to ARC interface for new connections. | |
| if (process_runner_->iptables( | |
| Iptables::Table::kNat, Iptables::Command::kA, chain, | |
| {"-i", shill_device.ifname, "-p", "tcp", "-j", "DNAT", | |
| "--to-destination", ipv4_addr_str, "-w"}) != 0) { | |
| success = false; | |
| } | |
| if (process_runner_->iptables( | |
| Iptables::Table::kNat, Iptables::Command::kA, chain, | |
| {"-i", shill_device.ifname, "-p", "udp", "-j", "DNAT", | |
| "--to-destination", ipv4_addr_str, "-w"}) != 0) { | |
| success = false; | |
| } | |
| if (!success) { | |
| LOG(ERROR) << "Failed to configure ingress DNAT rules on " | |
| << shill_device.ifname << " to " << ipv4_addr_str; | |
| RemoveInboundIPv4DNAT(auto_dnat_target, shill_device, ipv4_addr); | |
| } | |
| } | |
| void Datapath::RemoveInboundIPv4DNAT(AutoDNATTarget auto_dnat_target, | |
| const ShillClient::Device& shill_device, | |
| const IPv4Address& ipv4_addr) { | |
| const std::string ipv4_addr_str = ipv4_addr.ToString(); | |
| const std::string chain = AutoDNATTargetChainName(auto_dnat_target); | |
| process_runner_->iptables(Iptables::Table::kNat, Iptables::Command::kD, chain, | |
| {"-i", shill_device.ifname, "-p", "udp", "-j", | |
| "DNAT", "--to-destination", ipv4_addr_str, "-w"}); | |
| process_runner_->iptables(Iptables::Table::kNat, Iptables::Command::kD, chain, | |
| {"-i", shill_device.ifname, "-p", "tcp", "-j", | |
| "DNAT", "--to-destination", ipv4_addr_str, "-w"}); | |
| process_runner_->iptables(Iptables::Table::kNat, Iptables::Command::kD, chain, | |
| {"-i", shill_device.ifname, "-m", "socket", | |
| "--nowildcard", "-j", "ACCEPT", "-w"}); | |
| } | |
| bool Datapath::AddRedirectDnsRule(const ShillClient::Device& shill_device, | |
| std::string_view dns_ipv4_addr) { | |
| std::string_view ifname = shill_device.ifname; | |
| bool success = true; | |
| success &= RemoveRedirectDnsRule(shill_device); | |
| // Use Insert operation to ensure that the new DNS address is used first. | |
| success &= ModifyRedirectDnsDNATRule(Iptables::Command::kI, "tcp", ifname, | |
| dns_ipv4_addr); | |
| success &= ModifyRedirectDnsDNATRule(Iptables::Command::kI, "udp", ifname, | |
| dns_ipv4_addr); | |
| physical_dns_addresses_[ifname] = dns_ipv4_addr; | |
| return success; | |
| } | |
| bool Datapath::RemoveRedirectDnsRule(const ShillClient::Device& shill_device) { | |
| std::string_view ifname = shill_device.ifname; | |
| const auto it = physical_dns_addresses_.find(ifname); | |
| if (it == physical_dns_addresses_.end()) | |
| return true; | |
| bool success = true; | |
| success &= ModifyRedirectDnsDNATRule(Iptables::Command::kD, "tcp", ifname, | |
| it->second); | |
| success &= ModifyRedirectDnsDNATRule(Iptables::Command::kD, "udp", ifname, | |
| it->second); | |
| physical_dns_addresses_.erase(it); | |
| return success; | |
| } | |
| bool Datapath::ModifyRedirectDnsDNATRule(Iptables::Command op, | |
| std::string_view protocol, | |
| std::string_view ifname, | |
| std::string_view dns_ipv4_addr) { | |
| std::vector<std::string_view> args = { | |
| "-p", protocol, "--dport", "53", "-o", ifname, | |
| "-j", "DNAT", "--to-destination", dns_ipv4_addr, "-w"}; | |
| return ModifyIptables(IpFamily::kIPv4, Iptables::Table::kNat, op, | |
| kRedirectDnsChain, args); | |
| } | |
| bool Datapath::ModifyRedirectDnsJumpRule(IpFamily family, | |
| Iptables::Command op, | |
| std::string_view chain, | |
| std::string_view ifname, | |
| std::string_view target_chain, | |
| Fwmark mark, | |
| Fwmark mask, | |
| bool redirect_on_mark) { | |
| std::vector<std::string_view> args; | |
| if (!ifname.empty()) { | |
| args.insert(args.end(), {"-i", ifname}); | |
| } | |
| std::string mark_str = base::StrCat({mark.ToString(), "/", mask.ToString()}); | |
| if (mark.Value() != 0 && mask.Value() != 0) { | |
| args.insert(args.end(), {"-m", "mark"}); | |
| if (!redirect_on_mark) { | |
| args.push_back("!"); | |
| } | |
| args.insert(args.end(), {"--mark", mark_str}); | |
| } | |
| args.insert(args.end(), {"-j", target_chain, "-w"}); | |
| return ModifyIptables(family, Iptables::Table::kNat, op, chain, args); | |
| } | |
| bool Datapath::ModifyDnsProxyAcceptRule(IpFamily family, | |
| const DnsRedirectionRule& rule, | |
| Iptables::Command op) { | |
| std::string proxy_addr = rule.proxy_address.ToString(); | |
| std::vector<std::string_view> args = {"-d", proxy_addr, "-j", "ACCEPT", "-w"}; | |
| return ModifyIptables(family, Iptables::Table::kFilter, op, | |
| kAcceptEgressToDnsProxyChain, args); | |
| } | |
| bool Datapath::ModifyDnsRedirectionSkipVpnRule(IpFamily family, | |
| Iptables::Command op) { | |
| bool success = true; | |
| for (const auto& protocol : {"udp", "tcp"}) { | |
| std::vector<std::string_view> args = { | |
| "-p", protocol, "--dport", kDefaultDnsPort, "-j", "ACCEPT", "-w", | |
| }; | |
| if (!ModifyIptables(family, Iptables::Table::kMangle, op, | |
| kSkipApplyVpnMarkChain, args)) { | |
| success = false; | |
| } | |
| } | |
| return success; | |
| } | |
| bool Datapath::ModifyDnsExcludeDestinationRule(IpFamily family, | |
| const DnsRedirectionRule& rule, | |
| Iptables::Command op, | |
| std::string_view chain) { | |
| bool success = true; | |
| std::string proxy_addr = rule.proxy_address.ToString(); | |
| for (const auto& protocol : {"udp", "tcp"}) { | |
| std::vector<std::string_view> args = { | |
| "-p", protocol, "!", "-d", proxy_addr, | |
| "--dport", kDefaultDnsPort, "-j", "RETURN", "-w", | |
| }; | |
| if (!ModifyIptables(family, Iptables::Table::kNat, op, chain, args)) { | |
| success = false; | |
| } | |
| } | |
| return success; | |
| } | |
| bool Datapath::MaskInterfaceFlags(std::string_view ifname, | |
| uint16_t on, | |
| uint16_t off) { | |
| base::ScopedFD sock(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)); | |
| if (!sock.is_valid()) { | |
| PLOG(ERROR) << "Failed to create control socket"; | |
| return false; | |
| } | |
| struct ifreq ifr; | |
| FillInterfaceRequest(ifname, &ifr); | |
| if (system_->Ioctl(sock.get(), SIOCGIFFLAGS, &ifr) < 0) { | |
| PLOG(WARNING) << "ioctl() failed to get interface flag on " << ifname; | |
| return false; | |
| } | |
| ifr.ifr_flags |= on; | |
| ifr.ifr_flags &= ~off; | |
| if (system_->Ioctl(sock.get(), SIOCSIFFLAGS, &ifr) < 0) { | |
| PLOG(WARNING) << "ioctl() failed to set flag 0x" << std::hex << on | |
| << " unset flag 0x" << std::hex << off << " on " << ifname; | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool Datapath::AddIPv6HostRoute( | |
| std::string_view ifname, | |
| const net_base::IPv6CIDR& ipv6_cidr, | |
| const std::optional<net_base::IPv6Address>& src_addr) { | |
| if (src_addr) { | |
| return process_runner_->ip6("route", "replace", | |
| {ipv6_cidr.ToString(), "dev", ifname, "src", | |
| src_addr->ToString()}) == 0; | |
| } else { | |
| return process_runner_->ip6("route", "replace", | |
| {ipv6_cidr.ToString(), "dev", ifname}) == 0; | |
| } | |
| } | |
| void Datapath::RemoveIPv6HostRoute(const net_base::IPv6CIDR& ipv6_cidr) { | |
| process_runner_->ip6("route", "del", {ipv6_cidr.ToString()}); | |
| } | |
| bool Datapath::AddIPv6NeighborProxy(std::string_view ifname, | |
| const net_base::IPv6Address& ipv6_addr) { | |
| return process_runner_->ip6("neighbor", "add", | |
| {"proxy", ipv6_addr.ToString(), "dev", ifname}) == | |
| 0; | |
| } | |
| void Datapath::RemoveIPv6NeighborProxy(std::string_view ifname, | |
| const net_base::IPv6Address& ipv6_addr) { | |
| process_runner_->ip6("neighbor", "del", | |
| {"proxy", ipv6_addr.ToString(), "dev", ifname}); | |
| } | |
| bool Datapath::AddIPv6Address(std::string_view ifname, | |
| std::string_view ipv6_addr) { | |
| return process_runner_->ip6("addr", "add", {ipv6_addr, "dev", ifname}) == 0; | |
| } | |
| void Datapath::RemoveIPv6Address(std::string_view ifname, | |
| std::string_view ipv6_addr) { | |
| process_runner_->ip6("addr", "del", {ipv6_addr, "dev", ifname}); | |
| } | |
| void Datapath::StartConnectionPinning(const ShillClient::Device& shill_device) { | |
| auto batch_mode = process_runner_->AcquireIptablesBatchMode(); | |
| std::string_view ext_ifname = shill_device.ifname; | |
| int ifindex = system_->IfNametoindex(ext_ifname); | |
| if (ifindex == 0) { | |
| // Can happen if the interface has already been removed (b/183679000). | |
| LOG(ERROR) << "Failed to set up connection pinning on " << ext_ifname; | |
| return; | |
| } | |
| std::string subchain = base::StrCat({"POSTROUTING_", ext_ifname}); | |
| // This can fail if patchpanel did not stopped correctly or failed to cleanup | |
| // the chain when |ext_ifname| was previously deleted. | |
| if (!AddChain(IpFamily::kDual, Iptables::Table::kMangle, subchain)) { | |
| LOG(ERROR) << "Failed to create mangle chain " << subchain; | |
| } | |
| // Make sure the chain is empty if patchpanel did not cleaned correctly that | |
| // chain before. | |
| if (!FlushChain(IpFamily::kDual, Iptables::Table::kMangle, subchain)) { | |
| LOG(ERROR) << "Could not flush " << subchain; | |
| } | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kA, "POSTROUTING", subchain, | |
| /*iif=*/"", ext_ifname); | |
| auto routing_mark = Fwmark::FromIfIndex(ifindex); | |
| if (!routing_mark.has_value()) { | |
| LOG(ERROR) << "Failed to compute fwmark value of interface " << ext_ifname | |
| << " with index " << ifindex; | |
| return; | |
| } | |
| LOG(INFO) << "Start connection pinning on " << ext_ifname | |
| << " fwmark=" << routing_mark.value().ToString(); | |
| // Set in CONNMARK the routing tag associated with |ext_ifname|. | |
| if (!ModifyConnmarkSet(IpFamily::kDual, subchain, Iptables::Command::kA, | |
| routing_mark.value(), kFwmarkRoutingMask)) { | |
| LOG(ERROR) << "Could not start connection pinning on " << ext_ifname; | |
| } | |
| // Save in CONNMARK the source tag for egress traffic of this connection. | |
| if (!ModifyConnmarkSave(IpFamily::kDual, subchain, Iptables::Command::kA, | |
| kFwmarkAllSourcesMask)) { | |
| LOG(ERROR) << "Failed to add POSTROUTING CONNMARK rule for saving fwmark " | |
| "source tag on " | |
| << ext_ifname; | |
| } | |
| // Restore from CONNMARK the source tag for ingress traffic of this connection | |
| // (returned traffic). | |
| if (!ModifyConnmarkRestore(IpFamily::kDual, "PREROUTING", | |
| Iptables::Command::kA, ext_ifname, | |
| kFwmarkAllSourcesMask)) { | |
| LOG(ERROR) << "Could not setup fwmark source tagging rule for return " | |
| "traffic received on " | |
| << ext_ifname; | |
| } | |
| } | |
| void Datapath::StopConnectionPinning(const ShillClient::Device& shill_device) { | |
| std::string_view ext_ifname = shill_device.ifname; | |
| std::string subchain = base::StrCat({"POSTROUTING_", ext_ifname}); | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kD, "POSTROUTING", subchain, /*iif=*/"", | |
| ext_ifname); | |
| FlushChain(IpFamily::kDual, Iptables::Table::kMangle, subchain); | |
| RemoveChain(IpFamily::kDual, Iptables::Table::kMangle, subchain); | |
| if (!ModifyConnmarkRestore(IpFamily::kDual, "PREROUTING", | |
| Iptables::Command::kD, ext_ifname, | |
| kFwmarkAllSourcesMask)) { | |
| LOG(ERROR) << "Could not remove fwmark source tagging rule for return " | |
| "traffic received on " | |
| << ext_ifname; | |
| } | |
| } | |
| void Datapath::StartVpnRouting(const ShillClient::Device& vpn_device) { | |
| std::string_view vpn_ifname = vpn_device.ifname; | |
| int ifindex = system_->IfNametoindex(vpn_ifname); | |
| if (ifindex == 0) { | |
| // Can happen if the interface has already been removed (b/183679000). | |
| LOG(ERROR) << "Failed to start VPN routing on " << vpn_ifname; | |
| return; | |
| } | |
| auto routing_mark = Fwmark::FromIfIndex(ifindex); | |
| if (!routing_mark.has_value()) { | |
| LOG(ERROR) << "Failed to compute fwmark value of interface " << vpn_ifname | |
| << " with index " << ifindex; | |
| return; | |
| } | |
| LOG(INFO) << "Start VPN routing on " << vpn_ifname | |
| << " fwmark=" << routing_mark.value().ToString(); | |
| ModifyJumpRule(IpFamily::kIPv4, Iptables::Table::kNat, Iptables::Command::kA, | |
| "POSTROUTING", "MASQUERADE", | |
| /*iif=*/"", vpn_ifname); | |
| StartConnectionPinning(vpn_device); | |
| std::string mark = base::StrCat({"0x0/", kFwmarkRoutingMask.ToString()}); | |
| // Any traffic that already has a routing tag applied is accepted. | |
| if (!ModifyIptables( | |
| IpFamily::kDual, Iptables::Table::kMangle, Iptables::Command::kA, | |
| kApplyVpnMarkChain, | |
| {"-m", "mark", "!", "--mark", mark, "-j", "ACCEPT", "-w"})) { | |
| LOG(ERROR) << "Failed to add ACCEPT rule to VPN tagging chain for marked " | |
| "connections"; | |
| } | |
| // Otherwise, any new traffic from a new connection gets marked with the | |
| // VPN routing tag. | |
| if (!ModifyFwmarkRoutingTag(kApplyVpnMarkChain, Iptables::Command::kA, | |
| routing_mark.value())) | |
| LOG(ERROR) << "Failed to set up VPN set-mark rule for " << vpn_ifname; | |
| // When the VPN client runs on the host, also route arcbr0 to that VPN so | |
| // that ARC can access the VPN network through arc0. | |
| if (vpn_ifname != kArcbr0Ifname) { | |
| StartRoutingDevice(vpn_device, kArcbr0Ifname, TrafficSource::kArc); | |
| } | |
| if (!ModifyRedirectDnsJumpRule( | |
| IpFamily::kIPv4, Iptables::Command::kA, "OUTPUT", | |
| /*ifname=*/"", kRedirectDnsChain, kFwmarkRouteOnVpn, kFwmarkVpnMask, | |
| /*redirect_on_mark=*/false)) { | |
| LOG(ERROR) << "Failed to set jump rule to " << kRedirectDnsChain; | |
| } | |
| // All traffic with the VPN routing tag are explicitly accepted in the filter | |
| // table. This prevents the VPN lockdown chain to reject that traffic when VPN | |
| // lockdown is enabled. | |
| mark = base::StrCat( | |
| {routing_mark.value().ToString(), "/", kFwmarkRoutingMask.ToString()}); | |
| if (!ModifyIptables(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kVpnAcceptChain, | |
| {"-m", "mark", "--mark", mark, "-j", "ACCEPT", "-w"})) { | |
| LOG(ERROR) << "Failed to set filter rule for accepting VPN marked traffic"; | |
| } | |
| } | |
| void Datapath::StopVpnRouting(const ShillClient::Device& vpn_device) { | |
| std::string_view vpn_ifname = vpn_device.ifname; | |
| LOG(INFO) << "Stop VPN routing on " << vpn_ifname; | |
| if (!FlushChain(IpFamily::kDual, Iptables::Table::kFilter, kVpnAcceptChain)) { | |
| LOG(ERROR) << "Could not flush " << kVpnAcceptChain; | |
| } | |
| if (vpn_ifname != kArcbr0Ifname) { | |
| StopRoutingDevice(kArcbr0Ifname, kArc); | |
| } | |
| if (!FlushChain(IpFamily::kDual, Iptables::Table::kMangle, | |
| kApplyVpnMarkChain)) { | |
| LOG(ERROR) << "Could not flush " << kApplyVpnMarkChain; | |
| } | |
| StopConnectionPinning(vpn_device); | |
| ModifyJumpRule(IpFamily::kIPv4, Iptables::Table::kNat, Iptables::Command::kD, | |
| "POSTROUTING", "MASQUERADE", | |
| /*iif=*/"", vpn_ifname); | |
| if (!ModifyRedirectDnsJumpRule( | |
| IpFamily::kIPv4, Iptables::Command::kD, "OUTPUT", | |
| /*ifname=*/"", kRedirectDnsChain, kFwmarkRouteOnVpn, kFwmarkVpnMask, | |
| /*redirect_on_mark=*/false)) { | |
| LOG(ERROR) << "Failed to remove jump rule to " << kRedirectDnsChain; | |
| } | |
| } | |
| void Datapath::SetVpnLockdown(bool enable_vpn_lockdown) { | |
| if (enable_vpn_lockdown) { | |
| std::string mark = base::StrCat( | |
| {kFwmarkRouteOnVpn.ToString(), "/", kFwmarkVpnMask.ToString()}); | |
| if (!ModifyIptables(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kA, kVpnLockdownChain, | |
| {"-m", "mark", "--mark", mark, "-j", "REJECT", "-w"})) { | |
| LOG(ERROR) << "Failed to start VPN lockdown mode"; | |
| } | |
| } else { | |
| if (!FlushChain(IpFamily::kDual, Iptables::Table::kFilter, | |
| kVpnLockdownChain)) { | |
| LOG(ERROR) << "Failed to stop VPN lockdown mode"; | |
| } | |
| } | |
| } | |
| void Datapath::StartSourceIPv6PrefixEnforcement( | |
| const ShillClient::Device& shill_device) { | |
| VLOG(2) << __func__ << ": " << shill_device; | |
| std::string subchain = EgressSubChainName(shill_device.ifname); | |
| if (!AddChain(IpFamily::kIPv6, Iptables::Table::kFilter, subchain)) { | |
| LOG(ERROR) << __func__ << ": Failed to create chain " << subchain; | |
| return; | |
| } | |
| if (!ModifyJumpRule(IpFamily::kIPv6, Iptables::Table::kFilter, | |
| Iptables::Command::kI, "OUTPUT", subchain, | |
| /*iif=*/"", /*oif=*/shill_device.ifname)) { | |
| return; | |
| } | |
| // By default, immediately start jumping to "enforce_ipv6_src_prefix" to drop | |
| // traffic until the prefix RETURN rule is installed. | |
| UpdateSourceEnforcementIPv6Prefix(shill_device, std::nullopt); | |
| } | |
| void Datapath::StopSourceIPv6PrefixEnforcement( | |
| const ShillClient::Device& shill_device) { | |
| VLOG(2) << __func__ << ": " << shill_device; | |
| std::string subchain = EgressSubChainName(shill_device.ifname); | |
| if (!FlushChain(IpFamily::kIPv6, Iptables::Table::kFilter, subchain)) { | |
| LOG(ERROR) << __func__ << ": Failed to flush " << subchain; | |
| } | |
| if (!ModifyJumpRule(IpFamily::kIPv6, Iptables::Table::kFilter, | |
| Iptables::Command::kD, "OUTPUT", subchain, | |
| /*iif=*/"", /*oif=*/shill_device.ifname)) { | |
| return; | |
| } | |
| if (!RemoveChain(IpFamily::kIPv6, Iptables::Table::kFilter, subchain)) { | |
| LOG(ERROR) << __func__ << ": Failed to remove chain " << subchain; | |
| } | |
| } | |
| void Datapath::UpdateSourceEnforcementIPv6Prefix( | |
| const ShillClient::Device& shill_device, | |
| const std::optional<net_base::IPv6CIDR>& prefix) { | |
| VLOG(2) << __func__ << ": " << shill_device << ", {" | |
| << (prefix ? prefix->ToString() : "") << "}"; | |
| std::string subchain = EgressSubChainName(shill_device.ifname); | |
| if (!FlushChain(IpFamily::kIPv6, Iptables::Table::kFilter, subchain)) { | |
| LOG(ERROR) << __func__ << ": Failed to flush " << subchain; | |
| } | |
| if (prefix) { | |
| std::string prefix_str = prefix->ToString(); | |
| if (!ModifyIptables(IpFamily::kIPv6, Iptables::Table::kFilter, | |
| Iptables::Command::kA, subchain, | |
| {"-s", prefix_str, "-j", "RETURN", "-w"})) { | |
| LOG(ERROR) << __func__ << ": Failed to add " << prefix_str | |
| << " RETURN rule in " << subchain; | |
| } | |
| } | |
| ModifyJumpRule(IpFamily::kIPv6, Iptables::Table::kFilter, | |
| Iptables::Command::kA, subchain, kEnforceSourcePrefixChain, | |
| /*iif=*/"", /*oif=*/""); | |
| } | |
| bool Datapath::StartDownstreamNetwork(const DownstreamNetworkInfo& info) { | |
| if (info.topology == DownstreamNetworkTopology::kTethering && | |
| !info.upstream_device) { | |
| LOG(ERROR) << __func__ << " " << info << ": no upstream Device defined"; | |
| return false; | |
| } else if (info.topology == DownstreamNetworkTopology::kLocalOnly && | |
| info.upstream_device) { | |
| LOG(ERROR) << __func__ << " " << info | |
| << ": invalid upstream Device argument"; | |
| return false; | |
| } | |
| if (!ConfigureInterface(info.downstream_ifname, /*mac_addr=*/std::nullopt, | |
| info.ipv4_cidr, | |
| /*ipv6_cidr=*/std::nullopt, | |
| /*up=*/true, | |
| /*enable_multicast=*/true)) { | |
| LOG(ERROR) << __func__ << " " << info | |
| << ": Cannot configure downstream interface " | |
| << info.downstream_ifname; | |
| return false; | |
| } | |
| if (!ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kI, "OUTPUT", | |
| GetEgressFilterChainName(info.topology), | |
| /*iif=*/"", /*oif=*/info.downstream_ifname)) { | |
| return false; | |
| } | |
| if (!ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kI, kIngressDownstreamNetworkChain, | |
| GetIngressFilterChainName(info.topology), | |
| /*iif=*/info.downstream_ifname, /*oif=*/"")) { | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kD, "OUTPUT", | |
| GetEgressFilterChainName(info.topology), | |
| /*iif=*/"", /*oif=*/info.downstream_ifname); | |
| return false; | |
| } | |
| switch (info.topology) { | |
| case DownstreamNetworkTopology::kLocalOnly: | |
| // TODO(b:309710428): Replace StartRoutingDeviceAsSystem with local-only | |
| // routing mode to prevent forwarding to an external physical or virtual | |
| // network. | |
| StartRoutingDeviceAsSystem(info.downstream_ifname, | |
| info.GetTrafficSource(), | |
| /*static_ipv6=*/false); | |
| break; | |
| case DownstreamNetworkTopology::kTethering: | |
| // int_ipv4_addr is not necessary if route_on_vpn == false | |
| StartRoutingDevice(*info.upstream_device, info.downstream_ifname, | |
| info.GetTrafficSource()); | |
| break; | |
| } | |
| return true; | |
| } | |
| void Datapath::StopDownstreamNetwork(const DownstreamNetworkInfo& info) { | |
| if (info.topology == DownstreamNetworkTopology::kTethering && | |
| !info.upstream_device) { | |
| LOG(ERROR) << __func__ << " " << info << ": no upstream Device defined"; | |
| return; | |
| } else if (info.topology == DownstreamNetworkTopology::kLocalOnly && | |
| info.upstream_device) { | |
| LOG(ERROR) << __func__ << " " << info | |
| << ": invalid upstream Device argument"; | |
| return; | |
| } | |
| // Skip unconfiguring the downstream interface: shill will either destroy it | |
| // or flip it back to client mode and restart a Network on top. | |
| StopRoutingDevice(info.downstream_ifname, info.GetTrafficSource()); | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kD, "OUTPUT", | |
| GetEgressFilterChainName(info.topology), | |
| /*iif=*/"", /*oif=*/info.downstream_ifname); | |
| ModifyJumpRule(IpFamily::kDual, Iptables::Table::kFilter, | |
| Iptables::Command::kD, kIngressDownstreamNetworkChain, | |
| GetIngressFilterChainName(info.topology), | |
| /*iif=*/info.downstream_ifname, /*oif=*/""); | |
| } | |
| bool Datapath::ModifyConnmarkSet(IpFamily family, | |
| std::string_view chain, | |
| Iptables::Command op, | |
| Fwmark mark, | |
| Fwmark mask) { | |
| std::string mark_str = base::StrCat({mark.ToString(), "/", mask.ToString()}); | |
| return ModifyIptables(family, Iptables::Table::kMangle, op, chain, | |
| {"-j", "CONNMARK", "--set-mark", mark_str, "-w"}); | |
| } | |
| bool Datapath::ModifyConnmarkRestore(IpFamily family, | |
| std::string_view chain, | |
| Iptables::Command op, | |
| std::string_view iif, | |
| Fwmark mask, | |
| bool skip_on_non_empty_mark) { | |
| std::vector<std::string_view> args; | |
| if (!iif.empty()) { | |
| args.insert(args.end(), {"-i", iif}); | |
| } | |
| std::string mark_str; | |
| std::string mask_str = mask.ToString(); | |
| if (skip_on_non_empty_mark) { | |
| mark_str = base::StrCat({"0x0/", mask_str}); | |
| args.insert(args.end(), {"-m", "mark", "--mark", mark_str}); | |
| } | |
| args.insert(args.end(), | |
| {"-j", "CONNMARK", "--restore-mark", "--mask", mask_str, "-w"}); | |
| return ModifyIptables(family, Iptables::Table::kMangle, op, chain, args); | |
| } | |
| bool Datapath::ModifyConnmarkSave(IpFamily family, | |
| std::string_view chain, | |
| Iptables::Command op, | |
| Fwmark mask) { | |
| std::string mask_str = mask.ToString(); | |
| std::vector<std::string_view> args = {"-j", "CONNMARK", "--save-mark", | |
| "--mask", mask_str, "-w"}; | |
| return ModifyIptables(family, Iptables::Table::kMangle, op, chain, args); | |
| } | |
| bool Datapath::ModifyFwmarkRoutingTag(std::string_view chain, | |
| Iptables::Command op, | |
| Fwmark routing_mark) { | |
| return ModifyFwmark(IpFamily::kDual, chain, op, /*int_ifname=*/"", | |
| /*uid_name=*/"", /*classid=*/0, routing_mark, | |
| kFwmarkRoutingMask); | |
| } | |
| bool Datapath::ModifyFwmarkSourceTag(std::string_view chain, | |
| Iptables::Command op, | |
| TrafficSource source) { | |
| return ModifyFwmark(IpFamily::kDual, chain, op, /*iif=*/"", /*uid_name=*/"", | |
| /*classid=*/0, Fwmark::FromSource(source), | |
| kFwmarkAllSourcesMask); | |
| } | |
| bool Datapath::ModifyFwmark(IpFamily family, | |
| std::string_view chain, | |
| Iptables::Command op, | |
| std::string_view iif, | |
| std::string_view uid_name, | |
| uint32_t classid, | |
| Fwmark mark, | |
| Fwmark mask, | |
| bool log_failures) { | |
| std::vector<std::string_view> args; | |
| if (!iif.empty()) { | |
| args.insert(args.end(), {"-i", iif}); | |
| } | |
| if (!uid_name.empty()) { | |
| args.insert(args.end(), {"-m", "owner", "--uid-owner", uid_name}); | |
| } | |
| std::string id = base::StringPrintf("0x%08x", classid); | |
| if (classid != 0) { | |
| args.insert(args.end(), {"-m", "cgroup", "--cgroup", id}); | |
| } | |
| std::string mask_str = base::StrCat({mark.ToString(), "/", mask.ToString()}); | |
| args.insert(args.end(), {"-j", "MARK", "--set-mark", mask_str, "-w"}); | |
| return ModifyIptables(family, Iptables::Table::kMangle, op, chain, args, | |
| log_failures); | |
| } | |
| bool Datapath::ModifyJumpRule(IpFamily family, | |
| Iptables::Table table, | |
| Iptables::Command op, | |
| std::string_view chain, | |
| std::string_view target, | |
| std::string_view iif, | |
| std::string_view oif, | |
| bool log_failures) { | |
| std::vector<std::string_view> args; | |
| if (!iif.empty()) { | |
| args.insert(args.end(), {"-i", iif}); | |
| } | |
| if (!oif.empty()) { | |
| args.insert(args.end(), {"-o", oif}); | |
| } | |
| args.insert(args.end(), {"-j", target, "-w"}); | |
| if (!ModifyIptables(family, table, op, chain, args, log_failures)) { | |
| if (log_failures) { | |
| LOG(ERROR) << __func__ << " failure: " << family << " -t " << table << " " | |
| << op << " " << chain | |
| << (iif.empty() ? "" : base::StrCat({" -i ", iif})) | |
| << (oif.empty() ? "" : base::StrCat({" -o ", oif})) << " -j " | |
| << target; | |
| } | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool Datapath::ModifyFwmarkVpnJumpRule(std::string_view chain, | |
| Iptables::Command op, | |
| Fwmark mark, | |
| Fwmark mask) { | |
| std::vector<std::string_view> args; | |
| std::string mark_str = base::StrCat({mark.ToString(), "/", mask.ToString()}); | |
| if (mark.Value() != 0 && mask.Value() != 0) { | |
| args.insert(args.end(), {"-m", "mark", "--mark", mark_str}); | |
| } | |
| args.insert(args.end(), {"-j", kApplyVpnMarkChain, "-w"}); | |
| return ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, op, chain, | |
| args); | |
| } | |
| bool Datapath::CheckChain(IpFamily family, | |
| Iptables::Table table, | |
| std::string_view name) { | |
| return ModifyChain(family, table, Iptables::Command::kC, name, | |
| /*log_failures=*/false); | |
| } | |
| bool Datapath::AddChain(IpFamily family, | |
| Iptables::Table table, | |
| std::string_view chain) { | |
| DCHECK(chain.size() <= kIptablesMaxChainLength) | |
| << "chain name " << chain << " is longer than " | |
| << kIptablesMaxChainLength; | |
| return ModifyChain(family, table, Iptables::Command::kN, chain); | |
| } | |
| bool Datapath::RemoveChain(IpFamily family, | |
| Iptables::Table table, | |
| std::string_view chain) { | |
| return ModifyChain(family, table, Iptables::Command::kX, chain); | |
| } | |
| bool Datapath::FlushChain(IpFamily family, | |
| Iptables::Table table, | |
| std::string_view chain) { | |
| return ModifyChain(family, table, Iptables::Command::kF, chain); | |
| } | |
| bool Datapath::ModifyChain(IpFamily family, | |
| Iptables::Table table, | |
| Iptables::Command command, | |
| std::string_view chain, | |
| bool log_failures) { | |
| return ModifyIptables(family, table, command, chain, {"-w"}, log_failures); | |
| } | |
| bool Datapath::ModifyIptables(IpFamily family, | |
| Iptables::Table table, | |
| Iptables::Command command, | |
| std::string_view chain, | |
| const std::vector<std::string_view>& argv, | |
| bool log_failures) { | |
| bool success = true; | |
| if (family == IpFamily::kIPv4 || family == IpFamily::kDual) { | |
| // TODO(b/325359902): Change |argv| to span type and delete conversion. | |
| success &= process_runner_->iptables( | |
| table, command, chain, | |
| const_cast<std::vector<std::string_view>&>(argv), | |
| log_failures) == 0; | |
| } | |
| if (family == IpFamily::kIPv6 || family == IpFamily::kDual) { | |
| // TODO(b/325359902): Change |argv| to span type and delete conversion. | |
| success &= process_runner_->ip6tables( | |
| table, command, chain, | |
| const_cast<std::vector<std::string_view>&>(argv), | |
| log_failures) == 0; | |
| } | |
| return success; | |
| } | |
| std::string Datapath::DumpIptables(IpFamily family, Iptables::Table table) { | |
| std::string result; | |
| std::vector<std::string_view> argv = {"-x", "-v", "-n", "-w"}; | |
| switch (family) { | |
| case IpFamily::kIPv4: | |
| if (process_runner_->iptables(table, Iptables::Command::kL, /*chain=*/"", | |
| argv, | |
| /*log_failures=*/true, &result) != 0) { | |
| LOG(ERROR) << "Could not dump iptables " << table; | |
| } | |
| break; | |
| case IpFamily::kIPv6: | |
| if (process_runner_->ip6tables(table, Iptables::Command::kL, /*chain=*/"", | |
| argv, | |
| /*log_failures=*/true, &result) != 0) { | |
| LOG(ERROR) << "Could not dump ip6tables " << table; | |
| } | |
| break; | |
| case IpFamily::kDual: | |
| LOG(ERROR) << "Cannot dump iptables and ip6tables at the same time"; | |
| break; | |
| } | |
| return result; | |
| } | |
| bool Datapath::AddIPv4RouteToTable(std::string_view ifname, | |
| const net_base::IPv4CIDR& ipv4_cidr, | |
| int table_id) { | |
| return process_runner_->ip("route", "add", | |
| {ipv4_cidr.ToString(), "dev", ifname, "table", | |
| base::NumberToString(table_id)}) == 0; | |
| } | |
| void Datapath::DeleteIPv4RouteFromTable(std::string_view ifname, | |
| const net_base::IPv4CIDR& ipv4_cidr, | |
| int table_id) { | |
| process_runner_->ip("route", "del", | |
| {ipv4_cidr.ToString(), "dev", ifname, "table", | |
| base::NumberToString(table_id)}); | |
| } | |
| bool Datapath::AddIPv4Route(const IPv4Address& gateway_addr, | |
| const IPv4CIDR& subnet_cidr) { | |
| struct rtentry route; | |
| memset(&route, 0, sizeof(route)); | |
| SetSockaddrIn(&route.rt_gateway, gateway_addr); | |
| SetSockaddrIn(&route.rt_dst, subnet_cidr.GetPrefixCIDR().address()); | |
| SetSockaddrIn(&route.rt_genmask, subnet_cidr.ToNetmask()); | |
| route.rt_flags = RTF_UP | RTF_GATEWAY; | |
| return ModifyIPv4Rtentry(SIOCADDRT, &route); | |
| } | |
| bool Datapath::DeleteIPv4Route(const IPv4Address& gateway_addr, | |
| const IPv4CIDR& subnet_cidr) { | |
| struct rtentry route; | |
| memset(&route, 0, sizeof(route)); | |
| SetSockaddrIn(&route.rt_gateway, gateway_addr); | |
| SetSockaddrIn(&route.rt_dst, subnet_cidr.GetPrefixCIDR().address()); | |
| SetSockaddrIn(&route.rt_genmask, subnet_cidr.ToNetmask()); | |
| route.rt_flags = RTF_UP | RTF_GATEWAY; | |
| return ModifyIPv4Rtentry(SIOCDELRT, &route); | |
| } | |
| bool Datapath::AddIPv6Route(const IPv6Address& gateway_addr, | |
| const IPv6CIDR& subnet_cidr) { | |
| struct in6_rtmsg route; | |
| memset(&route, 0, sizeof(route)); | |
| route.rtmsg_gateway = gateway_addr.ToIn6Addr(); | |
| route.rtmsg_dst = subnet_cidr.GetPrefixCIDR().address().ToIn6Addr(); | |
| route.rtmsg_dst_len = static_cast<uint16_t>(subnet_cidr.prefix_length()); | |
| route.rtmsg_flags = RTF_UP | RTF_GATEWAY; | |
| return ModifyIPv6Rtentry(SIOCADDRT, &route); | |
| } | |
| bool Datapath::DeleteIPv6Route(const IPv6Address& gateway_addr, | |
| const IPv6CIDR& subnet_cidr) { | |
| struct in6_rtmsg route; | |
| memset(&route, 0, sizeof(route)); | |
| route.rtmsg_gateway = gateway_addr.ToIn6Addr(); | |
| route.rtmsg_dst = subnet_cidr.GetPrefixCIDR().address().ToIn6Addr(); | |
| route.rtmsg_dst_len = static_cast<uint16_t>(subnet_cidr.prefix_length()); | |
| route.rtmsg_flags = RTF_UP | RTF_GATEWAY; | |
| return ModifyIPv6Rtentry(SIOCDELRT, &route); | |
| } | |
| bool Datapath::ModifyIPv4Rtentry(ioctl_req_t op, struct rtentry* route) { | |
| DCHECK(route); | |
| if (op != SIOCADDRT && op != SIOCDELRT) { | |
| LOG(ERROR) << "Invalid operation " << op << " for rtentry " << *route; | |
| return false; | |
| } | |
| base::ScopedFD fd(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)); | |
| if (!fd.is_valid()) { | |
| PLOG(ERROR) << "Failed to create socket for adding rtentry " << *route; | |
| return false; | |
| } | |
| if (HANDLE_EINTR(system_->Ioctl(fd.get(), op, route)) != 0) { | |
| // b/190119762: Ignore "No such process" errors when deleting a struct | |
| // rtentry if some other prior or concurrent operation already resulted in | |
| // this route being deleted. | |
| if (op == SIOCDELRT && errno == ESRCH) { | |
| return true; | |
| } | |
| std::string opname = op == SIOCADDRT ? "add" : "delete"; | |
| PLOG(ERROR) << "Failed to " << opname << " rtentry " << *route; | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool Datapath::ModifyIPv6Rtentry(ioctl_req_t op, struct in6_rtmsg* route) { | |
| DCHECK(route); | |
| if (op != SIOCADDRT && op != SIOCDELRT) { | |
| LOG(ERROR) << "Invalid operation " << op << " for rtentry " << *route; | |
| return false; | |
| } | |
| base::ScopedFD fd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)); | |
| if (!fd.is_valid()) { | |
| PLOG(ERROR) << "Failed to create socket for adding rtentry " << *route; | |
| return false; | |
| } | |
| if (HANDLE_EINTR(system_->Ioctl(fd.get(), op, route)) != 0) { | |
| // b/190119762: Ignore "No such process" errors when deleting a struct | |
| // rtentry if some other prior or concurrent operation already resulted in | |
| // this route being deleted. | |
| if (op == SIOCDELRT && errno == ESRCH) { | |
| return true; | |
| } | |
| std::string opname = op == SIOCADDRT ? "add" : "delete"; | |
| PLOG(ERROR) << "Failed to " << opname << " rtentry " << *route; | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool Datapath::AddAdbPortForwardRule(std::string_view ifname) { | |
| return firewall_->AddIpv4ForwardRule(patchpanel::ModifyPortRuleRequest::TCP, | |
| kArcAddr, kAdbServerPort, ifname, | |
| kLocalhostAddr, kAdbProxyTcpListenPort); | |
| } | |
| void Datapath::DeleteAdbPortForwardRule(std::string_view ifname) { | |
| firewall_->DeleteIpv4ForwardRule(patchpanel::ModifyPortRuleRequest::TCP, | |
| kArcAddr, kAdbServerPort, ifname, | |
| kLocalhostAddr, kAdbProxyTcpListenPort); | |
| } | |
| bool Datapath::AddAdbPortAccessRule(std::string_view ifname) { | |
| return firewall_->AddAcceptRules(patchpanel::ModifyPortRuleRequest::TCP, | |
| kAdbProxyTcpListenPort, ifname); | |
| } | |
| void Datapath::DeleteAdbPortAccessRule(std::string_view ifname) { | |
| firewall_->DeleteAcceptRules(patchpanel::ModifyPortRuleRequest::TCP, | |
| kAdbProxyTcpListenPort, ifname); | |
| } | |
| bool Datapath::SetConntrackHelpers(const bool enable_helpers) { | |
| return system_->SysNetSet(System::SysNet::kConntrackHelper, | |
| enable_helpers ? "1" : "0"); | |
| } | |
| bool Datapath::SetRouteLocalnet(std::string_view ifname, const bool enable) { | |
| return system_->SysNetSet(System::SysNet::kIPv4RouteLocalnet, | |
| enable ? "1" : "0", ifname); | |
| } | |
| bool Datapath::ModprobeAll(const std::vector<std::string>& modules) { | |
| return process_runner_->modprobe_all(modules) == 0; | |
| } | |
| bool Datapath::ModifyPortRule( | |
| const patchpanel::ModifyPortRuleRequest& request) { | |
| switch (request.proto()) { | |
| case patchpanel::ModifyPortRuleRequest::TCP: | |
| case patchpanel::ModifyPortRuleRequest::UDP: | |
| break; | |
| default: | |
| LOG(ERROR) << "Unknown protocol " << request.proto(); | |
| return false; | |
| } | |
| if (request.input_dst_port() > UINT16_MAX) { | |
| LOG(ERROR) << "Invalid matching destination port " | |
| << request.input_dst_port(); | |
| return false; | |
| } | |
| if (request.dst_port() > UINT16_MAX) { | |
| LOG(ERROR) << "Invalid forwarding destination port " << request.dst_port(); | |
| return false; | |
| } | |
| const auto input_dst_ip = | |
| net_base::IPv4Address::CreateFromString(request.input_dst_ip()); | |
| if (!request.input_dst_ip().empty() && !input_dst_ip) { | |
| LOG(ERROR) << "Invalid input destination ip: " << request.input_dst_ip(); | |
| return false; | |
| } | |
| const auto dst_ip = net_base::IPv4Address::CreateFromString(request.dst_ip()); | |
| if (request.type() == patchpanel::ModifyPortRuleRequest::FORWARDING && | |
| !dst_ip) { | |
| LOG(ERROR) << "Invalid forwarding destination address: " | |
| << request.dst_ip(); | |
| return false; | |
| } | |
| uint16_t input_dst_port = static_cast<uint16_t>(request.input_dst_port()); | |
| uint16_t dst_port = static_cast<uint16_t>(request.dst_port()); | |
| switch (request.op()) { | |
| case patchpanel::ModifyPortRuleRequest::CREATE: | |
| switch (request.type()) { | |
| case patchpanel::ModifyPortRuleRequest::ACCESS: { | |
| return firewall_->AddAcceptRules(request.proto(), input_dst_port, | |
| request.input_ifname()); | |
| } | |
| case patchpanel::ModifyPortRuleRequest::LOCKDOWN: | |
| return firewall_->AddLoopbackLockdownRules(request.proto(), | |
| input_dst_port); | |
| case patchpanel::ModifyPortRuleRequest::FORWARDING: | |
| return firewall_->AddIpv4ForwardRule( | |
| request.proto(), input_dst_ip, input_dst_port, | |
| request.input_ifname(), *dst_ip, dst_port); | |
| default: | |
| LOG(ERROR) << "Unknown port rule type " << request.type(); | |
| return false; | |
| } | |
| case patchpanel::ModifyPortRuleRequest::DELETE: | |
| switch (request.type()) { | |
| case patchpanel::ModifyPortRuleRequest::ACCESS: | |
| return firewall_->DeleteAcceptRules(request.proto(), input_dst_port, | |
| request.input_ifname()); | |
| case patchpanel::ModifyPortRuleRequest::LOCKDOWN: | |
| return firewall_->DeleteLoopbackLockdownRules(request.proto(), | |
| input_dst_port); | |
| case patchpanel::ModifyPortRuleRequest::FORWARDING: | |
| return firewall_->DeleteIpv4ForwardRule( | |
| request.proto(), input_dst_ip, input_dst_port, | |
| request.input_ifname(), *dst_ip, dst_port); | |
| default: | |
| LOG(ERROR) << "Unknown port rule type " << request.type(); | |
| return false; | |
| } | |
| default: | |
| LOG(ERROR) << "Unknown operation " << request.op(); | |
| return false; | |
| } | |
| } | |
| void Datapath::EnableQoSDetection() { | |
| ModifyQoSDetectJumpRule(Iptables::Command::kA); | |
| } | |
| void Datapath::DisableQoSDetection() { | |
| ModifyQoSDetectJumpRule(Iptables::Command::kD); | |
| } | |
| void Datapath::EnableQoSApplyingDSCP(std::string_view ifname) { | |
| LOG(INFO) << "Enable QoS DSCP application on " << ifname; | |
| ModifyQoSApplyDSCPJumpRule(Iptables::Command::kA, ifname); | |
| } | |
| void Datapath::DisableQoSApplyingDSCP(std::string_view ifname) { | |
| LOG(INFO) << "Disable QoS DSCP application on " << ifname; | |
| ModifyQoSApplyDSCPJumpRule(Iptables::Command::kD, ifname); | |
| } | |
| void Datapath::ModifyQoSDetectJumpRule(Iptables::Command command) { | |
| ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, command, | |
| kQoSDetectStaticChain, {"-j", kQoSDetectChain, "-w"}); | |
| } | |
| void Datapath::ModifyQoSApplyDSCPJumpRule(Iptables::Command command, | |
| std::string_view ifname) { | |
| ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, command, | |
| "POSTROUTING", {"-o", ifname, "-j", kQoSApplyDSCPChain, "-w"}); | |
| } | |
| void Datapath::AddBorealisQoSRule(std::string_view ifname) { | |
| std::string mark = QoSFwmarkWithMask(QoSCategory::kRealTimeInteractive); | |
| ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kA, kQoSDetectBorealisChain, | |
| {"-i", ifname, "-j", "MARK", "--set-xmark", mark, "-w"}); | |
| } | |
| void Datapath::RemoveBorealisQoSRule(std::string_view ifname) { | |
| std::string mark = QoSFwmarkWithMask(QoSCategory::kRealTimeInteractive); | |
| ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kD, kQoSDetectBorealisChain, | |
| {"-i", ifname, "-j", "MARK", "--set-xmark", mark, "-w"}); | |
| } | |
| void Datapath::UpdateDoHProvidersForQoS( | |
| IpFamily family, const std::vector<net_base::IPAddress>& doh_provider_ips) { | |
| // Clear all the rules for the previous DoH providers. | |
| FlushChain(family, Iptables::Table::kMangle, kQoSDetectDoHChain); | |
| if (doh_provider_ips.empty()) { | |
| return; | |
| } | |
| // Get the string format of the IP addresses. | |
| std::vector<std::string> ip_strs; | |
| ip_strs.reserve(doh_provider_ips.size()); | |
| for (const auto& ip : doh_provider_ips) { | |
| ip_strs.push_back(ip.ToString()); | |
| } | |
| // Mark all the TCP and UDP traffic to the 443 port of the DoH servers. This | |
| // may have false positives if the server is also used for non-DNS HTTPS | |
| // traffic. | |
| std::string ip = base::JoinString(ip_strs, ","); | |
| std::string mark = QoSFwmarkWithMask(QoSCategory::kNetworkControl); | |
| for (std::string_view protocol : {"udp", "tcp"}) { | |
| ModifyIptables(family, Iptables::Table::kMangle, Iptables::Command::kA, | |
| kQoSDetectDoHChain, | |
| {"-p", protocol, "--dport", "443", "-d", ip, "-j", "MARK", | |
| "--set-xmark", mark, "-w"}); | |
| } | |
| } | |
| bool Datapath::ModifyIsolatedGuestDropRule(Iptables::Command command, | |
| std::string_view ifname) { | |
| bool success = true; | |
| success &= ModifyIptables(IpFamily::kDual, Iptables::Table::kFilter, command, | |
| kDropForwardToBruschettaChain, | |
| {"-o", ifname, "-j", "DROP", "-w"}); | |
| success &= ModifyIptables( | |
| IpFamily::kDual, Iptables::Table::kFilter, command, | |
| kDropOutputToBruschettaChain, | |
| {"-m", "state", "--state", "NEW", "-o", ifname, "-j", "DROP", "-w"}); | |
| return success; | |
| } | |
| bool Datapath::ModifyClatAcceptRules(Iptables::Command command, | |
| std::string_view ifname) { | |
| bool success = true; | |
| success &= ModifyJumpRule(IpFamily::kIPv6, Iptables::Table::kFilter, command, | |
| "FORWARD", "ACCEPT", /*iif=*/ifname, /*oif=*/""); | |
| success &= ModifyJumpRule(IpFamily::kIPv6, Iptables::Table::kFilter, command, | |
| "FORWARD", "ACCEPT", /*iif=*/"", /*oif=*/ifname); | |
| return success; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, | |
| const ConnectedNamespace& nsinfo) { | |
| stream << "{ pid: " << nsinfo.pid | |
| << ", source: " << TrafficSourceName(nsinfo.source); | |
| if (!nsinfo.outbound_ifname.empty()) { | |
| stream << ", outbound_ifname: " << nsinfo.outbound_ifname; | |
| } | |
| stream << ", route_on_vpn: " << nsinfo.route_on_vpn | |
| << ", host_ifname: " << nsinfo.host_ifname | |
| << ", peer_ifname: " << nsinfo.peer_ifname | |
| << ", peer_ipv4_subnet: " << nsinfo.peer_ipv4_subnet->base_cidr(); | |
| if (nsinfo.static_ipv6_config.has_value()) { | |
| stream << ", static_ipv6_subnet: " | |
| << nsinfo.static_ipv6_config->host_cidr.GetPrefixCIDR(); | |
| } | |
| stream << '}'; | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const DnsRedirectionRule& rule) { | |
| stream << "{ type: " | |
| << SetDnsRedirectionRuleRequest::RuleType_Name(rule.type); | |
| if (!rule.input_ifname.empty()) { | |
| stream << ", input_ifname: " << rule.input_ifname; | |
| } | |
| stream << ", proxy_address: " << rule.proxy_address; | |
| if (!rule.nameservers.empty()) { | |
| stream << ", nameserver(s): "; | |
| for (const auto& nameserver : rule.nameservers) { | |
| stream << nameserver << ","; | |
| } | |
| } | |
| stream << " }"; | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, IpFamily family) { | |
| switch (family) { | |
| case IpFamily::kIPv4: | |
| return stream << "IPv4"; | |
| case IpFamily::kIPv6: | |
| return stream << "IPv6"; | |
| case IpFamily::kDual: | |
| return stream << "IPv4v6"; | |
| } | |
| } | |
| } // namespace patchpanel |