Skip to main content

qemu_command_builder/args/
vnc.rs

1use crate::common::OnOff;
2use crate::parsers::ARG_VNC;
3use crate::parsers::DELIM_COMMA;
4use crate::to_command::ToArg;
5use crate::to_command::ToCommand;
6use bon::Builder;
7use proptest_derive::Arbitrary;
8use std::path::PathBuf;
9use std::str::FromStr;
10
11/// A VNC server endpoint for `-vnc`.
12#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
13pub enum VNCDisplay {
14    To(usize),
15    Network { host: Option<String>, display: usize },
16    Unix(PathBuf),
17    None,
18}
19
20/// VNC display sharing policy for `share=`.
21#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
22pub enum AllowExclusiveForceSharedIgnore {
23    AllowExclusive,
24    ForceShared,
25    Ignore,
26}
27
28impl ToArg for AllowExclusiveForceSharedIgnore {
29    fn to_arg(&self) -> &str {
30        match self {
31            AllowExclusiveForceSharedIgnore::AllowExclusive => "allow-exclusive",
32            AllowExclusiveForceSharedIgnore::ForceShared => "force-shared",
33            AllowExclusiveForceSharedIgnore::Ignore => "ignore",
34        }
35    }
36}
37/// Configure the QEMU VNC server.
38///
39/// `Display` and `FromStr` round-trip the canonical comma-separated
40/// `-vnc` argument forms that this crate models.
41#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
42pub struct VNC {
43    display: VNCDisplay,
44
45    /// Connect to a listening VNC client via a "reverse" connection.
46    /// The client is specified by the display. For reverse network
47    /// connections (host:d,``reverse``), the d argument is a TCP port
48    /// number, not a display number.    
49    reverse: Option<OnOff>,
50
51    /// Opens an additional TCP listening port dedicated to VNC
52    /// Websocket connections. If a bare websocket option is given, the
53    /// Websocket port is 5700+display. An alternative port can be
54    /// specified with the syntax ``websocket``\ =port.
55    ///
56    /// If host is specified connections will only be allowed from this
57    /// host. It is possible to control the websocket listen address
58    /// independently, using the syntax ``websocket``\ =host:port.
59    ///
60    /// Websocket could be allowed over UNIX domain socket, using the syntax
61    /// ``websocket``\ =unix:path, where path is the location of a unix socket
62    /// to listen for connections on.
63    ///
64    /// If no TLS credentials are provided, the websocket connection
65    /// runs in unencrypted mode. If TLS credentials are provided, the
66    /// websocket connection requires encrypted client connections.
67    websocket: Option<String>,
68
69    /// Require that password based authentication is used for client
70    /// connections.
71    ///
72    /// The password must be set separately using the ``set_password``
73    /// command in the :ref:`QEMU monitor`. The
74    /// syntax to change your password is:
75    /// ``set_password <protocol> <password>`` where <protocol> could be
76    /// either "vnc" or "spice".
77    ///
78    /// If you would like to change <protocol> password expiration, you
79    /// should use ``expire_password <protocol> <expiration-time>``
80    /// where expiration time could be one of the following options:
81    ///     now, never, +seconds or UNIX time of expiration, e.g. +60 to
82    /// make password expire in 60 seconds, or 1335196800 to make
83    /// password expire on "Mon Apr 23 12:00:00 EDT 2012" (UNIX time for
84    /// this date and time).
85    ///
86    /// You can also use keywords "now" or "never" for the expiration
87    /// time to allow <protocol> password to expire immediately or never
88    /// expire.
89    password: Option<OnOff>,
90
91    /// Require that password based authentication is used for client
92    /// connections, using the password provided by the ``secret``
93    /// object identified by ``secret-id``.
94    password_secret: Option<String>,
95
96    /// Provides the ID of a set of TLS credentials to use to secure the
97    /// VNC server. They will apply to both the normal VNC server socket
98    /// and the websocket socket (if enabled). Setting TLS credentials
99    /// will cause the VNC server socket to enable the VeNCrypt auth
100    /// mechanism. The credentials should have been previously created
101    /// using the ``-object tls-creds`` argument.
102    tls_creds: Option<String>,
103
104    /// Provides the ID of the QAuthZ authorization object against which
105    /// the client's x509 distinguished name will validated. This object
106    /// is only resolved at time of use, so can be deleted and recreated
107    /// on the fly while the VNC server is active. If missing, it will
108    /// default to denying access.
109    tls_authz: Option<String>,
110
111    /// Require that the client use SASL to authenticate with the VNC
112    /// server. The exact choice of authentication method used is
113    /// controlled from the system / user's SASL configuration file for
114    /// the 'qemu' service. This is typically found in
115    /// /etc/sasl2/qemu.conf. If running QEMU as an unprivileged user,
116    /// an environment variable SASL\_CONF\_PATH can be used to make it
117    /// search alternate locations for the service config. While some
118    /// SASL auth methods can also provide data encryption (eg GSSAPI),
119    /// it is recommended that SASL always be combined with the 'tls'
120    /// and 'x509' settings to enable use of SSL and server
121    /// certificates. This ensures a data encryption preventing
122    /// compromise of authentication credentials. See the
123    /// :ref:`VNC security` section in the System Emulation Users Guide
124    /// for details on using SASL authentication.    
125    sasl: Option<OnOff>,
126
127    /// Provides the ID of the QAuthZ authorization object against which
128    /// the client's SASL username will validated. This object is only
129    /// resolved at time of use, so can be deleted and recreated on the
130    /// fly while the VNC server is active. If missing, it will default
131    /// to denying access.
132    sasl_authz: Option<String>,
133
134    /// Legacy method for enabling authorization of clients against the
135    /// x509 distinguished name and SASL username. It results in the
136    /// creation of two ``authz-list`` objects with IDs of
137    /// ``vnc.username`` and ``vnc.x509dname``. The rules for these
138    /// objects must be configured with the HMP ACL commands.
139    ///
140    /// This option is deprecated and should no longer be used. The new
141    /// ``sasl-authz`` and ``tls-authz`` options are a replacement.
142    acl: Option<OnOff>,
143
144    /// Enable lossy compression methods (gradient, JPEG, ...). If this
145    /// option is set, VNC client may receive lossy framebuffer updates
146    /// depending on its encoding settings. Enabling this option can
147    /// save a lot of bandwidth at the expense of quality.   
148    lossy: Option<OnOff>,
149
150    /// Disable adaptive encodings. Adaptive encodings are enabled by
151    /// default. An adaptive encoding will try to detect frequently
152    /// updated screen regions, and send updates in these regions using
153    /// a lossy encoding (like JPEG). This can be really helpful to save
154    /// bandwidth when playing videos. Disabling adaptive encodings
155    /// restores the original static behavior of encodings like Tight.
156    non_adaptive: Option<OnOff>,
157
158    /// Set display sharing policy. 'allow-exclusive' allows clients to
159    /// ask for exclusive access. As suggested by the rfb spec this is
160    /// implemented by dropping other connections. Connecting multiple
161    /// clients in parallel requires all clients asking for a shared
162    /// session (vncviewer: -shared switch). This is the default.
163    /// 'force-shared' disables exclusive client access. Useful for
164    /// shared desktop sessions, where you don't want someone forgetting
165    /// specify -shared disconnect everybody else. 'ignore' completely
166    /// ignores the shared flag and allows everybody connect
167    /// unconditionally. Doesn't conform to the rfb spec but is
168    /// traditional QEMU behavior.
169    share: Option<AllowExclusiveForceSharedIgnore>,
170
171    /// Set keyboard delay, for key down and key up events, in
172    /// milliseconds. Default is 10. Keyboards are low-bandwidth
173    /// devices, so this slowdown can help the device and guest to keep
174    /// up and not lose events in case events are arriving in bulk.
175    /// Possible causes for the latter are flaky network connections, or
176    /// scripts for automated testing.
177    key_delay_ms: Option<usize>,
178
179    /// Use the specified audiodev when the VNC client requests audio
180    /// transmission. When not using an -audiodev argument, this option
181    /// must be omitted, otherwise is must be present and specify a
182    /// valid audiodev.
183    audiodev: Option<String>,
184
185    /// Permit the remote client to issue shutdown, reboot or reset power
186    /// control requests.
187    power_control: Option<OnOff>,
188}
189
190impl ToCommand for VNC {
191    fn command(&self) -> String {
192        ARG_VNC.to_string()
193    }
194    fn to_args(&self) -> Vec<String> {
195        let mut args = vec![];
196        match &self.display {
197            VNCDisplay::To(l) => {
198                args.push(format!("to={}", l));
199            }
200            VNCDisplay::Network { host, display } => {
201                if let Some(host) = host {
202                    args.push(format!("{}:{}", host, display));
203                } else {
204                    args.push(format!(":{}", display));
205                }
206            }
207            VNCDisplay::Unix(path) => {
208                args.push(format!("unix={}", path.display()));
209            }
210            VNCDisplay::None => {
211                args.push("none".to_string());
212            }
213        }
214
215        if let Some(reverse) = &self.reverse {
216            args.push(format!("reverse={}", reverse.to_arg()));
217        }
218        if let Some(websocket) = &self.websocket {
219            args.push(format!("websocket={}", websocket));
220        }
221        if let Some(password) = &self.password {
222            args.push(format!("password={}", password.to_arg()));
223        }
224        if let Some(password_secret) = &self.password_secret {
225            args.push(format!("password-secret={}", password_secret));
226        }
227        if let Some(tls_creds) = &self.tls_creds {
228            args.push(format!("tls-creds={}", tls_creds));
229        }
230        if let Some(tls_authz) = &self.tls_authz {
231            args.push(format!("tls-authz={}", tls_authz));
232        }
233        if let Some(sasl) = &self.sasl {
234            args.push(format!("sasl={}", sasl.to_arg()));
235        }
236        if let Some(sasl_authz) = &self.sasl_authz {
237            args.push(format!("sasl-authz={}", sasl_authz));
238        }
239        if let Some(acl) = &self.acl {
240            args.push(format!("acl={}", acl.to_arg()));
241        }
242        if let Some(lossy) = &self.lossy {
243            args.push(format!("lossy={}", lossy.to_arg()));
244        }
245        if let Some(non_adaptive) = &self.non_adaptive {
246            args.push(format!("non-adaptive={}", non_adaptive.to_arg()));
247        }
248        if let Some(share) = &self.share {
249            args.push(format!("share={}", share.to_arg()));
250        }
251        if let Some(key_delay_ms) = &self.key_delay_ms {
252            args.push(format!("key-delay-ms={}", key_delay_ms));
253        }
254        if let Some(audiodev) = &self.audiodev {
255            args.push(format!("audiodev={}", audiodev));
256        }
257        if let Some(power_control) = &self.power_control {
258            args.push(format!("power-control={}", power_control.to_arg()));
259        }
260
261        vec![args.join(DELIM_COMMA)]
262    }
263}
264
265impl FromStr for VNC {
266    type Err = String;
267
268    fn from_str(s: &str) -> Result<Self, Self::Err> {
269        let mut parts = s.split(DELIM_COMMA);
270        let display = parse_display(parts.next().ok_or_else(|| "empty -vnc argument".to_string())?)?;
271        let mut value = VNC::builder().display(display).build();
272
273        for part in parts {
274            let (key, raw) = part.split_once('=').ok_or_else(|| format!("invalid -vnc option: {part}"))?;
275            match key {
276                "reverse" => value.reverse = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid reverse value: {raw}"))?),
277                "websocket" => value.websocket = Some(raw.to_string()),
278                "password" => value.password = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid password value: {raw}"))?),
279                "password-secret" => value.password_secret = Some(raw.to_string()),
280                "tls-creds" => value.tls_creds = Some(raw.to_string()),
281                "tls-authz" => value.tls_authz = Some(raw.to_string()),
282                "sasl" => value.sasl = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid sasl value: {raw}"))?),
283                "sasl-authz" => value.sasl_authz = Some(raw.to_string()),
284                "acl" => value.acl = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid acl value: {raw}"))?),
285                "lossy" => value.lossy = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid lossy value: {raw}"))?),
286                "non-adaptive" => value.non_adaptive = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid non-adaptive value: {raw}"))?),
287                "share" => value.share = Some(parse_share(raw)?),
288                "key-delay-ms" => value.key_delay_ms = Some(raw.parse::<usize>().map_err(|e| e.to_string())?),
289                "audiodev" => value.audiodev = Some(raw.to_string()),
290                "power-control" => value.power_control = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid power-control value: {raw}"))?),
291                other => return Err(format!("unsupported -vnc option: {other}")),
292            }
293        }
294        Ok(value)
295    }
296}
297
298fn parse_display(value: &str) -> Result<VNCDisplay, String> {
299    if let Some(limit) = value.strip_prefix("to=") {
300        return Ok(VNCDisplay::To(limit.parse::<usize>().map_err(|e| e.to_string())?));
301    }
302    if let Some(path) = value.strip_prefix("unix:") {
303        return Ok(VNCDisplay::Unix(PathBuf::from(path)));
304    }
305    if value == "none" {
306        return Ok(VNCDisplay::None);
307    }
308    if let Some((host, display)) = value.rsplit_once(':') {
309        let display = display.parse::<usize>().map_err(|e| e.to_string())?;
310        let host = if host.is_empty() { None } else { Some(host.to_string()) };
311        return Ok(VNCDisplay::Network { host, display });
312    }
313    Err(format!("unsupported vnc display: {value}"))
314}
315
316fn parse_share(value: &str) -> Result<AllowExclusiveForceSharedIgnore, String> {
317    match value {
318        "allow-exclusive" => Ok(AllowExclusiveForceSharedIgnore::AllowExclusive),
319        "force-shared" => Ok(AllowExclusiveForceSharedIgnore::ForceShared),
320        "ignore" => Ok(AllowExclusiveForceSharedIgnore::Ignore),
321        _ => Err(format!("invalid share value: {value}")),
322    }
323}