def expose_local_services(processes: Subprocesses, ssh: SSH, port_numbers: List[Tuple[int, int]]) -> None: """Create SSH tunnels from remote proxy pod to local host. :param processes: A `Subprocesses` instance. :param ssh: A 'SSH` instance. :param port_numbers: List of pairs of (local port, remote port). """ output = sys.stderr.isatty() if not port_numbers and output: print( "No traffic is being forwarded from the remote Deployment to your" " local machine. You can use the --expose option to specify which" " ports you want to forward.", file=sys.stderr) remote_forward_arguments = [] for local_port, remote_port in port_numbers: if output: print("Forwarding remote port {} to local port {}.".format( remote_port, local_port, ), file=sys.stderr) remote_forward_arguments.extend([ "-R", "*:{}:127.0.0.1:{}".format(remote_port, local_port), ]) if remote_forward_arguments: processes.append(ssh.popen(remote_forward_arguments)) if output: print("", file=sys.stderr)
def connect_sshuttle( runner: Runner, remote_info: RemoteInfo, args: argparse.Namespace, subprocesses: Subprocesses, env: Dict[str, str], ssh: SSH ): """Connect to Kubernetes using sshuttle.""" # Make sure we have sudo credentials by doing a small sudo in advance # of sshuttle using it: Popen(["sudo", "true"]).wait() sshuttle_method = "auto" if sys.platform.startswith("linux"): # sshuttle tproxy mode seems to have issues: sshuttle_method = "nat" subprocesses.append( runner.popen([ "sshuttle-telepresence", "-v", "--dns", "--method", sshuttle_method, "-e", ( "ssh -oStrictHostKeyChecking=no " + "-oUserKnownHostsFile=/dev/null -F /dev/null" ), # DNS proxy running on remote pod: "--to-ns", "127.0.0.1:9053", "-r", "telepresence@localhost:" + str(ssh.port), ] + get_proxy_cidrs( runner, args, remote_info, env["KUBERNETES_SERVICE_HOST"] )) ) # sshuttle will take a while to startup. We can detect it being up when # DNS resolution of services starts working. We use a specific single # segment so any search/domain statements in resolv.conf are applied, # which then allows the DNS proxy to detect the suffix domain and # filter it out. def get_hellotelepresence(counter=iter(range(10000))): # On Macs, and perhaps elsewhere, there is OS-level caching of # NXDOMAIN, so bypass caching by sending new domain each time. Another, # less robust alternative, is to `killall -HUP mDNSResponder`. runner.get_output([ "python3", "-c", "import socket; socket.gethostbyname('hellotelepresence{}')". format(next(counter)) ]) start = time() while time() - start < 20: try: get_hellotelepresence() sleep(1) # just in case there's more to startup break except CalledProcessError: sleep(0.1) get_hellotelepresence()
def proxy(config: dict): """Start sshuttle proxy to Kubernetes.""" port = config["port"] if "ip" in config: # Typically host is macOS: ip = config["ip"] else: # Typically host is Linux, use default route: ip = None route_output = str(check_output(["route", "-n"]), "ascii") for line in route_output.splitlines(): parts = line.split() if parts[0] == "default" or parts[0] == "0.0.0.0": ip = parts[1] break assert ip is not None, route_output cidrs = config["cidrs"] expose_ports = config["expose_ports"] # Start the sshuttle VPN-like thing: # XXX duplicates code in telepresence, remove duplication main_process = Popen([ "sshuttle-telepresence", "-v", "--dns", "--method", "nat", "-e", ("ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null " + "-F /dev/null"), "--to-ns", "127.0.0.1:9053", "-r", "telepresence@{}:{}".format(ip, port) ] + cidrs) # Start the SSH tunnels to expose local services: subps = Subprocesses() runner = Runner.open("-", "kubectl", False) ssh = SSH(runner, port, ip) expose_local_services(subps, ssh, expose_ports) # Wait for everything to exit: wait_for_exit(runner, main_process, subps)
def run_docker_command( runner: Runner, remote_info: RemoteInfo, args: argparse.Namespace, remote_env: Dict[str, str], subprocesses: Subprocesses, ssh: SSH, ) -> None: """ --docker-run support. Connect using sshuttle running in a Docker container, and then run user container. :param args: Command-line args to telepresence binary. :param remote_env: Dictionary with environment on remote pod. :param mount_dir: Path to local directory where remote pod's filesystem is mounted. """ # Mount remote filesystem. We allow all users if we're using Docker because # we don't know what uid the Docker container will use: mount_dir, mount_cleanup = mount_remote_volumes( runner, remote_info, ssh, True, ) # Update environment: remote_env["TELEPRESENCE_ROOT"] = mount_dir remote_env["TELEPRESENCE_METHOD"] = "container" # mostly just for tests :( # Start the sshuttle container: name = random_name() config = { "port": ssh.port, "cidrs": get_proxy_cidrs(runner, args, remote_info, remote_env["KUBERNETES_SERVICE_HOST"]), "expose_ports": list(args.expose.local_to_remote()), } if sys.platform == "darwin": config["ip"] = MAC_LOOPBACK_IP # Image already has tini init so doesn't need --init option: subprocesses.append( runner.popen( docker_runify([ "--rm", "--privileged", "--name=" + name, TELEPRESENCE_LOCAL_IMAGE, "proxy", json.dumps(config) ])), make_docker_kill(runner, name)) # Write out env file: with NamedTemporaryFile("w", delete=False) as envfile: for key, value in remote_env.items(): envfile.write("{}={}\n".format(key, value)) atexit.register(os.remove, envfile.name) # Wait for sshuttle to be running: while True: try: runner.check_call( docker_runify([ "--network=container:" + name, "--rm", TELEPRESENCE_LOCAL_IMAGE, "wait" ])) except CalledProcessError as e: if e.returncode == 100: # We're good! break return name, envfile.name elif e.returncode == 125: # Docker failure, probably due to original container not # starting yet... so sleep and try again: sleep(1) continue else: raise else: raise RuntimeError( "Waiting container exited prematurely. File a bug, please!") # Start the container specified by the user: container_name = random_name() docker_command = docker_runify([ "--volume={}:{}".format(mount_dir, mount_dir), "--name=" + container_name, "--network=container:" + name, "--env-file", envfile.name, ]) # Older versions of Docker don't have --init: if "--init" in runner.get_output(["docker", "run", "--help"]): docker_command += ["--init"] docker_command += args.docker_run p = Popen(docker_command) def terminate_if_alive(): runner.write("Shutting down containers...\n") if p.poll() is None: runner.write("Killing local container...\n") make_docker_kill(runner, container_name)() mount_cleanup() atexit.register(terminate_if_alive) wait_for_exit(runner, p, subprocesses)
def connect( runner: Runner, remote_info: RemoteInfo, cmdline_args: argparse.Namespace ) -> Tuple[Subprocesses, int, SSH]: """ Start all the processes that handle remote proxying. Return (Subprocesses, local port of SOCKS proxying tunnel, SSH instance). """ span = runner.span() processes = Subprocesses() # Keep local copy of pod logs, for debugging purposes: processes.append( runner.popen( runner.kubectl( cmdline_args.context, remote_info.namespace, [ "logs", "-f", remote_info.pod_name, "--container", remote_info.container_name ] ), bufsize=0, ) ) ssh = SSH(runner, find_free_port()) # forward remote port to here, by tunneling via remote SSH server: processes.append( runner.popen( runner.kubectl( cmdline_args.context, remote_info.namespace, [ "port-forward", remote_info.pod_name, "{}:8022".format(ssh.port) ] ) ) ) if cmdline_args.method == "container": # kubectl port-forward currently only listens on loopback. So we # portforward from the docker0 interface on Linux, and the lo0 alias we # added on OS X, to loopback (until we can use kubectl port-forward # option to listen on docker0 - # https://github.com/kubernetes/kubernetes/pull/46517, or all our users # have latest version of Docker for Mac, which has nicer solution - # https://github.com/datawire/telepresence/issues/224). if sys.platform == "linux": # If ip addr is available use it if not fall back to ifconfig. if which("ip"): docker_interfaces = re.findall( r"(\d+\.\d+\.\d+\.\d+)", runner.get_output(["ip", "addr", "show", "dev", "docker0"]) ) elif which("ifconfig"): docker_interfaces = re.findall( r"(\d+\.\d+\.\d+\.\d+)", runner.get_output(["ifconfig", "docker0"]) ) else: raise SystemExit("'ip addr' nor 'ifconfig' available") if len(docker_interfaces) == 0: raise SystemExit("No interface for docker found") docker_interface = docker_interfaces[0] else: # The way to get routing from container to host is via an alias on # lo0 (https://docs.docker.com/docker-for-mac/networking/). We use # an IP range that is assigned for testing network devices and # therefore shouldn't conflict with real IPs or local private # networks (https://tools.ietf.org/html/rfc6890). runner.check_call([ "sudo", "ifconfig", "lo0", "alias", MAC_LOOPBACK_IP ]) atexit.register( runner.check_call, ["sudo", "ifconfig", "lo0", "-alias", MAC_LOOPBACK_IP] ) docker_interface = MAC_LOOPBACK_IP processes.append( runner.popen([ "socat", "TCP4-LISTEN:{},bind={},reuseaddr,fork".format( ssh.port, docker_interface, ), "TCP4:127.0.0.1:{}".format(ssh.port) ]) ) ssh.wait() # In Docker mode this happens inside the local Docker container: if cmdline_args.method != "container": expose_local_services( processes, ssh, cmdline_args.expose.local_to_remote(), ) # Start tunnels for the SOCKS proxy (local -> remote) # and the local server for the proxy to poll (remote -> local). socks_port = find_free_port() local_server_port = find_free_port() local_server = LocalServer(local_server_port, runner.output) processes.append(local_server, local_server.kill) forward_args = [ "-L127.0.0.1:{}:127.0.0.1:9050".format(socks_port), "-R9055:127.0.0.1:{}".format(local_server_port) ] processes.append(ssh.popen(forward_args)) span.end() return processes, socks_port, ssh
def run_docker_command( runner: Runner, remote_info: RemoteInfo, args: argparse.Namespace, remote_env: Dict[str, str], subprocesses: Subprocesses, ssh: SSH, mount_dir: Optional[str], ) -> Popen: """ --docker-run support. Connect using sshuttle running in a Docker container, and then run user container. :param args: Command-line args to telepresence binary. :param remote_env: Dictionary with environment on remote pod. :param mount_dir: Path to local directory where remote pod's filesystem is mounted. """ # Update environment: remote_env["TELEPRESENCE_METHOD"] = "container" # mostly just for tests :( # Extract --publish flags and add them to the sshuttle container, which is # responsible for defining the network entirely. docker_args, publish_args = parse_docker_args(args.docker_run) # Start the sshuttle container: name = random_name() config = { "port": ssh.port, "cidrs": get_proxy_cidrs(runner, args, remote_info, remote_env["KUBERNETES_SERVICE_HOST"]), "expose_ports": list(args.expose.local_to_remote()), } if sys.platform == "darwin": config["ip"] = MAC_LOOPBACK_IP # Image already has tini init so doesn't need --init option: span = runner.span() subprocesses.append( runner.popen( docker_runify(publish_args + [ "--rm", "--privileged", "--name=" + name, TELEPRESENCE_LOCAL_IMAGE, "proxy", json.dumps(config) ])), make_docker_kill(runner, name)) # Wait for sshuttle to be running: while True: try: runner.check_call( docker_runify([ "--network=container:" + name, "--rm", TELEPRESENCE_LOCAL_IMAGE, "wait" ])) except CalledProcessError as e: if e.returncode == 100: # We're good! break elif e.returncode == 125: # Docker failure, probably due to original container not # starting yet... so sleep and try again: sleep(1) continue else: raise else: raise RuntimeError( "Waiting container exited prematurely. File a bug, please!") # Start the container specified by the user: container_name = random_name() docker_command = docker_runify([ "--name=" + container_name, "--network=container:" + name, ]) # Prepare container environment for key in remote_env: docker_command.append("-e={}".format(key)) docker_env = os.environ.copy() docker_env.update(remote_env) if mount_dir: docker_command.append("--volume={}:{}".format(mount_dir, mount_dir)) # Don't add --init if the user is doing something with it init_args = [ arg for arg in docker_args if arg == "--init" or arg.startswith("--init=") ] # Older versions of Docker don't have --init: if not init_args and "--init" in runner.get_output( ["docker", "run", "--help"]): docker_command += ["--init"] docker_command += docker_args span.end() p = Popen(docker_command, env=docker_env) def terminate_if_alive(): runner.write("Shutting down containers...\n") if p.poll() is None: runner.write("Killing local container...\n") make_docker_kill(runner, container_name)() atexit.register(terminate_if_alive) return p