Skip to content

Commit

Permalink
feat(dgw): network scan HTTP API (#689)
Browse files Browse the repository at this point in the history
  • Loading branch information
irvingoujAtDevolution authored Feb 13, 2024
1 parent f3dcf4d commit 846f21d
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions devolutions-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ ironrdp-pdu = { version = "0.1", git = "https://github.com/Devolutions/IronRDP",
ironrdp-rdcleanpath = { version = "0.1", git = "https://github.com/Devolutions/IronRDP", rev = "4844e77b7f65024d85ba74b1824013eda6eb32b2" }
ceviche = "0.5"
picky-krb = "0.8"
network-scanner = { version = "0.0.0", path = "../crates/network-scanner" }

# Serialization
serde = "1.0"
Expand Down
4 changes: 3 additions & 1 deletion devolutions-gateway/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod jmux;
pub mod jrec;
pub mod jrl;
pub mod kdc_proxy;
pub mod network_scan;
pub mod rdp;
pub mod session;
pub mod sessions;
Expand All @@ -25,7 +26,8 @@ pub fn make_router<S>(state: crate::DgwState) -> axum::Router<S> {
.route("/jet/jmux", axum::routing::get(jmux::handler))
.route("/jet/rdp", axum::routing::get(rdp::handler))
.nest("/jet/fwd", fwd::make_router(state.clone()))
.nest("/jet/webapp", webapp::make_router(state.clone()));
.nest("/jet/webapp", webapp::make_router(state.clone()))
.route("/jet/network-scan", axum::routing::get(network_scan::handler));

if state.conf_handle.get_conf().webapp_is_enabled() {
router = router.route(
Expand Down
132 changes: 132 additions & 0 deletions devolutions-gateway/src/api/network_scan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use std::net::IpAddr;

use axum::extract::ws::Message;
use axum::extract::WebSocketUpgrade;
use axum::response::Response;
use network_scanner::scanner::NetworkScannerParams;
use serde::Serialize;

use crate::http::HttpError;
use crate::token::{ApplicationProtocol, Protocol};

pub async fn handler(
_token: crate::extract::NetScanToken,
ws: WebSocketUpgrade,
query_params: axum::extract::Query<NetworkScanQueryParams>,
) -> Result<Response, HttpError> {
let scanner_params: NetworkScannerParams = query_params.0.into();

let scanner = network_scanner::scanner::NetworkScanner::new(scanner_params).map_err(|e| {
tracing::error!("failed to create network scanner: {:?}", e);
HttpError::internal().build(e)
})?;

let res = ws.on_upgrade(move |mut websocket| async move {
let stream = match scanner.start() {
Ok(stream) => stream,
Err(e) => {
tracing::error!("failed to start network scan: {:?}", e);
return;
}
};
info!("network scan started");
loop {
tokio::select! {
result = stream.recv() => {
let Some((ip, dns, port)) = result else{
break;
};
let response = NetworkScanResponse::new(ip, port, dns);
let Ok(response) = serde_json::to_string(&response) else {
warn!("Failed to serialize response");
continue;
};

if let Err(e) = websocket.send(Message::Text(response)).await {
warn!("Failed to send message: {:?}", e);
break;
}

},
msg = websocket.recv() => {

let Some(msg) = msg else {
break;
};

if let Ok(Message::Close(_)) = msg {
break;
}
}
}
}
info!("Network scan finished");
stream.stop();
});
Ok(res)
}

#[derive(Debug, Deserialize)]
pub struct NetworkScanQueryParams {
/// Interval in milliseconds (default is 200)
pub ping_interval: Option<u64>,
/// Timeout in milliseconds (default is 500)
pub ping_timeout: Option<u64>,
/// Timeout in milliseconds (default is 1000)
pub broadcast_timeout: Option<u64>,
/// Timeout in milliseconds (default is 1000)
pub port_scan_timeout: Option<u64>,
/// Timeout in milliseconds (default is 1000)
pub netbios_timeout: Option<u64>,
/// Interval in milliseconds (default is 200)
pub netbios_interval: Option<u64>,
/// The maximum duration for whole networking scan in milliseconds. Highly suggested!
pub max_wait: Option<u64>,
}

const COMMON_PORTS: [u16; 10] = [22, 23, 80, 443, 389, 636, 3389, 5900, 5985, 5986];

impl From<NetworkScanQueryParams> for NetworkScannerParams {
fn from(val: NetworkScanQueryParams) -> Self {
NetworkScannerParams {
ports: COMMON_PORTS.to_vec(),
ping_interval: val.ping_interval.unwrap_or(200),
ping_timeout: val.ping_timeout.unwrap_or(500),
broadcast_timeout: val.broadcast_timeout.unwrap_or(1000),
port_scan_timeout: val.port_scan_timeout.unwrap_or(1000),
netbios_timeout: val.netbios_timeout.unwrap_or(1000),
max_wait_time: val.max_wait.unwrap_or(120 * 1000),
netbios_interval: val.netbios_interval.unwrap_or(200),
}
}
}

#[derive(Debug, Serialize)]
pub struct NetworkScanResponse {
pub ip: IpAddr,
pub hostname: Option<String>,
#[serde(rename = "type")]
pub ty: ApplicationProtocol,
}

impl NetworkScanResponse {
fn new(ip: IpAddr, port: u16, dns: Option<String>) -> Self {
let hostname = dns;

let ty = match port {
22 => ApplicationProtocol::Known(Protocol::Ssh),
23 => ApplicationProtocol::Known(Protocol::Telnet),
80 => ApplicationProtocol::Known(Protocol::Http),
443 => ApplicationProtocol::Known(Protocol::Https),
389 => ApplicationProtocol::Known(Protocol::Ldap),
636 => ApplicationProtocol::Known(Protocol::Ldaps),
3389 => ApplicationProtocol::Known(Protocol::Rdp),
5900 => ApplicationProtocol::Known(Protocol::Vnc),
5985 => ApplicationProtocol::Known(Protocol::WinrmHttpPwsh),
5986 => ApplicationProtocol::Known(Protocol::WinrmHttpsPwsh),
_ => ApplicationProtocol::unknown(),
};

Self { ip, hostname, ty }
}
}
19 changes: 19 additions & 0 deletions devolutions-gateway/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,22 @@ where
}
}
}

#[derive(Clone, Copy)]
pub struct NetScanToken;

#[async_trait]
impl<S> FromRequestParts<S> for NetScanToken
where
S: Send + Sync,
{
type Rejection = HttpError;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
if let AccessTokenClaims::NetScan(_) = AccessToken::from_request_parts(parts, state).await?.0 {
Ok(Self)
} else {
Err(HttpError::forbidden().msg("token not allowed (expected NETSCAN)"))
}
}
}

0 comments on commit 846f21d

Please sign in to comment.