Skip to content

Commit a3aa448

Browse files
feat(dgw): align gateway scans with local planning (#1776)
## Summary Brings the gateway's `/jet/net/scan` API closer to feature parity with the local network scanner, so the same scan request shape can drive both. ### New query parameters - **Targets** — combinable `target=<ip>` (single host) and `range=<lower>-<upper>`. IPv4/IPv6 family validation; rejects ranges over 65 536 addresses with HTTP 400. - **Source selection** — repeatable `interface_id=<id>` plus `range_interface_policy=intersect_selected_interfaces|allow_cross_interface_range`. Missing/down/loopback-only/no-scan-capable sources or out-of-interface ranges return a structured 400 (`{ error, interfaceId, reason }` or `{ error, ranges, interfaceIds }`). - **Probe controls** — `enable_tcp_probes=false` suppresses all TCP probes regardless of `probe=`; otherwise `probe=22|rdp|https|...` sets the probe list. An explicit probe list (even just `probe=ping`) is honored verbatim — the default `COMMON_PORTS` fallback only kicks in when the caller sends no `probe=` at all. - **Result toggles** — `report_ping_start`, `report_ping_success`, `report_ping_failure`, `report_tcp_failure`, `include_host_results`. The previous `enable_failure` and `enable_ping_start` flags remain as deprecated aliases (see "Behavior changes" below for the `enable_failure` semantics split). - **Concurrency** — `max_concurrency`, `max_ping_concurrency`, `max_tcp_probe_concurrency`. - **Strict bind** — `interface_bind_strict=true` fails the scan if a per-interface socket bind doesn't take effect (default: warn and fall back). ### New endpoint `GET /jet/net/interfaces` (v2): returns each scan source with a stable `id`, address, `startAddress`/`endAddress`, broadcast address, prefix length, link metadata (MAC, MTU, speed, link type), and per-source capability flags (`ipv4`, `ipv6`, `subnet`, `broadcast`, `zeroConf`, `tcpProbe`, `dnsResolve`). The `id` is what `interface_id=` accepts. The legacy `GET /jet/net/config` is retained and now carries RFC 8594 `Deprecation: true` plus `Link: rel="successor-version"` headers pointing at `/jet/net/interfaces`. (No `Sunset` date until product confirms one.) ### New result format `response_format=network_scan_result_v1` opts into a new wire shape: ```json { "kind": "host", "address": "...", "source": "gateway", "discoverySource": "subnet", "isReachable": true, "hostScanState": "reachable", "responseTimeMs": 7, "interfaceId": "...", "interfaceName": "..." } { "kind": "service", "address": "...", "source": "gateway", "discoverySource": "tcp_probe", "isReachable": true, "port": 3389, "serviceLabel": "RDP", "serviceType": "RDP", "responseTimeMs": 12 } ``` Fields are camelCase with closed lowercase enums (`hostScanState ∈ { queued, probing, reachable, unreachable }`, `discoverySource ∈ { subnet, broadcast, tcp_probe, gateway, zero_conf }`). `macAddress` is in the schema but only populated when neighbor-discovery information is available; it is omitted from the JSON otherwise. Without the parameter, the legacy event format continues to be emitted unchanged. ### Internals - Per-interface socket bind for ping and TCP probes on Linux (`IP_UNICAST_IF`/`IPV6_UNICAST_IF`), macOS (`IP_BOUND_IF`/`IPV6_BOUND_IF`), and Windows (`IP_UNICAST_IF`). Other targets fail to compile (`compile_error!`). - Event-bus capacity raised to 8192 and `TypedReceiver::recv` now treats `RecvError::Lagged` as recoverable (warn-and-continue) instead of terminating its consumer task. This fixes a class of dropouts where a wave of timeouts could starve the forwarder and cause subsequent successful events to be lost. - `ScannerConfig` decomposed into `{ ports, timing, limits, targeting }` sub-structs. - Discovered hosts/services emit explicit `ServiceReachability` and `LinkType` ADTs instead of bare booleans/strings. - OpenAPI schema regenerated to cover the new query parameters, the new endpoint, and the v1 result DTOs. ## Behavior changes worth flagging These are intentional and align the API with the design plan that drove this PR, but they will surprise any client relying on the pre-PR semantics. None of them affect a client that sends no query parameters at all (the default behavior is unchanged). 1. **`enable_failure=true` now controls only ping-failure events.** Pre-PR it enabled both ping-failure and TCP-probe failure entries in one knob, which generated a lot of noise that callers were filtering out client-side. The two streams are now independently gated — to restore the old "both at once" semantics, send `enable_failure=true&report_tcp_failure=true` together. New clients should prefer the explicit `report_ping_failure=true` / `report_tcp_failure=true` flags. 2. **`probe=ping` no longer triggers the COMMON_PORTS fallback.** An explicit probe list is taken literally. To also scan the default port list, either omit `probe=` entirely or list the ports explicitly. 3. **`probe=ping` no longer auto-emits ping-failure events.** Matches pre-PR baseline of start + success only. To receive failure events when `probe=ping` is set, add `report_ping_failure=true` (preferred) or `enable_failure=true` (legacy alias). 4. **`/jet/net/config` now sets `Deprecation: true`** plus a `Link: ...; rel="successor-version"` pointing to `/jet/net/interfaces`. No `Sunset` header yet, so clients have unbounded migration time — but they should plan to migrate. 5. **`response_format=network_scan_result_v1` is opt-in.** Legacy is still the default. Anyone parsing the legacy shape needs no change. 6. **The `report_ping_status` parameter was added and then removed in the course of this PR review** and was never shipped to a release. Clients should use the three explicit `report_ping_{start,success,failure}` flags directly. ## Test plan - [x] `cargo +nightly fmt --all -- --check` - [x] `cargo clippy -p devolutions-gateway -p network-scanner --tests -- -D warnings` - [x] `cargo test -p devolutions-gateway --lib api::net` - [x] `cargo test -p testsuite --test integration_tests -- network_scanner` - [x] Cross-platform target builds — covered by CI - [x] Manual smoke test via a standalone Angular/JS harness exercising the new wire shapes and toggles end-to-end
1 parent 45de0e6 commit a3aa448

36 files changed

Lines changed: 5826 additions & 622 deletions

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/network-scanner-net/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ socket2 = { version = "0.5", features = ["all"] }
1717
thiserror = "2"
1818
tracing = "0.1"
1919

20+
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
21+
libc = "0.2"
22+
2023
[dev-dependencies]
2124
tracing-cov-mark = { path = "../tracing-cov-mark" }
2225
tracing-subscriber = "0.3"

crates/network-scanner-net/src/socket.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ impl AsyncRawSocket {
5252
self.socket.bind(addr)
5353
}
5454

55+
/// Bind this socket to a specific network interface by OS index.
56+
///
57+
/// Per-platform mechanism:
58+
///
59+
/// | Platform | Option used | Notes |
60+
/// |----------|------------------------------------------|-----------------------------|
61+
/// | Linux | `IP_UNICAST_IF` / `IPV6_UNICAST_IF` | No `CAP_NET_RAW` required. |
62+
/// | macOS | `IP_BOUND_IF` / `IPV6_BOUND_IF` | Apple-specific socket opts. |
63+
/// | Windows | `IP_UNICAST_IF` / `IPV6_UNICAST_IF` | Via `ws2_32!setsockopt`. |
64+
///
65+
/// Other platforms return [`std::io::ErrorKind::Unsupported`]. Taking a
66+
/// [`std::num::NonZeroU32`] makes it impossible to pass the
67+
/// "unbind / use default routing" sentinel by accident — drop the call
68+
/// site instead of binding to ifindex 0.
69+
pub fn bind_to_interface(&self, family: socket2::Domain, if_index: std::num::NonZeroU32) -> std::io::Result<()> {
70+
bind_socket_to_interface(&self.socket, family, if_index)
71+
}
72+
5573
pub async fn set_ttl(&self, ttl: u32) -> std::io::Result<()> {
5674
self.socket.set_ttl(ttl)
5775
}
@@ -65,6 +83,137 @@ impl AsyncRawSocket {
6583
}
6684
}
6785

