Skip to content

Commit 0a1cf52

Browse files
committed
feat(mmds): Support token generation via PUT in MMDS v1
To enable smoother migration from v1 to v2, make MMDS v1 support PUT request to /latest/api/token for token generation. We do NOT make MMDS v1 deny GET requests even with invalid tokens not to break existing workloads. It does not validate a given toekn at this point, but it will validate to increment a metric that will be added to count such requests having invalid tokens. Signed-off-by: Takahiro Itazuri <[email protected]>
1 parent 55eb796 commit 0a1cf52

File tree

11 files changed

+156
-166
lines changed

11 files changed

+156
-166
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ and this project adheres to
3535
- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290): Changed
3636
MMDS to validate the value of "X-metadata-token-ttl-seconds" header only if it
3737
is a PUT request to /latest/api/token, as in EC2 IMDS.
38+
- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290): Changed
39+
MMDS version 1 to support the session oriented method as in version 2,
40+
allowing easier migration to version 2. Note that MMDS version 1 accepts a GET
41+
request even with no token or an invalid token so that existing workloads
42+
continue to work.
3843

3944
### Deprecated
4045

docs/mmds/mmds-user-guide.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,13 @@ must be issued. The requested resource can be referenced by its corresponding
231231
the MMDS request. The HTTP response content will contain the referenced metadata
232232
resource.
233233

