Skip to content

Commit fc15ef1

Browse files
committed
Add VNC video display including password
The other display options open a window always, while the vnc is more "on demand" by using a separate vnc viewer. Add localhost and password support for some minimal security. The password is generated, and is stored as an instance file. Signed-off-by: Anders F Björklund <[email protected]>
1 parent b4cb081 commit fc15ef1

File tree

7 files changed

+107
-3
lines changed

7 files changed

+107
-3
lines changed

examples/default.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ video:
215215
# on performance on macOS hosts: https://gitlab.com/qemu-project/qemu/-/issues/334
216216
# 🟢 Builtin default: "none"
217217
display: null
218+
# VNC (Virtual Network Computing) is a platform-independent graphical
219+
# desktop-sharing system that uses the Remote Frame Buffer protocol (RFB)
220+
vnc:
221+
# VNC display, e.g., "127.0.0.1:0", "host:d", "unix:path", "none"
222+
# By convention the TCP port is 5900+d, connections from any host.
223+
display: null
218224

219225
# The instance can get routable IP addresses from the vmnet framework using
220226
# https://github.com/lima-vm/socket_vmnet.

pkg/hostagent/hostagent.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"io"
11+
"math/rand"
1112
"net"
1213
"os"
1314
"os/exec"
@@ -243,6 +244,15 @@ func logPipeRoutine(r io.Reader, header string) {
243244
}
244245
}
245246

247+
func randomPassword(length int) string {
248+
passwd := ""
249+
rand.Seed(time.Now().UnixNano())
250+
for i := 0; i < length; i++ {
251+
passwd += strconv.Itoa(rand.Intn(10))
252+
}
253+
return passwd
254+
}
255+
246256
type qArgTemplateApplier struct {
247257
files []*os.File
248258
}
@@ -357,6 +367,30 @@ func (a *HostAgent) Run(ctx context.Context) error {
357367
qWaitCh <- qCmd.Wait()
358368
}()
359369

370+
if a.y.Video.VNC.Display != nil {
371+
vncdisplay := *a.y.Video.VNC.Display
372+
vnchost, vncnum, err := net.SplitHostPort(vncdisplay)
373+
if err != nil {
374+
return err
375+
}
376+
n, err := strconv.Atoi(vncnum)
377+
if err != nil {
378+
return err
379+
}
380+
vncport := strconv.Itoa(5900 + n)
381+
vncurl := "vnc://" + net.JoinHostPort(vnchost, vncport)
382+
vncpwdfile := filepath.Join(a.instDir, filenames.VNCPasswordFile)
383+
vncpasswd := randomPassword(8)
384+
if err := a.changeVNCPassword(ctx, vncpasswd); err != nil {
385+
return err
386+
}
387+
if err := os.WriteFile(vncpwdfile, []byte(vncpasswd), 0600); err != nil {
388+
return err
389+
}
390+
logrus.Infof("VNC Password for %s <%s> is:", vncdisplay, vncurl)
391+
logrus.Infof(" \"%s\" | `%s`", vncpasswd, vncpwdfile)
392+
}
393+
360394
stBase := events.Status{
361395
SSHLocalPort: a.sshLocalPort,
362396
}
@@ -398,6 +432,46 @@ func (a *HostAgent) Info(ctx context.Context) (*hostagentapi.Info, error) {
398432
return info, nil
399433
}
400434

