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:
parent
66da323481
commit
6c10d8eece
4 changed files with 707 additions and 103 deletions
747
Cargo.lock
generated
747
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,7 @@ bench = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
bollard = "0.17.0"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
copypasta = "0.10.1"
|
copypasta = "0.10.1"
|
||||||
crossterm = { version = "0.25", features = ["event-stream"] }
|
crossterm = { version = "0.25", features = ["event-stream"] }
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ async fn server_loop<Reader: AsyncRead + Unpin>(
|
||||||
match reader.read().await? {
|
match reader.read().await? {
|
||||||
Ping => (),
|
Ping => (),
|
||||||
Refresh => {
|
Refresh => {
|
||||||
let ports = match refresh::get_entries() {
|
let ports = match refresh::get_entries().await {
|
||||||
Ok(ports) => ports,
|
Ok(ports) => ports,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error scanning: {:?}", e);
|
error!("Error scanning: {:?}", e);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
use crate::message::PortDesc;
|
use crate::message::PortDesc;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
pub fn get_entries() -> Result<Vec<PortDesc>> {
|
pub async fn get_entries() -> Result<Vec<PortDesc>> {
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
bail!("Not supported on this operating system");
|
bail!("Not supported on this operating system");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[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 procfs::process::FDTarget;
|
||||||
use std::collections::HashMap;
|
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
|
// 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
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue