Architecture
┌──────────────────────────────────────────────────┐│ PortFinder binary (Rust, single-crate) ││ ││ ┌────────────────┐ ┌──────────┐ ││ │ gpui window │ ── flume chan ─►│ tokio │ ││ │ (app_view.rs) │ ◄── results ────│ runtime │ ││ └────────────────┘ └────┬─────┘ ││ ▲ │ ││ │ argv (no subcommand) │ ││ ┌────────┴────────┐ ┌────▼─────┐ ││ │ main.rs │ │ capture/ │ ││ │ CLI ↔ GUI │ │ pcap orch│ ││ │ dispatch │ └────┬─────┘ ││ └────────┬────────┘ │ ││ │ argv (subcommand) │ ││ ┌────────▼────────┐ ┌────▼─────┐ ││ │ cli.rs │ │ parsers: │ ││ │ clap headless │ │ cdp/lldp │ ││ └─────────────────┘ │ /mndp │ ││ └────┬─────┘ ││ │ ││ ┌────▼─────┐ ││ │privilege/│ ││ │BPF helper│ ││ │installer │ ││ └────┬─────┘ │└──────────────────────────────────────────┼───────┘ │ ▼ libpcap / Npcap (wpcap.dll)PortFinder 4.x is a single Rust crate. The GUI (gpui) and the headless
CLI (portfinder capture …) share one binary — main.rs decides
which path to take based on whether argv carries a subcommand. The
capture engine, parsers, and BPF privilege flow are identical along
both paths.
Layout
Section titled “Layout”| Path | Purpose |
|---|---|
src/lib.rs | Library crate. Hosts the module declarations and the shared types (CaptureRequest, CaptureResult, InterfaceInfo) plus init_logging. Both binary entry points consume this. |
src/main.rs | PortFinder binary entry point (GUI). windows_subsystem = "windows" on Windows release builds. Dispatches CLI vs GUI by argv on macOS / Linux; on Windows the dispatch is preserved for cargo run but shipping CLI usage routes through the sibling binary below. |
src/bin/portfinder-cli.rs | portfinder-cli binary entry point. No windows_subsystem attribute → console subsystem on Windows, so PowerShell waits for it and stdio routes correctly. Gated behind [features] windows-cli so non-Windows builds don’t compile it. |
src/app_view.rs | gpui UI: interface picker, protocol selector, Start / Stop, result panel, privilege banner, settings popover, history popover. Hosts the tokio runtime that drives capture::run. |
src/cli.rs | Headless clap-based CLI (capture / list / privileges subcommands plus the global -v / -vv / -q / --log-file flags). |
src/capture/ | pcap capture orchestration plus hand-rolled CDP, LLDP, and MNDP TLV parsers. |
src/privilege/ | Per-platform privilege detection + macOS BPF helper installer (install_darwin.rs inlines the install script). |
src/settings.rs | Persistent settings (settings.json) + history (history.json) + live logger pipe (LogPipe). set_logging_enabled swaps RwLock<Option<File>> so the in-app toggle takes effect without a relaunch. |
src/updater.rs | Boot-time GitHub-Releases check for a newer version, behind the footer’s “Update available” pill. |
Cargo.toml | Root crate manifest. [package.metadata.packager] carries the cargo-packager bundle config (icons, identifiers, deb deps, macOS plist path). |
build.rs | Embeds resources/icons/icon.ico into PortFinder.exe (Windows). Marks wpcap.dll as delay-loaded so the binary launches even if Npcap isn’t installed. |
resources/Info.plist | macOS bundle Info.plist. CFBundleIdentifier = io.github.packetThrower.PortFinder. |
resources/icons/ | .icns / .ico / .png consumed by cargo-packager + the Windows .rc. |
packaging/macos/ | Standalone BPF helper .pkg builder + scripts. PortFinder BPF Helper.sh is the actual helper; the matching io.github.packetThrower.PortFinder.BPFHelper.plist is the LaunchDaemon. |
packaging/linux/ | portfinder.desktop + post-install hook (sets CAP_NET_RAW) + DEP-5 copyright + changelog.Debian.template (lintian compliance). |
packaging/windows/ | Windows-only cargo-packager override (Packager.json) that adds the portfinder-cli.exe sibling to the NSIS / WiX bundles. See its README for the drift warning. |
Windows dual binary
Section titled “Windows dual binary”The Windows installer ships both PortFinder.exe (GUI) and portfinder-cli.exe. A PE binary has a single “subsystem” byte set at link time and one .exe has to pick one:
IMAGE_SUBSYSTEM_WINDOWS_GUI(windows_subsystem = "windows"insrc/main.rs): no console allocated on launch; the shell fire-and-forgets the process. Right for File Explorer double-click — no black console window flashes up next to the GUI.IMAGE_SUBSYSTEM_WINDOWS_CUI(default forsrc/bin/portfinder-cli.rs, which has nowindows_subsystemattribute): kernel allocates a console; PowerShell waits for the process to exit before redrawing the prompt; stdio routes correctly. Right for CLI usage.
There’s no runtime override. AttachConsole works for stdio routing but the shell has already redrawn its prompt by the time CLI output arrives. Real-world precedent: wezterm ships wezterm + wezterm-gui; Zed ships cli + zed.
Cargo wires this up via [features] windows-cli = [] plus required-features = ["windows-cli"] on [[bin]] name = "portfinder-cli", so non-Windows cargo build --release doesn’t compile it. The Windows release workflow passes --config "$(Get-Content packaging/windows/Packager.json -Raw)" to cargo-packager, which adds the CLI to the bundle and runs cargo build --release --features windows-cli for the build.
Capture flow
Section titled “Capture flow”- The user clicks Start in the gpui window (or runs
portfinder capture …). Both paths build aCaptureRequestwith the interface name + protocol string. - In the GUI, the request is dispatched onto a dedicated tokio runtime running on a background OS thread; the gpui task awaits a
flumechannel for the result. In the CLI, the samecapture::runfuture runs on a freshtokio::runtime::Runtimesynchronously viablock_on. capture::runcreates a freshCancellationToken(replacing any previous one, which gets cancelled) and dispatches to either:- Single interface:
tokio::task::spawn_blockingopens apcap::Capture, sets the BPF filter, and pollsnext_packet()with a 50 ms timeout. The token is checked between reads, so Stop responds within ~60 ms. - Sniff-all (
interface_name == ""):JoinSetspawns one task per non-loopback interface.tokio::select!returns the first captured frame and cancels the rest.
- Single interface:
- The raw frame is parsed by
cdp::parse,lldp::parse, ormndp::parse(hand-rolled TLV iterators) into aCaptureResult. - Stop cancels the token; the in-flight blocking task exits on its next loop tick. The GUI receives the resulting
Err("capture cancelled")over the flume channel and renders “Stopped” in the status row.
Privilege flow
Section titled “Privilege flow”macOS:
privilege::get_privilege_status()probes/dev/bpf0(readable when the user is in theaccess_bpfgroup) and checks for the LaunchDaemon plist on disk.- The 4.x label is
io.github.packetThrower.PortFinder.BPFHelper. The legacy 3.x labelcoop.otec.portfinder.ChmodBPFis also probed so an existing 3.x install registers as “helper installed” until the user re-runs the new installer (which then unloads + removes the 3.x daemon). - Install BPF Helper in the GUI runs
install_darwin.rs— an inlined shell script invoked viaosascript ... with administrator privileges. The standalonePortFinder-BPF-<version>.pkg(built bypackaging/macos/build-pkg.sh, shipped on every release) performs the same install for sysadmins deploying via MDM.
Linux: capture works if the user is root or the binary has CAP_NET_RAW (CapEff in /proc/self/status). The .deb / .rpm / .pkg.tar.zst post-install runs setcap cap_net_raw,cap_net_admin=eip on /usr/bin/PortFinder.
Windows: pcap::Device::list() would crash the process if wpcap.dll isn’t present; build.rs marks the DLL as delay-loaded so the binary launches anyway, and privilege::get_privilege_status() reports npcap_installed = false to drive the in-app “Download Npcap” banner.
| Workflow | Trigger | Output |
|---|---|---|
.github/workflows/ci.yml | push / PR to main | cargo clippy -D warnings + cargo test on macOS arm64+amd64, Windows amd64+arm64, Linux amd64+arm64 |
.github/workflows/release.yml | push of v* tag | cargo-packager builds .dmg / .deb / .rpm / .AppImage / .pkg.tar.zst / NSIS / WiX → GitHub Release |
.github/workflows/docs.yml | push to main | Astro Starlight build → GitHub Pages |