234-
The only HTTP method supported by MMDS version 1 is `GET`. Requests containing
235-
any other HTTP method will receive **405 Method Not Allowed** error.
234+
As in version 2, version 1 also supports a session oriented method in order to
235+
make the migration easier. See [the next section](#version-2) for the session
236+
oriented method. Note that version 1 returns a successful response to a `GET`
237+
request even with an invalid token or no token not to break existing workloads.
238+
239+
Requests containing any other HTTP methods than `GET` and `PUT` will receive
240+
**405 Method Not Allowed** error.
236241

237242
```bash
238243
MMDS_IPV4_ADDR=169.254.170.2

src/vmm/src/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ pub(crate) mod tests {
987987
net_builder.build(net_config).unwrap();
988988
let net = net_builder.iter().next().unwrap();
989989
let mut mmds = Mmds::default();
990-
mmds.set_version(mmds_version).unwrap();
990+
mmds.set_version(mmds_version);
991991
net.lock().unwrap().configure_mmds_network_stack(
992992
MmdsNetworkStack::default_ipv4_addr(),
993993
Arc::new(Mutex::new(mmds)),

src/vmm/src/device_manager/persist.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ impl<'a> Persist<'a> for MMIODeviceManager {
569569
// If there's at least one network device having an mmds_ns, it means
570570
// that we are restoring from a version that did not persist the `MmdsVersionState`.
571571
// Init with the default.
572-
constructor_args.vm_resources.mmds_or_default();
572+
constructor_args.vm_resources.mmds_or_default()?;
573573
}
574574

575575
for net_state in &state.net_devices {

src/vmm/src/mmds/data_store.rs

Lines changed: 20 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ use crate::mmds::token::{MmdsTokenError as TokenError, TokenAuthority};
1212
/// The Mmds is the Microvm Metadata Service represented as an untyped json.
1313
#[derive(Debug)]
1414
pub struct Mmds {
15+
version: MmdsVersion,
1516
data_store: Value,
16-
// None when MMDS V1 is configured, Some for MMDS V2.
17-
token_authority: Option<TokenAuthority>,
17+
token_authority: TokenAuthority,
1818
is_initialized: bool,
1919
data_store_limit: usize,
2020
}
@@ -65,19 +65,20 @@ pub enum MmdsDatastoreError {
6565
// Used for ease of use in tests.
6666
impl Default for Mmds {
6767
fn default() -> Self {
68-
Self::default_with_limit(51200)
68+
Self::try_new(51200).unwrap()
6969
}
7070
}
7171

7272
impl Mmds {
7373
/// MMDS default instance with limit `data_store_limit`
74-
pub fn default_with_limit(data_store_limit: usize) -> Self {
75-
Mmds {
74+
pub fn try_new(data_store_limit: usize) -> Result<Self, MmdsDatastoreError> {
75+
Ok(Mmds {
76+
version: MmdsVersion::default(),
7677
data_store: Value::default(),
77-
token_authority: None,
78+
token_authority: TokenAuthority::try_new()?,
7879
is_initialized: false,
7980
data_store_limit,
80-
}
81+
})
8182
}
8283

8384
/// This method is needed to check if data store is initialized.
@@ -92,52 +93,29 @@ impl Mmds {
9293
}
9394

9495
/// Set the MMDS version.
95-
pub fn set_version(&mut self, version: MmdsVersion) -> Result<(), MmdsDatastoreError> {
96-
match version {
97-
MmdsVersion::V1 => {
98-
self.token_authority = None;
99-
Ok(())
100-
}
101-
MmdsVersion::V2 => {
102-
if self.token_authority.is_none() {
103-
self.token_authority = Some(TokenAuthority::try_new()?);
104-
}
105-
Ok(())
106-
}
107-
}
96+
pub fn set_version(&mut self, version: MmdsVersion) {
97+
self.version = version;
10898
}
10999

110-
/// Return the MMDS version by checking the token authority field.
100+
/// Get the MMDS version.
111101
pub fn version(&self) -> MmdsVersion {
112-
if self.token_authority.is_none() {
113-
MmdsVersion::V1
114-
} else {
115-
MmdsVersion::V2
116-
}
102+
self.version
117103
}
118104

119105
/// Sets the Additional Authenticated Data to be used for encryption and
120-
/// decryption of the session token when MMDS version 2 is enabled.
106+
/// decryption of the session token.
121107
pub fn set_aad(&mut self, instance_id: &str) {
122-
if let Some(ta) = self.token_authority.as_mut() {
123-
ta.set_aad(instance_id);
124-
}
108+
self.token_authority.set_aad(instance_id);
125109
}
126110

127111
/// Checks if the provided token has not expired.
128-
pub fn is_valid_token(&self, token: &str) -> Result<bool, TokenError> {
129-
self.token_authority
130-
.as_ref()
131-
.ok_or(TokenError::InvalidState)
132-
.map(|ta| ta.is_valid(token))
112+
pub fn is_valid_token(&self, token: &str) -> bool {
113+
self.token_authority.is_valid(token)
133114
}
134115

135116
/// Generate a new Mmds token using the token authority.
136117
pub fn generate_token(&mut self, ttl_seconds: u32) -> Result<String, TokenError> {
137-
self.token_authority
138-
.as_mut()
139-
.ok_or(TokenError::InvalidState)
140-
.and_then(|ta| ta.generate_token_secret(ttl_seconds))
118+
self.token_authority.generate_token_secret(ttl_seconds)
141119
}
142120

143121
/// set MMDS data store limit to `data_store_limit`
@@ -304,11 +282,11 @@ mod tests {
304282
assert_eq!(mmds.version(), MmdsVersion::V1);
305283

306284
// Test setting MMDS version to v2.
307-
mmds.set_version(MmdsVersion::V2).unwrap();
285+
mmds.set_version(MmdsVersion::V2);
308286
assert_eq!(mmds.version(), MmdsVersion::V2);
309287

310-
// Test setting MMDS version back to default.
311-
mmds.set_version(MmdsVersion::V1).unwrap();
288+
// Test setting MMDS version back to v1.
289+
mmds.set_version(MmdsVersion::V1);
312290
assert_eq!(mmds.version(), MmdsVersion::V1);
313291
}
314292

@@ -593,37 +571,4 @@ mod tests {
593571

594572
assert_eq!(mmds.get_data_str().len(), 2);
595573
}
596-
597-
#[test]
598-
fn test_is_valid() {
599-
let mut mmds = Mmds::default();
600-
// Set MMDS version to V2.
601-
mmds.set_version(MmdsVersion::V2).unwrap();
602-
assert_eq!(mmds.version(), MmdsVersion::V2);
603-
604-
assert!(!mmds.is_valid_token("aaa").unwrap());
605-
606-
mmds.token_authority = None;
607-
assert_eq!(
608-
mmds.is_valid_token("aaa").unwrap_err().to_string(),
609-
TokenError::InvalidState.to_string()
610-
)
611-
}
612-
613-
#[test]
614-
fn test_generate_token() {
615-
let mut mmds = Mmds::default();
616-
// Set MMDS version to V2.
617-
mmds.set_version(MmdsVersion::V2).unwrap();
618-
assert_eq!(mmds.version(), MmdsVersion::V2);
619-
620-
let token = mmds.generate_token(1).unwrap();
621-
assert!(mmds.is_valid_token(&token).unwrap());
622-
623-
mmds.token_authority = None;
624-
assert_eq!(
625-
mmds.generate_token(1).err().unwrap().to_string(),
626-
TokenError::InvalidState.to_string()
627-
);
628-
}
629574
}

src/vmm/src/mmds/mod.rs

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ fn sanitize_uri(mut uri: String) -> String {
108108

109109
/// Build a response for `request` and return response based on MMDS version
110110
pub fn convert_to_response(mmds: Arc<Mutex<Mmds>>, request: Request) -> Response {
111+
// Check URI is not empty
111112
let uri = request.uri().get_abs_path();
112113
if uri.is_empty() {
113114
return build_response(
@@ -120,16 +121,13 @@ pub fn convert_to_response(mmds: Arc<Mutex<Mmds>>, request: Request) -> Response
120121

121122
let mut mmds_guard = mmds.lock().expect("Poisoned lock");
122123

123-
match mmds_guard.version() {
124-
MmdsVersion::V1 => respond_to_request_mmdsv1(&mmds_guard, request),
125-
MmdsVersion::V2 => respond_to_request_mmdsv2(&mut mmds_guard, request),
126-
}
127-
}
128-
129-
fn respond_to_request_mmdsv1(mmds: &Mmds, request: Request) -> Response {
130-
// Allow only GET requests.
124+
// Allow only GET and PUT requests
131125
match request.method() {
132-
Method::Get => respond_to_get_request_unchecked(mmds, request),
126+
Method::Get => match mmds_guard.version() {
127+
MmdsVersion::V1 => respond_to_get_request_v1(&mmds_guard, request),
128+
MmdsVersion::V2 => respond_to_get_request_v2(&mmds_guard, request),
129+
},
130+
Method::Put => respond_to_put_request(&mut mmds_guard, request),
133131
_ => {
134132
let mut response = build_response(
135133
request.http_version(),
@@ -138,31 +136,18 @@ fn respond_to_request_mmdsv1(mmds: &Mmds, request: Request) -> Response {
138136
Body::new(VmmMmdsError::MethodNotAllowed.to_string()),
139137
);
140138
response.allow_method(Method::Get);
139+
response.allow_method(Method::Put);
141140
response
142141
}
143142
}
144143
}
145144

146-
fn respond_to_request_mmdsv2(mmds: &mut Mmds, request: Request) -> Response {
147-
// Allow only GET and PUT requests.
148-
match request.method() {
149-
Method::Get => respond_to_get_request_checked(mmds, request),
150-
Method::Put => respond_to_put_request(mmds, request),
151-
_ => {
152-
let mut response = build_response(
153-
request.http_version(),
154-
StatusCode::MethodNotAllowed,
155-
MediaType::PlainText,
156-
Body::new(VmmMmdsError::MethodNotAllowed.to_string()),
157-
);
158-
response.allow_method(Method::Get);
159-
response.allow_method(Method::Put);
160-
response
161-
}
162-
}
145+
fn respond_to_get_request_v1(mmds: &Mmds, request: Request) -> Response {
146+
// TODO: Increments metrics that will be added in an upcoming commit.
147+
respond_to_get_request(mmds, request)
163148
}
164149

165-
fn respond_to_get_request_checked(mmds: &Mmds, request: Request) -> Response {
150+
fn respond_to_get_request_v2(mmds: &Mmds, request: Request) -> Response {
166151
// Check whether a token exists.
167152
let token = match get_header_value_pair(
168153
request.headers.custom_entries(),
@@ -182,18 +167,17 @@ fn respond_to_get_request_checked(mmds: &Mmds, request: Request) -> Response {
182167

183168
// Validate the token.
184169
match mmds.is_valid_token(token) {
185-
Ok(true) => respond_to_get_request_unchecked(mmds, request),
186-
Ok(false) => build_response(
170+
true => respond_to_get_request(mmds, request),
171+
false => build_response(
187172
request.http_version(),
188173
StatusCode::Unauthorized,
189174
MediaType::PlainText,
190175
Body::new(VmmMmdsError::InvalidToken.to_string()),
191176
),
192-
Err(_) => unreachable!(),
193177
}
194178
}
195179

196-
fn respond_to_get_request_unchecked(mmds: &Mmds, request: Request) -> Response {
180+
fn respond_to_get_request(mmds: &Mmds, request: Request) -> Response {
197181
let uri = request.uri().get_abs_path();
198182

199183
// The data store expects a strict json path, so we need to
@@ -483,8 +467,7 @@ mod tests {
483467
// Set version to V1.
484468
mmds.lock()
485469
.expect("Poisoned lock")
486-
.set_version(MmdsVersion::V1)
487-
.unwrap();
470+
.set_version(MmdsVersion::V1);
488471
assert_eq!(
489472
mmds.lock().expect("Poisoned lock").version(),
490473
MmdsVersion::V1
@@ -512,7 +495,7 @@ mod tests {
512495
assert_eq!(actual_response, expected_response);
513496

514497
// Test not allowed HTTP Method.
515-
let not_allowed_methods = ["PUT", "PATCH"];
498+
let not_allowed_methods = ["PATCH"];
516499
for method in not_allowed_methods.iter() {
517500
let request = Request::try_from(
518501
format!("{method} http://169.254.169.255/ HTTP/1.0\r\n\r\n").as_bytes(),
@@ -524,6 +507,7 @@ mod tests {
524507
expected_response.set_content_type(MediaType::PlainText);
525508
expected_response.set_body(Body::new(VmmMmdsError::MethodNotAllowed.to_string()));
526509
expected_response.allow_method(Method::Get);
510+
expected_response.allow_method(Method::Put);
527511
let actual_response = convert_to_response(mmds.clone(), request);
528512
assert_eq!(actual_response, expected_response);
529513
}
@@ -546,12 +530,47 @@ mod tests {
546530
let actual_response = convert_to_response(mmds.clone(), request);
547531
assert_eq!(actual_response, expected_response);
548532

549-
// Test Ok path.
533+
// Test valid v1 request.
550534
let (request, expected_response) = generate_request_and_expected_response(
551535
b"GET http://169.254.169.254/ HTTP/1.0\r\n\
552536
Accept: application/json\r\n\r\n",
553537
MediaType::ApplicationJson,
554538
);
539+
let actual_response = convert_to_response(mmds.clone(), request);
540+
assert_eq!(actual_response, expected_response);
541+
542+
// Test valid v2 request.
543+
let request = Request::try_from(
544+
b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\
545+
X-metadata-token-ttl-seconds: 60\r\n\r\n",
546+
None,
547+
)
548+
.unwrap();
549+
let actual_response = convert_to_response(mmds.clone(), request);
550+
assert_eq!(actual_response.status(), StatusCode::OK);
551+
assert_eq!(actual_response.content_type(), MediaType::PlainText);
552+
553+
let valid_token = String::from_utf8(actual_response.body().unwrap().body).unwrap();
554+
#[rustfmt::skip]
555+
let (request, expected_response) = generate_request_and_expected_response(
556+
format!(
557+
"GET http://169.254.169.254/ HTTP/1.0\r\n\
558+
Accept: application/json\r\n\
559+
X-metadata-token: {valid_token}\r\n\r\n",
560+
)
561+
.as_bytes(),
562+
MediaType::ApplicationJson,
563+
);
564+
let actual_response = convert_to_response(mmds.clone(), request);
565+
assert_eq!(actual_response, expected_response);
566+
567+
// Test GET request with invalid token is accepted when v1 is configured.
568+
let (request, expected_response) = generate_request_and_expected_response(
569+
b"GET http://169.254.169.254/ HTTP/1.0\r\n\
570+
Accept: application/json\r\n\
571+
X-metadata-token: INVALID_TOKEN\r\n\r\n",
572+
MediaType::ApplicationJson,
573+
);
555574
let actual_response = convert_to_response(mmds, request);
556575
assert_eq!(actual_response, expected_response);
557576
}
@@ -564,8 +583,7 @@ mod tests {
564583
// Set version to V2.
565584
mmds.lock()
566585
.expect("Poisoned lock")
567-
.set_version(MmdsVersion::V2)
568-
.unwrap();
586+
.set_version(MmdsVersion::V2);
569587
assert_eq!(
570588
mmds.lock().expect("Poisoned lock").version(),
571589
MmdsVersion::V2

src/vmm/src/mmds/token.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ pub enum MmdsTokenError {
5959
EntropyPool(#[from] io::Error),
6060
/// Failed to extract expiry value from token.
6161
ExpiryExtraction,
62-
/// Invalid token authority state.
63-
InvalidState,
6462
/// Invalid time to live value provided for token: {0}. Please provide a value between {MIN_TOKEN_TTL_SECONDS:} and {MAX_TOKEN_TTL_SECONDS:}.
6563
InvalidTtlValue(u32),
6664
/// Bincode serialization failed: {0}.

0 commit comments

Comments
 (0)