-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implement an rexec_client; move previous rexec code to rexec_server #36
Changes from all commits
1cab7b2
cf58575
e853ade
b4ea728
c48f093
7c0d9c7
c6e66cc
d241564
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
include RExec_common | ||
|
||
open Lwt.Infix | ||
|
||
let src = Logs.Src.create "qubes.rexec_client" ~doc:"Qubes qrexec-client" | ||
module Log = (val Logs.src_log src : Logs.LOG) | ||
|
||
module Flow = struct | ||
type t = { | ||
connection : QV.t; | ||
mutable stderr_buf : Cstruct.t; | ||
mutable stdout_buf : Cstruct.t; | ||
} | ||
|
||
let create connection = { connection; stderr_buf = Cstruct.empty; | ||
stdout_buf = Cstruct.empty; | ||
} | ||
|
||
let write t data = send ~ty:`Data_stdin t.connection data | ||
|
||
let writef t fmt = | ||
fmt |> Printf.ksprintf @@ fun s -> | ||
send ~ty:`Data_stdin t.connection (Cstruct.of_string (s ^ "\n")) | ||
|
||
let next_msg t = | ||
recv t.connection >|= function | ||
| `Ok (`Data_stdout, data) -> | ||
t.stdout_buf <- Cstruct.append t.stdout_buf data; | ||
`Ok t | ||
| `Ok (`Data_stderr, data) -> | ||
t.stderr_buf <- Cstruct.append t.stderr_buf data; | ||
`Ok t | ||
| `Ok (`Data_exit_code, data) -> | ||
`Done (Formats.Qrexec.get_exit_status_return_code data) | ||
| `Ok (ty, _) -> | ||
Log.debug Formats.Qrexec.(fun f -> f "unexpected message of type %ld (%s) received; \ | ||
ignoring it" (int_of_type ty) (string_of_type ty)); | ||
`Ok t | ||
| `Eof -> `Eof | ||
|
||
let read t = | ||
let rec aux = function | ||
| `Eof | `Done _ as s -> Lwt.return s | ||
| `Ok t -> | ||
let drain_stdout () = | ||
let output = t.stdout_buf in | ||
t.stdout_buf <- Cstruct.empty; | ||
Lwt.return @@ `Stdout output | ||
and drain_stderr () = | ||
let output = t.stderr_buf in | ||
t.stderr_buf <- Cstruct.empty; | ||
Lwt.return @@ `Stderr output | ||
in | ||
if Cstruct.len t.stdout_buf > 0 then drain_stdout () | ||
else if Cstruct.len t.stderr_buf > 0 then drain_stderr () | ||
else next_msg t >>= aux | ||
in | ||
aux (`Ok t) | ||
|
||
let rec read_line t = | ||
let stdout = Cstruct.to_string t.stdout_buf | ||
and stderr = Cstruct.to_string t.stderr_buf | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was about to pose the cheeky question of whether converting both to Perhaps we can do something like storing the length of the previously examined I'd still prefer if we could avoid converting both buffers if we know we are always going to go with the first if there's a match; would it be OK with you to serialize these so the second conversion only happens when |
||
in | ||
let newline buf = String.index_opt buf '\n' in | ||
match newline stdout, newline stderr with | ||
| Some i, _ -> | ||
let retval = String.sub stdout 0 i in | ||
t.stdout_buf <- Cstruct.shift t.stdout_buf (i + 1); | ||
Lwt.return (`Stdout retval) | ||
| _, Some i -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this is a problem, but as long as there's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is similar to how |
||
let retval = String.sub stderr 0 i in | ||
t.stderr_buf <- Cstruct.shift t.stderr_buf (i + 1); | ||
Lwt.return (`Stderr retval) | ||
| None, None -> | ||
next_msg t >>= function | ||
| `Done _ | `Eof as s -> Lwt.return s | ||
| `Ok t -> read_line t | ||
end | ||
|
||
let start_connection data = | ||
let domid = Formats.Qrexec.get_exec_params_connect_domain data in | ||
let port = Formats.Qrexec.get_exec_params_connect_port data in | ||
Log.debug (fun f -> f "service_connect message received: domain %ld, port %ld" domid port); | ||
Log.debug (fun f -> f "Connecting..."); | ||
match Vchan.Port.of_string (Int32.to_string port) with | ||
| `Error msg -> Lwt.return @@ Error (`Msg msg) | ||
| `Ok port -> | ||
QV.server ~domid:(Int32.to_int domid) ~port () >>= fun remote -> | ||
send_hello remote >>= fun () -> | ||
recv_hello remote >>= fun version -> | ||
Log.debug (fun f -> f "server connected on port %s, using protocol version %ld" (Vchan.Port.to_string port) version); | ||
Lwt.return @@ Ok (Flow.create remote) | ||
|
||
let connect ~vm ~service ~identifier = | ||
let write_trigger_service_parameters_into buf = | ||
let write_string s ~dst_offset ~max_len = | ||
Cstruct.blit_from_string s 0 buf dst_offset (min (String.length s) max_len) | ||
in | ||
write_string service ~dst_offset:0 ~max_len:64; | ||
write_string vm ~dst_offset:64 ~max_len:32; | ||
write_string identifier ~dst_offset:(64+32) ~max_len:32; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason this doesn't use the In the definition they seem to suggest these should be 0-terminated, so maybe we should pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code in QubesOS does add a zero byte at the very end of the char array in any case. But I agree that we should ensure it's also zero-terminated on our end because otherwise we potentially lose one byte of the identifier. https://github.com/QubesOS/qubes-core-qrexec/blob/master/daemon/qrexec-daemon.c#L817 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed in qrexec-client-vm that they just pass |
||
buf | ||
in | ||
let tsp = write_trigger_service_parameters_into @@ | ||
Cstruct.create Formats.Qrexec.sizeof_trigger_service_params in | ||
Log.debug (fun f -> f "Initiating connection to dom0 (to request service start)"); | ||
QV.server ~domid:0 ~port:vchan_base_port () >>= fun server -> | ||
RExec_common.send_hello server >>= fun () -> | ||
RExec_common.recv_hello server >>= fun version -> | ||
Log.debug (fun f -> f "connection with dom0 established (version %ld)" version); | ||
RExec_common.send server ~ty:`Trigger_service tsp >>= function | ||
| `Eof -> Lwt.return @@ Error `Closed | ||
| `Ok () -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. XXX Not related to this PR, but I went looking for the receiving side of this, and fell over if ((hdr->type == MSG_EXEC_CMDLINE || hdr->type == MSG_JUST_EXEC) &&
!strstr(buf, ":nogui:")) {
/* seems to me this strstr() should probably be a memmem() bounded to buf_len */ |
||
let rec try_recv () = | ||
recv server >>= function | ||
| `Eof -> Lwt.return @@ Error `Closed | ||
| `Ok (`Service_refused, _) -> Lwt.return @@ Error `Permission_denied | ||
| `Ok (`Service_connect, data) -> | ||
(* we have everything we need, so close the server connection *) | ||
QV.disconnect server >>= fun () -> | ||
start_connection data | ||
| `Ok (ty, _) -> | ||
let open Formats.Qrexec in | ||
Log.debug (fun f -> f | ||
"unhandled qrexec message type received in response to \ | ||
trigger service request: %ld (%s)" | ||
(int_of_type ty) (string_of_type ty)); | ||
try_recv () | ||
in | ||
try_recv () | ||
|
||
let close t = | ||
QV.disconnect t.Flow.connection |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
module Flow : sig | ||
type t | ||
|
||
val write : t -> Cstruct.t -> [ `Ok of unit | `Eof ] Lwt.t | ||
|
||
val writef : t -> ('a, unit, string, unit S.or_eof Lwt.t) format4 -> 'a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like documenting the trailing It seems confusing to me that |
||
|
||
val read : t -> [ `Stderr of Cstruct.t | ||
| `Stdout of Cstruct.t | ||
| `Done of int32 | `Eof ] Lwt.t | ||
|
||
val read_line : t -> [ `Stderr of string | ||
| `Stdout of string | ||
| `Done of int32 | `Eof ] Lwt.t | ||
|
||
end | ||
|
||
val connect : vm:string -> service:string -> identifier:string -> | ||
(Flow.t, [`Closed | `Permission_denied | `Msg of string ]) result Lwt.t | ||
(** Attempt to establish a qrexec connection to the guest named [vm], | ||
and try to start the provided [service]. | ||
Use [identifier] to disambiguate this traffic. | ||
*) | ||
|
||
val close : Flow.t -> unit Lwt.t | ||
(** Close the underlying vchan without waiting for the remote side to complete. | ||
Any remaining messages will be discarded. *) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
open Formats.Qrexec | ||
open Utils | ||
open Lwt.Infix | ||
|
||
module QV = Msg_chan.Make(Framing) | ||
|
||
type t = QV.t | ||
|
||
let (>>!=) = Msg_chan.(>>!=) | ||
|
||
let split chr s = | ||
try | ||
let i = String.index s chr in | ||
Some (String.sub s 0 i, String.sub s (i + 1) (String.length s - i - 1)) | ||
with Not_found -> | ||
None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could avoid exceptions with something like: let split chr s =
match String.index_opt s chr with
| None -> None
| Some i -> Some (String.sub s 0 i, String.sub s (i + 1) (String.length s - i - 1)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
let or_fail = function | ||
| `Ok y -> return y | ||
| `Error (`Unknown msg) -> fail (Failure msg) | ||
| `Eof -> fail End_of_file | ||
|
||
let disconnect = QV.disconnect | ||
|
||
let vchan_base_port = | ||
match Vchan.Port.of_string "512" with | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does this constant come from? |
||
| `Error msg -> failwith msg | ||
| `Ok port -> port | ||
|
||
let max_data_chunk = 4096 | ||
(** Max size for data chunks. See MAX_DATA_CHUNK in qubes-linux-utils/qrexec-lib/qrexec.h *) | ||
|
||
let rec send t ~ty data = | ||
let data, data' = Cstruct.split data (min max_data_chunk (Cstruct.len data)) in | ||
let hdr = Cstruct.create sizeof_msg_header in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we wrapped the body in a recursive function we could avoid an allocation of |
||
set_msg_header_ty hdr (int_of_type ty); | ||
set_msg_header_len hdr (Cstruct.len data |> Int32.of_int); | ||
if Cstruct.len data' = 0 | ||
then QV.send t [hdr; data] | ||
else QV.send t [hdr; data] >>= function | ||
| `Eof -> return `Eof | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really want to |
||
| `Ok () -> | ||
send t ~ty data' | ||
|
||
let recv t = | ||
QV.recv t >>!= fun (hdr, data) -> | ||
let ty = get_msg_header_ty hdr |> type_of_int in | ||
return (`Ok (ty, data)) | ||
|
||
let send_hello t = | ||
let hello = Cstruct.create sizeof_peer_info in | ||
set_peer_info_version hello 2l; | ||
send t ~ty:`Hello hello >>= function | ||
| `Eof -> fail (error "End-of-file sending msg_hello") | ||
| `Ok () -> return () | ||
|
||
let recv_hello t = | ||
recv t >>= function | ||
| `Eof -> fail (error "End-of-file waiting for msg_hello") | ||
| `Ok (`Hello, resp) -> return (get_peer_info_version resp) | ||
| `Ok (ty, _) -> fail (error "Expected msg_hello, got %ld" (int_of_type ty)) | ||
|
||
let port_of_int i = | ||
match Int32.to_string i |> Vchan.Port.of_string with | ||
| `Ok p -> p | ||
| `Error msg -> failwith msg | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make sense to preserve the
these should be \0-terminated
comments from the original header file?