diff options
author | Devaev Maxim <[email protected]> | 2020-04-23 11:17:22 +0300 |
---|---|---|
committer | Devaev Maxim <[email protected]> | 2020-04-23 11:17:31 +0300 |
commit | 75669466cf6b68fbe209a6d2232aec6d49a51044 (patch) | |
tree | 74bdd3064eb76e0d5b6cabd2c6df07bc425e8ea5 | |
parent | 820ef178710d8442e30c5b23d0ac0cb90be5150c (diff) |
vnc: anon tls encryption
-rw-r--r-- | kvmd/apps/__init__.py | 6 | ||||
-rw-r--r-- | kvmd/apps/vnc/__init__.py | 13 | ||||
-rw-r--r-- | kvmd/apps/vnc/rfb/__init__.py | 29 | ||||
-rw-r--r-- | kvmd/apps/vnc/rfb/stream.py | 26 | ||||
-rw-r--r-- | kvmd/apps/vnc/server.py | 69 | ||||
-rw-r--r-- | kvmd/validators/net.py | 12 | ||||
-rw-r--r-- | testenv/tests/validators/test_net.py | 13 |
7 files changed, 131 insertions, 37 deletions
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index 1c29b182..7d9b7425 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -69,6 +69,7 @@ from ..validators.net import valid_ip_or_host from ..validators.net import valid_ip from ..validators.net import valid_port from ..validators.net import valid_mac +from ..validators.net import valid_ssl_ciphers from ..validators.kvm import valid_stream_quality from ..validators.kvm import valid_stream_fps @@ -328,8 +329,11 @@ def _get_config_scheme() -> Dict: "server": { "host": Option("::", type=valid_ip_or_host), "port": Option(5900, type=valid_port), - # TODO: timeout "max_clients": Option(10, type=(lambda arg: valid_number(arg, min=1))), + "tls": { + "ciphers": Option("ALL:@SECLEVEL=0", type=valid_ssl_ciphers), + "timeout": Option(5.0, type=valid_float_f01), + }, }, "kvmd": { diff --git a/kvmd/apps/vnc/__init__.py b/kvmd/apps/vnc/__init__.py index 8d863bcc..8bd9be63 100644 --- a/kvmd/apps/vnc/__init__.py +++ b/kvmd/apps/vnc/__init__.py @@ -42,10 +42,17 @@ def main(argv: Optional[List[str]]=None) -> None: # pylint: disable=protected-access VncServer( + host=config.server.host, + port=config.server.port, + max_clients=config.server.max_clients, + + tls_ciphers=config.server.tls.ciphers, + tls_timeout=config.server.tls.timeout, + + desired_fps=config.desired_fps, + symmap=build_symmap(config.keymap), + kvmd=KvmdClient(**config.kvmd._unpack()), streamer=StreamerClient(**config.streamer._unpack()), vnc_auth_manager=VncAuthManager(**config.auth.vncauth._unpack()), - desired_fps=config.desired_fps, - symmap=build_symmap(config.keymap), - **config.server._unpack(), ).run() diff --git a/kvmd/apps/vnc/rfb/__init__.py b/kvmd/apps/vnc/rfb/__init__.py index 698b5084..77f29422 100644 --- a/kvmd/apps/vnc/rfb/__init__.py +++ b/kvmd/apps/vnc/rfb/__init__.py @@ -21,6 +21,7 @@ import asyncio +import ssl from typing import Tuple from typing import List @@ -45,7 +46,7 @@ from .stream import RfbClientStream # ===== -class RfbClient(RfbClientStream): +class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attributes # https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst # https://www.toptal.com/java/implementing-remote-framebuffer-server-java # https://github.com/TigerVNC/tigervnc @@ -54,6 +55,8 @@ class RfbClient(RfbClientStream): self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, + tls_ciphers: str, + tls_timeout: float, width: int, height: int, @@ -63,6 +66,9 @@ class RfbClient(RfbClientStream): super().__init__(reader, writer) + self.__tls_ciphers = tls_ciphers + self.__tls_timeout = tls_timeout + self._width = width self._height = height self.__name = name @@ -98,7 +104,7 @@ class RfbClient(RfbClientStream): raise except RfbConnectionError as err: logger.info("[%s] Client %s: Gone (%s): Disconnected", name, self._remote, str(err)) - except RfbError as err: + except (RfbError, ssl.SSLError) as err: logger.error("[%s] Client %s: %s: Disconnected", name, self._remote, str(err)) except Exception: logger.exception("[%s] Unhandled exception with client %s: Disconnected", name, self._remote) @@ -225,13 +231,19 @@ class RfbClient(RfbClientStream): await self._write_struct("B", 0) - auth_types = {256: ("VeNCrypt/Plain", self.__handshake_security_vencrypt_userpass)} + auth_types = { + 256: ("VeNCrypt/Plain", False, self.__handshake_security_vencrypt_userpass), + 259: ("VeNCrypt/TLSPlain", True, self.__handshake_security_vencrypt_userpass), + } if self.__vnc_passwds: # Vinagre не умеет работать с VNC Auth через VeNCrypt, но это его проблемы, # так как он своеобразно трактует рекомендации VeNCrypt. # Подробнее: https://bugzilla.redhat.com/show_bug.cgi?id=692048 # Hint: используйте любой другой нормальный VNC-клиент. - auth_types[2] = ("VeNCrypt/VNCAuth", self.__handshake_security_vnc_auth) + auth_types.update({ + 2: ("VeNCrypt/VNCAuth", False, self.__handshake_security_vnc_auth), + 258: ("VeNCrypt/TLSVNCAuth", True, self.__handshake_security_vnc_auth), + }) await self._write_struct("B" + "L" * len(auth_types), len(auth_types), *auth_types) @@ -239,8 +251,15 @@ class RfbClient(RfbClientStream): if auth_type not in auth_types: raise RfbError(f"Invalid VeNCrypt auth type: {auth_type}") - (auth_name, handler) = auth_types[auth_type] + (auth_name, tls, handler) = auth_types[auth_type] get_logger(0).info("[main] Client %s: Using %s auth type", self._remote, auth_name) + + if tls: + await self._write_struct("B", 1) # Ack + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.set_ciphers(self.__tls_ciphers) + await self._start_tls(ssl_context, self.__tls_timeout) + await handler() async def __handshake_security_vencrypt_userpass(self) -> None: diff --git a/kvmd/apps/vnc/rfb/stream.py b/kvmd/apps/vnc/rfb/stream.py index 843cfe54..49d86931 100644 --- a/kvmd/apps/vnc/rfb/stream.py +++ b/kvmd/apps/vnc/rfb/stream.py @@ -21,6 +21,7 @@ import asyncio +import ssl import struct from typing import Tuple @@ -102,6 +103,31 @@ class RfbClientStream: # ===== + async def _start_tls(self, ssl_context: ssl.SSLContext, ssl_timeout: float) -> None: + loop = asyncio.get_event_loop() + + ssl_reader = asyncio.StreamReader() + protocol = asyncio.StreamReaderProtocol(ssl_reader) + + transport = await loop.start_tls( + self.__writer.transport, + protocol, + ssl_context, + server_side=True, + ssl_handshake_timeout=ssl_timeout, + ) + + ssl_reader.set_transport(transport) + ssl_writer = asyncio.StreamWriter( + transport=transport, + protocol=protocol, + reader=ssl_reader, + loop=loop, + ) + + self.__reader = ssl_reader + self.__writer = ssl_writer + def _close(self) -> None: try: self.__writer.close() diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py index 084f9b0f..36be7944 100644 --- a/kvmd/apps/vnc/server.py +++ b/kvmd/apps/vnc/server.py @@ -59,29 +59,40 @@ class _SharedParams: class _Client(RfbClient): # pylint: disable=too-many-instance-attributes - def __init__( + def __init__( # pylint: disable=too-many-arguments self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, + tls_ciphers: str, + tls_timeout: float, + + desired_fps: int, + symmap: Dict[int, str], kvmd: KvmdClient, streamer: StreamerClient, - desired_fps: int, - symmap: Dict[int, str], vnc_credentials: Dict[str, VncAuthKvmdCredentials], - shared_params: _SharedParams, ) -> None: self.__vnc_credentials = vnc_credentials - super().__init__(reader, writer, vnc_passwds=list(vnc_credentials), **dataclasses.asdict(shared_params)) + super().__init__( + reader=reader, + writer=writer, + tls_ciphers=tls_ciphers, + tls_timeout=tls_timeout, + vnc_passwds=list(vnc_credentials), + **dataclasses.asdict(shared_params), + ) - self.__kvmd = kvmd - self.__streamer = streamer self.__desired_fps = desired_fps self.__symmap = symmap + + self.__kvmd = kvmd + self.__streamer = streamer + self.__shared_params = shared_params self.__authorized = asyncio.Future() # type: ignore @@ -271,32 +282,46 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes # ===== class VncServer: # pylint: disable=too-many-instance-attributes - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, port: int, max_clients: int, - kvmd: KvmdClient, - streamer: StreamerClient, - vnc_auth_manager: VncAuthManager, + tls_ciphers: str, + tls_timeout: float, desired_fps: int, symmap: Dict[int, str], + + kvmd: KvmdClient, + streamer: StreamerClient, + vnc_auth_manager: VncAuthManager, ) -> None: self.__host = host self.__port = port self.__max_clients = max_clients - self.__kvmd = kvmd - self.__streamer = streamer self.__vnc_auth_manager = vnc_auth_manager - self.__desired_fps = desired_fps - self.__symmap = symmap + shared_params = _SharedParams() + + async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + await _Client( + reader=reader, + writer=writer, + tls_ciphers=tls_ciphers, + tls_timeout=tls_timeout, + desired_fps=desired_fps, + symmap=symmap, + kvmd=kvmd, + streamer=streamer, + vnc_credentials=(await self.__vnc_auth_manager.read_credentials())[0], + shared_params=shared_params, + ).run() - self.__shared_params = _SharedParams() + self.__handle_client = handle_client def run(self) -> None: logger = get_logger(0) @@ -332,15 +357,3 @@ class VncServer: # pylint: disable=too-many-instance-attributes loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) loop.close() logger.info("Bye-bye") - - async def __handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: - await _Client( - reader=reader, - writer=writer, - kvmd=self.__kvmd, - streamer=self.__streamer, - desired_fps=self.__desired_fps, - symmap=self.__symmap, - vnc_credentials=(await self.__vnc_auth_manager.read_credentials())[0], - shared_params=self.__shared_params, - ).run() # type: ignore diff --git a/kvmd/validators/net.py b/kvmd/validators/net.py index 247122cd..7862c391 100644 --- a/kvmd/validators/net.py +++ b/kvmd/validators/net.py @@ -21,11 +21,13 @@ import ipaddress +import ssl from typing import List from typing import Callable from typing import Any +from . import ValidatorError from . import check_re_match from . import check_any @@ -75,3 +77,13 @@ def valid_port(arg: Any) -> int: def valid_mac(arg: Any) -> str: pattern = ":".join([r"[0-9a-fA-F]{2}"] * 6) return check_re_match(arg, "MAC address", pattern).lower() + + +def valid_ssl_ciphers(arg: Any) -> str: + name = "SSL ciphers" + arg = valid_stripped_string_not_empty(arg, name) + try: + ssl.SSLContext().set_ciphers(arg) + except Exception as err: + raise ValidatorError(f"The argument {arg!r} is not a valid {name}: {str(err)}") + return arg diff --git a/testenv/tests/validators/test_net.py b/testenv/tests/validators/test_net.py index 269db9e1..486154fc 100644 --- a/testenv/tests/validators/test_net.py +++ b/testenv/tests/validators/test_net.py @@ -30,6 +30,7 @@ from kvmd.validators.net import valid_ip from kvmd.validators.net import valid_rfc_host from kvmd.validators.net import valid_port from kvmd.validators.net import valid_mac +from kvmd.validators.net import valid_ssl_ciphers # ===== @@ -142,3 +143,15 @@ def test_ok__valid_mac(arg: Any) -> None: def test_fail__valid_mac(arg: Any) -> None: with pytest.raises(ValidatorError): print(valid_mac(arg)) + + +# ===== [email protected]("arg", ["ALL", " ALL:@SECLEVEL=0 "]) +def test_ok__valid_ssl_ciphers(arg: Any) -> None: + assert valid_ssl_ciphers(arg) == str(arg).strip() + + [email protected]("arg", ["test", "all", "", None]) +def test_fail__valid_ssl_ciphers(arg: Any) -> None: + with pytest.raises(ValidatorError): + print(valid_ssl_ciphers(arg)) |