86+
/// Bind a `socket2::Socket` to a specific interface index.
87+
///
88+
/// Per-platform mechanism (`IP_UNICAST_IF` on Linux + Windows,
89+
/// `IP_BOUND_IF` on macOS — see [`AsyncRawSocket::bind_to_interface`] for
90+
/// the full table). The supported target list is exhaustive: only Linux,
91+
/// macOS, and Windows are valid build targets for this crate, so other
92+
/// platforms intentionally fail to compile rather than silently degrade.
93+
fn bind_socket_to_interface(
94+
socket: &Socket,
95+
family: socket2::Domain,
96+
if_index: std::num::NonZeroU32,
97+
) -> std::io::Result<()> {
98+
let if_index = if_index.get();
99+
100+
#[cfg(any(target_os = "linux", target_os = "macos"))]
101+
{
102+
use std::os::fd::AsRawFd;
103+
104+
// Resolve (level, name, value) per platform / family. Linux and
105+
// macOS share the libc::setsockopt call shape, only the constants
106+
// and IPv4 byte-order differ.
107+
let (level, name, value): (libc::c_int, libc::c_int, u32) = match (family, cfg!(target_os = "linux")) {
108+
// Linux
109+
#[cfg(target_os = "linux")]
110+
(socket2::Domain::IPV4, _) => {
111+
// IPPROTO_IP, IP_UNICAST_IF; the kernel expects net-order.
112+
(0, 50, if_index.to_be())
113+
}
114+
#[cfg(target_os = "linux")]
115+
(socket2::Domain::IPV6, _) => {
116+
// IPPROTO_IPV6, IPV6_UNICAST_IF; host byte order.
117+
(41, 76, if_index)
118+
}
119+
// macOS
120+
#[cfg(target_os = "macos")]
121+
(socket2::Domain::IPV4, _) => {
122+
// IPPROTO_IP, IP_BOUND_IF
123+
(0, 25, if_index)
124+
}
125+
#[cfg(target_os = "macos")]
126+
(socket2::Domain::IPV6, _) => {
127+
// IPPROTO_IPV6, IPV6_BOUND_IF
128+
(41, 125, if_index)
129+
}
130+
_ => {
131+
return Err(std::io::Error::new(
132+
std::io::ErrorKind::InvalidInput,
133+
"interface bind only supported for IPv4 / IPv6 sockets",
134+
));
135+
}
136+
};
137+
138+
let fd = socket.as_raw_fd();
139+
// `socklen_t` is u32 on Linux / macOS but the cast from usize still
140+
// trips `clippy::cast_possible_truncation` on 64-bit pointer widths;
141+
// route through `try_from` to document intent.
142+
let optlen = libc::socklen_t::try_from(size_of::<u32>()).expect("size of u32 fits in socklen_t");
143+
// SAFETY: `fd` is a valid descriptor borrowed from `socket`;
144+
// `&value` is a stack pointer valid for the duration of the call.
145+
let ret = unsafe { libc::setsockopt(fd, level, name, &value as *const u32 as *const libc::c_void, optlen) };
146+
if ret == 0 {
147+
Ok(())
148+
} else {
149+
Err(std::io::Error::last_os_error())
150+
}
151+
}
152+
153+
#[cfg(target_os = "windows")]
154+
{
155+
// Windows IP_UNICAST_IF (option 31) takes a u32: net-order for IPv4,
156+
// host-order for IPv6.
157+
use std::os::windows::io::AsRawSocket;
158+
const IPPROTO_IP: i32 = 0;
159+
const IPPROTO_IPV6: i32 = 41;
160+
const IP_UNICAST_IF: i32 = 31;
161+
const IPV6_UNICAST_IF: i32 = 31;
162+
// `SOCKET` is u64 in the Windows headers; on 32-bit Windows it
163+
// still fits in `usize` since SOCKET handles never exceed pointer
164+
// width.
165+
let raw: usize = usize::try_from(socket.as_raw_socket())
166+
.map_err(|_| std::io::Error::other("Windows socket handle does not fit in usize on this target"))?;
167+
let (level, name, value): (i32, i32, u32) = if family == socket2::Domain::IPV4 {
168+
(IPPROTO_IP, IP_UNICAST_IF, if_index.to_be())
169+
} else if family == socket2::Domain::IPV6 {
170+
(IPPROTO_IPV6, IPV6_UNICAST_IF, if_index)
171+
} else {
172+
return Err(std::io::Error::new(
173+
std::io::ErrorKind::InvalidInput,
174+
"interface bind only supported for IPv4 / IPv6 sockets",
175+
));
176+
};
177+
// setsockopt takes optlen as i32; size_of::<u32>() is 4 — the cast
178+
// is infallible, but try_from documents intent and avoids a lint.
179+
let optlen = i32::try_from(size_of::<u32>()).expect("size of u32 fits in i32");
180+
// SAFETY: setsockopt is invoked on a valid Windows socket handle
181+
// that outlives this call; `&value` lives on the stack across it.
182+
let ret = unsafe { windows_setsockopt(raw, level, name, &value as *const u32 as *const u8, optlen) };
183+
if ret == 0 {
184+
Ok(())
185+
} else {
186+
// Winsock surfaces failures through `WSAGetLastError`, not the
187+
// generic `GetLastError` that `std::io::Error::last_os_error`
188+
// reads. Going through `last_os_error` here would occasionally
189+
// yield stale or unrelated codes.
190+
// SAFETY: WSAGetLastError takes no arguments and is always safe to
191+
// call from any thread; it reads thread-local state.
192+
let raw_errno = unsafe { WSAGetLastError() };
193+
Err(std::io::Error::from_raw_os_error(raw_errno))
194+
}
195+
}
196+
}
197+
198+
#[cfg(target_os = "windows")]
199+
unsafe extern "system" {
200+
/// `setsockopt` from `ws2_32.dll`. We deliberately bind the symbol here
201+
/// instead of pulling in the full `windows-sys` crate so the only
202+
/// network-scanner-net Windows dependency stays within libstd's
203+
/// already-linked import library.
204+
#[link_name = "setsockopt"]
205+
fn windows_setsockopt(s: usize, level: i32, optname: i32, optval: *const u8, optlen: i32) -> i32;
206+
}
207+
208+
#[cfg(target_os = "windows")]
209+
#[link(name = "ws2_32")]
210+
unsafe extern "system" {
211+
/// Winsock's thread-local error accessor. Linked separately from
212+
/// `windows_setsockopt` so the symbol can be reused by future Winsock
213+
/// callers without splitting the extern block.
214+
fn WSAGetLastError() -> i32;
215+
}
216+
68217
impl<'a> AsyncRawSocket {
69218
#[tracing::instrument(skip(self, buf))]
70219
pub fn recv_from(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
//! Wire-format helpers for the protocols the network scanner speaks.
2+
//!
3+
//! - [`icmp_v4`] / [`icmp_v6`] — ICMP echo and friends used by ping.
4+
//! - [`netbios`] — NetBIOS-over-UDP name service queries.
5+
//!
6+
//! All modules are pure byte-level: they neither own sockets nor perform
7+
//! I/O. The companion `network-scanner` crate composes them with raw
8+
//! sockets to send and receive packets.
9+
110
pub mod icmp_v4;
211
pub mod icmp_v6;
312
pub mod netbios;

crates/network-scanner-proto/src/netbios.rs

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -74,126 +74,3 @@ impl<'a> NetBiosPacket<'a> {
7474
)
7575
}
7676
}
77-
78-
#[cfg(test)]
79-
mod tests {
80-
use super::*;
81-
82-
#[test]
83-
fn create_nbt_packet_from_data_slice() {
84-
let mut data = [0u8; 1024];
85-
let packet = [
86-
0xA2, 0x48, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41,
87-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
88-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
89-
0x00, 0x77, 0x04, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
90-
0x20, 0x44, 0x00, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
91-
0x00, 0x44, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
92-
0x00, 0xC4, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
93-
0x1E, 0xC4, 0x00, 0x2C, 0x41, 0x38, 0xBA, 0xC3, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
94-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
95-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
96-
];
97-
for (i, elem) in packet.iter().enumerate() {
98-
data[i] = *elem;
99-
}
100-
let _actual = NetBiosPacket::from(Ipv4Addr::from([127, 0, 0, 1]), &data);
101-
assert_eq!(true, true)
102-
}
103-
104-
#[test]
105-
fn parse_name_from_data_correctly() {
106-
let mut data = [0u8; 1024];
107-
let packet = [
108-
0xA2, 0x48, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41,
109-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
110-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
111-
0x00, 0x77, 0x04, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
112-
0x20, 0x44, 0x00, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
113-
0x00, 0x44, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
114-
0x00, 0xC4, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
115-
0x1E, 0xC4, 0x00, 0x2C, 0x41, 0x38, 0xBA, 0xC3, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
116-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
117-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
118-
];
119-
for (i, elem) in packet.iter().enumerate() {
120-
data[i] = *elem;
121-
}
122-
let expected = "JACKIEG-WS";
123-
let actual = NetBiosPacket::from(Ipv4Addr::from([127, 0, 0, 1]), &data);
124-
125-
assert_eq!(expected, actual.name());
126-
}
127-
128-
#[test]
129-
fn parse_group_from_data_correctly() {
130-
let mut data = [0u8; 1024];
131-
let packet = [
132-
0xA2, 0x48, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41,
133-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
134-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
135-
0x00, 0x77, 0x04, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
136-
0x20, 0x44, 0x00, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
137-
0x00, 0x44, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
138-
0x00, 0xC4, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
139-
0x1E, 0xC4, 0x00, 0x2C, 0x41, 0x38, 0xBA, 0xC3, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
140-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
141-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
142-
];
143-
for (i, elem) in packet.iter().enumerate() {
144-
data[i] = *elem;
145-
}
146-
let expected = String::from("JACKIEG-WS");
147-
let actual = NetBiosPacket::from(Ipv4Addr::from([127, 0, 0, 1]), &data);
148-
149-
assert_eq!(Some(expected), actual.group());
150-
}
151-
152-
#[test]
153-
fn parse_name_and_group_from_data_correctly_2() {
154-
let mut data = [0u8; 1024];
155-
let packet = [
156-
0xA2, 0x48, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41,
157-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
158-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
159-
0x00, 0x77, 0x04, 0x41, 0x4C, 0x45, 0x58, 0x4B, 0x2D, 0x50, 0x43, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
160-
0x00, 0x44, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
161-
0x00, 0xC4, 0x00, 0x41, 0x4C, 0x45, 0x58, 0x4B, 0x2D, 0x50, 0x43, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
162-
0x20, 0x44, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
163-
0x1E, 0xC4, 0x00, 0xD0, 0xBF, 0x9C, 0xE4, 0x24, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
164-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
165-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
166-
];
167-
for (i, elem) in packet.iter().enumerate() {
168-
data[i] = *elem;
169-
}
170-
let expected = "ALEXK-PC";
171-
let actual = NetBiosPacket::from(Ipv4Addr::from([127, 0, 0, 1]), &data);
172-
173-
assert_eq!(expected, actual.name());
174-
}
175-
176-
#[test]
177-
fn parse_mac_from_data_correctly() {
178-
let mut data = [0u8; 1024];
179-
let packet = [
180-
0xA2, 0x48, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41,
181-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
182-
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
183-
0x00, 0x77, 0x04, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
184-
0x20, 0x44, 0x00, 0x4A, 0x41, 0x43, 0x4B, 0x49, 0x45, 0x47, 0x2D, 0x57, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20,
185-
0x00, 0x44, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
186-
0x00, 0xC4, 0x00, 0x53, 0x50, 0x49, 0x43, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
187-
0x1E, 0xC4, 0x00, 0x2C, 0x41, 0x38, 0xBA, 0xC3, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
188-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
189-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
190-
];
191-
for (i, elem) in packet.iter().enumerate() {
192-
data[i] = *elem;
193-
}
194-
let expected = "2C:41:38:BA:C3:64";
195-
let actual = NetBiosPacket::from(Ipv4Addr::from([127, 0, 0, 1]), &data);
196-
197-
assert_eq!(expected, actual.mac_address());
198-
}
199-
}

