Engine.IO accepts a connection, and holds one of socketiod's 40 threads
for it, from its first request; the console token is checked only in
Socket.IO's connect(), which a client need never send. Anyone who could
reach the panel, without an account, could open 40 websockets, answer
their pings, and no serial console would open for anybody (checked: the
41st handshake got no answer until they were closed).
The daemon's WSGI app now refuses a request whose session cookie names
no signed-in user, with 403, before Engine.IO sees it; the page always
sends that cookie. A token without its session no longer connects either,
as for novncd. Checked again: 60 such websockets are each answered 403 at
once and hold nothing.
The process tests sign their client in with a session cookie, as a
browser; python-socketio sends a custom Cookie header only with its first
request, so the client gets a cookie jar instead.
The token now names the console type it was made for: novncd accepts only
a VNC page's token and socketiod only a serial page's, so a VNC page's
token no longer opens the serial console without the VNC password.
A VNC session re-checks every 10 s, as the serial one did, that its user
may still open the console; a deactivated user or a removed owner loses
it. The check (may_still_open) moves to console.tokens for both daemons.
The serial page had a white strip of inline styles with an untranslated
status and a browser-default button above the terminal, sent input while
disconnected, sized the terminal only on window load and resize, and left
the keyboard elsewhere after the key menu. When a superuser opened a
console open in another window, its user saw "[An administrator opened
this console]" and a plain "disconnected".
- console-xterm.html shares the VNC page's bar: status badge (Connecting,
Connected, Disconnected, the server's refusal), Disconnect, then
Reconnect (a new page for a new token). The script is
static/js/console_serial.js; the token reaches it as json_script data.
- The terminal takes the keyboard on connect and after the key menu; input
is sent only while connected; it fits its box on connect and whenever
the box changes size (ResizeObserver).
- A console that writes nothing yet gets a dim local hint: press Enter.
- socketiod sends the displaced client "taken_over" with the superuser's
username; the page shows "Taken over by <name>" in the bar and the
terminal. The superuser is not asked first (owner's decision).
- Fullscreen uses the shared console_keys.js handler (keyboard lock).
- The worker told why a console did not open or closed only in the user's
terminal (as users see errors: a host that cannot be reached is not
named). It now also logs the full reason to socketiod's own stderr, the
journal or docker logs, which socketiod hands it as a descriptor.
- A refused token says the link has expired or the user may no longer
open the console: a valid link for a user who lost access was told only
that it had expired.
- A send that takes nothing (0 bytes) waits like a busy stream instead of
spinning.
- socketiod wrote input to the worker's terminal while holding the
session's lock, and the write could wait for the worker; the output
forwarder needs that lock. A large paste into a guest that echoes it
could stop both for good. The terminal is now non-blocking: input is
written as far as the worker takes it, and the wait for room is outside
the lock.
- The terminal is raw from the start. Until the worker made it raw, it
kept at most one 4 KiB line of a paste, and the worker's own setraw
dropped what was typed meanwhile; the worker no longer sets it.
- A superuser's takeover ends the old worker, and waits for it, before
starting the new one: libvirt gives a VM's console to one at a time.
- A resize while the worker ends no longer raises.
- novncd and socketiod log to stderr: the journal on hosts, docker logs in
the container (runit no longer sends them to unrotated files under
/var/log). Django's settings gave them the panel's own log file, which
every process rotated on its own, and made novncd's -v and -d have no
effect; both work again.
- A refused console says why in the log: novncd's denials are warnings
(they were shown only with -v), socketiod logs each refusal and who
opened which VM's serial console.
- The README's serial console notes say what #228 changed (access
re-checked, superuser takeover, reasons) and where the logs are.
- The console worker learns whose session it is and checks every ten
seconds that the user may still open the console: an owner removed or a
user deactivated meanwhile loses it, with a line in the terminal. Access
was checked only when the session started, which could last for days.
- One console per VM: a console left open in a forgotten tab locked
everyone out, superusers included. A superuser's connection now takes
the console over, and the other window is told so.
- socketiod's handlers run on their own threads. A connection is now
admitted under a lock, so two at once can no longer both open a VM's
console or pass the session cap. A session's terminal is used and closed
only under its own lock while it is open: a closed descriptor's number,
taken again by a new session, never receives another session's input
or gives it the other's output.
- A refused connection says why: serial console off, the link expired,
the VM's console open in another window, too many consoles. The page
shows it instead of "refused".
- The page no longer retries on its own with a token that has expired
after two minutes; Reconnect loads a new page. Input the browser cannot
encode no longer raises in the daemon.
pty.fork returns an inheritable master descriptor, so each new console
worker, and the ssh it may start, held the terminals of every other open
session until it ended. A worker now closes every descriptor above its own
terminal before it starts.
eventlet is in maintenance mode. socketiod now runs python-socketio in
threading mode under gunicorn (one gthread worker, which holds the
sessions), with simple-websocket, already a python-engineio dependency.
gunicorn waits 1 s on stop: an open console never finishes, and its
worker ends when the pty closes. The forward loop waits in select
instead of polling, and a stopped worker is reaped by a blocking
waitpid. eventlet leaves the requirements.
The xterm console never connected: socketiod expected a different token
format, its worker had no compute credentials, one global console was
shared by every client and its output broadcast to all of them, and the
Docker image did not run socketiod at all.
- console/tokens.py: one authorize() for noVNC and socketiod; the signed
token is the only credential, checked at connect time.
- socketiod: one pty worker per client (consolecallback with the
instance id), one session per VM, a session cap, input and resize
checked; the parent never opens libvirt. The engine.io same-origin
check is on.
- consolecallback: opens the console with the compute's credentials and
exits with a message when it cannot.
- Docker runs socketiod as a runit service; nginx proxies /socket.io/
with Host and the scheme for the origin check. Supervisor and systemd
start it as well; WEBVIRTCLOUD_SERIAL_CONSOLE=1 enables it.
- Tests: token and session unit tests, socketiod as a process, and a
live test that reads SeaBIOS from a wvc-test VM on the TCP and SSH
hosts.
When the serial console process exited on its own (a bad token, an
unreachable host, a killed consolecallback), the read task died on EIO
while fd and child_pid stayed set: the child was a zombie and every later
connection wrote to the dead pty until socketiod was restarted. A child
that failed before exec also returned into the server code and sent its
traceback to the client.
Now the read loop treats EOF/EIO as the end: it reaps the child, closes
the pty, tells the clients "[console closed]" and disconnects them, so
the next connection starts a new console. The child always leaves with
os._exit, also without a token; output is decoded incrementally, and the
unused imports are gone.
The web app (wsgi.py), novncd and socketiod set SIGCHLD to SIG_IGN at
import. The kernel then reaps every child at once, so subprocess loses
the exit status and a failing command reports 0, in the server and in
anything else run in that process: the test suite had to reset the
signal in two tests.
It was added in 2023 against one defunct bash per gunicorn worker and
per novncd. That bash came from the settings template of the time,
which ran .dec_ldap_pwd.sh with Popen and never waited for it; the
template stopped doing that in 561fedf. A settings.py still holding
that line leaves at most one zombie per process, not a growing number.
- wsgi.py, novncd, socketiod: the global SIG_IGN is removed. subprocess
reaps its own children, websockify installs its own SIGCHLD reaper
for novncd's connection processes, and novncd's SSH tunnels wait for
their ssh process.
- socketiod: the pty child execs consolecallback instead of running it
and then returning into the server's code; disconnect reaps that
child (waitpid) instead of any child (wait).
- The settings template drops its unused subprocess import; the two
test workarounds are removed.
New tests load the web app and novncd in a fresh interpreter and check
that SIGCHLD is untouched and a failing command still returns 1; both
fail on the previous code. A patched novncd in the test container
served five connections and left no child behind.
Comments, docstrings and live test names cited item IDs of a roadmap
that is not part of the repository. They now say what is meant, or
nothing where the sentence stands without them. Test assertions and
behavior are unchanged.
socketiod does not validate the console token, keeps a single global PTY
and broadcasts its output to every connected client, so anyone who can
reach port 6081 can read and write a serial console that someone opened.
It is also broken for legitimate users, since console/views.py now writes
signed tokens that socketiod cannot parse.
Until it is rewritten, turn the serial console off by default:
- Add SERIAL_CONSOLE_ENABLED = False to the settings template; installs
whose settings.py lacks it are treated as disabled.
- The console view shows an error instead of the xterm page for pty
consoles while the setting is off.
- socketiod refuses to start unless SERIAL_CONSOLE_ENABLED is True.
- Supervisor: autostart=false for socketiod; drop -d (debug) from the
supervisor and systemd definitions; fix the systemd unit description;
conf/systemd/install.sh no longer suggests enabling socketiod.
- README: stop opening port 6081 and document how to enable the serial
console on a trusted network.