Verified Commit bc286a97 authored by Karel Koci's avatar Karel Koci 🤘
Browse files

nsfarm/lxd: add ability to proxy TCP and UDP ports

This allows access of TCP and UDP services trough container. This can be
used not only to access services running locally in container but also
any service accessible from container network.

The simple use case of this is access of web interfaces of router.
parent a77aaddd
...@@ -60,6 +60,13 @@ def parser(parser): ...@@ -60,6 +60,13 @@ def parser(parser):
device specification from image and RESOURCE is resource mapped to it. device specification from image and RESOURCE is resource mapped to it.
""" """
) )
inspect.add_argument(
'-p', '--proxy',
action='append',
help="""Adds socket to be proxied to host. The argument has to be proto:address:port, for example:
tcp:127.0.0.1:80. The port this is forwarded to is printed on to the terminal when container is started.
"""
)
return { return {
None: parser, None: parser,
...@@ -129,6 +136,17 @@ def op_inspect(args, parser): ...@@ -129,6 +136,17 @@ def op_inspect(args, parser):
connection = LXDConnection() connection = LXDConnection()
with Container(connection, args.IMAGE, device_map=device_map, strict=False, **kwargs) as cont: with Container(connection, args.IMAGE, device_map=device_map, strict=False, **kwargs) as cont:
if args.proxy:
for proxy in args.proxy:
el = proxy.split(':', maxsplit=2)
if len(el) == 1:
args = {"port": el[0]}
elif len(el) == 2:
args = {"address": el[0], "port": el[1]}
else:
args = {"proto": el[0], "address": el[1], "port": el[2]}
localport = cont.network.proxy_open(**args)
print(f"Proxy '{proxy}' to: {localport}")
sys.exit(subprocess.call(['lxc', 'exec', cont.name, '/bin/sh'])) sys.exit(subprocess.call(['lxc', 'exec', cont.name, '/bin/sh']))
......
...@@ -3,11 +3,8 @@ ...@@ -3,11 +3,8 @@
import os import os
import typing import typing
import logging import logging
import pexpect
import functools
import typing
import warnings import warnings
import ipaddress import pexpect
from .. import cli from .. import cli
from .connection import LXDConnection from .connection import LXDConnection
from .image import Image from .image import Image
...@@ -26,7 +23,6 @@ class Container: ...@@ -26,7 +23,6 @@ class Container:
def __init__(self, lxd_connection: LXDConnection, image: typing.Union[str, Image], device_map: dict = None, def __init__(self, lxd_connection: LXDConnection, image: typing.Union[str, Image], device_map: dict = None,
internet: typing.Optional[bool] = None, strict: bool = True): internet: typing.Optional[bool] = None, strict: bool = True):
self._lxd = lxd_connection self._lxd = lxd_connection
self._internet = False
self._device_map = device_map self._device_map = device_map
self._override_wants_internet = internet self._override_wants_internet = internet
self._strict = strict self._strict = strict
...@@ -135,9 +131,7 @@ class Container: ...@@ -135,9 +131,7 @@ class Container:
def name(self) -> typing.Union[str, None]: def name(self) -> typing.Union[str, None]:
"""Name of container if prepared, otherwise None. """Name of container if prepared, otherwise None.
""" """
if self.lxd_container is None: return self.lxd_container.name if self.lxd_container is not None else None
return None
return self.lxd_container.name
@property @property
def image(self) -> Image: def image(self) -> Image:
......
import ipaddress import ipaddress
import contextlib
import socket
def _find_free_port(proto):
"""Simple function that gives us random free port.
The possible issue is that it might be taken in the meantime but because it is random port that is less likely.
"""
socktp = {'tcp': socket.SOCK_STREAM, 'udp': socket.SOCK_DGRAM}[proto]
with contextlib.closing(socket.socket(socket.AF_INET, socktp)) as sock:
sock.bind(('', 0))
return sock.getsockname()[1]
class NetworkInterface(): class NetworkInterface():
...@@ -42,3 +54,35 @@ class NetworkInterface(): ...@@ -42,3 +54,35 @@ class NetworkInterface():
"""returns list of available interfaces """returns list of available interfaces
""" """
return self._network.keys() return self._network.keys()
def proxy_open(self, proto="tcp", address="127.0.0.1", port="80"):
"""Proxy socket connection through container.
Warning: This supports only TCP and UDP sockets right now!
Returns local port number service is proxied to.
"""
assert self._container.lxd_container is not None
freeport = _find_free_port(proto)
self._container.lxd_container.devices[f"proxy-{proto}-{freeport}"] = {
"connect": f"{proto}:{address}:{port}",
"listen": f"{proto}:127.0.0.1:{freeport}",
"type": "proxy"
}
self._container.lxd_container.save()
return freeport
def proxy_close(self, localport, proto="tcp"):
"""Close existing proxy.
"""
del self._container.lxd_container.devices[f"proxy-{proto}-{localport}"]
self._container.lxd_container.save()
@contextlib.contextmanager
def proxy(self, **args):
"""Open proxy for limited context.
This is using proxy_open and proxy_close.
"""
port = self.proxy_open(**args)
try:
yield port
finally:
self.proxy_close(port)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment