Skip to content

Commit ee86ee9

Browse files
CP-54217: Add a new pool level field to limit the vnc console access
This change introduces a new pool-level parameter that restricts VNC console access to a single active session per VM/host. This prevents multiple users from simultaneously connecting to the same VM console, preventing one user 'watching' another user operating a session. When the `limit_console_sessions` is true. - Enforced a single active VNC console connection per VM/host - Disable connection to websocket Signed-off-by: Stephen Cheng <[email protected]>
1 parent cc2f09e commit ee86ee9

File tree

7 files changed

+144
-66
lines changed

7 files changed

+144
-66
lines changed

ocaml/idl/datamodel_lifecycle.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ let prototyped_of_field = function
137137
Some "23.18.0"
138138
| "VM", "actions__after_softreboot" ->
139139
Some "23.1.0"
140+
| "pool", "limit_console_sessions" ->
141+
Some "25.30.0-next"
140142
| "pool", "ha_reboot_vm_on_internal_shutdown" ->
141143
Some "25.16.0"
142144
| "pool", "license_server" ->

ocaml/idl/datamodel_pool.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2250,6 +2250,12 @@ let t =
22502250
"Indicates whether an HA-protected VM that is shut down from \
22512251
inside (not through the API) should be automatically rebooted \
22522252
when HA is enabled"
2253+
; field ~writer_roles:_R_POOL_OP ~qualifier:RW ~lifecycle:[] ~ty:Bool
2254+
~default_value:(Some (VBool false)) "limit_console_sessions"
2255+
"When true, only one console connection per VM/host in the pool is \
2256+
accepted. Otherwise every connection for a VM/host's console is \
2257+
accepted. Note: when true, connection attempts via websocket will \
2258+
be rejected."
22532259
]
22542260
)
22552261
()

ocaml/idl/schematest.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ let hash x = Digest.string x |> Digest.to_hex
33
(* BEWARE: if this changes, check that schema has been bumped accordingly in
44
ocaml/idl/datamodel_common.ml, usually schema_minor_vsn *)
55

6-
let last_known_schema_hash = "7586cb039918e573594fc358e90b0f04"
6+
let last_known_schema_hash = "cf1c1e26b4288dd53cf6da5a4d6ad13c"
77

88
let current_schema_hash : string =
99
let open Datamodel_types in

ocaml/tests/common/test_common.ml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,8 @@ let make_pool ~__context ~master ?(name_label = "") ?(name_description = "")
305305
?(last_update_sync = API.Date.epoch) ?(update_sync_frequency = `daily)
306306
?(update_sync_day = 0L) ?(update_sync_enabled = false)
307307
?(recommendations = []) ?(license_server = [])
308-
?(ha_reboot_vm_on_internal_shutdown = true) () =
308+
?(ha_reboot_vm_on_internal_shutdown = true)
309+
?(limit_console_sessions = false) () =
309310
let pool_ref = Ref.make () in
310311
Db.Pool.create ~__context ~ref:pool_ref ~uuid:(make_uuid ()) ~name_label
311312
~name_description ~master ~default_SR ~suspend_image_SR ~crash_dump_SR
@@ -326,7 +327,7 @@ let make_pool ~__context ~master ?(name_label = "") ?(name_description = "")
326327
~ext_auth_cache_enabled:false ~ext_auth_cache_size:50L
327328
~ext_auth_cache_expiry:300L ~update_sync_frequency ~update_sync_day
328329
~update_sync_enabled ~recommendations ~license_server
329-
~ha_reboot_vm_on_internal_shutdown ;
330+
~ha_reboot_vm_on_internal_shutdown ~limit_console_sessions ;
330331
pool_ref
331332

332333
let default_sm_features =

ocaml/xapi-cli-server/records.ml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,13 @@ let pool_record rpc session_id pool =
15951595
~value:(safe_bool_of_string "ssh-auto-mode" value)
15961596
)
15971597
()
1598+
; make_field ~name:"limit-console-sessions"
1599+
~get:(fun () -> string_of_bool (x ()).API.pool_limit_console_sessions)
1600+
~set:(fun x ->
1601+
Client.Pool.set_limit_console_sessions ~rpc ~session_id ~self:pool
1602+
~value:(safe_bool_of_string "limit-console-sessions" x)
1603+
)
1604+
()
15981605
]
15991606
}
16001607

ocaml/xapi/console.ml

Lines changed: 124 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,51 @@ type address =
3434

3535
(* console is listening on a Unix domain socket *)
3636

37+
(* This module limits VNC console sessions to at most one per VM/host.
38+
Depending on configuration, either unlimited connections are allowed,
39+
or only a single active connection per VM/host is allowed. *)
40+
module Connection_limit = struct
41+
module VMSet = Set.Make (String)
42+
43+
let active_connections : VMSet.t ref = ref VMSet.empty
44+
45+
let mutex = Mutex.create ()
46+
47+
let add vm_id =
48+
Mutex.lock mutex ;
49+
active_connections := VMSet.add vm_id !active_connections ;
50+
Mutex.unlock mutex
51+
52+
let remove vm_id =
53+
Mutex.lock mutex ;
54+
active_connections := VMSet.remove vm_id !active_connections ;
55+
Mutex.unlock mutex
56+
57+
let exists vm_id =
58+
Mutex.lock mutex ;
59+
let present = VMSet.mem vm_id !active_connections in
60+
Mutex.unlock mutex ; present
61+
62+
let can_add vm_id limit_console_sessions =
63+
if not limit_console_sessions then
64+
true
65+
else
66+
not (exists vm_id)
67+
end
68+
69+
let connection_limit_reached __context vm_id =
70+
let pool = Helpers.get_pool ~__context in
71+
let limit_console_sessions =
72+
Db.Pool.get_limit_console_sessions ~__context ~self:pool
73+
in
74+
if not (Connection_limit.can_add vm_id limit_console_sessions) then (
75+
debug
76+
"Console session limit reached: only one active session allowed for VM %s"
77+
vm_id ;
78+
true
79+
) else
80+
false
81+
3782
let string_of_address = function
3883
| Port x ->
3984
"localhost:" ^ string_of_int x
@@ -84,72 +129,88 @@ let address_of_console __context console : address option =
84129
) ;
85130
address_option
86131

87-
let real_proxy __context _ _ vnc_port s =
132+
let real_proxy __context console _ _ vnc_port s =
88133
try
89-
Http_svr.headers s (Http.http_200_ok ()) ;
90-
let vnc_sock =
91-
match vnc_port with
92-
| Port x ->
93-
Unixext.open_connection_fd "127.0.0.1" x
94-
| Path x ->
95-
Unixext.open_connection_unix_fd x
96-
in
97-
(* Unixext.proxy closes fds itself so we must dup here *)
98-
let s' = Unix.dup s in
99-
debug "Connected; running proxy (between fds: %d and %d)"
100-
(Unixext.int_of_file_descr vnc_sock)
101-
(Unixext.int_of_file_descr s') ;
102-
Unixext.proxy vnc_sock s' ;
103-
debug "Proxy exited"
134+
let vm = Db.Console.get_VM ~__context ~self:console in
135+
let vm_id = Ref.string_of vm in
136+
if connection_limit_reached __context vm_id then
137+
Http_svr.headers s (Http.http_503_service_unavailable ())
138+
else (
139+
Http_svr.headers s (Http.http_200_ok ()) ;
140+
let vnc_sock =
141+
match vnc_port with
142+
| Port x ->
143+
Unixext.open_connection_fd "127.0.0.1" x
144+
| Path x ->
145+
Unixext.open_connection_unix_fd x
146+
in
147+
(* Unixext.proxy closes fds itself so we must dup here *)
148+
let s' = Unix.dup s in
149+
debug "Connected; running proxy (between fds: %d and %d)"
150+
(Unixext.int_of_file_descr vnc_sock)
151+
(Unixext.int_of_file_descr s') ;
152+
Connection_limit.add vm_id ;
153+
Unixext.proxy vnc_sock s' ;
154+
Connection_limit.remove vm_id ;
155+
debug "Proxy exited"
156+
)
104157
with exn -> debug "error: %s" (ExnHelper.string_of_exn exn)
105158

106-
let ws_proxy __context req protocol address s =
107-
let addr = match address with Port p -> string_of_int p | Path p -> p in
108-
let protocol =
109-
match protocol with `rfb -> "rfb" | `vt100 -> "vt100" | `rdp -> "rdp"
110-
in
111-
let real_path = Filename.concat "/var/lib/xcp" "websockproxy" in
112-
let sock =
113-
try Some (Fecomms.open_unix_domain_sock_client real_path)
114-
with e ->
115-
debug "Error connecting to wsproxy (%s)" (Printexc.to_string e) ;
116-
Http_svr.headers s (Http.http_501_method_not_implemented ()) ;
117-
None
159+
let ws_proxy __context _ req protocol address s =
160+
let pool = Helpers.get_pool ~__context in
161+
let limit_console_sessions =
162+
Db.Pool.get_limit_console_sessions ~__context ~self:pool
118163
in
119-
(* Ensure we always close the socket *)
120-
finally
121-
(fun () ->
122-
let upgrade_successful =
123-
Option.map
124-
(fun sock ->
125-
try
126-
let result = (sock, Some (Ws_helpers.upgrade req s)) in
127-
result
128-
with _ -> (sock, None)
129-
)
130-
sock
131-
in
132-
Option.iter
133-
(function
134-
| sock, Some ty ->
135-
let wsprotocol =
136-
match ty with
137-
| Ws_helpers.Hixie76 ->
138-
"hixie76"
139-
| Ws_helpers.Hybi10 ->
140-
"hybi10"
141-
in
142-
let message =
143-
Printf.sprintf "%s:%s:%s" wsprotocol protocol addr
144-
in
145-
let len = String.length message in
146-
ignore (Unixext.send_fd_substring sock message 0 len [] s)
147-
| _, None ->
148-
Http_svr.headers s (Http.http_501_method_not_implemented ())
149-
)
150-
upgrade_successful
151-
)
152-
(fun () -> Option.iter (fun sock -> Unix.close sock) sock)
164+
(* Disable connection via websocket if the limit is set *)
165+
if limit_console_sessions = true then
166+
Http_svr.headers s (Http.http_503_service_unavailable ())
167+
else
168+
let addr = match address with Port p -> string_of_int p | Path p -> p in
169+
let protocol =
170+
match protocol with `rfb -> "rfb" | `vt100 -> "vt100" | `rdp -> "rdp"
171+
in
172+
let real_path = Filename.concat "/var/lib/xcp" "websockproxy" in
173+
let sock =
174+
try Some (Fecomms.open_unix_domain_sock_client real_path)
175+
with e ->
176+
debug "Error connecting to wsproxy (%s)" (Printexc.to_string e) ;
177+
Http_svr.headers s (Http.http_501_method_not_implemented ()) ;
178+
None
179+
in
180+
(* Ensure we always close the socket *)
181+
finally
182+
(fun () ->
183+
let upgrade_successful =
184+
Option.map
185+
(fun sock ->
186+
try
187+
let result = (sock, Some (Ws_helpers.upgrade req s)) in
188+
result
189+
with _ -> (sock, None)
190+
)
191+
sock
192+
in
193+
Option.iter
194+
(function
195+
| sock, Some ty ->
196+
let wsprotocol =
197+
match ty with
198+
| Ws_helpers.Hixie76 ->
199+
"hixie76"
200+
| Ws_helpers.Hybi10 ->
201+
"hybi10"
202+
in
203+
let message =
204+
Printf.sprintf "%s:%s:%s" wsprotocol protocol addr
205+
in
206+
let len = String.length message in
207+
ignore (Unixext.send_fd_substring sock message 0 len [] s)
208+
| _, None ->
209+
Http_svr.headers s (Http.http_501_method_not_implemented ())
210+
)
211+
upgrade_successful
212+
)
213+
(fun () -> Option.iter (fun sock -> Unix.close sock) sock)
153214

154215
let default_console_of_vm ~__context ~self =
155216
try
@@ -247,7 +308,7 @@ let handler proxy_fn (req : Request.t) s _ =
247308
check_vm_is_running_here __context console ;
248309
match address_of_console __context console with
249310
| Some vnc_port ->
250-
proxy_fn __context req protocol vnc_port s
311+
proxy_fn __context console req protocol vnc_port s
251312
| None ->
252313
Http_svr.headers s (Http.http_404_missing ())
253314
)

ocaml/xapi/dbsync_master.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ let create_pool_record ~__context =
5555
~ext_auth_max_threads:1L ~ext_auth_cache_enabled:false
5656
~ext_auth_cache_size:50L ~ext_auth_cache_expiry:300L ~recommendations:[]
5757
~license_server:[] ~ha_reboot_vm_on_internal_shutdown:true
58+
~limit_console_sessions:false
5859

5960
let set_master_ip ~__context =
6061
let ip =

0 commit comments

Comments
 (0)