vnc: anon tls encryption

This commit is contained in:
Devaev Maxim
2020-04-23 11:17:22 +03:00
parent 820ef17871
commit 75669466cf
7 changed files with 131 additions and 37 deletions

View File

@@ -69,6 +69,7 @@ from ..validators.net import valid_ip_or_host
from ..validators.net import valid_ip from ..validators.net import valid_ip
from ..validators.net import valid_port from ..validators.net import valid_port
from ..validators.net import valid_mac 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_quality
from ..validators.kvm import valid_stream_fps from ..validators.kvm import valid_stream_fps
@@ -328,8 +329,11 @@ def _get_config_scheme() -> Dict:
"server": { "server": {
"host": Option("::", type=valid_ip_or_host), "host": Option("::", type=valid_ip_or_host),
"port": Option(5900, type=valid_port), "port": Option(5900, type=valid_port),
# TODO: timeout
"max_clients": Option(10, type=(lambda arg: valid_number(arg, min=1))), "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": { "kvmd": {

View File

@@ -42,10 +42,17 @@ def main(argv: Optional[List[str]]=None) -> None:
# pylint: disable=protected-access # pylint: disable=protected-access
VncServer( 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()), kvmd=KvmdClient(**config.kvmd._unpack()),
streamer=StreamerClient(**config.streamer._unpack()), streamer=StreamerClient(**config.streamer._unpack()),
vnc_auth_manager=VncAuthManager(**config.auth.vncauth._unpack()), vnc_auth_manager=VncAuthManager(**config.auth.vncauth._unpack()),
desired_fps=config.desired_fps,
symmap=build_symmap(config.keymap),
**config.server._unpack(),
).run() ).run()

View File

@@ -21,6 +21,7 @@
import asyncio import asyncio
import ssl
from typing import Tuple from typing import Tuple
from typing import List 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://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst
# https://www.toptal.com/java/implementing-remote-framebuffer-server-java # https://www.toptal.com/java/implementing-remote-framebuffer-server-java
# https://github.com/TigerVNC/tigervnc # https://github.com/TigerVNC/tigervnc
@@ -54,6 +55,8 @@ class RfbClient(RfbClientStream):
self, self,
reader: asyncio.StreamReader, reader: asyncio.StreamReader,
writer: asyncio.StreamWriter, writer: asyncio.StreamWriter,
tls_ciphers: str,
tls_timeout: float,
width: int, width: int,
height: int, height: int,
@@ -63,6 +66,9 @@ class RfbClient(RfbClientStream):
super().__init__(reader, writer) super().__init__(reader, writer)
self.__tls_ciphers = tls_ciphers
self.__tls_timeout = tls_timeout
self._width = width self._width = width
self._height = height self._height = height
self.__name = name self.__name = name
@@ -98,7 +104,7 @@ class RfbClient(RfbClientStream):
raise raise
except RfbConnectionError as err: except RfbConnectionError as err:
logger.info("[%s] Client %s: Gone (%s): Disconnected", name, self._remote, str(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)) logger.error("[%s] Client %s: %s: Disconnected", name, self._remote, str(err))
except Exception: except Exception:
logger.exception("[%s] Unhandled exception with client %s: Disconnected", name, self._remote) 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) 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: if self.__vnc_passwds:
# Vinagre не умеет работать с VNC Auth через VeNCrypt, но это его проблемы, # Vinagre не умеет работать с VNC Auth через VeNCrypt, но это его проблемы,
# так как он своеобразно трактует рекомендации VeNCrypt. # так как он своеобразно трактует рекомендации VeNCrypt.
# Подробнее: https://bugzilla.redhat.com/show_bug.cgi?id=692048 # Подробнее: https://bugzilla.redhat.com/show_bug.cgi?id=692048
# Hint: используйте любой другой нормальный VNC-клиент. # 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) 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: if auth_type not in auth_types:
raise RfbError(f"Invalid VeNCrypt auth type: {auth_type}") 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) 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() await handler()
async def __handshake_security_vencrypt_userpass(self) -> None: async def __handshake_security_vencrypt_userpass(self) -> None:

View File

@@ -21,6 +21,7 @@
import asyncio import asyncio
import ssl
import struct import struct
from typing import Tuple 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: def _close(self) -> None:
try: try:
self.__writer.close() self.__writer.close()

