underhill_core/
inspect_internal.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Generally we hold the position that inspect paths are completely unstable,
5//! may change at any time, and can not be depended on by anything. However, we
6//! at Microsoft have implemented stable, versioned, supported, internal
7//! diagnostic tools that depend on inspect paths under the hood. This would be
8//! fine so long as we are guaranteed that our diagnostic tooling and OpenHCL
9//! builds are always in sync, and indeed they are most of the time. However
10//! there are cases where they may diverge temporarily, such as during servicing.
11//! We still want to be able to provide as much information as possible during
12//! these time periods, but in order for that to be possible we need some kind
13//! of stable-ish interface to talk to OpenHCL with.
14//!
15//! This module provides that interface by creating known, controlled inspect
16//! paths that are unlikely to change accidentally. The implementation details
17//! of these paths are free to change in order to preserve their interface, and
18//! all diagnostics commands should have extensive tests to ensure compatibility
19//! with a matching build of OpenHCL.
20//!
21//! This approach is not particularly scalable, especially if other parties want
22//! to add their own stabilized diagnostics and versioning schemes. If such a
23//! need arises we should consider a more general and extensible solution.
24//!
25//! At the time of writing, our support policy is that these interfaces will be
26//! preserved for at least 2 internal releases. This means that diagnostic
27//! commands may fail in any way they see fit if the version of OpenHCL in the
28//! VM is more than 2 releases old.
29//!
30//! In the future we may choose a different interface for these commands,
31//! and this code may be deleted, however such a change will still follow the
32//! above support policy.
33
34use inspect::Deferred;
35use inspect::InspectionBuilder;
36use inspect::Node;
37use inspect::Request;
38use inspect::Response;
39use inspect::SensitivityLevel;
40use mesh::Sender;
41use pal_async::DefaultDriver;
42use pal_async::task::Spawn;
43
44pub(crate) fn inspect_internal_diagnostics(
45    req: Request<'_>,
46    reinspect: Sender<Deferred>,
47    driver: DefaultDriver,
48) {
49    req.respond()
50        .sensitivity_field("build_info", SensitivityLevel::Safe, build_info::get())
51        .sensitivity_child("net", SensitivityLevel::Safe, |req| {
52            net(req, reinspect, driver)
53        });
54}
55
56fn net(req: Request<'_>, reinspect: Sender<Deferred>, driver: DefaultDriver) {
57    let defer = req.defer();
58    let driver2 = driver.clone();
59    driver
60        .spawn("inspect-diagnostics-net", async move {
61            // Note the use of Sensitive here so we can inspect under the VM node,
62            // which isn't Safe. The data we produce will still use the underlying
63            // sensitivity of the data nodes, so nothing will be improperly exposed.
64            let mut vm_inspection = InspectionBuilder::new("vm")
65                .depth(Some(0))
66                .sensitivity(Some(SensitivityLevel::Sensitive))
67                .inspect(inspect::adhoc(|req| reinspect.send(req.defer())));
68            vm_inspection.resolve().await;
69
70            let Node::Dir(nodes) = vm_inspection.results() else {
71                return defer.value("Error: No VM node.");
72            };
73
74            defer.respond(|resp| {
75                for nic_entry in nodes
76                    .into_iter()
77                    .filter(|entry| entry.name.starts_with("net:f8615163-"))
78                {
79                    // Inspect node names for MANA nics are in the format:
80                    // net:f8615163-0000-1000-2000-<mac address>
81                    // So the mac address string starts at index 28
82                    let mac_name = nic_entry.name[28..].to_owned();
83
84                    // The existence of a mac address is always known to the host, so this can always be Safe.
85                    resp.sensitivity_child(&mac_name, SensitivityLevel::Safe, |req| {
86                        net_nic(req, nic_entry.name, reinspect.clone(), driver2.clone());
87                    });
88                }
89            })
90        })
91        .detach();
92}
93
94// net/mac_address
95// Format for mac address is no separators, lowercase letters, e.g. 00155d121212.
96fn net_nic(req: Request<'_>, name: String, reinspect: Sender<Deferred>, driver: DefaultDriver) {
97    let defer = req.defer();
98    driver
99        .spawn("inspect-diagnostics-net-nic", async move {
100            // Note the use of Sensitive here so we can inspect under the VM node,
101            // which isn't Safe. The data we produce will still use the underlying
102            // sensitivity of the data nodes, so nothing will be improperly exposed.
103            let mut vm_inspection = InspectionBuilder::new(&format!("vm/{name}"))
104                .depth(Some(5))
105                .sensitivity(Some(SensitivityLevel::Sensitive))
106                .inspect(inspect::adhoc(|req| reinspect.send(req.defer())));
107            vm_inspection.resolve().await;
108
109            if let Node::Dir(nodes) = vm_inspection.results() {
110                defer.respond(|resp| {
111                    for entry in nodes {
112                        let sensitivity = entry.sensitivity;
113                        if [
114                            "endpoint",
115                            "ndis_config",
116                            "offload_support",
117                            "primary_channel_state",
118                        ]
119                        .contains(&&*entry.name)
120                        {
121                            flatten_with_prefix(resp, &entry.name, entry.node, sensitivity, &[]);
122                        } else if entry.name == "queues" {
123                            let Node::Dir(queues) = entry.node else {
124                                continue;
125                            };
126                            resp.sensitivity_child("queues", sensitivity, |req| {
127                                let mut resp = req.respond();
128                                for queue_entry in queues {
129                                    let queue_sensitivity = queue_entry.sensitivity;
130                                    resp.sensitivity_child(
131                                        &queue_entry.name,
132                                        queue_sensitivity,
133                                        |req| {
134                                            flatten_with_prefix(
135                                                &mut req.respond(),
136                                                "",
137                                                queue_entry.node,
138                                                queue_sensitivity,
139                                                &["ring"],
140                                            );
141                                        },
142                                    );
143                                }
144                            });
145                        }
146                    }
147                })
148            } else {
149                defer.value(format!("Unexpected node when looking for NIC {name}."));
150            }
151        })
152        .detach();
153}
154
155fn flatten_with_prefix(
156    resp: &mut Response<'_>,
157    prefix: &str,
158    node: Node,
159    sensitivity: SensitivityLevel,
160    ignore_list: &[&str],
161) {
162    match node {
163        Node::Dir(d) => {
164            for entry in d {
165                if ignore_list.contains(&&*entry.name) {
166                    continue;
167                }
168                let next_prefix = if !prefix.is_empty() {
169                    format!("{}_{}", prefix, entry.name)
170                } else {
171                    entry.name
172                };
173                // Since we're traversing multiple nodes and emitting only one,
174                // emit the final node as the highest sensitivity of all the
175                // nodes we traversed, to be safe.
176                flatten_with_prefix(
177                    resp,
178                    &next_prefix,
179                    entry.node,
180                    sensitivity.max(entry.sensitivity),
181                    ignore_list,
182                );
183            }
184        }
185        Node::Value(v) => {
186            resp.sensitivity_field(prefix, sensitivity, v);
187        }
188        Node::Failed(_) | Node::Unevaluated => {}
189    }
190}