use super::container::{
canonical_id_or_fallback, extract_container_dns_info, extract_container_name,
};
use super::proxying::proxy_to_system_vm;
use crate::api::AppState;
use crate::error::{DockerError, Result};
use axum::body::Body;
use axum::extract::{OriginalUri, State};
use axum::http::{Request, Uri};
use axum::response::Response;
#[tracing::instrument(
name = "docker.network.connect",
skip(state, req),
fields(uri = %uri, utility_vm = "native"),
err
)]
pub async fn network_connect(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
proxy_then_refresh_dns(&state, &uri, req).await
}
#[tracing::instrument(
name = "docker.network.disconnect",
skip(state, req),
fields(uri = %uri, utility_vm = "native"),
err
)]
pub async fn network_disconnect(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
proxy_then_refresh_dns(&state, &uri, req).await
}
async fn proxy_then_refresh_dns(
state: &AppState,
uri: &Uri,
req: Request<Body>,
) -> Result<Response> {
let (parts, body) = req.into_parts();
let body_bytes = http_body_util::BodyExt::collect(body)
.await
.map_err(|e| DockerError::Server(format!("failed to read body: {e}")))?
.to_bytes();
let container = container_from_network_body(&body_bytes);
let req = Request::from_parts(parts, Body::from(body_bytes));
let response = proxy_to_system_vm(state, uri, req).await?;
if response.status().is_success() {
match container {
Some(token) => refresh_container_dns(state, &token).await,
None => tracing::debug!(
uri = %uri,
"network operation body had no Container field; skipping DNS refresh"
),
}
}
Ok(response)
}
async fn refresh_container_dns(state: &AppState, token: &str) {
let Some(inspect) = crate::guest_query::inspect_container(state.proxy.client(), token).await
else {
tracing::debug!(
container = token,
"could not inspect container after network change; DNS entry left unchanged"
);
return;
};
let canonical = canonical_id_or_fallback(token, &inspect);
state.runtime.deregister_dns_by_id(&canonical).await;
if let Some(name) = extract_container_name(&inspect) {
state
.runtime
.register_container_alias(&name, &canonical)
.await;
}
if let Some((aliases, ip)) = extract_container_dns_info(&inspect) {
state.runtime.register_dns(&canonical, &aliases, ip).await;
}
}
fn container_from_network_body(body: &[u8]) -> Option<String> {
let value: serde_json::Value = serde_json::from_slice(body).ok()?;
value
.get("Container")?
.as_str()
.filter(|s| !s.is_empty())
.map(String::from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn container_field_from_connect_body() {
let body = br#"{"Container":"web","EndpointConfig":{"IPAMConfig":{}}}"#;
assert_eq!(container_from_network_body(body).as_deref(), Some("web"));
}
#[test]
fn container_field_from_disconnect_body() {
let body = br#"{"Container":"abc123","Force":true}"#;
assert_eq!(container_from_network_body(body).as_deref(), Some("abc123"));
}
#[test]
fn missing_or_empty_container_field_is_none() {
assert_eq!(container_from_network_body(br#"{"Force":true}"#), None);
assert_eq!(container_from_network_body(br#"{"Container":""}"#), None);
assert_eq!(container_from_network_body(b"not json"), None);
}
}