mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
vnc: anon tls encryption
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user