View File

@@ -59,29 +59,40 @@ class _SharedParams:
class _Client(RfbClient): # pylint: disable=too-many-instance-attributes class _Client(RfbClient): # pylint: disable=too-many-instance-attributes
def __init__( def __init__( # pylint: disable=too-many-arguments
self, self,
reader: asyncio.StreamReader, reader: asyncio.StreamReader,
writer: asyncio.StreamWriter, writer: asyncio.StreamWriter,
tls_ciphers: str,
tls_timeout: float,
desired_fps: int,
symmap: Dict[int, str],
kvmd: KvmdClient, kvmd: KvmdClient,
streamer: StreamerClient, streamer: StreamerClient,
desired_fps: int,
symmap: Dict[int, str],
vnc_credentials: Dict[str, VncAuthKvmdCredentials], vnc_credentials: Dict[str, VncAuthKvmdCredentials],
shared_params: _SharedParams, shared_params: _SharedParams,
) -> None: ) -> None:
self.__vnc_credentials = vnc_credentials 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.__desired_fps = desired_fps
self.__symmap = symmap
self.__kvmd = kvmd self.__kvmd = kvmd
self.__streamer = streamer self.__streamer = streamer
self.__desired_fps = desired_fps
self.__symmap = symmap
self.__shared_params = shared_params self.__shared_params = shared_params
self.__authorized = asyncio.Future() # type: ignore 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 class VncServer: # pylint: disable=too-many-instance-attributes
def __init__( def __init__( # pylint: disable=too-many-arguments
self, self,
host: str, host: str,
port: int, port: int,
max_clients: int, max_clients: int,
kvmd: KvmdClient, tls_ciphers: str,
streamer: StreamerClient, tls_timeout: float,
vnc_auth_manager: VncAuthManager,
desired_fps: int, desired_fps: int,
symmap: Dict[int, str], symmap: Dict[int, str],
kvmd: KvmdClient,
streamer: StreamerClient,
vnc_auth_manager: VncAuthManager,
) -> None: ) -> None:
self.__host = host self.__host = host
self.__port = port self.__port = port
self.__max_clients = max_clients self.__max_clients = max_clients
self.__kvmd = kvmd
self.__streamer = streamer
self.__vnc_auth_manager = vnc_auth_manager self.__vnc_auth_manager = vnc_auth_manager
self.__desired_fps = desired_fps shared_params = _SharedParams()
self.__symmap = symmap
self.__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.__handle_client = handle_client
def run(self) -> None: def run(self) -> None:
logger = get_logger(0) 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.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
loop.close() loop.close()
logger.info("Bye-bye") 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

View File

@@ -21,11 +21,13 @@
import ipaddress import ipaddress
import ssl
from typing import List from typing import List
from typing import Callable from typing import Callable
from typing import Any from typing import Any
from . import ValidatorError
from . import check_re_match from . import check_re_match
from . import check_any from . import check_any
@@ -75,3 +77,13 @@ def valid_port(arg: Any) -> int:
def valid_mac(arg: Any) -> str: def valid_mac(arg: Any) -> str:
pattern = ":".join([r"[0-9a-fA-F]{2}"] * 6) pattern = ":".join([r"[0-9a-fA-F]{2}"] * 6)
return check_re_match(arg, "MAC address", pattern).lower() 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

View File

@@ -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_rfc_host
from kvmd.validators.net import valid_port from kvmd.validators.net import valid_port
from kvmd.validators.net import valid_mac 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: def test_fail__valid_mac(arg: Any) -> None:
with pytest.raises(ValidatorError): with pytest.raises(ValidatorError):
print(valid_mac(arg)) print(valid_mac(arg))
# =====
@pytest.mark.parametrize("arg", ["ALL", " ALL:@SECLEVEL=0 "])
def test_ok__valid_ssl_ciphers(arg: Any) -> None:
assert valid_ssl_ciphers(arg) == str(arg).strip()
@pytest.mark.parametrize("arg", ["test", "all", "", None])
def test_fail__valid_ssl_ciphers(arg: Any) -> None:
with pytest.raises(ValidatorError):
print(valid_ssl_ciphers(arg))