435+
func waitFileExists(path string, timeout time.Duration) error {
436+
startWaiting := time.Now()
437+
for {
438+
_, err := os.Stat(path)
439+
if err == nil {
440+
break
441+
}
442+
if !errors.Is(err, os.ErrNotExist) {
443+
return err
444+
}
445+
if time.Since(startWaiting) > timeout {
446+
return fmt.Errorf("timeout waiting for %s", path)
447+
}
448+
time.Sleep(500 * time.Millisecond)
449+
}
450+
return nil
451+
}
452+
453+
func (a *HostAgent) changeVNCPassword(ctx context.Context, password string) error {
454+
qmpSockPath := filepath.Join(a.instDir, filenames.QMPSock)
455+
err := waitFileExists(qmpSockPath, 30*time.Second)
456+
if err != nil {
457+
return err
458+
}
459+
qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second)
460+
if err != nil {
461+
return err
462+
}
463+
if err := qmpClient.Connect(); err != nil {
464+
return err
465+
}
466+
defer func() { _ = qmpClient.Disconnect() }()
467+
rawClient := raw.NewMonitor(qmpClient)
468+
err = rawClient.ChangeVNCPassword(password)
469+
if err != nil {
470+
return err
471+
}
472+
return nil
473+
}
474+
401475
func (a *HostAgent) shutdownQEMU(ctx context.Context, timeout time.Duration, qCmd *exec.Cmd, qWaitCh <-chan error) error {
402476
logrus.Info("Shutting down QEMU with ACPI")
403477
qmpSockPath := filepath.Join(a.instDir, filenames.QMPSock)

pkg/limayaml/defaults.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
170170
y.Video.Display = pointer.String("none")
171171
}
172172

173+
if y.Video.VNC.Display == nil {
174+
y.Video.VNC.Display = d.Video.VNC.Display
175+
}
176+
if o.Video.VNC.Display != nil {
177+
y.Video.VNC.Display = o.Video.VNC.Display
178+
}
179+
173180
if y.Firmware.LegacyBIOS == nil {
174181
y.Firmware.LegacyBIOS = d.Firmware.LegacyBIOS
175182
}

pkg/limayaml/limayaml.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,14 @@ type Firmware struct {
106106
LegacyBIOS *bool `yaml:"legacyBIOS,omitempty" json:"legacyBIOS,omitempty"`
107107
}
108108

109+
type VNCOptions struct {
110+
Display *string `yaml:"display,omitempty" json:"display,omitempty"`
111+
}
112+
109113
type Video struct {
110114
// Display is a QEMU display string
111-
Display *string `yaml:"display,omitempty" json:"display,omitempty"`
115+
Display *string `yaml:"display,omitempty" json:"display,omitempty"`
116+
VNC VNCOptions `yaml:"vnc" json:"vnc"`
112117
}
113118

114119
type ProvisionMode = string

pkg/limayaml/validate.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ func Validate(y LimaYAML, warn bool) error {
8888
}
8989
}
9090

91+
if y.Video.VNC.Display != nil {
92+
if *y.Video.Display != "none" && *y.Video.Display != "vnc" {
93+
return fmt.Errorf("field `video.display` has conflicting type: %q", *y.Video.Display)
94+
}
95+
}
96+
9197
if *y.CPUs == 0 {
9298
return errors.New("field `cpus` must be set")
9399
}

pkg/qemu/qemu.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,13 @@ func Cmdline(cfg Config) (string, []string, error) {
536536
args = append(args, "-device", "virtio-rng-pci")
537537

538538
// Graphics
539-
if *y.Video.Display != "" {
540-
args = appendArgsIfNoConflict(args, "-display", *y.Video.Display)
539+
if *y.Video.Display != "" || y.Video.VNC.Display != nil {
540+
display := *y.Video.Display
541+
if y.Video.VNC.Display != nil {
542+
display = "vnc=" + *y.Video.VNC.Display
543+
display += ",password=on"
544+
}
545+
args = appendArgsIfNoConflict(args, "-display", display)
541546
}
542547
switch *y.Arch {
543548
case limayaml.X8664:

pkg/store/filenames/filenames.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const (
3737
SerialLog = "serial.log"
3838
SerialSock = "serial.sock"
3939
SSHSock = "ssh.sock"
40+
VNCPasswordFile = "vncpassword"
4041
GuestAgentSock = "ga.sock"
4142
HostAgentPID = "ha.pid"
4243
HostAgentSock = "ha.sock"

0 commit comments

Comments
 (0)