Remote desktop solution for Sculpt via VNC
Thanks to alex-ab, we already have a native VNC client available in Sculpt to connect to VMs remotely. How nice would it be to also bring a VNC server to Sculpt and thereby enable remote desktop scenarios? In this article, I want to share my experiences made when developing a VNC server application with Goa and ultimately present a ready-to-use package for Sculpt.
As Alex had already ported the client library from libvncserver (available in genode-world), I had a good starting point for developing a VNC server component. Adding the server library and providing the corresponding api/libvnserver and src/libvncserver packages was pretty straightforward, hence I won’t bore you with the details. The task at hand was then to create a Genode component that connects to a Capture and an Event session as provided by the new GUI stack. Of course, Goa seemed to be a perfect match for this task and provided a convenient workflow.
By the way, if you are not interested in the journey, you can also fast-forward to the Usage section.
As usual, I first needed to create a new Goa project with a src directory and a Makefile as well as an artifacts and used_apis file. Or, for those who speak Unix fluently:
mkdir -p vnc_server/src echo "vnc_server: vnc_server.cc" > vnc_server/src/Makefile echo "vnc_server" > vnc_server/artifacts echo "jschlatow/api/libvncserver \n\ nfeske/api/libc \n\ nfeske/api/base \n\ nfeske/api/capture_session \n\ nfeske/api/event_session" > vnc_server/used_apis
For implementing vnc_server.cc, I took the test-capture package as a blueprint. This package periodically copies screen data from a Capture session to a GUI session. What remained to be done was using the libvncserver API to initialise the VNC server, register a keyboard callback and a pointer callback, and to copy the screen data from the Capture session to the libvncserver framebuffer instead of the GUI session buffer. All in all a pretty straightforward job.
One thing to keep in mind though is that key events are supplied as keysyms to the keyboard callback. In order to forward these to the Event session correctly, the keysyms must thus be translated. Fortunately, the Event session supports the injection of character events via the Codepoint argument. Conveniently, the keysyms of all ASCII characters are mapped to their ASCII code. I therefore opted for forwarding the ASCII character events as KEY_UNKNOWN with their ASCII code as Codepoint argument. For the other standard key events (Enter, Space, Shift, cursor movements, etc.), I created a lookup table. The remaining keys events are just ignored and not forwarded at all.
The next step was to goa build the component and encounter a few missing header files:
fatal error: os/surface.h: No such file or directory fatal error: blit/painter.h: No such file or directory fatal error: zlib.h: No such file or directory
After adding the missing apis (os, blit, zlib) to the used_apis file, I got a bunch of these:
[...]/depot/jschlatow/api/libvncserver/2021-06-04/include/rfb/rfb.h:83:38: error: unnecessary parentheses in declaration of ‘cursorMute’ [-Werror=parentheses] #define MUTEX(mutex) pthread_mutex_t (mutex)
I found a remedy in consulting goa help and added a make_args file to disable the parentheses error:
echo "CXXFLAGS+="-Wno-parentheses" > make_args
With this, compilation succeeded but linking failed because of an undefined reference:
vnc_server.cc:(.text._ZN9Vncserver4Main13_handle_timerEv[_ZN9Vncserver4Main13_handle_timerEv]+0x201): undefined reference to `blit' collect2: error: ld returned 1 exit status make: *** [<builtin>: vnc_server] Error 1
I had a look at the api/blit package and noticed that it is not provided as a dynamic library. Yet, Goa does not support static libraries. Furthermore, it is not (yet) aware of the Genode build system and therefore cannot be easily extended to process the lib/mk/*.mk file contained in api/blit. After pondering about possible solutions/workarounds, I settled for adding a quirk to Goa since there are not many statically linked libraries provided as packages.
This quirk solves two issues of the blit api:
It sets the include directory according to the spec scheme of the Genode build system.
It populates the environment variable $LIB_SRC that I can then use in my Makefile to compile the source files from api/blit.
With this workaround the build succeeded.
Now, I needed to create a deployable package for the newly developed component. As usual, I created the pkg/vnc_server directory and filled the corresponding README, archives and runtime files. For the runtime, I decided to deploy the vnc_server in conjunction with the event_filter component. On the one hand, the event filter can be used for debugging purposes (using the new <log> feature of the event filter available in the 21.05 release). On the other hand, it allows introducing the common remapping of KEY_F12 to KEY_DASHBOARD. For better reusability, I externalised the runtime config in raw/vnc.config.
Next, goa export informed me about a missing version and LICENSE before I could start with testing the exported package.
One of the coolest features of Goa is that I can run packages directly on my development machine. Yet, goa run is not very helpful in case of the vnc_server as it requires a GUI component such as qt5_textedit that we can then control via VNC. Nonetheless, the vnc_server package exported in the previous step, serves as a package that can be deployed on Sculpt or any other Genode-based system such as Goa. For interactive testing in Goa, I only needed to create a test project that deploys vnc_server in conjunction with qt5_textedit.
I thus created a new Goa project test-vnc_server that depends on the archive jschlatow/pkg/vnc_server. In the package’s runtime, the vnc_server is deployed as follows:
<start name="vnc_server" caps="1000"> <binary name="init"/> <resource name="RAM" quantum="130M"/> <route> <service name="ROM" label="config"> <parent label="vnc.config"/> </service> <service name="ROM" label="event.config"> <parent label="event.config"/> </service> <service name="Nic"> <child name="nic_router"/> </service> <any-service> <parent/> <any-child/> </any-service> </route> </start>
The vnc_server subsystem reuses the vnc.config and event.config provided by raw/vnc_server. It also requires a Nic session, a Capture session and an Event session. For the latter, I added support for <capture/> and <event/> requirements to Goa. Support for the <nic/> requirement was already added by chelmuth. However, if we look more closely, the vnc_server subsystem uses '<lxip dhcp="yes"/>' and thus requires a DHCP server. This could either be installed on the Linux host and activated for the tap device, or, I could use the nic_router in my runtime. The second choice seemed more appealing, however, required another extension of Goa to support <uplink/> requirements. With these extensions, I can now put the following requirements into my runtime file:
<requirements> <capture/> <event/> <uplink label="tap_goa"/> </requirements>
The event and capture requirements instruct Goa to instantiate a nitpicker and to route the Capture and Event sessions to this instance. For the uplink requirement, on the other hand, Goa will instantiate a linux_nic_drv that tries to connect to an Uplink session. As for <nic> requirements, the label "tap_goa" specifies which tap device shall be used by the linux_nic_drv. Since the linux_nic_drv acts as an Uplink client (not a server), I must indicate that my subsystem provides an Uplink service and add a corresponding routing policy:
<service name="Uplink"> <default-policy> <child name="nic_router"/> </default-policy> </service>
I set up the nic router with a static IP on the uplink domain and the default domain. On the default domain, to which the vnc_server subsystem connects, it also acts as a DHCP server.
<start name="nic_router"> <resource name="RAM" quantum="10M"/> <provides> <service name="Nic"/> <service name="Uplink"/> </provides> <config> <default-policy domain="default"/> <policy label="nic_drv -> " domain="uplink"/> <domain name="uplink" interface="10.0.11.12/24"> <nat domain="default" tcp-ports="1000" udp-ports="1000" icmp-ports="1000"/> <tcp-forward port="5900" domain="default" to="10.0.1.2"/> </domain> <domain name="default" interface="10.0.1.1/24"> <dhcp-server ip_first="10.0.1.2" ip_last="10.0.1.3"/> <tcp dst="0.0.0.0/0"> <permit-any domain="uplink"/> </tcp> <udp dst="0.0.0.0/0"> <permit-any domain="uplink"/> </udp> <icmp dst="0.0.0.0/0" domain="uplink"/> </domain> </config> <route> <any-service> <parent/> <any-child/> </any-service> </route> </start>
Last not least, I added the textedit component from pkg/qt5_textedit to have a GUI component that reacts to mouse and keyboard events:
<start name="textedit"> <resource name="RAM" quantum="100M"/> <route> <service name="ROM" label="config"> <parent label="textedit.config"/> </service> <any-service> <parent/> <any-child/> </any-service> </route> </start>
Now, I can goa run the project, connect with a VNC client (I used gtk-vnc) to 10.0.11.12:0 and remotely control the Qt textedit. Nice! Note, most clients interpret the :0 as a display id and translate it to TCP port 5900.
If you are interested in the complete picture, you can find all project files in my goa-pkgs repository.
After finishing the labour, we can now reap the fruit and deploy the vnc_server package in Sculpt. The package is available in my personal depot, which you must manually add to Sculpt 21.03b. Future Sculpt releases will contain my depot files though. Until then, you can take the download and pubkey file from this pull request.
Alright, when installing and deploying the vnc_server package, you route the Capture and Event sessions to the system GUI for the complete remote-desktop experience. The system GUI is the global nitpicker instance. The VNC server will thus get the same data as your physical display. As usual, the Network session goes to the nic_router (don’t forget to enable your network first).
Moreover, to expose the VNC server to the network, we must forward the TCP port 5900 from the uplink domain to the vnc_server component. For this purpose, just take a look into the log and find a line like this:
[runtime -> vnc_server -> vnc_server] my address is 10.0.1.4
Memorise the IP address, copy /config/managed/nic_router to /config/nic_router and add the following into '<domain name="uplink">':
<tcp-forward port="5900" domain="default" to="10.0.1.4"/>
That’s it. If you set thing up correctly, you should be able to connect with any VNC client to your Sculpt system. I successfully tested gtk-vnc 1.2.0 and Alex’ sdl_vnc package.
A cool feature is the automatic resizing if you attach a display with larger resolution. When detaching the display, however, you must restart the VNC server since nitpicker will keep the bounding box of all Capture client buffers as the joint screen size.
Another neat feature is that you can only serve a section of the actual screen via VNC by setting the xpos, ypos, width and height arguments the vnc.config from jschlatow/raw/vnc_server, e.g.:
<start name="vnc_server"> [...] <config period_ms="40" width="800" height="600" xpos="100" ypos="100"> [...] </config> </start>
Have a look at [http://genodians.org/skalk/2019-03-18-hybrid-packages - Stefan’s guide] on how to modify existing packages.
The server only accepts a single client connection at a time. In case the client has not disconnected properly, it may therefore occur that you must restart the server.
Scroll events are not (yet) forwarded by the VNC server.
sdl_vnc does not seem to respond to resize events.
Let me know if you discover any further issues.