diff --git a/bin/recipes/gnome/notes.md b/bin/recipes/gnome/notes.md new file mode 100644 index 0000000..29ed46b --- /dev/null +++ b/bin/recipes/gnome/notes.md @@ -0,0 +1 @@ +sudo apt-get install task-gnome-desktop \ No newline at end of file diff --git a/bin/recipes/net/net-up b/bin/recipes/net/net-up new file mode 100755 index 0000000..faa413a --- /dev/null +++ b/bin/recipes/net/net-up @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +sudo mkdir -p /etc/systemd/network +cat <<'EOF' | sudo tee /etc/systemd/network/20-enp0s1.network +[Match] +Name=enp0s1 + +[Network] +DHCP=yes +EOF + +sudo systemctl enable --now systemd-networkd +sudo systemctl enable --now systemd-resolved + +sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf + +sudo ip link set enp0s1 up +sudo networkctl reload +sudo networkctl reconfigure enp0s1 + diff --git a/bin/recipes/net/notes.md b/bin/recipes/net/notes.md new file mode 100644 index 0000000..84b1c01 --- /dev/null +++ b/bin/recipes/net/notes.md @@ -0,0 +1,16 @@ +# bring the link up +sudo ip link set enp0s1 up + +# give yourself an address in QEMU's usernet +sudo ip addr add 10.0.2.15/24 dev enp0s1 + +# default gateway for slirp +sudo ip route add default via 10.0.2.2 + +# DNS via slirp (or use 1.1.1.1 if you prefer) +echo "nameserver 10.0.2.3" | sudo tee /etc/resolv.conf > /dev/null + +# sanity checks +ip -4 a show enp0s1 +ping -c2 10.0.2.2 +ping -c2 deb.debian.org diff --git a/bin/recipes/vnc/notes.md b/bin/recipes/vnc/notes.md new file mode 100644 index 0000000..8525f78 --- /dev/null +++ b/bin/recipes/vnc/notes.md @@ -0,0 +1,93 @@ +Here’s what those two pieces typically look like in a QEMU VNC + TLS + SASL setup. + +# `/etc/pki/qemu` (TLS x509 creds) + +QEMU’s `-object tls-creds-x509,...,dir=/etc/pki/qemu,endpoint=server` expects this directory to hold the server’s cert/key and the CA it should trust for client certs. + +Example layout: + +``` +/etc/pki/qemu/ +├── ca-cert.pem # CA cert used to verify client certificates (if verify-peer=yes) +├── server-cert.pem # Server certificate (CN should match the host, or use subjectAltName) +├── server-key.pem # Private key for server-cert.pem (chmod 600, root-only) +└── crl.pem # (optional) Certificate Revocation List +``` + +Typical QEMU arg: + +``` +-object tls-creds-x509,id=tls0,dir=/etc/pki/qemu,endpoint=server,verify-peer=yes +-vnc :0,tls-creds=tls0,sasl +``` + +Notes: + +* `verify-peer=yes` means the VNC client must present a client certificate signed by `ca-cert.pem`. Omit or set `no` if you only want encryption without client certs (and rely on SASL for auth). +* File names above are the conventional ones QEMU looks for in that dir in server mode. +* Permissions matter: keep `server-key.pem` readable only by the QEMU user (e.g., `chmod 600`). + +(If you wanted the client side for certificate auth, you’d create a *client* bundle with `client-cert.pem` / `client-key.pem` and give the server’s CA to the client.) + +# `/etc/sasl2/qemu.conf` (SASL settings) + +This file tells Cyrus SASL how to authenticate for the “qemu” service. A simple, local-password (sasldb) setup might be: + +``` +# /etc/sasl2/qemu.conf +mech_list: scram-sha-256 digest-md5 +pwcheck_method: auxprop +auxprop_plugin: sasldb +sasldb_path: /etc/sasldb2 +``` + +What each line does: + +* `mech_list`: Which SASL mechanisms to allow. (If your build doesn’t have SCRAM, use `digest-md5` and/or `plain`/`login` — but prefer SCRAM or DIGEST over PLAIN.) +* `pwcheck_method: auxprop` + `auxprop_plugin: sasldb`: use the local SASL database. +* `sasldb_path`: where the password DB lives (default is `/etc/sasldb2` on many distros). + +Create users in the sasldb: + +```bash +sudo saslpasswd2 -a qemu -c alice +# (it will prompt for a password) +sudo sasldblistusers2 -f /etc/sasldb2 # verify entry exists +``` + +Then run QEMU with both TLS and SASL: + +```bash +qemu-system-x86_64 \ + -object tls-creds-x509,id=tls0,dir=/etc/pki/qemu,endpoint=server,verify-peer=no \ + -vnc :0,tls-creds=tls0,sasl +``` + +* Client connects over **TLS** (encrypted), then is prompted for **SASL username/password** (“alice” and the password you set). +* If you set `verify-peer=yes`, the client must also present a valid client cert signed by your CA. + +## Quick OpenSSL one-liners (for testing) + +> For production, use a proper CA workflow and strong key handling. + +```bash +# CA +openssl genrsa -out ca.key 4096 +openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca-cert.pem -subj "/CN=QEMU Test CA" + +# Server cert +openssl genrsa -out server-key.pem 2048 +openssl req -new -key server-key.pem -out server.csr -subj "/CN=your.host.name" +openssl x509 -req -in server.csr -CA ca-cert.pem -CAkey ca.key -CAcreateserial -out server-cert.pem -days 825 -sha256 +install -m 600 server-key.pem /etc/pki/qemu/ +install -m 644 server-cert.pem ca-cert.pem /etc/pki/qemu/ +``` + +## Common pitfalls + +* **Wrong filenames/paths** in `/etc/pki/qemu` → QEMU won’t find the certs. +* **Permissions too open** on `server-key.pem` → QEMU may refuse or it’s a security risk. +* **SASL mechanism mismatch** → ensure your client supports one from `mech_list`. +* **No TLS but SASL with PLAIN/LOGIN** → credentials go over the wire unencrypted; always pair PLAIN/LOGIN with TLS. + +If you tell me your distro, I can tailor the exact package names (Cyrus SASL modules) and service paths. diff --git a/bin/sq b/bin/sq new file mode 100755 index 0000000..78b9236 --- /dev/null +++ b/bin/sq @@ -0,0 +1,100 @@ +#!/usr/bin/env ruby +require 'socket' + +# Path to QEMU monitor socket +sock_path = "/tmp/qemu-monitor.sock" + +# Connect to UNIX socket +sock = UNIXSocket.new(sock_path) + +# Read greeting from QEMU (optional) +puts sock.readline rescue nil + +# Function to send a single keystroke +def send_key(sock, key) + cmd = "sendkey #{key}\n" + puts ">>> #{cmd.strip}" + sock.write(cmd) +end + +def char_to_key(ch) + case ch + # Control characters + when "\n" then "ret" + when "\t" then "tab" + when " " then "spc" + when "\e" then "esc" + when "\b" then "backspace" + + # Lowercase letters + when "a".."z" then ch + + # Uppercase letters (shift + letter) + when "A".."Z" then "shift-#{ch.downcase}" + + # Digits + when "0".."9" then ch + + # Shifted number row + when "!" then "shift-1" + when "@" then "shift-2" + when "#" then "shift-3" + when "$" then "shift-4" + when "%" then "shift-5" + when "^" then "shift-6" + when "&" then "shift-7" + when "*" then "shift-8" + when "(" then "shift-9" + when ")" then "shift-0" + + when "-" then "minus" + when "_" then "shift-minus" + when "=" then "equal" + when "+" then "shift-equal" + when "[" then "bracket_left" + when "{" then "shift-bracket_left" + when "]" then "bracket_right" + when "}" then "shift-bracket_right" + when "\\" then "backslash" + when "|" then "shift-backslash" + when ";" then "semicolon" + when ":" then "shift-semicolon" + when "'" then "apostrophe" + when "\"" then "shift-apostrophe" + when "," then "comma" + when "<" then "shift-comma" + when "." then "dot" + when ">" then "shift-dot" + when "/" then "slash" + when "?" then "shift-slash" + when "`" then "grave_accent" + when "~" then "shift-grave_accent" + + else + raise "Unsupported character: #{ch.inspect}" + end +end + +if ARGV[0] + puts ARGV[0] + input = ARGV[0] + puts "Typing: #{input.inspect}" + + input.chars.each do |ch| + send_key(sock, char_to_key(ch)) + sleep 0.1 + end + +else + # Example: type "hello" + %w[p a s s ret].each do |k| + send_key(sock, k) + sleep 0.1 + end +end + +sock.close + + +# (qemu) screendump /Volumes/Cache/homes/debian/guaset.ppm +# ffmpeg -y -i guaset.ppm guest.png \ No newline at end of file diff --git a/bin/vm b/bin/vm index 322c850..5923500 100755 --- a/bin/vm +++ b/bin/vm @@ -31,6 +31,11 @@ OptionParser.new do |opt| options.tpm = true end + opt.on('--shell', 'QEMU process must be detached to exec ssh') do + options.shell = true + options.detached = true + end + opt.on('--detached') do options.detached = true end diff --git a/lib/system.rb b/lib/system.rb index bbedf0b..4c56b00 100644 --- a/lib/system.rb +++ b/lib/system.rb @@ -47,4 +47,26 @@ module System def self.arch_to_symbol(arch) normalize_architecture_string(arch) end + + + def self.exec_ssh(port) + require "socket" + + host = "localhost" + cmd = "ssh -p #{port} user@#{host}" + + # Wait until the port is open + puts "Waiting for #{host}:#{port}..." + until begin + TCPSocket.new(host, port).close + true + rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError + false + end + sleep 0.1 + end + + puts "Port open! Executing: #{cmd}" + exec cmd + end end diff --git a/lib/virtual-machine.rb b/lib/virtual-machine.rb index 898c54f..e5b1af4 100644 --- a/lib/virtual-machine.rb +++ b/lib/virtual-machine.rb @@ -109,7 +109,8 @@ module VirtualMachine vars_fd: options[:vars_fd], cpus: [1, System.cpus - 2].max, detach: options[:detached], - tpm: options[:tpm] + tpm: options[:tpm], + shell: options[:shell] ) end diff --git a/lib/vm/qemu.rb b/lib/vm/qemu.rb index 60aeb0f..506e6bc 100644 --- a/lib/vm/qemu.rb +++ b/lib/vm/qemu.rb @@ -13,6 +13,10 @@ module DisplayMode def self.window 2 end + + def self.vnc + 3 + end end module Qemu @@ -81,9 +85,14 @@ module Qemu arch: System::ARCH, cdrom: nil, detach: true, + shell: false, ram: 2048 * 8, cpus: 1, - display: DisplayMode::none + display: DisplayMode::none, + mount: { + wd: nil, + home: nil + } } # for testing only @@ -92,6 +101,7 @@ module Qemu defaults[:display] = DisplayMode.fullscreen defaults[:display] = DisplayMode.window # defaults[:display] = DisplayMode.none + #defaults[:display] = DisplayMode.vnc opts = defaults.merge(options) @@ -119,14 +129,79 @@ module Qemu args += ["-drive", "if=pflash,format=raw,unit=0,readonly=on,file=#{opts[:code_fd]}"] args += ["-drive", "if=pflash,format=raw,unit=1,file=#{opts[:vars_fd]}"] + ssh_port = nil + + if opts[:shell] + ssh_port = rand(4000..9999) + # args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{ssh_port}-:22"] + # args += ['-device', 'virtio-net-device,netdev=net0'] + + # args += ['-netdev', "user,id=n0,hostfwd=tcp:127.0.0.1:#{ssh_port}-10.0.2.15:22"] + # args += ['-device', 'virtio-net,netdev=n0'] + + # args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{ssh_port}-10.0.2.15:22"] + # args += ['-device', 'virtio-net-device,netdev=net0"'] + + args += ['-nic', "user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:#{ssh_port}-:22"] + + puts "ssh -p #{ssh_port} user@localhost" + + # conf that works + #args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] + end + + # -virtfs local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs + # mount -t 9p -o trans=virtio,version=9p2000.L hostfs /mnt + # sudo mount -t 9p hostfs /home/user/Share -o trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap + + # hostfs /home 9p trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap,nofail 0 0 + # hostfs /share 9p trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap,nofail 0 0 + + if opts[:mount][:wd] + args += ['-virtfs', 'local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs'] + end + + if opts[:mount][:home] + #args += ['-homefs', "local,path=#{VMDATA},mount_tag=hostfs,security_model=passthrough,id=hostfs"] + end + if opts[:display] == DisplayMode::none port = 2222 args += ['-nographic'] - args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22"] + args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22,udp:127.0.0.1:6544-:6544=on"] #args += ['-device', 'e1000,netdev=net0'] args += ['-device', 'virtio-net-pci,netdev=net0'] puts "ssh -p #{port} user@localhost" + + + elsif opts[:display] == DisplayMode::vnc + # Note: this outputs serial on the console + #args += ['-nographic'] + + args += ["-display", "none"] + args += ["-device", "virtio-gpu-pci"] + args += ["-device", "virtio-keyboard-pci"] + args += ["-device", "virtio-mouse-pci"] + + args += ['-vnc', '127.0.0.1:0,password=on'] + + # tunnel ssh -L 5900:127.0.0.1:5900 user@your-host + # -monitor unix:/tmp/qemu-mon,server,nowait + # -vnc 127.0.0.1:0,password=on + # printf 'change vnc password\nMySecret\n' | socat - UNIX-CONNECT:/tmp/qemu-mon + + + # SASL auth (username + password) + # -vnc :0,sasl + + # TLS (certificates, optional password) + # -object tls-creds-x509,id=tls0,... + # -vnc :0,tls-creds=tls0 + + # qemu-system-x86_64 \ + # -object tls-creds-x509,id=tls0,dir=/etc/pki/qemu,endpoint=server \ + # -vnc :0,tls-creds=tls0,sasl else #args += ["-device", "virtio-gpu-device"] if opts[:display] == DisplayMode::fullscreen @@ -156,7 +231,19 @@ module Qemu args += ["-device", "virtio-gpu"] #args += ['-nic', 'user,model=virtio-net-pci'] - args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] + unless opts[:shell] + args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] + end + + + ### TODO: remove + port = 2222 + args += ['-device', 'virtio-net-pci,netdev=net0'] + args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22,hostfwd=udp:127.0.0.1:6544-:6544"] + args += ['-virtfs', 'local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs'] + ### TODO END + + # macOS vmnet (shares Mac’s LAN) # -netdev vmnet-shared,id=n1 \ # -device virtio-net-pci,netdev=n1 @@ -211,6 +298,8 @@ module Qemu # -netdev vmnet-shared,id=net0 end + args += ['-monitor', 'stdio'] + # args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] # user-mode NAT # optional: uncomment to run headless with VNC on :5901 # args += ["-display", "none", "-vnc", "127.0.0.1:1"] @@ -224,6 +313,11 @@ module Qemu pid = Process.spawn(*cmd, pgroup: false, out: log, err: log) Process.detach(pid) puts "QEMU pid=#{pid}" + + if opts[:shell] + System.exec_ssh(ssh_port) + end + else pid = Process.spawn(*cmd, pgroup: true, out: $stdout, err: $stderr) Process.wait(pid) @@ -234,3 +328,17 @@ module Qemu end + + +## Works on MacOS= +# -monitor unix:/tmp/qemu-monitor.sock,server,nowait +# nc -U /tmp/qemu-monitor.sock +# instead of args += ['-monitor', 'stdio'] + + + + +# 9P +# sudo mount -t 9p hostfs /home/user/Share \ +# -o trans=virtio,version=9p2000.L,msize=262144,cache=mmap,access=any,dfltuid=1000,dfltgid=1000 +# sudo chown -hR 1000:1000 /home/user/Share \ No newline at end of file diff --git a/recipes/vnc-viewer.yml b/recipes/vnc-viewer.yml new file mode 100644 index 0000000..8a755fb --- /dev/null +++ b/recipes/vnc-viewer.yml @@ -0,0 +1,45 @@ + # # 1) Build & install FLTK 1.4 to /opt/fltk-1.4 + # git clone https://github.com/fltk/fltk.git + # cd fltk + # git checkout branch-1.4 + # cmake -S . -B build \ + # -DOPTION_BUILD_SHARED_LIBS=ON \ + # -DOPTION_USE_SYSTEM_LIBPNG=ON \ + # -DOPTION_USE_SYSTEM_LIBJPEG=ON \ + # -DOPTION_USE_SYSTEM_ZLIB=ON \ + # -DCMAKE_INSTALL_PREFIX=/opt/fltk-1.4 + # cmake --build build -j + # sudo cmake --install build + + # # 2) Point pkg-config and CMake at the new install + # export PKG_CONFIG_PATH=/opt/fltk-1.4/lib/pkgconfig:$PKG_CONFIG_PATH + # export CMAKE_PREFIX_PATH=/opt/fltk-1.4:$CMAKE_PREFIX_PATH + # # (optional but sometimes necessary) + # export FLTK_DIR=/opt/fltk-1.4/lib/cmake/FLTK + + +packages: + - cmake + - ninja + - pkg-config + - fltk + - jpeg-turbo + - libpng + - zlib + - gnutls + - ffmpeg + +# --recursive +repository: + url: https://github.com/TigerVNC/tigervnc.git + url: v1.15.0 + +steps: + - mkdir build && cd build + - | + cmake -G Ninja .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_VIEWER=ON \ + -DBUILD_SERVER=OFF \ + -DWITH_GNUTLS=ON + - ninja \ No newline at end of file