blob: 9c794906ba8a4fab9adc554f43302406987ca6e1 [file] [edit]
#!/usr/bin/env python3
# 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.
"""Ethernet-hide (ehide)
Ethernet-hide (ehide) is a tool that creates an environment that hides the
Ethernet interface on DUT but still allows SSH'ing to it through that Ethernet
interface. This tool can help CrOS developers test in a no-network or WiFi-
only environment with SSH connections unaffected. This tool can also help
protect SSH connection when developers' behavior, such as restarting or
deploying shill, can potentially affect it. Ehide only runs on test images.
"""
import argparse
import logging
import logging.handlers
import time
from typing import Dict, List
import ehide.ehide_daemon
import ehide.interface
def get_parser() -> argparse.ArgumentParser:
"""Gets the argument parser.
Returns:
The argument parser.
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"action",
choices=("start", "stop", "state"),
help="To start, stop, or show the current state of ehide.",
)
parser.add_argument(
"-a",
"--approach",
default=ehide.ehide_daemon.Approach.FORWARDING.value,
choices=(
ehide.ehide_daemon.Approach.FORWARDING.value,
ehide.ehide_daemon.Approach.SHELL.value,
),
help=(
"The approach to enabling SSH after hiding Ethernet. Must be "
"'forwarding' or 'shell'. The forwarding approach uses socat to "
"bidirectionally forward traffic between the port:22 in the ehide "
"network namespace and the port:22 in the root network namespace. "
"The shell approach starts an SSH server inside the created "
"namespace and configures the SSH server to start a shell in the "
"root network namespace using nsenter command in the ForceCommand "
"option. The default value is 'forwarding'."
),
)
parser.add_argument(
"-d",
"--dhclient-dir",
type=str,
default="/run/dhclient",
help=(
"Directory to save dhclient lease file and pid file. The default "
"value is '/run/dhclient'."
),
)
parser.add_argument(
"-i",
"--interface",
type=str,
action="append",
help=(
"Manually specify the name of the Ethernet interface to hide. Can "
"appear multiple times to specify multiple interfaces. Two formats "
"are allowed: <ifname> or <ifname>=<cidr>. If only <ifname> is "
"specified then DHCP will be used for dynamic IPv4 provisioning. "
"If the format is <ifname>=<cidr> then the IPv4 address will be "
"statically set to <cidr>. If no interface is specified, ehide "
"will automatically detect all the connected Ethernet interfaces "
"to hide. "
),
)
parser.add_argument(
"-l",
"--log-path",
type=str,
help="File path for logging.",
)
parser.add_argument(
"-n",
"--netns-name",
type=str,
default="netns-ehide",
help=(
"Name of the network namespace to hide Ethernet interface. The "
"default value is 'netns-ehide'."
),
)
return parser
def parse_interfaces(
interfaces: List[str],
) -> Dict[str, ehide.interface.Interface]:
"""Parses interfaces from the command line arguments.
Args:
interfaces: A list of interfaces from the command line arguments. Each
is in the format of either <ifname> or <ifname>=<cidr>.
Returns:
A dictionary of interfaces that maps Ethernet interface names to
ehide.interface.Interface objects.
"""
ret: Dict[str, ehide.interface.Interface] = {}
for interface in interfaces:
interface_split = interface.split("=")
if len(interface_split) == 1: # <ifname>
ifname = interface_split[0]
ret[ifname] = ehide.interface.Interface(ifname)
elif len(interface_split) == 2: # <ifname>=<cidr>
ifname, cidr = interface_split
ret[ifname] = ehide.interface.Interface(ifname)
ret[ifname].static_ipv4_cidr = cidr
else:
raise ValueError(f"Invalid interface: {interface}.")
return ret
def main():
opts = get_parser().parse_args()
action: str = opts.action
# Set up logging.
logging_handlers = [
logging.handlers.SysLogHandler(
"/dev/log", facility=logging.handlers.SysLogHandler.LOG_LOCAL1
)
]
if opts.log_path:
logging_handlers.append(logging.FileHandler(opts.log_path))
logging.basicConfig(
format="ehide: %(asctime)s.%(msecs)03d %(levelname)s %(message)s",
level=logging.INFO,
datefmt="%Y-%m-%dT%H:%M:%S",
handlers=logging_handlers,
)
# Use GMT timezone.
logging.Formatter.converter = time.gmtime
if opts.interface:
interfaces = parse_interfaces(opts.interface)
else:
interfaces = {}
ehide_daemon = ehide.ehide_daemon.EhideDaemon(
action=action,
approach=ehide.ehide_daemon.Approach.from_str(opts.approach),
interfaces=interfaces,
dhclient_dir=opts.dhclient_dir,
netns_name=opts.netns_name,
)
if action == "start":
ehide_daemon.start()
elif action == "stop":
ehide_daemon.stop()
elif action == "state":
print(ehide_daemon.get_state().value)
else:
raise ValueError(f"Unknown action: {action}.")
if __name__ == "__main__":
main()