Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

virtme: enable ssh support #210

Merged
merged 2 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,12 @@ Requirements
* Optionally, you may need virtiofsd 1.7.0 (or higher) for better filesystem
performance inside the virtme-ng guests.

* Optionally, you may need `socat` for the `--server` and `--client` options,
and the host's kernel should support VSOCK (`CONFIG_VHOST_VSOCK`).
* Optionally, you may need `socat` for the `--console` and
`--console-client` options, and the host's kernel should support VSOCK
(`CONFIG_VHOST_VSOCK`).
matttbe marked this conversation as resolved.
Show resolved Hide resolved

* Optionally, you may need `sshd` installed for the `--ssh` and
`--ssh-client` options.

Examples
========
Expand Down Expand Up @@ -382,10 +386,31 @@ Examples
- Connect to a simple remote shell (`socat` is required, VSOCK will be used):
```
# Start the vng instance with server support:
$ vng --server
$ vng --console

# In a separate terminal run the following command to connect to a remote shell:
$ vng --client
$ vng --console-client
```

- Enable ssh in the vng guest:
```
# Start the vng instance with ssh server support:
$ vng --ssh
matttbe marked this conversation as resolved.
Show resolved Hide resolved

# Connect to the vng guest from the host via ssh:
$ vng --ssh-client
```

- Generate some results inside the vng guest and copy them back to the
host using scp:
```
# Start the vng instance with SSH server support:
arighi@host~> vng --ssh
...
arighi@virtme-ng~> ./run.sh > result.txt

# In another terminal, copy result.txt from the guest to the host using scp:
arighi@gpd3~> scp -P 2222 localhost:~/result.txt .
```

- Run virtme-ng inside a docker container:
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def run(self):
package_files = [
"virtme-init",
"virtme-udhcpc-script",
"virtme-sshd-script",
"virtme-snapd-script",
"virtme-sound-script",
]
Expand Down
49 changes: 46 additions & 3 deletions virtme/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from shutil import which
from time import sleep
from base64 import b64encode
from virtme_ng.utils import CACHE_DIR
matttbe marked this conversation as resolved.
Show resolved Hide resolved
from .. import virtmods
from .. import modfinder
from .. import mkinitramfs
Expand Down Expand Up @@ -330,7 +331,7 @@ def make_parser() -> argparse.ArgumentParser:
)

g = parser.add_argument_group(title="Remote Console")
cli_srv_choices = ["console"]
cli_srv_choices = ["console", "ssh"]
g.add_argument(
"--server",
action="store",
Expand Down Expand Up @@ -968,6 +969,42 @@ def cleanup_console_script():
qemuargs.extend(["-device", "vhost-vsock-pci,guest-cid=%d" % args.port])


def ssh_client(args):
if args.remote_cmd is not None:
exec_escaped = args.remote_cmd.replace('"', '\\"')
remote_cmd = ["bash", "-c", exec_escaped]
else:
remote_cmd = []

cmd = ["ssh", "-p", str(args.port), "localhost"] + remote_cmd

if args.dry_run:
print(" ".join(cmd))
else:
os.execvp("ssh", cmd)


def ssh_server(args, arch, qemuargs, kernelargs):
# Check if we need to generate the ssh host keys for the guest.
ssh_key_dir = f"{CACHE_DIR}/.ssh"
matttbe marked this conversation as resolved.
Show resolved Hide resolved
matttbe marked this conversation as resolved.
Show resolved Hide resolved
os.makedirs(f"{ssh_key_dir}/etc/ssh", exist_ok=True)
os.system(f"ssh-keygen -A -f {ssh_key_dir}")

# Implicitly enable dhcp to automatically get an IP on the network
# interface and prevent interface renaming.
kernelargs.extend(["virtme.dhcp", "net.ifnames=0", "biosdevname=0"])

# Tell virtme-ng-init / virtme-init to start sshd and use the current
# username keys/credentials.
username = get_username()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if --user is passed? Should it eventually be:

username = args.user if args.user else get_username()

Or maybe you do want the current user, because if --user root is used, but vng is not launched as root, that will be an issue?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ssh should be allowed only for the user that runs vng, independently on the --user option. Otherwise it's a bit too tricky to figure out file permissions, etc. and we may have more problems than benefits.... In fact I think I may also ignore --user in --ssh-client and just allow the current user to log into the vng instance for now. It seems safer and simpler and we can improve the logic later if needed. :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for me to keep it simple for the moment. Should we have this "limitation" written somewhere?

kernelargs.extend(["virtme.ssh"])
kernelargs.extend([f"virtme_ssh_user={username}"])

# Setup a port forward network interface for the guest.
qemuargs.extend(["-device", "%s,netdev=ssh" % (arch.virtio_dev_type("net"))])
qemuargs.extend(["-netdev", "user,id=ssh,hostfwd=tcp::%d-:22" % args.port])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned on arighi/virtme-ng-init#16 (review), this should be modified:

"hostfwd=tcp:127.0.0.1:%d-:22" % args.port



# Allowed characters in mount paths. We can extend this over time if needed.
_SAFE_PATH_PATTERN = "[a-zA-Z0-9_+ /.-]+"
_RWDIR_RE = re.compile("^(%s)(?:=(%s))?$" % (_SAFE_PATH_PATTERN, _SAFE_PATH_PATTERN))
Expand All @@ -980,7 +1017,10 @@ def do_it() -> int:
if args.server is not None:
arg_fail('--client cannot be used with --server.')

console_client(args)
if args.client == 'console':
console_client(args)
elif args.client == 'ssh':
ssh_client(args)
sys.exit(0)

arch = architectures.get(args.arch)
Expand Down Expand Up @@ -1526,7 +1566,10 @@ def get_net_mac(index):
)

