1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00

Merge branch 'fix/wave0-socketiod-disable' into wave0-integration

This commit is contained in:
catborise 2026-10-02 22:14:29 +03:00
commit dc90ab0437
9 changed files with 127 additions and 9 deletions

View file

@ -165,7 +165,7 @@ pip install -r conf/requirements.txt
python3 manage.py migrate
python3 manage.py collectstatic --noinput
# 6. Configure Supervisor (gunicorn, novncd and socketiod, running as nginx)
# 6. Configure Supervisor (gunicorn and novncd, running as nginx; socketiod is disabled by default)
sed 's/^user=.*/user=nginx/' conf/supervisor/webvirtcloud.conf | sudo tee /etc/supervisord.d/webvirtcloud.ini > /dev/null
# 7. Configure Nginx
@ -180,7 +180,6 @@ sudo setsebool -P httpd_can_network_connect on 2>/dev/null || true
sudo firewall-cmd --add-service=http --permanent 2>/dev/null || true
sudo firewall-cmd --add-port=6080/tcp --permanent 2>/dev/null || true
sudo firewall-cmd --add-port=6081/tcp --permanent 2>/dev/null || true
sudo firewall-cmd --reload 2>/dev/null || true
# 9. Start and enable services
@ -217,7 +216,7 @@ python3 manage.py collectstatic --noinput
# 6. Configure Nginx
sudo cp conf/nginx/suse_nginx.conf /etc/nginx/vhosts.d/webvirtcloud.conf 2>/dev/null || sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
# 7. Configure Supervisor (gunicorn, novncd and socketiod, running as nginx)
# 7. Configure Supervisor (gunicorn and novncd, running as nginx; socketiod is disabled by default)
sudo zypper --non-interactive install -y python3-supervisor || sudo zypper --non-interactive install -y supervisor
sudo mkdir -p /etc/supervisord.d
sed 's/^user=.*/user=nginx/' conf/supervisor/webvirtcloud.conf | sudo tee /etc/supervisord.d/webvirtcloud.ini > /dev/null
@ -279,7 +278,7 @@ python manage.py runserver 0.0.0.0:8000 --settings=webvirtcloud.settings-dev --n
Open `http://127.0.0.1:8000`. `settings-dev` enables `DEBUG` and the Django Debug Toolbar. `--nostatic` is required: the CSS/JS live in `static/` (`STATIC_ROOT`), which WhiteNoise serves but runserver's own static handler does not.
For the browser consoles, start the websocket proxies in separate terminals: `python console/novncd` (VNC, port 6080) and `python console/socketiod` (serial/xterm.js, port 6081).
For the browser console, start the websocket proxy in a separate terminal: `python console/novncd` (VNC, port 6080). The serial console (`python console/socketiod`, port 6081) is disabled by default; see [Serial Console](#serial-console-disabled-by-default).
## Compute Node (Hypervisor) Setup
@ -380,6 +379,15 @@ datasource:
metadata_urls: [ "http://webvirtcloud.domain.com/datasource" ]
```
### Serial Console (disabled by default)
The xterm.js serial console is served by `console/socketiod`, which does **not authenticate connections yet**: anyone who can reach its port (6081) can read and write a serial console that someone has opened. Until this is fixed, the serial console is disabled:
- the console page shows "Serial console is disabled" for VMs with a `pty` console, and
- `socketiod` refuses to start, and is not started by the bundled supervisor/systemd configs.
To enable it anyway on a trusted network, set `SERIAL_CONSOLE_ENABLED = True` in `webvirtcloud/settings.py`, set `autostart=true` for `[program:socketiod]` in the supervisor config (or `systemctl enable --now webvirt-socketiod`), and restrict access to port 6081 with a firewall. Existing installs whose `settings.py` predates this setting are treated as disabled.
### Reverse-Proxy & Port Forwarding
If WebVirtCloud runs behind a reverse proxy terminating SSL or forwarding port 80/443, set the public console ports in `webvirtcloud/settings.py` (defaults: 6080 for noVNC, 6081 for Socket.IO). The bundled nginx config already proxies `/novncd/` and `/socket.io/`:

View file

@ -14,10 +14,13 @@ autostart=true
autorestart=true
redirect_stderr=true
# Serial console daemon. Disabled by default because it does not authenticate
# connections yet; set SERIAL_CONSOLE_ENABLED = True in settings.py and
# autostart=true here only on a trusted network.
[program:socketiod]
command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/socketiod -d
command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/socketiod
directory=/srv/webvirtcloud
user=www-data
autostart=true
autostart=false
autorestart=true
redirect_stderr=true

View file

@ -4,4 +4,7 @@ SCRIPTPATH=$(dirname "$FILEPATH");
cd "$SCRIPTPATH"
cp webvirt-* /lib/systemd/system/
cp webvirt /etc/default/
echo Run to start services \"systemctl daemon-reload\; systemctl enable --now $(ls webvirt-* | tr "\n" " ")\"
# webvirt-socketiod (serial console) is not enabled by default: it does not
# authenticate connections yet and refuses to start unless
# SERIAL_CONSOLE_ENABLED = True is set in settings.py.
echo Run to start services \"systemctl daemon-reload\; systemctl enable --now $(ls webvirt-* | grep -v socketiod | tr "\n" " ")\"

View file

@ -1,5 +1,5 @@
[Unit]
Description=Webvirt Cloud NOVNCD
Description=Webvirt Cloud serial console (socketiod; requires SERIAL_CONSOLE_ENABLED = True)
Wants=network-online.target
After=network-online.target
@ -13,7 +13,7 @@ LimitNOFILE=64000
AmbientCapabilities=CAP_NET_BIND_SERVICE
EnvironmentFile=/etc/default/webvirt
ExecStart=/bin/bash -c "cd ${WEBVIRT_PATH};${WEBVIRT_PATH}/venv/bin/python3 ${WEBVIRT_PATH}/console/socketiod -d"
ExecStart=/bin/bash -c "cd ${WEBVIRT_PATH};${WEBVIRT_PATH}/venv/bin/python3 ${WEBVIRT_PATH}/console/socketiod"
# Make sure stderr/stdout is captured in the systemd journal.
StandardOutput=journal

View file

@ -17,6 +17,19 @@ if ROOT_PATH not in sys.path:
django.setup()
from django.conf import settings as django_settings
# socketiod does not authenticate connections yet (ROADMAP S-01): any client
# that can reach the port can read and write an open serial console. Refuse
# to start unless an admin explicitly opts in.
if not getattr(django_settings, "SERIAL_CONSOLE_ENABLED", False):
sys.stderr.write(
"socketiod: the serial console is disabled (SERIAL_CONSOLE_ENABLED is not "
"True in settings). It is unauthenticated; only enable it on a trusted "
"network.\n"
)
sys.exit(1)
import re
import tempfile
import io

View file

@ -0,0 +1,57 @@
import os
import signal
import subprocess
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SOCKETIOD = ROOT / "console" / "socketiod"
class SocketiodDisabledTestCase(unittest.TestCase):
"""ROADMAP S-01: socketiod must refuse to start unless explicitly enabled."""
def test_socketiod_exits_when_serial_console_disabled(self):
settings_dir = ROOT / "console" / "_test_settings"
settings_dir.mkdir(exist_ok=True)
try:
(settings_dir / "__init__.py").write_text("")
(settings_dir / "disabled.py").write_text(
"from webvirtcloud.settings import * # noqa\n"
"SERIAL_CONSOLE_ENABLED = False\n"
"SOCKETIO_HOST = '127.0.0.1'\n"
"SOCKETIO_PORT = 0\n"
)
env = dict(
os.environ,
DJANGO_SETTINGS_MODULE="console._test_settings.disabled",
PYTHONPATH=str(ROOT),
)
# Other test modules import novncd, which sets SIGCHLD to SIG_IGN in
# this process; with that, the child's exit status is lost (always 0).
previous_sigchld = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
try:
result = subprocess.run(
[sys.executable, str(SOCKETIOD)],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
timeout=20,
)
except subprocess.TimeoutExpired:
self.fail("socketiod kept running although SERIAL_CONSOLE_ENABLED=False")
finally:
signal.signal(signal.SIGCHLD, previous_sigchld)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("SERIAL_CONSOLE_ENABLED", result.stderr)
finally:
for f in settings_dir.glob("*"):
if f.is_file():
f.unlink()
for d in settings_dir.glob("__pycache__"):
for f in d.glob("*"):
f.unlink()
d.rmdir()
settings_dir.rmdir()

View file

@ -121,6 +121,7 @@ class ConsoleViewsTestCase(TestCase):
self.assertTemplateUsed(response, "console-vnc-lite.html")
self.assertIn("SPICE", str(response.context["console_error"]))
@override_settings(SERIAL_CONSOLE_ENABLED=True)
@patch("console.views.wvmInstance")
def test_console_superuser_pty(self, mock_wvm):
mock_conn = MagicMock()
@ -134,6 +135,23 @@ class ConsoleViewsTestCase(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "console-xterm.html")
@override_settings(SERIAL_CONSOLE_ENABLED=False)
@patch("console.views.wvmInstance")
def test_console_pty_refused_when_serial_console_disabled(self, mock_wvm):
# ROADMAP S-01: socketiod is unauthenticated, so the serial console is
# disabled unless an admin explicitly enables it.
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "pty"
mock_wvm.return_value = mock_conn
self.client.force_login(self.admin_user)
url = reverse("console") + f"?token={self.token}"
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateNotUsed(response, "console-xterm.html")
self.assertIn("Serial console is disabled", str(response.context["console_error"]))
@patch("console.views.wvmInstance")
def test_console_user_assigned_instance_access(self, mock_wvm):
mock_conn = MagicMock()

View file

@ -133,6 +133,16 @@ def console(request):
if console_type == "vnc":
console_page = "console-vnc-" + view_type + ".html"
response = render(request, console_page, locals())
elif console_type == "pty" and not getattr(
settings, "SERIAL_CONSOLE_ENABLED", False
):
# socketiod does not authenticate connections yet (ROADMAP S-01), so
# the serial console stays off unless an admin explicitly enables it.
console_error = _(
"Serial console is disabled on this server. "
"Please switch VM graphics to VNC or ask an administrator."
)
response = render(request, "console-vnc-lite.html", locals())
elif console_type == "pty":
socketio_public_host = getattr(settings, "SOCKETIO_PUBLIC_HOST", None)
socketio_public_port = getattr(settings, "SOCKETIO_PUBLIC_PORT", 6081)

View file

@ -218,6 +218,12 @@ WS_PUBLIC_PATH = "novncd/"
# Websock Certificate for SSL
WS_CERT = None
# Serial console (xterm.js over socketiod). Disabled by default: socketiod does
# not authenticate connections yet, so anyone who can reach SOCKETIO_PORT can
# read and write an open serial console. Only enable it on a trusted network,
# and also start the socketiod service (autostart=false in the supervisor config).
SERIAL_CONSOLE_ENABLED = False
SOCKETIO_PORT = 6081
SOCKETIO_HOST = "0.0.0.0"