1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/test_gstfsd.py
catborise a6364f5d9b fix(gstfsd): append SSH keys, IPv6 loopback bind, server-side timeout
- Add the public key to authorized_keys instead of replacing the file.
- Use AF_INET6 when GSTFSD_BIND_HOST is an IPv6 address (binding ::1
  crashed with the AF_INET server).
- Close idle client connections after 30 s.

Tests run the real daemon against a small fake guestfs module over a
socket; this also exercises the earlier sendall() fix.
2026-10-03 00:02:01 +03:00

134 lines
4.7 KiB
Python

import json
import os
import signal
import socket
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
GSTFSD = Path(__file__).resolve().parent.parent / "conf" / "daemon" / "gstfsd"
# A stand-in for libguestfs: one partition whose files live in a JSON file.
FAKE_GUESTFS = '''
import json, os
STATE = os.environ["FAKE_GUESTFS_STATE"]
def _load():
with open(STATE) as f:
return json.load(f)
def _save(state):
with open(STATE, "w") as f:
json.dump(state, f)
class GuestFS:
def __init__(self, **kwargs): pass
def add_domain(self, name): pass
def launch(self): pass
def list_partitions(self): return ["/dev/sda1"]
def mount(self, part, mountpoint): pass
def umount(self, part): pass
def shutdown(self): pass
def close(self): pass
def is_file(self, path): return path in _load()["files"]
def is_dir(self, path): return path in _load()["dirs"]
def cat(self, path): return _load()["files"][path]
def write(self, path, content):
state = _load(); state["files"][path] = content; _save(state)
def mkdir(self, path):
state = _load(); state["dirs"].append(path); _save(state)
def chmod(self, mode, path): pass
'''
def free_port(family=socket.AF_INET, host="127.0.0.1"):
with socket.socket(family, socket.SOCK_STREAM) as s:
s.bind((host, 0))
return s.getsockname()[1]
class GstfsdTestCase(unittest.TestCase):
"""ROADMAP S-08: gstfsd hardening."""
def setUp(self):
# novncd (imported by other tests) sets SIGCHLD to SIG_IGN, which hides exit codes.
previous = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
self.addCleanup(signal.signal, signal.SIGCHLD, previous)
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
Path(self.tmp.name, "guestfs.py").write_text(FAKE_GUESTFS)
self.state = Path(self.tmp.name, "state.json")
self.state.write_text(json.dumps({
"files": {"/etc/shadow": "root:x:1::::::\n", "/root/.ssh/authorized_keys": "ssh-rsa OLD old@host\n"},
"dirs": ["/root/.ssh"],
}))
def _env(self, **extra):
return dict(
os.environ,
PYTHONPATH=self.tmp.name,
FAKE_GUESTFS_STATE=str(self.state),
**extra,
)
def _start(self, host, port, family=socket.AF_INET):
proc = subprocess.Popen(
[sys.executable, str(GSTFSD)],
env=self._env(GSTFSD_BIND_HOST=host, GSTFSD_PORT=str(port)),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
self.addCleanup(proc.wait)
self.addCleanup(proc.kill)
for _ in range(100):
if proc.poll() is not None:
self.fail("gstfsd exited: " + proc.stderr.read())
try:
with socket.socket(family, socket.SOCK_STREAM) as s:
s.connect((host, port))
return proc
except OSError:
time.sleep(0.05)
self.fail("gstfsd did not start listening")
def _request(self, host, port, data, family=socket.AF_INET):
with socket.socket(family, socket.SOCK_STREAM) as s:
s.settimeout(10)
s.connect((host, port))
s.sendall(json.dumps(data).encode())
return json.loads(s.recv(1024))
def test_refuses_non_loopback_bind_without_opt_in(self):
result = subprocess.run(
[sys.executable, str(GSTFSD)],
env=self._env(GSTFSD_BIND_HOST="0.0.0.0"),
capture_output=True,
text=True,
timeout=20,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("GSTFSD_ALLOW_REMOTE", result.stderr)
def test_public_key_is_appended_not_overwritten(self):
port = free_port()
self._start("127.0.0.1", port)
reply = self._request(
"127.0.0.1", port, {"action": "publickey", "vname": "vm", "key": "ssh-ed25519 NEW new@host"}
)
self.assertEqual(reply["return"], "success")
keys = json.loads(self.state.read_text())["files"]["/root/.ssh/authorized_keys"]
self.assertEqual(keys.splitlines(), ["ssh-rsa OLD old@host", "ssh-ed25519 NEW new@host"])
@unittest.skipUnless(socket.has_ipv6, "no IPv6")
def test_listens_on_ipv6_loopback(self):
try:
port = free_port(socket.AF_INET6, "::1")
except OSError:
self.skipTest("IPv6 loopback not available")
self._start("::1", port, socket.AF_INET6)
reply = self._request(
"::1", port, {"action": "publickey", "vname": "vm", "key": "ssh-ed25519 V6 v6@host"},
socket.AF_INET6,
)
self.assertEqual(reply["return"], "success")