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.
Developing the vnc_server component
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\ genodelabs/api/libc \n\ genodelabs/api/base \n\ genodelabs/api/capture_session \n\ genodelabs/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 key 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.
Packaging and publishing
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. Originally, I deployed the vnc_server in conjunction with the event_filter component, because Sculpt only provided an unfiltered Event session. Since Sculpt 24.04, however, the events submitted by the vnc_server pass Sculpt's central event_filter component so that I removed the additional event_filter component from the runtime. 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.
Testing in Goa
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:
<runtime ram="250M" caps="2000" binary="init"> <requires> <!-- [...] see below --> </requires> <config> <parent-provides> <!-- [...] see below --> </parent-provides> <start name="vnc_server" caps="1000"> <resource name="RAM" quantum="130M"/> <route> <service name="ROM" label="config"> <parent label="vnc.config"/> </service> <service name="Nic"> <child name="nic_router"/> </service> <any-service> <parent/> <any-child/> </any-service> </route> </start> <!-- [...] see below --> </config> </runtime>
The vnc_server start node reuses the vnc.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. Since the introduction of the Uplink session, however, Goa needs to add a NIC router between the test scenario and the network driver. The default config of the NIC router lacks any port forwarding rule, though. In the scope of developing Goa Testbed, I added support for customising Goa's NIC-router config (see below).
In addition to the vnc_server, I added the textedit component from pkg/qt5_textedit to have a GUI component that reacts to mouse and keyboard events:
<runtime ram="250M" caps="2000" binary="init"> <requires> <!-- [...] see below --> </requires> <config> <parent-provides> <!-- [...] see below --> </parent-provides> <!-- [...] see above --> <start name="textedit" caps="300"> <resource name="RAM" quantum="100M"/> <route> <service name="ROM" label="config"> <parent label="textedit.config"/> </service> <service name="ROM" label="mesa_gpu_drv.lib.so"> <parent label="mesa_gpu_drv.lib.so"/> </service> <service name="ROM" label="clipboard"> <parent label="clipboard"/> </service> <service name="Report" label="clipboard"> <parent label="clipboard"/> </service> <service name="Report" label="shape"> <parent label="shape"/> </service> <any-service> <parent/> <any-child/> </any-service> </route> </start> </config> </runtime>
The component reuses the textedit.config from pkg/qt5_textedit. The remaining requirements are automatically routed by Goa so that I ended up with the following <requires> section:
<requires> <capture/> <event/> <gui/> <nic tap_name="tap_goa"> <policy label_suffix="vnc" domain="vnc"/> <domain name="vnc" interface="10.0.59.1/24"> <dhcp-server ip_first="10.0.59.2" ip_last="10.0.59.2" dns_config_from="uplink"/> </domain> <tcp-forward port="5900" domain="vnc" to="10.0.59.2"/> </nic> <file_system/> <report label="shape"/> <report label="clipboard"/> <rom label="clipboard"/> <rom label="mesa_gpu_drv.lib.so"/> <timer/> </requires>
The event, capture and gui requirements instruct Goa to instantiate a nitpicker and to route the Capture, Event and Gui sessions to this instance. For the nic requirement, on the other hand, Goa will instantiate a linux_nic_drv and a NIC router. The tap_name attribute specifies which tap device shall be used by the linux_nic_drv. The content of the <nic> node is merged into the NIC router config as follows: <policy> and <domain> nodes are simply added to the config whereas <tcp-forward> and <udp-forward> nodes are added into router's uplink domain. You may have a look at goa help targets for more details on how Goa routes each requirement.
I also needed to add the parent-provided services to the <parent-provides> section. Note that Goa checks for consistency between the required services of a runtime and the <parent-provides> section and prints warnings if there is a mismatch.
<parent-provides> <service name="ROM"/> <service name="LOG"/> <service name="RM"/> <service name="CPU"/> <service name="PD"/> <service name="Gui"/> <service name="Timer"/> <service name="Capture"/> <service name="Event"/> <service name="Report"/> <service name="Nic"/> <service name="File_system"/> </parent-provides>
Before I can goa run the project, I must make sure that tap_goa exists and that something responds to DHCP request of the NIC router (for its uplink domain). I do this with the following commands:
sudo ip tuntap add dev tap_goa mode tap user $(whoami); sudo ip addr add 10.0.11.1/24 dev tap_goa; sudo ip link set dev tap_goa up; sudo dnsmasq -C dnsmasq.conf;
Here, my dnsmasq.conf has the following content:
port=5353 interface=tap_goa domain=lan dhcp-range=10.0.11.2,10.0.11.2,12h
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-projects repository.
Usage
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 index.
When installing and deploying the vnc_server package, you route the Capture session 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). The Event session can be routed to the filtered input events (i.e. the central event_filter component) or (if installed) the black_hole component if you want to have a view-only VNC server. As a file system, you best use a recall_fs.
Note that you need to add a policy to /config/event_filter. If the file is not present, copy /config/managed/event_filter to /config/event_filter to create the self-managed event-filter config. Then, add a <policy> node to the bottom of the config where the other <policy> nodes are:
<policy label_prefix="runtime -> vnc_server -> " input="vnc"/>
This instructs the event_filter component to accept input events from the VNC server. You also need to add merge these input events into the output, simply by adding <input name="vnc"/> where the other <input> nodes are located.
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 the sdl_vnc component, which is also available in my depot.
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 in the vnc.config, which is found in the file system used by vnc_server package.
<config period_ms="40" width="800" height="600" xpos="100" ypos="100"> [...] </config>
You may also enable password protection by adding the requires_password="yes" attribute to the config and by creating a passwd file containing the plaintext password.
Limitations
-
sdl_vnc does not seem to respond to resize events.
-
The VNC server only supports a single connection at a time. If another client tries to connect, the current client will be disconnected.
Let me know if you discover any further issues.
Edit 2023-05-08: Updated api sources for goa project (moved from nfeske to genodelabs).
Edit 2024-05-06: Adapted to Sculpt 24.04.
Edit 2024-05-17: Read vnc.config from file system; added scroll events and password support.