if args.server is not None:
console_server(args, qemu, arch, qemuargs, kernelargs)
if args.server == "console":
console_server(args, qemu, arch, qemuargs, kernelargs)
elif args.server == "ssh":
ssh_server(args, arch, qemuargs, kernelargs)

if args.pwd:
rel_pwd = os.path.relpath(os.getcwd(), args.root)
Expand Down
4 changes: 4 additions & 0 deletions virtme/guest/virtme-init
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ if cat /proc/cmdline |grep -q -E '(^| )virtme.dhcp($| )'; then
wait
fi

if cat /proc/cmdline |grep -q -E '(^| )virtme.ssh($| )'; then
$(dirname $0)/virtme-sshd-script
fi

if cat /proc/cmdline |grep -q -E '(^| )virtme.snapd($| )'; then
# If snapd is present in the system try to start it, to properly support snaps.
snapd_bin="/usr/lib/snapd/snapd";
Expand Down
39 changes: 39 additions & 0 deletions virtme/guest/virtme-sshd-script
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
#
# Initialize ssh server for remote connections (option `--server ssh`)

if [ -z "${virtme_ssh_user}" ]; then
echo "ssh: virtme_ssh_user is not defined" >&2
exit 1
fi

SSH_HOME=$(getent passwd "${virtme_ssh_user}" | cut -d: -f6)
if [ ! -e "${SSH_HOME}" ]; then
# Setup an arbitrary ssh location, just to be able to start sshd.
SSH_HOME=/run/ssh
fi

# Update authorized_keys by adding the user's public keys, but only if the
# changes are confined to the guest (no modifications made to the host).
#
# Overwriting authorized_keys is considered safe only when the guest rootfs
# is mounted as read-only, with an overlayfs on top to handle writes within
# the guest environment (`--rw` not specified as argument).
if grep ' / ' /proc/mounts | grep -q ' ro,'; then
matttbe marked this conversation as resolved.
Show resolved Hide resolved
cat "${SSH_HOME}"/.ssh/id_*.pub >> "${SSH_HOME}/.ssh/authorized_keys" 2>/dev/null
chown "${virtme_ssh_user}" "${SSH_HOME}/.ssh/authorized_keys" 2>/dev/null
fi

# Generate ssh host keys (if they don't exist already).
CACHE_DIR=${SSH_HOME}/.cache/virtme-ng/.ssh
mkdir -p "${CACHE_DIR}/etc/ssh"
ssh-keygen -A -f "${CACHE_DIR}"
ARGS=()
for key in "${CACHE_DIR}"/etc/ssh/ssh_host_*_key; do
ARGS+=(-h "${key}")
done

# Start sshd.
mkdir -p /run/sshd
rm -f /var/run/nologin
/usr/sbin/sshd "${ARGS[@]}"
95 changes: 66 additions & 29 deletions virtme_ng/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,32 +487,46 @@ def make_parser():
)

g_remote = parser.add_argument_group(title="Remote Console")
cli_srv_choices = ["console"]

g_remote.add_argument(
"--server",
"--console",
action="store",
const=cli_srv_choices[0],
nargs="?",
choices=cli_srv_choices,
help="Enable a server to communicate later from the host to the device using '--client'. "
type=int,
const=2222,
metavar="PORT",
help="Enable a server to communicate later from the host using '--console-client'. "
+ "By default, a simple console will be offered using a VSOCK connection, and 'socat' for the proxy."
)

