agent: iptables: get/set handling for iptables

Initial support for getting and setting iptables in the guest.

Signed-off-by: Eric Ernst <eric_ernst@apple.com>
This commit is contained in:
Eric Ernst 2022-04-21 21:36:36 -07:00
parent 03176a9e09
commit bd50d463b2

View File

@ -23,8 +23,9 @@ use cgroups::freezer::FreezerState;
use oci::{LinuxNamespace, Root, Spec}; use oci::{LinuxNamespace, Root, Spec};
use protobuf::{Message, RepeatedField, SingularPtrField}; use protobuf::{Message, RepeatedField, SingularPtrField};
use protocols::agent::{ use protocols::agent::{
AddSwapRequest, AgentDetails, CopyFileRequest, GuestDetailsResponse, Interfaces, Metrics, AddSwapRequest, AgentDetails, CopyFileRequest, GetIPTablesRequest, GetIPTablesResponse,
OOMEvent, ReadStreamResponse, Routes, StatsContainerResponse, VolumeStatsRequest, GuestDetailsResponse, Interfaces, Metrics, OOMEvent, ReadStreamResponse, Routes,
SetIPTablesRequest, SetIPTablesResponse, StatsContainerResponse, VolumeStatsRequest,
WaitProcessResponse, WriteStreamResponse, WaitProcessResponse, WriteStreamResponse,
}; };
use protocols::csi::{VolumeCondition, VolumeStatsResponse, VolumeUsage, VolumeUsage_Unit}; use protocols::csi::{VolumeCondition, VolumeStatsResponse, VolumeUsage, VolumeUsage_Unit};
@ -75,18 +76,29 @@ use std::time::Duration;
use nix::unistd::{Gid, Uid}; use nix::unistd::{Gid, Uid};
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader, Write};
use std::os::unix::fs::FileExt; use std::os::unix::fs::FileExt;
use std::path::PathBuf; use std::path::PathBuf;
const CONTAINER_BASE: &str = "/run/kata-containers"; const CONTAINER_BASE: &str = "/run/kata-containers";
const MODPROBE_PATH: &str = "/sbin/modprobe"; const MODPROBE_PATH: &str = "/sbin/modprobe";
const IPTABLES_SAVE: &str = "/sbin/iptables-save";
const IPTABLES_RESTORE: &str = "/sbin/iptables-restore";
const IP6TABLES_SAVE: &str = "/sbin/ip6tables-save";
const IP6TABLES_RESTORE: &str = "/sbin/ip6tables-restore";
const ERR_CANNOT_GET_WRITER: &str = "Cannot get writer"; const ERR_CANNOT_GET_WRITER: &str = "Cannot get writer";
const ERR_INVALID_BLOCK_SIZE: &str = "Invalid block size"; const ERR_INVALID_BLOCK_SIZE: &str = "Invalid block size";
const ERR_NO_LINUX_FIELD: &str = "Spec does not contain linux field"; const ERR_NO_LINUX_FIELD: &str = "Spec does not contain linux field";
const ERR_NO_SANDBOX_PIDNS: &str = "Sandbox does not have sandbox_pidns"; const ERR_NO_SANDBOX_PIDNS: &str = "Sandbox does not have sandbox_pidns";
// IPTABLES_RESTORE_WAIT_SEC is the timeout value provided to iptables-restore --wait. Since we
// don't expect other writers to iptables, we don't expect contention for grabbing the iptables
// filesystem lock. Based on this, 5 seconds seems a resonable timeout period in case the lock is
// not available.
const IPTABLES_RESTORE_WAIT_SEC: u64 = 5;
// Convenience macro to obtain the scope logger // Convenience macro to obtain the scope logger
macro_rules! sl { macro_rules! sl {
() => { () => {
@ -994,6 +1006,140 @@ impl protocols::agent_ttrpc::AgentService for AgentService {
}) })
} }
async fn get_ip_tables(
&self,
ctx: &TtrpcContext,
req: GetIPTablesRequest,
) -> ttrpc::Result<GetIPTablesResponse> {
trace_rpc_call!(ctx, "get_iptables", req);
is_allowed!(req);
info!(sl!(), "get_ip_tables: request received");
let cmd = if req.is_ipv6 {
IP6TABLES_SAVE
} else {
IPTABLES_SAVE
}
.to_string();
match Command::new(cmd.clone()).output() {
Ok(output) => Ok(GetIPTablesResponse {
data: output.stdout,
..Default::default()
}),
Err(e) => {
warn!(sl!(), "failed to run {}: {:?}", cmd, e.kind());
return Err(ttrpc_error!(ttrpc::Code::INTERNAL, e));
}
}
}
async fn set_ip_tables(
&self,
ctx: &TtrpcContext,
req: SetIPTablesRequest,
) -> ttrpc::Result<SetIPTablesResponse> {
trace_rpc_call!(ctx, "set_iptables", req);
is_allowed!(req);
info!(sl!(), "set_ip_tables request received");
let cmd = if req.is_ipv6 {
IP6TABLES_RESTORE
} else {
IPTABLES_RESTORE
}
.to_string();
let mut child = match Command::new(cmd.clone())
.arg("--wait")
.arg(IPTABLES_RESTORE_WAIT_SEC.to_string())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(e) => {
warn!(sl!(), "failure to spawn {}: {:?}", cmd, e.kind());
return Err(ttrpc_error!(ttrpc::Code::INTERNAL, e));
}
};
let mut stdin = match child.stdin.take() {
Some(si) => si,
None => {
println!("failed to get stdin from child");
return Err(ttrpc_error!(
ttrpc::Code::INTERNAL,
"failed to take stdin from child".to_string()
));
}
};
let (tx, rx) = tokio::sync::oneshot::channel::<i32>();
let handle = tokio::spawn(async move {
let _ = match stdin.write_all(&req.data) {
Ok(o) => o,
Err(e) => {
warn!(sl!(), "error writing stdin: {:?}", e.kind());
return;
}
};
if tx.send(1).is_err() {
warn!(sl!(), "stdin writer thread receiver dropped");
};
});
if tokio::time::timeout(Duration::from_secs(IPTABLES_RESTORE_WAIT_SEC), rx)
.await
.is_err()
{
return Err(ttrpc_error!(
ttrpc::Code::INTERNAL,
"timeout waiting for stdin writer to complete".to_string()
));
}
if handle.await.is_err() {
return Err(ttrpc_error!(
ttrpc::Code::INTERNAL,
"stdin writer thread failure".to_string()
));
}
let output = match child.wait_with_output() {
Ok(o) => o,
Err(e) => {
warn!(
sl!(),
"failure waiting for spawned {} to complete: {:?}",
cmd,
e.kind()
);
return Err(ttrpc_error!(ttrpc::Code::INTERNAL, e));
}
};
if !output.status.success() {
warn!(sl!(), "{} failed: {:?}", cmd, output.stderr);
return Err(ttrpc_error!(
ttrpc::Code::INTERNAL,
format!(
"{} failed: {:?}",
cmd,
String::from_utf8_lossy(&output.stderr)
)
));
}
Ok(SetIPTablesResponse {
data: output.stdout,
..Default::default()
})
}
async fn list_interfaces( async fn list_interfaces(
&self, &self,
ctx: &TtrpcContext, ctx: &TtrpcContext,
@ -1879,6 +2025,7 @@ mod tests {
skip_if_not_root, skip_if_not_root,
}; };
use nix::mount; use nix::mount;
use nix::sched::{unshare, CloneFlags};
use oci::{Hook, Hooks, Linux, LinuxNamespace}; use oci::{Hook, Hooks, Linux, LinuxNamespace};
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use ttrpc::{r#async::TtrpcContext, MessageHeader}; use ttrpc::{r#async::TtrpcContext, MessageHeader};
@ -2815,4 +2962,162 @@ OtherField:other
assert_eq!(stats.used, 3); assert_eq!(stats.used, 3);
assert_eq!(stats.available, available - 2); assert_eq!(stats.available, available - 2);
} }
#[tokio::test]
async fn test_ip_tables() {
skip_if_not_root!();
let logger = slog::Logger::root(slog::Discard, o!());
let sandbox = Sandbox::new(&logger).unwrap();
let agent_service = Box::new(AgentService {
sandbox: Arc::new(Mutex::new(sandbox)),
});
let ctx = mk_ttrpc_context();
// Move to a new netns in order to ensure we don't trash the hosts' iptables
unshare(CloneFlags::CLONE_NEWNET).unwrap();
// Get initial iptables, we expect to be empty:
let result = agent_service
.get_ip_tables(
&ctx,
GetIPTablesRequest {
is_ipv6: false,
..Default::default()
},
)
.await;
assert!(result.is_ok(), "get ip tables should succeed");
assert_eq!(
result.unwrap().data.len(),
0,
"ip tables should be empty initially"
);
// Initial ip6 ip tables should also be empty:
let result = agent_service
.get_ip_tables(
&ctx,
GetIPTablesRequest {
is_ipv6: true,
..Default::default()
},
)
.await;
assert!(result.is_ok(), "get ip6 tables should succeed");
assert_eq!(
result.unwrap().data.len(),
0,
"ip tables should be empty initially"
);
// Verify that attempting to write 'empty' iptables results in no error:
let empty_rules = "";
let result = agent_service
.set_ip_tables(
&ctx,
SetIPTablesRequest {
is_ipv6: false,
data: empty_rules.as_bytes().to_vec(),
..Default::default()
},
)
.await;
assert!(result.is_ok(), "set ip tables with no data should succeed");
// Verify that attempting to write "garbage" iptables results in an error:
let garbage_rules = r#"
this
is
just garbage
"#;
let result = agent_service
.set_ip_tables(
&ctx,
SetIPTablesRequest {
is_ipv6: false,
data: garbage_rules.as_bytes().to_vec(),
..Default::default()
},
)
.await;
assert!(result.is_err(), "set iptables with garbage should fail");
// Verify setup of valid iptables:Setup valid set of iptables:
let valid_rules = r#"
*nat
-A PREROUTING -d 192.168.103.153/32 -j DNAT --to-destination 192.168.188.153
COMMIT
"#;
let result = agent_service
.set_ip_tables(
&ctx,
SetIPTablesRequest {
is_ipv6: false,
data: valid_rules.as_bytes().to_vec(),
..Default::default()
},
)
.await;
assert!(result.is_ok(), "set ip tables should succeed");
let result = agent_service
.get_ip_tables(
&ctx,
GetIPTablesRequest {
is_ipv6: false,
..Default::default()
},
)
.await
.unwrap();
assert!(!result.data.is_empty(), "we should have non-zero output:");
assert!(
std::str::from_utf8(&*result.data).unwrap().contains(
"PREROUTING -d 192.168.103.153/32 -j DNAT --to-destination 192.168.188.153"
),
"We should see the resulting rule"
);
// Verify setup of valid ip6tables:
let valid_ipv6_rules = r#"
*filter
-A INPUT -s 2001:db8:100::1/128 -i sit+ -p tcp -m tcp --sport 512:65535
COMMIT
"#;
let result = agent_service
.set_ip_tables(
&ctx,
SetIPTablesRequest {
is_ipv6: true,
data: valid_ipv6_rules.as_bytes().to_vec(),
..Default::default()
},
)
.await;
assert!(result.is_ok(), "set ip6 tables should succeed");
let result = agent_service
.get_ip_tables(
&ctx,
GetIPTablesRequest {
is_ipv6: true,
..Default::default()
},
)
.await
.unwrap();
assert!(!result.data.is_empty(), "we should have non-zero output:");
assert!(
std::str::from_utf8(&*result.data)
.unwrap()
.contains("INPUT -s 2001:db8:100::1/128 -i sit+ -p tcp -m tcp --sport 512:65535"),
"We should see the resulting rule"
);
}
} }