crates/network-scanner/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ authors = ["Devolutions Inc. <infos@devolutions.net>"]
55
edition = "2024"
66
publish = false
77

8+
[features]
9+
# Exposes a handful of internal helpers (e.g. `task_utils::ContextConfig`,
10+
# `task_utils::TaskExecutionContext`) so out-of-crate integration tests under
11+
# `testsuite/` can construct them directly. Never enable in production builds.
12+
test-utils = []
13+
814
[lints]
915
workspace = true
1016

@@ -17,6 +23,8 @@ network-interface = "2.0"
1723
network-scanner-net = { path = "../network-scanner-net" }
1824
network-scanner-proto = { path = "../network-scanner-proto" }
1925
parking_lot = "0.12"
26+
serde = { version = "1", features = ["derive"] }
27+
serde_json = "1"
2028
socket2 = "0.5"
2129
thiserror = "2"
2230
tokio = { version = "1.52", features = ["rt", "sync", "time", "fs"] }
@@ -25,6 +33,12 @@ typed-builder = "0.21"
2533

2634
[target.'cfg(target_os = "windows")'.dependencies]
2735
ipconfig = "0.3"
36+
windows-sys = { version = "0.61", features = [
37+
"Win32_Foundation",
38+
"Win32_NetworkManagement_IpHelper",
39+
"Win32_NetworkManagement_Ndis",
40+
"Win32_Networking_WinSock",
41+
] }
2842

2943
[target.'cfg(target_os = "linux")'.dependencies]
3044
futures-util = "0.3"

crates/network-scanner/examples/ping_range.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub async fn main() -> anyhow::Result<()> {
3131
range,
3232
ping_interval,
3333
ping_wait_time,
34+
Some(64),
35+
network_scanner::scanner::InterfaceBind::none(),
3436
should_ping,
3537
TaskManager::new(),
3638
)?;

0 commit comments

Comments
 (0)