feat: Discover docker ports as well

If processes are running in a container then the fwd process
can't read their internal FDs without the CAP_SYS_ADMIN property
which is equivalent to sudo. Even with sudo, I think you need to do
a lot of work to be able to read them -- spawning a process within
the cgroup, doing work there, and then communicating back.

This just uses the docker api to populate some default ports, which
later get overwritten if fwd can find a native process.

The Docker port scan takes about 1.5ms, and the full port scan takes
40+ms, so this adds basically no overhead.
This commit is contained in:
Brandon W Maister 2024-07-31 10:27:30 -04:00 committed by John Doty
parent 66da323481
commit 6c10d8eece
4 changed files with 707 additions and 103 deletions

747
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@ bench = false
[dependencies]
anyhow = "1.0"
bollard = "0.17.0"
bytes = "1"
copypasta = "0.10.1"
crossterm = { version = "0.25", features = ["event-stream"] }

View file

@ -32,7 +32,7 @@ async fn server_loop<Reader: AsyncRead + Unpin>(
match reader.read().await? {
Ping => (),
Refresh => {
let ports = match refresh::get_entries() {
let ports = match refresh::get_entries().await {
Ok(ports) => ports,
Err(e) => {
error!("Error scanning: {:?}", e);

View file

@ -1,14 +1,17 @@
use crate::message::PortDesc;
use anyhow::Result;
#[cfg(target_os = "linux")]
use std::collections::HashMap;
#[cfg(not(target_os = "linux"))]
pub fn get_entries() -> Result<Vec<PortDesc>> {
pub async fn get_entries() -> Result<Vec<PortDesc>> {
use anyhow::bail;
bail!("Not supported on this operating system");
}
#[cfg(target_os = "linux")]
pub fn get_entries() -> Result<Vec<PortDesc>> {
pub async fn get_entries() -> Result<Vec<PortDesc>> {
let start = std::time::Instant::now();
use procfs::process::FDTarget;
use std::collections::HashMap;
@ -31,8 +34,13 @@ pub fn get_entries() -> Result<Vec<PortDesc>> {
}
}
}
log::trace!("procfs elapsed={:?}", start.elapsed());
let mut h: HashMap<u16, PortDesc> = HashMap::new();
let mut h = if let Ok(ports) = find_docker_ports().await {
ports
} else {
HashMap::new()
};
// Go through all the listening IPv4 and IPv6 sockets and take the first
// instance of listening on each port *if* the address is loopback or
@ -57,5 +65,49 @@ pub fn get_entries() -> Result<Vec<PortDesc>> {
}
}
Ok(h.into_values().collect())
let vals = h.into_values().collect();
log::trace!("total portscan elapsed={:?}", start.elapsed());
Ok(vals)
}
#[cfg(target_os = "linux")]
async fn find_docker_ports(
) -> Result<HashMap<u16, PortDesc>, bollard::errors::Error> {
use bollard::container::ListContainersOptions;
use bollard::Docker;
let start = std::time::Instant::now();
let client = Docker::connect_with_defaults()?;
log::trace!("docker connect elapsed={:?}", start.elapsed());
let port_start = std::time::Instant::now();
let mut port_to_name = HashMap::new();
let opts: ListContainersOptions<String> =
ListContainersOptions { all: false, ..Default::default() };
for container in client.list_containers(Some(opts)).await? {
let name = container
.names
.into_iter()
.flatten()
.next()
.unwrap_or_else(|| "<unknown docker>".to_owned());
for port in container.ports.iter().flatten() {
if let Some(public_port) = port.public_port {
let private_port = port.private_port;
port_to_name.insert(
public_port,
PortDesc {
port: public_port,
desc: format!("{name} (docker->{private_port})"),
},
);
}
}
}
log::trace!(
"docker port elapsed={:?} total docker elapsed={:?}",
port_start.elapsed(),
start.elapsed()
);
Ok(port_to_name)
}