Contain legacy VNC behind a loopback inetd listener
This commit is contained in:
1 parent
73b767d807
commit
815c5d752e
5 files changed
+56
-13
No files matched your search
@@ -13,7 +13,10 @@ capabilities, no privilege escalation, RuntimeDefault seccomp, no service-accoun
|
||||
token, and bounded ephemeral writable mounts. A short init container runs as
|
||||
UID 0 with the same restrictions solely to copy bundled noVNC assets out of the
|
||||
upstream image's private `/root` directory. It downloads nothing. X11 TCP is
|
||||
disabled and VNC listens on localhost; only noVNC HTTP/WebSocket is served.
|
||||
disabled; only noVNC HTTP/WebSocket is served. A tiny Python listener binds
|
||||
IPv4 loopback and starts x11vnc in inetd mode per connection. This avoids the
|
||||
upstream LibVNCServer IPv6 listener that ignores x11vnc's interface/disable
|
||||
flags. x11vnc itself opens no TCP listeners; CI verifies every listening socket.
|
||||
Chromium retains upstream's `--no-sandbox` compatibility flag.
|
||||
|
||||
One shared desktop is used by every authorized visitor. Closing your tab does
|
||||
|
||||
@@ -10,8 +10,10 @@ command=/usr/bin/Xvfb :0 -screen 0 %(ENV_RES)s -nolisten tcp
|
||||
autorestart=true
|
||||
|
||||
[program:x11vnc]
|
||||
command=/usr/bin/x11vnc -localhost -listen 127.0.0.1 -no6 -forever -shared -nopw
|
||||
command=/usr/bin/python /etc/oldipmi/vnc-listener.py
|
||||
autorestart=true
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
|
||||
[program:novnc]
|
||||
command=/usr/bin/python /opt/novnc/site/utils/websockify --web /opt/novnc/site 8080 localhost:5900
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/python
|
||||
"""Keep this old LibVNCServer behind an explicitly IPv4-loopback listener.
|
||||
|
||||
Its built-in IPv6 listener ignores x11vnc's interface/disable flags. Inetd mode
|
||||
uses an already-connected socket instead, so x11vnc opens no listening sockets.
|
||||
"""
|
||||
import SocketServer
|
||||
import subprocess
|
||||
|
||||
|
||||
class VNC(SocketServer.BaseRequestHandler):
|
||||
def handle(self):
|
||||
subprocess.call(
|
||||
['/usr/bin/x11vnc', '-inetd', '-shared', '-nopw'],
|
||||
stdin=self.request, stdout=self.request, close_fds=True)
|
||||
|
||||
|
||||
class LoopbackServer(SocketServer.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
LoopbackServer(('127.0.0.1', 5900), VNC).serve_forever()
|
||||
@@ -5,3 +5,5 @@ metadata:
|
||||
data:
|
||||
supervisord.conf: |
|
||||
{{ .Files.Get "files/supervisord.conf" | indent 4 }}
|
||||
vnc-listener.py: |
|
||||
{{ .Files.Get "files/vnc-listener.py" | indent 4 }}
|
||||
@@ -67,18 +67,28 @@ with tempfile.TemporaryDirectory(prefix='oldipmi-test-') as tmp:
|
||||
else:
|
||||
raise AssertionError('noVNC did not start')
|
||||
# A real WebSocket upgrade must reach the local X11 VNC server.
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=10) as ws:
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
ws.sendall((f'GET /websockify HTTP/1.1\r\nHost: localhost:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: binary\r\n\r\n').encode())
|
||||
received = b''
|
||||
while b'RFB ' not in received:
|
||||
data = ws.recv(4096)
|
||||
assert data, 'VNC handshake closed early'
|
||||
received += data
|
||||
assert b'101 Switching Protocols' in received
|
||||
for attempt in range(15):
|
||||
try:
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=10) as ws:
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
ws.sendall((f'GET /websockify HTTP/1.1\r\nHost: localhost:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: binary\r\n\r\n').encode())
|
||||
received = b''
|
||||
while b'RFB ' not in received:
|
||||
data = ws.recv(4096)
|
||||
assert data, 'VNC handshake closed early'
|
||||
received += data
|
||||
assert b'101 Switching Protocols' in received
|
||||
break
|
||||
except (AssertionError, OSError):
|
||||
if attempt == 14:
|
||||
raise
|
||||
time.sleep(1)
|
||||
listeners = run('podman', 'exec', cid, 'netstat', '-lntp')
|
||||
vnc_listeners = [line.split()[3] for line in listeners.splitlines() if 'x11vnc' in line]
|
||||
assert vnc_listeners and all(address.startswith('127.0.0.1:') for address in vnc_listeners), listeners
|
||||
assert not any('x11vnc' in line for line in listeners.splitlines()), listeners
|
||||
http_port = str(app['ports'][0]['containerPort'])
|
||||
for line in listeners.splitlines()[2:]:
|
||||
address = line.split()[3]
|
||||
assert address == '0.0.0.0:' + http_port or address.rsplit(':', 1)[0] == '127.0.0.1', listeners
|
||||
status = run('podman', 'exec', cid, 'cat', '/proc/1/status')
|
||||
assert 'NoNewPrivs:\t1' in status and 'CapEff:\t0000000000000000' in status
|
||||
assert run('podman', 'exec', cid, 'id', '-u') != '0'
|
||||
@@ -86,6 +96,9 @@ with tempfile.TemporaryDirectory(prefix='oldipmi-test-') as tmp:
|
||||
assert '/usr/lib/chromium-browser/chromium-browser' in processes
|
||||
assert '/usr/bin/Xvfb' in processes
|
||||
print('Non-root read-only runtime, browser, HTTP, WebSocket/RFB, and zero capabilities passed')
|
||||
except Exception:
|
||||
print(run('podman', 'exec', cid, 'sh', '-c', 'tail -n 25 /tmp/x11vnc-stderr* /tmp/novnc-stderr*'))
|
||||
raise
|
||||
finally:
|
||||
subprocess.run(['podman', 'rm', '-f', cid], check=True, stdout=subprocess.DEVNULL)
|
||||
subprocess.run(container_args(init) + ['--rm', '--network=none', init['image'], '/bin/sh', '-ec', 'rm -rf /opt/novnc/site'], check=True)
|
||||
Reference in new issue
Block a user