| # Copyright 2024 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """A daemon for hiding Ethernet while still enabling the SSH connection.""" | |
| from __future__ import annotations | |
| import datetime | |
| import enum | |
| import glob | |
| import json | |
| import logging | |
| import os | |
| import pathlib | |
| import shlex | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| from typing import List, NoReturn, Optional | |
| import psutil | |
| from . import daemon | |
| class Approach(enum.Enum): | |
| """An enum class for the ehide approaches.""" | |
| FORWARDING = "forwarding" | |
| SHELL = "shell" | |
| @classmethod | |
| def from_str(cls, s: str) -> Optional[Approach]: | |
| """Creates an Approach enum from string. | |
| Args: | |
| s: The string. | |
| Returns: | |
| An Approach enum. If fails then return None. | |
| """ | |
| for approach in Approach: | |
| if approach.value == s: | |
| return approach | |
| return None | |
| class IpFamily(enum.Enum): | |
| """An enum class for different IP families (IPv4 and IPv6).""" | |
| IPv4 = 4 | |
| IPv6 = 6 | |
| EHIDE_DIR = "/run/ehide" | |
| SSHD_PATH = "/usr/sbin/sshd" | |
| THIS_FILE = pathlib.Path(__file__).resolve() | |
| THIS_DIR = THIS_FILE.parent | |
| FORCE_COMMAND_PATH = THIS_DIR / "force_cmd.sh" | |
| DHCLIENT_SCRIPT_PATH = "/usr/local/sbin/dhclient-script" | |
| RECOVER_DUTS_SERVICE_NAME = "recover_duts" | |
| # The maximum time to wait for a process to exit gracefully. | |
| GRACEFUL_EXIT_TIMEOUT = datetime.timedelta(seconds=5) | |
| # The maximum time to wait for an interface to appear in the root netns. | |
| INTERFACE_APPEARANCE_TIMEOUT = datetime.timedelta(seconds=15) | |
| # The maximum time to wait for an IP address to be set. | |
| IP_SET_TIMEOUT = datetime.timedelta(seconds=10) | |
| # The maximum time to wait for socat to start listening on port:22 (only in the | |
| # forwarding approach). | |
| SOCAT_STARTUP_TIMEOUT = datetime.timedelta(seconds=5) | |
| def run(*args: str) -> subprocess.CompletedProcess: | |
| """Runs a command with subprocess.run() and check=False. | |
| This function does not capture the output. | |
| Args: | |
| *args: The command to run. | |
| Returns: | |
| A subprocess.CompletedProcess instance that is returned by | |
| subprocess.run(). | |
| """ | |
| logging.info("> %s", shlex.join(args)) | |
| return subprocess.run(args, check=False) | |
| def run_output(*args: str) -> str: | |
| """Captures the output from subprocess.run() and check=False. | |
| Args: | |
| *args: The command to run. | |
| Returns: | |
| A string that is the command output. | |
| """ | |
| return subprocess.run( | |
| args, check=False, capture_output=True, encoding="utf-8" | |
| ).stdout | |
| def check_service_running(service_name: str) -> bool: | |
| """Checks whether an init.d service is running. | |
| Args: | |
| service_name: The name of the service. | |
| Returns: | |
| A bool that indicates whether the service is running. | |
| """ | |
| output = run_output("status", service_name) | |
| return "running" in output | |
| def start_service(service_name: str) -> None: | |
| """Starts an init.d service. | |
| Args: | |
| service_name: The name of the service. | |
| """ | |
| run("start", service_name) | |
| def stop_service(service_name: str) -> None: | |
| """Stops an init.d service. | |
| Args: | |
| service_name: The name of the service. | |
| """ | |
| run("stop", service_name) | |
| def run_ip_link_show( | |
| ifname: str, netns_name: Optional[str] = None, pid: Optional[int] = None | |
| ) -> str: | |
| """Runs "ip link show" command for the given interface in the given netns. | |
| "-j" option is used for parsing. | |
| The network namespace can be specified either by the |netns_name| or the | |
| |pid|. | |
| If both |netns_name| and |pid| is assigned, then only the |netns_name| will | |
| be used. | |
| If neither of the |netns_name| or |pid| is assigned, then it defaults to | |
| return "ip link show" in the root network namespace. | |
| Args: | |
| ifname: The interface name. | |
| netns_name: The network namespace name. | |
| pid: Specifies the network namespace where the process |pid| runs. Only | |
| effective if it is not None and |netns_name| is None. | |
| Returns: | |
| The string output of the "ip link show" command. | |
| """ | |
| options = [] | |
| prefix = [] | |
| if netns_name: | |
| options = ["-n", netns_name] | |
| elif pid: | |
| prefix = ["nsenter", "-t", str(pid), "-n"] | |
| options += ["-j"] | |
| cmd = prefix + ["ip"] + options + ["link", "show", "dev", ifname] | |
| return run_output(*cmd) | |
| def get_mac_address_in_netns( | |
| ifname: str, netns_name: Optional[str] = None, pid: Optional[int] = None | |
| ) -> str: | |
| """Gets the MAC address of the given interface in the given netns. | |
| The network namespace can be specified either by the |netns_name| or the | |
| |pid|. | |
| If both |netns_name| and |pid| is assigned, then only the |netns_name| will | |
| be used. | |
| If neither of the |netns_name| or |pid| is assigned, then it defaults to | |
| return the MAC address in the root network namespace. | |
| Args: | |
| ifname: The interface name. | |
| netns_name: The network namespace name. | |
| pid: Specifies the network namespace where the process |pid| runs. Only | |
| effective if it is not None and |netns_name| is None. | |
| Returns: | |
| A string of the MAC address. | |
| """ | |
| ip_output = run_ip_link_show(ifname, netns_name, pid) | |
| if not ip_output: | |
| return "" | |
| try: | |
| ip_dict = json.loads(ip_output) | |
| except json.JSONDecodeError as e: | |
| logging.warning("Error in parsing %s: %s", ip_output, e) | |
| return "" | |
| try: | |
| mac = ip_dict[0]["address"] | |
| except (IndexError, KeyError) as e: | |
| logging.warning( | |
| "Error in getting the MAC address from %s: %s", ip_output, e | |
| ) | |
| return "" | |
| return mac | |
| def get_proc_name(pid: int) -> str: | |
| """Gets process name with |pid|. | |
| Args: | |
| pid: The process pid. | |
| Returns: | |
| The process name. Returns "" if the process does not exist. | |
| """ | |
| return run_output("ps", "-p", str(pid), "-o", "comm=").strip() | |
| def terminate_pid(pid: int) -> None: | |
| """Terminates process of |pid|. | |
| Tries to terminate gracefully first. If the process is still running after 1 | |
| second, then kills it by force. | |
| Args: | |
| pid: The process pid. | |
| """ | |
| if not psutil.pid_exists(pid): | |
| logging.warning("Pid %d is not running!", pid) | |
| return | |
| try: | |
| # Attempt graceful shutdown. | |
| logging.info("Terminating pid %d gracefully...", pid) | |
| os.kill(pid, signal.SIGTERM) | |
| # Polling for a maximum of GRACEFUL_EXIT_TIMEOUT. | |
| start_time = datetime.datetime.now() | |
| end_time = start_time + GRACEFUL_EXIT_TIMEOUT | |
| while datetime.datetime.now() < end_time: | |
| if not psutil.pid_exists(pid): | |
| logging.info("Pid %d has exited gracefully.", pid) | |
| return | |
| time.sleep(0.1) | |
| logging.warning("Pid %d still running, killing by force...", pid) | |
| os.kill(pid, signal.SIGKILL) | |
| except OSError as e: | |
| logging.error(e) | |
| def terminate_pids(pids: List[int]) -> None: | |
| """Terminates processes of |pids|. | |
| Args: | |
| pids: The process pids. | |
| """ | |
| for pid in pids: | |
| terminate_pid(pid) | |
| def get_pids_in_netns(netns_name: str) -> List[int]: | |
| """Gets pids running in a network namespace. | |
| Args: | |
| netns_name: The name of the network namespace. | |
| Returns: | |
| A list of the pids. | |
| """ | |
| pids_str = run_output("ip", "netns", "pids", netns_name) | |
| return [int(pid) for pid in pids_str.split()] | |
| def get_pids_from_proc_name_in_netns( | |
| proc_name: str, netns_name: str | |
| ) -> List[int]: | |
| """Gets pids of the process name in a network namespace. | |
| Args: | |
| proc_name: The process name. | |
| netns_name: The name of the network namespace. | |
| Returns: | |
| A list of pids. | |
| """ | |
| pids_in_netns = get_pids_in_netns(netns_name) | |
| return [pid for pid in pids_in_netns if get_proc_name(pid) == proc_name] | |
| def get_pids_in_netns_with_interface(ifname: str, mac: str) -> List[int]: | |
| """Gets all pids in the network namespace where the interface exists. | |
| Args: | |
| ifname: The name of the interface. | |
| mac: The MAC address of the interface. | |
| Returns: | |
| A list of all pids that run in the network namespace where the interface | |
| exists. | |
| """ | |
| # Get a list of interface statistics file paths. Each file path will be in | |
| # the format of /proc/<pid>/task/<pid>/net/*/|ifname|, so we can extract | |
| # the pid out of it. | |
| stat_file_paths = glob.glob(f"/proc/*/task/*/net/*/{ifname}") | |
| pids = [] | |
| for path in stat_file_paths: | |
| # First of all, the path must be a file, not a directory or anything | |
| # else. | |
| if not os.path.isfile(path): | |
| continue | |
| # Extract the pid. | |
| pid_str = path.split("/")[2] | |
| try: | |
| pid = int(pid_str) | |
| except ValueError: | |
| continue | |
| # Now we get the pid, but we still need to check whether the MAC address | |
| # of the interface that lives in the network namespace of pid is the | |
| # same as |ifmac| (to exclude other interfaces with the same name). | |
| if mac == get_mac_address_in_netns(ifname, pid=pid): | |
| pids.append(pid) | |
| return pids | |
| def check_interface_in_netns( | |
| ifname: str, netns_name: Optional[str] = None | |
| ) -> bool: | |
| """Checks whether the given interface is in the given network namespace. | |
| Args: | |
| ifname: The interface name. | |
| netns_name: The network namespace name. The default is the root netns. | |
| Returns: | |
| A bool that indicates whether the interface is in the netns. | |
| """ | |
| return bool(run_ip_link_show(ifname, netns_name)) | |
| def wait_for_interface_in_root_netns(ifname: str) -> bool: | |
| """Waits for the given interface to appear in the root network namespace. | |
| The method is used for ehide recovery. When the ehide network namespace is | |
| deleted unexpectedly and is still not freed due to the remaining | |
| processes in the namespace, we need to kill all the processes and wait for | |
| the ehide network namespace to be freed. We achieve this by waiting for the | |
| Ethernet interface to appear in the root network namespace in a polling | |
| manner. | |
| Args: | |
| ifname: The interface name. | |
| Returns: | |
| A bool that indicates whether the interface has shown up in the root | |
| network namespace before timeout. | |
| """ | |
| logging.info("Waiting for %s to appear in the root netns...", ifname) | |
| # Polling for a maximum of INTERFACE_APPEARANCE_TIMEOUT. | |
| start_time = datetime.datetime.now() | |
| end_time = start_time + INTERFACE_APPEARANCE_TIMEOUT | |
| while datetime.datetime.now() < end_time: | |
| if check_interface_in_netns(ifname): | |
| logging.info("%s has appeared in the root netns.", ifname) | |
| return True | |
| time.sleep(0.2) | |
| return False | |
| def move_interface_to_netns(ifname: str, netns_name: str) -> None: | |
| """Moves the given interface from the root netns to the given netns. | |
| Args: | |
| ifname: The interface name. The interface must be in the root netns. | |
| netns_name: The name of the netns to move the given interface to. | |
| """ | |
| run("ip", "link", "set", "dev", ifname, "netns", netns_name) | |
| def move_interface_to_root_netns(ifname: str, original_netns_name: str) -> None: | |
| """Moves the given interface back to the root network namespace. | |
| Args: | |
| ifname: The interface name. The interface must not be in the root netns. | |
| original_netns_name: The name of the netns where the given interface | |
| stays in. | |
| """ | |
| run( | |
| "ip", | |
| "-n", | |
| original_netns_name, | |
| "link", | |
| "set", | |
| "dev", | |
| ifname, | |
| "netns", | |
| "1", | |
| ) | |
| def check_interface_up(ifname: str, netns_name: Optional[str] = None) -> bool: | |
| """Checks whether the given interface has the flag "UP" in the given netns. | |
| Args: | |
| ifname: The interface name. | |
| netns_name: The network namespace name. The default is the root netns. | |
| Returns: | |
| A bool that indicates whether the given interface has the flag "UP". | |
| """ | |
| ip_output = run_ip_link_show(ifname, netns_name) | |
| if ip_output == "": | |
| return False | |
| try: | |
| ip_dict = json.loads(ip_output) | |
| except json.JSONDecodeError as e: | |
| logging.warning("Error in parsing %s: %s", ip_output, e) | |
| return False | |
| try: | |
| flags = ip_dict[0]["flags"] | |
| except (IndexError, KeyError) as e: | |
| logging.warning("Error in parsing flags from %s: %s", ip_output, e) | |
| return False | |
| return "UP" in flags | |
| def bring_up_interface(ifname: str, netns_name: Optional[str] = None) -> None: | |
| """Brings up the given interface in the given namespace. | |
| Args: | |
| ifname: The interface name. | |
| netns_name: The network namespace name. The default is the root netns. | |
| """ | |
| if netns_name: | |
| options = ["-n", netns_name] | |
| else: | |
| options = [] | |
| cmd = ["ip"] + options + ["link", "set", "dev", ifname, "up"] | |
| run(*cmd) | |
| def check_interface_ip_in_netns( | |
| family: IpFamily, ifname: str, netns_name: Optional[str] = None | |
| ) -> bool: | |
| """Checks whether the given interface has any global IPv4 or IPv6 address. | |
| Args: | |
| family: The IP family. | |
| ifname: The interface name. | |
| netns_name: The name of the network namespace where the given interface | |
| stays. The default is the root netns. | |
| Returns: | |
| A bool indicates that whether the given interface has the specified type | |
| of IP address. | |
| """ | |
| options = [] | |
| if netns_name: | |
| options += ["-n", netns_name] | |
| options += [f"-{family.value}"] | |
| cmd = ["ip"] + options + ["addr", "show", ifname, "scope", "global"] | |
| ip_output = run_output(*cmd) | |
| return bool(ip_output) | |
| def wait_for_ip_set_in_netns( | |
| family: IpFamily, ifname: str, netns_name: Optional[str] = None | |
| ) -> bool: | |
| """Waits for IP address set on the given interface in a polling manner. | |
| Args: | |
| family: The IP family. | |
| ifname: The interface name. | |
| netns_name: The name of the network namespace where given interface | |
| stays. The default is the root netns. | |
| Returns: | |
| A bool indicates that whether the IP address is set before timeout. | |
| """ | |
| logging.info("Waiting for IPv%s address set...", family.value) | |
| # Polling for a maximum of IP_SET_TIMEOUT. | |
| start_time = datetime.datetime.now() | |
| end_time = start_time + IP_SET_TIMEOUT | |
| while datetime.datetime.now() < end_time: | |
| if check_interface_ip_in_netns(family, ifname, netns_name): | |
| logging.info("IPv%s address is set.", family.value) | |
| return True | |
| time.sleep(0.2) | |
| logging.error("Failed to wait for IPv%s address set.", family.value) | |
| return False | |
| def enable_slaac_in_netns(ifname: str, netns_name: str) -> None: | |
| """Enables SLAAC for the given interface in the given netns. | |
| Args: | |
| ifname: The interface name. | |
| netns_name: The network namespace name. | |
| """ | |
| logging.info("Enabling SLAAC...") | |
| run( | |
| "ip", | |
| "netns", | |
| "exec", | |
| netns_name, | |
| "sysctl", | |
| "-w", | |
| f"net.ipv6.conf.{ifname}.disable_ipv6=0", | |
| "-w", | |
| f"net.ipv6.conf.{ifname}.accept_dad=1", | |
| "-w", | |
| f"net.ipv6.conf.{ifname}.accept_ra=2", | |
| "-w", | |
| f"net.ipv6.conf.{ifname}.forwarding=1", | |
| ) | |
| def check_netns(netns_name: str) -> bool: | |
| """Checks whether the given network namespace exists. | |
| Args: | |
| netns_name: The network namespace name. | |
| Returns: | |
| A bool that indicates whether the network namespace exists. | |
| """ | |
| return netns_name in run_output("ip", "netns").split() | |
| class EhideDaemon(daemon.Daemon): | |
| """The Ethernet-hide daemon class. | |
| Attributes: | |
| approach: The approach to hiding Ethernet. Can be FORWARDING or SHELL. | |
| ether_ifname: The name of the Ethernet interface. | |
| static_ipv4_cidr: The static IPv4 CIDR used to set the Ethernet | |
| interface. If it is None then the IPv4 address will be set | |
| dynamically using DHCP. | |
| dhclient_dir: The directory path of dhclient files: the dhclient lease | |
| file, pid file, and the configuration file. | |
| netns_name: The network namespace name used to hide the Ethernet | |
| interface. | |
| ether_mac: Only exists in the daemon process (not the process that stops | |
| the daemon). The MAC address of the Ethernet interface. | |
| has_ipv4_initially: Only exists in the daemon process (not the process | |
| that stops the daemon). Whether the Ethernet interface has any IPv4 | |
| address before starting ehide. | |
| has_ipv6_initially: Only exists in the daemon process (not the process | |
| that stops the daemon). Whether the Ethernet interface has any IPv6 | |
| address before starting ehide. | |
| socat_proc: Only exists in the daemon process and when the approach is | |
| FORWARDING. It stores a subprocess.Popen object of the socat | |
| process. If socat is not running, then socat_proc is None. | |
| recover_duts_running_initially: Only exists in the daemon process. | |
| Whether the recover_duts service is running before starting ehide. | |
| This service has to be stopped before ehide starts, since it | |
| interferes with ehide. | |
| """ | |
| def __init__( | |
| self, | |
| approach: Approach, | |
| ether_ifname: str, | |
| static_ipv4_cidr: Optional[str], | |
| dhclient_dir: str, | |
| netns_name: str, | |
| ): | |
| """Initializes the ehide daemon. | |
| In addition to the attribute initialization, it also checks the | |
| environment if it is going to be forked to become a daemon process. | |
| These checks include whether the Ethernet interface has IPv4 or IPv6 | |
| addresses, and whether the recover_duts service is running. If the IPv4 | |
| address exists, the daemon needs to configure IPv4 address after moving | |
| the Ethernet interface to the ehide network namespace. Same for the IPv6 | |
| address. If the recover_duts is running, ehide will stop it when running | |
| and start it again when exiting. | |
| Args: | |
| approach: The approach to hiding Ethernet interface. Can be | |
| FORWARDING or SHELL. | |
| ether_ifname: The ethernet interface name. | |
| static_ipv4_cidr: Optional static IPv4 CIDR used to set the Ethernet | |
| interface. If it is None then the IPv4 address will be | |
| dynamically configured using DHCP. | |
| dhclient_dir: The directory path of dhclient files. | |
| netns_name: The name of the network namespace to hide Ethernet | |
| interface. | |
| """ | |
| os.makedirs(EHIDE_DIR, exist_ok=True) | |
| super().__init__( | |
| pidfile=os.path.join(EHIDE_DIR, "ehide.pid"), | |
| state_file=os.path.join(EHIDE_DIR, "ehide_state"), | |
| stdin="/dev/null", | |
| stdout=os.path.join(EHIDE_DIR, "stdout"), | |
| stderr=os.path.join(EHIDE_DIR, "stderr"), | |
| ) | |
| self.approach = approach | |
| self.ether_ifname = ether_ifname | |
| self.static_ipv4_cidr: Optional[str] = static_ipv4_cidr | |
| self.dhclient_dir = dhclient_dir | |
| self.netns_name = netns_name | |
| if self.get_state() == daemon.State.OFF: | |
| self.ether_mac = get_mac_address_in_netns(self.ether_ifname) | |
| if self.ether_mac: | |
| logging.info( | |
| "MAC address of %s: %s.", self.ether_ifname, self.ether_mac | |
| ) | |
| else: | |
| logging.error( | |
| "Failed to acquire MAC address of %s.", self.ether_ifname | |
| ) | |
| sys.exit(1) | |
| self.has_ipv4_initially = check_interface_ip_in_netns( | |
| IpFamily.IPv4, self.ether_ifname | |
| ) | |
| if self.has_ipv4_initially: | |
| logging.info("Init: Found IPv4 address.") | |
| else: | |
| logging.info("Init: Not found IPv4 address.") | |
| self.has_ipv6_initially = check_interface_ip_in_netns( | |
| IpFamily.IPv6, self.ether_ifname | |
| ) | |
| if self.has_ipv6_initially: | |
| logging.info("Init: Found IPv6 address.") | |
| else: | |
| logging.info("Init: Not found IPv6 address.") | |
| if self.approach == Approach.FORWARDING: | |
| self.socat_proc: Optional[subprocess.Popen] = None | |
| # If the service "Recover DUTs" is running, we need to disable it | |
| # otherwise it will try to recover the Ethernet interface (by | |
| # rebooting the DUT) when ehide is on. Although the | |
| # "check_ethernet.hook" invoked by the service offers a function to | |
| # pause itself, the 30-minute pause limit does not satisfy our needs | |
| # under all circumstances. | |
| self.recover_duts_running_initially = check_service_running( | |
| RECOVER_DUTS_SERVICE_NAME | |
| ) | |
| def set_up(self) -> bool: | |
| """Sets up the ehide environment. | |
| Returns: | |
| A bool indicates that whether the setup is successful. If it | |
| succeeds, the daemon will move to state on and run loop(). If not, | |
| the daemon will move to state tear_down and run tear_down(). | |
| """ | |
| logging.info("Setting up ehide...") | |
| if self.get_state() == daemon.State.SET_UP: | |
| # Ehide is being set up, not being recovered. | |
| # The 0.1s delay here is to allow the SSH connection from the client | |
| # to the DUT to close gracefully, for the environment setups require | |
| # moving the Ethernet interface to another network namespace, which | |
| # breaks the SSH connection. | |
| time.sleep(0.1) | |
| if self.recover_duts_running_initially: | |
| stop_service(RECOVER_DUTS_SERVICE_NAME) | |
| run("ip", "netns", "add", self.netns_name) | |
| if not check_netns(self.netns_name): | |
| logging.error( | |
| "Failed to create network namespace %s.", self.netns_name | |
| ) | |
| return False | |
| move_interface_to_netns(self.ether_ifname, self.netns_name) | |
| if not check_interface_in_netns(self.ether_ifname, self.netns_name): | |
| logging.error( | |
| "Failed to move %s to %s", self.ether_ifname, self.netns_name | |
| ) | |
| return False | |
| for ifname in [self.ether_ifname, "lo"]: | |
| bring_up_interface(ifname, self.netns_name) | |
| if not check_interface_up(ifname, self.netns_name): | |
| logging.error( | |
| "Failed to bring up %s in %s", ifname, self.netns_name | |
| ) | |
| return False | |
| if self.has_ipv4_initially or self.static_ipv4_cidr: | |
| if not self._set_up_ipv4(self.ether_ifname): | |
| return False | |
| if self.has_ipv6_initially: | |
| if not self._set_up_ipv6(self.ether_ifname): | |
| return False | |
| if self.approach == Approach.FORWARDING: | |
| return self._start_socat() | |
| elif self.approach == Approach.SHELL: | |
| return self._start_sshd() | |
| def tear_down(self) -> None: | |
| """Tears down the ehide environment.""" | |
| logging.info("Tearing down ehide...") | |
| # Again, sleep for 0.1s to allow the SSH connection to the DUT to close | |
| # gracefully. | |
| time.sleep(0.1) | |
| move_interface_to_root_netns(self.ether_ifname, self.netns_name) | |
| if self.approach == Approach.FORWARDING: | |
| self._stop_socat() | |
| pids = get_pids_in_netns(self.netns_name) | |
| terminate_pids(pids) | |
| run("ip", "netns", "delete", self.netns_name) | |
| if self.recover_duts_running_initially: | |
| start_service(RECOVER_DUTS_SERVICE_NAME) | |
| def loop(self) -> NoReturn: | |
| """Runs the infinite loop and monitors the ehide environment. | |
| The monitoring will be conducted every 3 seconds. If it fails, stops | |
| itself. | |
| """ | |
| while self.monitor(): | |
| time.sleep(3) | |
| logging.error("Encountered unrecoverable failure, stopping ehide.") | |
| run("ehide", "stop") | |
| # Wait for the SIGTERM here. | |
| while True: | |
| pass | |
| def monitor(self) -> bool: | |
| """Monitors the ehide environment. | |
| Monitors the ehide environment, if there is unexpected process | |
| termination or ehide network namespace deletion, try to recover. | |
| Returns: | |
| True if there is no failure or the failure can be recovered, | |
| otherwise False. | |
| """ | |
| if not check_netns(self.netns_name): | |
| logging.warning("%s is deleted unexpectedly.", self.netns_name) | |
| if not check_interface_in_netns(self.ether_ifname): | |
| logging.info( | |
| "%s is not freed yet, terminating remaining processes...", | |
| self.netns_name, | |
| ) | |
| if self.approach == Approach.FORWARDING: | |
| self._stop_socat() | |
| pids = get_pids_in_netns_with_interface( | |
| self.ether_ifname, self.ether_mac | |
| ) | |
| terminate_pids(pids) | |
| if not wait_for_interface_in_root_netns(self.ether_ifname): | |
| logging.error( | |
| "Failed to wait for %s to appear in the root netns.", | |
| self.ether_ifname, | |
| ) | |
| return False | |
| logging.info("Creating %s again...", self.netns_name) | |
| return self.set_up() | |
| # Check Ethernet interface in ehide netns. | |
| if not check_interface_in_netns(self.ether_ifname, self.netns_name): | |
| logging.error( | |
| "Could not detect %s in %s.", self.ether_ifname, self.netns_name | |
| ) | |
| return False | |
| # Check whether loopback and Ethernet interfaces are up. | |
| for ifname in ["lo", self.ether_ifname]: | |
| if not check_interface_up(ifname, self.netns_name): | |
| logging.error( | |
| "Interface %s in %s is down.", | |
| ifname, | |
| self.netns_name, | |
| ) | |
| return False | |
| if self.has_ipv4_initially or self.static_ipv4_cidr: | |
| # Check IPv4 availability. | |
| if not check_interface_ip_in_netns( | |
| IpFamily.IPv4, self.ether_ifname, self.netns_name | |
| ): | |
| logging.error("IPv4 address lost.") | |
| return False | |
| if self.has_ipv6_initially: | |
| # Check IPv6 availability. | |
| if not check_interface_ip_in_netns( | |
| IpFamily.IPv6, self.ether_ifname, self.netns_name | |
| ): | |
| logging.error("IPv6 address lost.") | |
| return False | |
| if self.has_ipv4_initially and not self.static_ipv4_cidr: | |
| # Check whether dhclient is running. | |
| dhclient_pids = get_pids_from_proc_name_in_netns( | |
| "dhclient", self.netns_name | |
| ) | |
| if not dhclient_pids: | |
| logging.warning("Could not detect dhclient, recovering...") | |
| self._start_dhclient() | |
| if not wait_for_ip_set_in_netns( | |
| IpFamily.IPv4, self.ether_ifname, self.netns_name | |
| ): | |
| return False | |
| if self.approach == Approach.FORWARDING: | |
| # Check whether socat is running. | |
| if self.socat_proc.poll() is not None: | |
| logging.warning("Socat has exited unexpectedly, recovering...") | |
| self._stop_socat() # clean its child processes | |
| if not self._start_socat(): | |
| return False | |
| if self.approach == Approach.SHELL: | |
| # Check whether sshd is running. | |
| sshd_pids = get_pids_from_proc_name_in_netns( | |
| "sshd", self.netns_name | |
| ) | |
| if not sshd_pids: | |
| logging.warning("Could not detect sshd, recovering...") | |
| if not self._start_sshd(): | |
| return False | |
| return True | |
| def _set_up_ipv4(self, ifname: str) -> bool: | |
| """Sets up IPv4 address on the given interface. | |
| Args: | |
| ifname: The interface name. | |
| Returns: | |
| A bool indicates whether the setup has been successful. | |
| """ | |
| if self.static_ipv4_cidr: | |
| run( | |
| "ip", | |
| "-n", | |
| self.netns_name, | |
| "addr", | |
| "add", | |
| self.static_ipv4_cidr, | |
| "dev", | |
| ifname, | |
| ) | |
| else: | |
| self._start_dhclient() | |
| return wait_for_ip_set_in_netns(IpFamily.IPv4, ifname, self.netns_name) | |
| def _set_up_ipv6(self, ifname: str) -> bool: | |
| """Sets up IPv6 address on the given interface. | |
| Args: | |
| ifname: The interface name. | |
| Returns: | |
| A bool indicates whether the setup has been successful. | |
| """ | |
| enable_slaac_in_netns(ifname, self.netns_name) | |
| return wait_for_ip_set_in_netns(IpFamily.IPv6, ifname, self.netns_name) | |
| def _start_dhclient(self) -> None: | |
| """Starts dhclient.""" | |
| old_pids = get_pids_from_proc_name_in_netns("dhclient", self.netns_name) | |
| if old_pids: | |
| logging.warning("Old dhclient running, terminating...") | |
| terminate_pids(old_pids) | |
| os.makedirs(self.dhclient_dir, exist_ok=True) | |
| conf_path = os.path.join(self.dhclient_dir, "dhclient.conf") | |
| logging.info("Writing dhclinet configuration to %s...", conf_path) | |
| with open(conf_path, "w", encoding="utf-8") as f: | |
| f.writelines( | |
| ( | |
| f'interface "{self.ether_ifname}" {{\n', | |
| f" send dhcp-client-identifier 01:{self.ether_mac};\n", | |
| "}\n", | |
| ) | |
| ) | |
| logging.info("Starting dhclient...") | |
| run( | |
| "ip", | |
| "netns", | |
| "exec", | |
| self.netns_name, | |
| "dhclient", | |
| "-4", | |
| "-lf", | |
| os.path.join(self.dhclient_dir, "dhclient.leases"), | |
| "-pf", | |
| os.path.join(self.dhclient_dir, "dhclient.pid"), | |
| "-sf", | |
| DHCLIENT_SCRIPT_PATH, | |
| "-cf", | |
| conf_path, | |
| ) | |
| def _start_socat(self) -> bool: | |
| """Starts socat. | |
| Returns: | |
| A bool that indicates whether socat has started successfully. | |
| """ | |
| logging.info("Starting socat...") | |
| try: | |
| # Since we need this self.socat_proc to stay open during ehide is | |
| # on, we cannot use 'with' statement here. Disabling pylint | |
| # consider-using-with. | |
| # pylint: disable=consider-using-with | |
| self.socat_proc = subprocess.Popen( | |
| [ | |
| "socat", | |
| "--experimental", | |
| "TCP6-LISTEN:22,reuseaddr,fork,netns=netns-ehide", | |
| "TCP6-CONNECT:localhost:22", | |
| ] | |
| ) | |
| except OSError as e: | |
| logging.error("Failed to start socat: %s", e) | |
| return False | |
| # Polling for a maximum of SOCAT_STARTUP_TIMEOUT to wait for socat to | |
| # start listening on port:22 in the ehide netns. | |
| start_time = datetime.datetime.now() | |
| end_time = start_time + SOCAT_STARTUP_TIMEOUT | |
| while datetime.datetime.now() < end_time: | |
| # Run ss command to check whether there is any listening socket on | |
| # port:22 in the ehide netns. | |
| # -t: Display TCP sockets. | |
| # -l: Display only listening sockets. | |
| # -H: Suppress header line. | |
| # -N NSNAME: Switch to the specified network namespace name. | |
| ss_output = run_output( | |
| "ss", "-tlH", "-N", self.netns_name, "src", ":22" | |
| ) | |
| if bool(ss_output): | |
| return True | |
| time.sleep(0.2) | |
| logging.error("Socat not listening on port:22 in %s.", self.netns_name) | |
| return False | |
| def _stop_socat(self) -> None: | |
| """Stops socat. Also terminates all its forked child processes.""" | |
| logging.info("Stopping socat and its child processes...") | |
| if self.socat_proc is None: | |
| return | |
| proc = psutil.Process(self.socat_proc.pid) | |
| for child in proc.children(recursive=True): | |
| child.kill() | |
| proc.kill() | |
| self.socat_proc = None | |
| def _start_sshd(self) -> bool: | |
| """Starts sshd. | |
| Returns: | |
| A bool that indicates whether sshd has started successfully. | |
| """ | |
| old_pids = get_pids_from_proc_name_in_netns("sshd", self.netns_name) | |
| if old_pids: | |
| logging.warning("Old sshd running, terminating...") | |
| terminate_pids(old_pids) | |
| logging.info("Starting sshd...") | |
| run( | |
| "ip", | |
| "netns", | |
| "exec", | |
| self.netns_name, | |
| SSHD_PATH, | |
| "-o", | |
| f"ForceCommand=/bin/bash {FORCE_COMMAND_PATH}", | |
| ) | |
| sshd_pids = get_pids_from_proc_name_in_netns("sshd", self.netns_name) | |
| if not sshd_pids: | |
| logging.error("Sshd has exited unexpectedly.") | |
| return False | |
| return True |