g_remote.add_argument(
"--client",
"--console-client",
action="store",
nargs="?",
type=int,
const=2222,
metavar="PORT",
help="Connect to a VM launched with the '--console' option for a remote control.",
)

g_remote.add_argument(
"--ssh",
action="store",
const=cli_srv_choices[0],
nargs="?",
choices=cli_srv_choices,
help="Connect to a VM launched with the '--server' option for a remote control.",
type=int,
const=2222,
metavar="PORT",
help="Enable SSH server to communicate later from the host to using '--ssh-client'."
)

g_remote.add_argument(
"--port",
"--ssh-client",
action="store",
nargs="?",
type=int,
help="Unique port to communicate with a VM.",
const=2222,
metavar="PORT",
help="Connect to a VM launched with the '--ssh' option for a remote control.",
)

g_remote.add_argument(
Expand Down Expand Up @@ -992,23 +1006,44 @@ def _get_virtme_net_mac_address(self, args):
else:
self.virtme_param["net_mac_address"] = ""

def _get_virtme_server(self, args):
if args.server is not None:
self.virtme_param["server"] = "--server " + args.server
def _get_virtme_console(self, args):
if args.console is not None:
self.virtme_param["console"] = f"--server console --port {args.console}"
else:
self.virtme_param["console"] = ""

def _get_virtme_console_client(self, args):
if args.console is not None and args.console_client is not None:
arg_fail('--console cannot be used with --console-client', show_usage=False)

if args.console_client is not None:
self.virtme_param["console_client"] = f"--client console --port {args.console_client}"
else:
self.virtme_param["server"] = ""
self.virtme_param["console_client"] = ""

def _get_virtme_client(self, args):
if args.client is not None:
self.virtme_param["client"] = "--client " + args.client
def _get_virtme_ssh(self, args):
if args.console is not None and args.ssh is not None:
arg_fail('--console cannot be used with --ssh', show_usage=False)
matttbe marked this conversation as resolved.
Show resolved Hide resolved

if args.ssh is not None:
self.virtme_param["ssh"] = f"--server ssh --port {args.ssh}"
else:
self.virtme_param["client"] = ""
self.virtme_param["ssh"] = ""

def _get_virtme_ssh_client(self, args):
if args.console_client is not None and args.ssh_client is not None:
arg_fail('--console-client cannot be used with --ssh-client', show_usage=False)

if args.ssh is not None and args.ssh_client is not None:
arg_fail('--ssh cannot be used with --ssh-client', show_usage=False)

if args.console is not None and args.ssh_client is not None:
arg_fail('--console cannot be used with --ssh-client', show_usage=False)

def _get_virtme_port(self, args):
if args.port is not None:
self.virtme_param["port"] = "--port " + str(args.port)
if args.ssh_client is not None:
self.virtme_param["ssh_client"] = f"--client ssh --port {args.ssh_client}"
else:
self.virtme_param["port"] = ""
self.virtme_param["ssh_client"] = ""

def _get_virtme_remote_cmd(self, args):
if args.remote_cmd is not None:
Expand Down Expand Up @@ -1190,9 +1225,10 @@ def run(self, args):
self._get_virtme_mods(args)
self._get_virtme_network(args)
self._get_virtme_net_mac_address(args)
self._get_virtme_server(args)
self._get_virtme_client(args)
self._get_virtme_port(args)
self._get_virtme_console(args)
self._get_virtme_console_client(args)
self._get_virtme_ssh(args)
self._get_virtme_ssh_client(args)
self._get_virtme_remote_cmd(args)
self._get_virtme_disk(args)
self._get_virtme_sound(args)
Expand Down Expand Up @@ -1234,9 +1270,10 @@ def run(self, args):
+ f'{self.virtme_param["mods"]} '
+ f'{self.virtme_param["network"]} '
+ f'{self.virtme_param["net_mac_address"]} '
+ f'{self.virtme_param["server"]} '
+ f'{self.virtme_param["client"]} '
+ f'{self.virtme_param["port"]} '
+ f'{self.virtme_param["console"]} '
+ f'{self.virtme_param["console_client"]} '
+ f'{self.virtme_param["ssh"]} '
+ f'{self.virtme_param["ssh_client"]} '
+ f'{self.virtme_param["remote_cmd"]} '
+ f'{self.virtme_param["disk"]} '
+ f'{self.virtme_param["sound"]} '
Expand Down
2 changes: 1 addition & 1 deletion virtme_ng_init
Submodule virtme_ng_init updated 1 files
+11 −0 src/main.rs
Loading