mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
fix(console): novncd reaches a VNC console where it listens
- A console listening on a given address is reached on that address; the compute's host name is used only for a wildcard listener (0.0.0.0, ::, none). It was always the host name, which may resolve to another interface. - ::1 and localhost count as loopback like 127.0.0.0/8: such a console on a TCP or TLS host is refused instead of being tried on the host's name. - The console page says why such a console cannot open and what to change (listen on all interfaces, or add the host over SSH) instead of a generic connection error.
This commit is contained in:
parent
68b104dfe4
commit
9c4c525800
3 changed files with 73 additions and 8 deletions
|
|
@ -15,7 +15,7 @@ if ROOT_PATH not in sys.path:
|
|||
|
||||
django.setup()
|
||||
|
||||
import re
|
||||
import ipaddress
|
||||
import socket
|
||||
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
|
@ -105,6 +105,17 @@ def get_connection_infos(token):
|
|||
return compute.hostname, compute.login, compute.type, console_host, console_port, console_socket
|
||||
|
||||
|
||||
WILDCARD = (None, "", "0.0.0.0", "::")
|
||||
|
||||
|
||||
def is_loopback(address):
|
||||
"""127.0.0.0/8, ::1 or localhost: reachable only on the compute itself."""
|
||||
try:
|
||||
return ipaddress.ip_address(address).is_loopback
|
||||
except ValueError:
|
||||
return address == "localhost"
|
||||
|
||||
|
||||
class CompatibilityMixIn(object):
|
||||
def _new_client(self, daemon, socket_factory):
|
||||
# The page puts its own console's token on its websocket URL: a cookie
|
||||
|
|
@ -125,7 +136,7 @@ class CompatibilityMixIn(object):
|
|||
)
|
||||
|
||||
tunnel = None
|
||||
loopback = re.match(r"^127\.", console_host or "")
|
||||
loopback = is_loopback(console_host)
|
||||
if conntype == CONN_SSH:
|
||||
# the console travels inside the SSH connection, encrypted,
|
||||
# whatever address it listens on
|
||||
|
|
@ -146,9 +157,10 @@ class CompatibilityMixIn(object):
|
|||
self.msg(message)
|
||||
raise Exception(message)
|
||||
else:
|
||||
# Direct access
|
||||
self.msg("connecting to: %s:%s" % (connhost, console_port))
|
||||
tsock = socket_factory(connhost, console_port, connect=True)
|
||||
# the address the console listens on, or the compute's for a wildcard
|
||||
address = connhost if console_host in WILDCARD else console_host
|
||||
self.msg("connecting to: %s:%s" % (address, console_port))
|
||||
tsock = socket_factory(address, console_port, connect=True)
|
||||
|
||||
if self.verbose and not daemon:
|
||||
print(self.traffic_legend)
|
||||
|
|
|
|||
|
|
@ -48,12 +48,13 @@ class ConsoleViewsTestCase(TestCase):
|
|||
username="bob", email="bob@example.com", password="bobpassword"
|
||||
)
|
||||
|
||||
# SSH: novncd reaches any console listener (a TCP host's loopback one it cannot)
|
||||
cls.compute = Compute.objects.create(
|
||||
name="test-compute",
|
||||
hostname="127.0.0.1",
|
||||
login="root",
|
||||
password="",
|
||||
type=1,
|
||||
type=2,
|
||||
)
|
||||
|
||||
cls.instance = Instance.objects.create(
|
||||
|
|
@ -275,6 +276,15 @@ class ConsoleViewsTestCase(TestCase):
|
|||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTemplateNotUsed(response, "console-vnc-lite.html")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_a_tcp_hosts_loopback_console_says_why_it_cannot_open(self, mock_wvm):
|
||||
Compute.objects.filter(pk=self.compute.pk).update(type=1)
|
||||
mock_wvm.return_value.get_console_type.return_value = "vnc"
|
||||
mock_wvm.return_value.console_listens_locally.return_value = True
|
||||
self.client.force_login(self.admin_user)
|
||||
response = self.client.get(reverse("console") + f"?token={self.token}")
|
||||
self.assertContains(response, "This console listens only on its host")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_both_pages_connect_to_novncd_behind_the_panels_own_address(self, mock_wvm):
|
||||
# nginx proxies /novncd/ on the panel's own scheme, host and port: no
|
||||
|
|
@ -494,7 +504,8 @@ class NovncdDaemonLogicTestCase(TestCase):
|
|||
handler._new_client(daemon=False, socket_factory=socket_factory)
|
||||
|
||||
mock_get_info.assert_called_once_with("1-uuid-from-query")
|
||||
socket_factory.assert_called_once_with("host2", 5901, connect=True)
|
||||
# the address the console listens on
|
||||
socket_factory.assert_called_once_with("192.168.1.50", 5901, connect=True)
|
||||
|
||||
@patch.object(novncd_mod, "get_connection_infos")
|
||||
def test_a_local_socket_host_reaches_a_console_on_its_loopback(self, mock_get_info):
|
||||
|
|
@ -536,6 +547,37 @@ class NovncdDaemonLogicTestCase(TestCase):
|
|||
# the tunnel ends with the session
|
||||
close_tunnel.assert_called_once_with(open_tunnel.return_value[1])
|
||||
|
||||
def test_a_tcp_hosts_console_is_reached_where_it_listens(self):
|
||||
for console_host, reached in (("0.0.0.0", "kvm1.example.com"), ("::", "kvm1.example.com"),
|
||||
(None, "kvm1.example.com"), ("192.0.2.40", "192.0.2.40")):
|
||||
with self.subTest(console_host=console_host), patch.object(
|
||||
novncd_mod, "get_connection_infos",
|
||||
return_value=("kvm1.example.com", "", 1,
|
||||
console_host, 5900, None),
|
||||
):
|
||||
handler = self.CompatibilityMixIn()
|
||||
handler.path = "/novncd/?token=1-uuid"
|
||||
handler.msg = MagicMock()
|
||||
handler.do_proxy = MagicMock()
|
||||
handler.verbose = False
|
||||
socket_factory = MagicMock()
|
||||
handler._new_client(daemon=False, socket_factory=socket_factory)
|
||||
socket_factory.assert_called_once_with(reached, 5900, connect=True)
|
||||
|
||||
def test_a_tcp_hosts_loopback_console_is_refused(self):
|
||||
for console_host in ("127.0.0.1", "::1", "localhost"):
|
||||
with self.subTest(console_host=console_host), patch.object(
|
||||
novncd_mod, "get_connection_infos", return_value=("kvm1.example.com", "", 1, console_host, 5900, None),
|
||||
):
|
||||
handler = self.CompatibilityMixIn()
|
||||
handler.path = "/novncd/?token=1-uuid"
|
||||
handler.msg = MagicMock()
|
||||
handler.verbose = False
|
||||
socket_factory = MagicMock()
|
||||
with self.assertRaises(Exception):
|
||||
handler._new_client(daemon=False, socket_factory=socket_factory)
|
||||
socket_factory.assert_not_called()
|
||||
|
||||
@patch.object(novncd_mod, "get_connection_infos")
|
||||
def test_compatibility_mixin_missing_token_safe_exit(self, mock_get_info):
|
||||
handler = self.CompatibilityMixIn()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from instances.models import Instance
|
||||
from instances.utils import can_open_console
|
||||
from libvirt import libvirtError
|
||||
from vrtManager.connection import CONN_TCP, CONN_TLS
|
||||
from vrtManager.instance import wvmInstance
|
||||
|
||||
|
||||
|
|
@ -105,8 +106,11 @@ def console(request):
|
|||
console_websocket_port = conn.get_console_websocket_port()
|
||||
console_passwd = conn.get_console_passwd()
|
||||
running = conn.instance.isActive()
|
||||
# novncd reaches a console that listens on its host's loopback only
|
||||
# through SSH (or on this host)
|
||||
unreachable = instance.compute.type in (CONN_TCP, CONN_TLS) and conn.console_listens_locally()
|
||||
except (libvirtError, ValueError, IndexError, Instance.DoesNotExist):
|
||||
running = False
|
||||
running = unreachable = False
|
||||
console_type = None
|
||||
console_websocket_port = None
|
||||
console_passwd = None
|
||||
|
|
@ -114,6 +118,13 @@ def console(request):
|
|||
if console_type and not running: # a shut-off VM has no console to connect to
|
||||
console_error = _("The VM is not running: start it to open its console.")
|
||||
response = render(request, "console-vnc-lite.html", locals())
|
||||
elif console_type == "vnc" and unreachable:
|
||||
console_error = _(
|
||||
"This console listens only on its host, which the panel reaches over TCP or TLS. "
|
||||
"Let the console listen on all interfaces in the VM's console settings, "
|
||||
"or add the host as an SSH host."
|
||||
)
|
||||
response = render(request, "console-vnc-lite.html", locals())
|
||||
elif console_type == "vnc":
|
||||
console_page = "console-vnc-" + view_type + ".html"
|
||||
response = render(request, console_page, locals())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue