1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/conf/daemon/gstfsd
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

92 lines
3.4 KiB
Python

#!/usr/bin/env python
#
# gstfsd - WebVirtCloud daemon for managing VM's filesystem
#
import ipaddress
import os
import socketserver
import json
import re
import socket
import sys
PORT = int(os.environ.get("GSTFSD_PORT", 16510))
ADDRESS = os.environ.get("GSTFSD_BIND_HOST", "127.0.0.1")
def is_loopback(host):
try:
return ipaddress.ip_address(host).is_loopback
except ValueError:
return host == "localhost"
# gstfsd is an unauthenticated root service that rewrites any VM's
# /etc/shadow and authorized_keys, so it only listens on loopback unless
# explicitly allowed (ROADMAP S-08).
if not is_loopback(ADDRESS) and os.environ.get("GSTFSD_ALLOW_REMOTE") != "1":
sys.stderr.write(
"gstfsd: refusing to listen on %s (unauthenticated root service). "
"Set GSTFSD_ALLOW_REMOTE=1 to allow it on a trusted network.\n" % ADDRESS
)
sys.exit(1)
import guestfs # noqa: E402
class MyTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
address_family = socket.AF_INET6 if ":" in ADDRESS else socket.AF_INET
class MyTCPServerHandler(socketserver.StreamRequestHandler):
timeout = 30 # seconds; an idle client must not hold a thread forever
def handle(self):
# recive data
d = self.request.recv(1024).strip()
data = json.loads(d)
# GuestFS
gfs = guestfs.GuestFS(python_return_dict=True)
try:
gfs.add_domain(data['vname'])
gfs.launch()
parts = gfs.list_partitions()
for part in parts:
try:
gfs.mount(part, '/')
if gfs.is_file('/etc/shadow'):
if data['action'] == 'password':
file_shadow = gfs.cat('/etc/shadow')
new_root_hash = "root:" + data['passwd'] + ":"
file_shadow_new = re.sub('^root:.*?:', new_root_hash, file_shadow)
gfs.write('/etc/shadow', file_shadow_new)
gfs.chmod(0o640, '/etc/shadow')
self.request.sendall(json.dumps({'return': 'success'}).encode())
if data['action'] == 'publickey':
if not gfs.is_dir('/root/.ssh'):
gfs.mkdir('/root/.ssh')
gfs.chmod(0o700, "/root/.ssh")
# Add the key; keep the ones already there.
keys_path = '/root/.ssh/authorized_keys'
keys = gfs.cat(keys_path) if gfs.is_file(keys_path) else ''
new_key = data['key'].strip()
if new_key not in keys.splitlines():
if keys and not keys.endswith('\n'):
keys += '\n'
gfs.write(keys_path, keys + new_key + '\n')
gfs.chmod(0o600, '/root/.ssh/authorized_keys')
self.request.sendall(json.dumps({'return': 'success'}).encode())
gfs.umount(part)
except RuntimeError:
pass
gfs.shutdown()
gfs.close()
except Exception as err:
self.request.sendall(bytes(json.dumps({'return': 'error', 'message': str(err)}).encode()))
server = MyTCPServer((ADDRESS, PORT), MyTCPServerHandler)
server.serve_forever()