How to find network interfaces in Rust with Tauri
🚀 Listing your network interfaces in Rust with pcap (Linux)
Section titled “🚀 Listing your network interfaces in Rust with pcap (Linux)”When you start dabbling in network capture in Rust, the first step is simple:
knowing which network interfaces are available on the machine.
With the pcap crate, it takes only a few lines of code.
In this guide, we will see how to:
- Install the required dependencies on Linux
- Create a minimal Rust project
- List the network interfaces and their addresses
- Add a few simple unit tests
🛠️ Prerequisites on Linux
Section titled “🛠️ Prerequisites on Linux”The pcap crate is a Rust binding to the libpcap library, widely used in C
(by Wireshark, tcpdump…).
For everything to work, install the development headers:
sudo apt updatesudo apt install libpcap-dev⚠️ Note:
Listing interfaces does not require sudo.
But if you want to capture packets later, you will need to:
- run your program with
sudo, or - grant rights to your binary with
setcap.
📦 Creating the project
Section titled “📦 Creating the project”cargo new pcap-list-interfacescd pcap-list-interfacesAdd the pcap crate to Cargo.toml:
[dependencies]pcap = "0.10.0"thiserror = "1"💻 The complete code
Section titled “💻 The complete code”Create src/main.rs:
use pcap::Device;use thiserror::Error;
fn main() -> Result<(), PcapError> { let interfaces = get_interfaces()?; print_interfaces_names(interfaces.clone()); print_interfaces_addresses(interfaces); Ok(())}
#[derive(Debug, Error)]pub enum PcapError { #[error("Unable to list the network interfaces")] DeviceListError(#[from] pcap::Error),}
fn get_interfaces() -> Result<Vec<Device>, PcapError> { let devices = Device::list()?; Ok(devices)}
fn print_interfaces_names(interfaces: Vec<Device>) { for interface in interfaces { println!("{}", interface.name); }}
fn print_interfaces_addresses(interfaces: Vec<Device>) { for interface in interfaces { for address in interface.addresses { println!("{:?}", address.addr); } }}🧪 Adding unit tests
Section titled “🧪 Adding unit tests”Even for a small utility, writing a few tests helps avoid nasty surprises:
#[cfg(test)]mod tests { use super::*;
#[test] fn get_interfaces_returns_ok() { let res = get_interfaces(); assert!(res.is_ok(), "expected Ok, got {:?}", res); }
#[test] fn print_interfaces_names_does_not_panic() { let interfaces = get_interfaces().unwrap_or_else(|_| Vec::new()); let outcome = std::panic::catch_unwind(|| { print_interfaces_names(interfaces.clone()); }); assert!(outcome.is_ok(), "print_interfaces_names panicked"); }
#[test] fn print_interfaces_addresses_does_not_panic() { let interfaces = get_interfaces().unwrap_or_else(|_| Vec::new()); let outcome = std::panic::catch_unwind(|| { print_interfaces_addresses(interfaces); }); assert!(outcome.is_ok(), "print_interfaces_addresses panicked"); }}These tests ensure that:
get_interfacesworks correctly- printing the names does not panic
- printing the addresses does not panic
▶️ Running the program
Section titled “▶️ Running the program”cargo runSample output on a Linux machine:
loeth0wlan0Some(192.168.1.42)Some(127.0.0.1)Some(::1)▶️ Running the tests
Section titled “▶️ Running the tests”cargo testExpected result:
running 3 teststest tests::get_interfaces_returns_ok ... oktest tests::print_interfaces_names_does_not_panic ... oktest tests::print_interfaces_addresses_does_not_panic ... ok✅ Conclusion
Section titled “✅ Conclusion”In fewer than 100 lines of Rust, we learned how to:
- List a machine’s network interfaces on Linux
- Display their IP addresses
- Handle errors cleanly with
thiserror - Write basic unit tests
🔭 Going further: integrating into Tauri
Section titled “🔭 Going further: integrating into Tauri”1. Creating the Tauri project
Section titled “1. Creating the Tauri project”deno run -A npm:create-tauri-appSample configuration:
✔ Project name · get_net_interfaces✔ Identifier · com.get_net_interfaces.app✔ Frontend · Vue (TypeScript)✔ Package manager · deno2. Tauri backend (Rust)
Section titled “2. Tauri backend (Rust)”Create a src-tauri/src/commandes/ folder and add:
use pcap::Device;use tauri::command;use thiserror::Error;
#[command]pub fn get_net_interfaces() -> Result<Vec<String>, PcapError> { let devices = get_interfaces()?; Ok(devices.into_iter().map(|d| d.name).collect())}
fn get_interfaces() -> Result<Vec<Device>, PcapError> { let devices = Device::list()?; Ok(devices)}
#[derive(Debug, Error)]pub enum PcapError { #[error("Unable to list the network interfaces")] DeviceListError(#[from] pcap::Error),}
impl serde::Serialize for PcapError { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::ser::Serializer, { serializer.serialize_str(self.to_string().as_ref()) }}3. Vue frontend (Composition API)
Section titled “3. Vue frontend (Composition API)”<script setup lang="ts">import { ref } from "vue";import { invoke } from "@tauri-apps/api/core";
const devices = ref<string[]>([]);
async function getNetInterfaces() { devices.value = await invoke("get_net_interfaces");}</script>
<template> <main class="container"> <button @click="getNetInterfaces">Get Net Interfaces</button> <ul> <li v-for="device in devices" :key="device">{{ device }}</li> </ul> </main></template>👉 Result:
Your Tauri app can list the network interfaces on the Rust side with pcap,
and return them to the Vue frontend through a #[command] command.
Going further
Section titled “Going further”1) Why it breaks in Tauri: the orphan rule
Section titled “1) Why it breaks in Tauri: the orphan rule”When a Tauri #[command] returns a value to the UI, Tauri serializes it (JSON)
with Serde. If you return an external type like pcap::Device directly,
there are two problems:
-
The type is not serializable (no
Serializeonpcap::Device). -
You cannot add
Serializetopcap::Devicebecause Rust enforces the orphan rule:You cannot implement an external trait (here
serde::Serialize) for an external type (herepcap::Device) from another crate.
In other words, what does not work:
// ❌ Forbidden by the orphan ruleimpl serde::Serialize for pcap::Device { fn serialize<S>(&self, _s: S) -> Result<S::Ok, S::Error> where S: serde::Serializer { /* ... */ }}2) The robust solution: a DTO (Data Transfer Object)
Section titled “2) The robust solution: a DTO (Data Transfer Object)”We create our own structures (which we own), derive Serialize on them,
and add From<pcap::*> conversions to map the useful fields.
Then the Tauri command returns our DTOs → serialization OK, UI happy.
3) Tauri v2 backend (Rust) — full DTO + command
Section titled “3) Tauri v2 backend (Rust) — full DTO + command”src-tauri/Cargo.toml (excerpts):
[dependencies]tauri = { version = "2", features = ["macros"] }pcap = "2"serde = { version = "1", features = ["derive"] }thiserror = "1"src-tauri/src/commandes/mod.rs:
use std::net::IpAddr;
use pcap::{ Address as PcapAddress, ConnectionStatus as PcapConnectionStatus, Device, DeviceFlags as PcapDeviceFlags, IfFlags as PcapIfFlags,};use serde::Serialize;use tauri::command;use thiserror::Error;
/// ===== Serializable DTOs for IPC =====
#[derive(Debug, Serialize)]pub struct NetDevice { pub name: String, pub desc: Option<String>, pub addresses: Vec<Address>, pub flags: DeviceFlags,}
#[derive(Debug, Serialize)]pub struct Address { pub addr: IpAddr, pub netmask: Option<IpAddr>, pub broadcast_addr: Option<IpAddr>, pub dst_addr: Option<IpAddr>,}
#[derive(Debug, Serialize)]pub struct DeviceFlags { pub if_flags: IfFlags, pub connection_status: ConnectionStatus,}
#[derive(Debug, Serialize)]pub struct IfFlags { /// Raw value (bitfield). Useful for decoding on the UI side or later. pub bits: u32,}
#[derive(Debug, Serialize)]pub enum ConnectionStatus { Unknown, Connected, Disconnected, NotApplicable,}
/// ===== pcap -> DTO conversions =====
impl From<PcapAddress> for Address { fn from(a: PcapAddress) -> Self { Address { addr: a.addr, netmask: a.netmask, broadcast_addr: a.broadcast_addr, dst_addr: a.dst_addr, } }}
impl From<PcapIfFlags> for IfFlags { fn from(f: PcapIfFlags) -> Self { IfFlags { bits: f.bits() } }}
impl From<PcapConnectionStatus> for ConnectionStatus { fn from(s: PcapConnectionStatus) -> Self { match s { PcapConnectionStatus::Unknown => ConnectionStatus::Unknown, PcapConnectionStatus::Connected => ConnectionStatus::Connected, PcapConnectionStatus::Disconnected => ConnectionStatus::Disconnected, PcapConnectionStatus::NotApplicable => ConnectionStatus::NotApplicable, } }}
impl From<PcapDeviceFlags> for DeviceFlags { fn from(df: PcapDeviceFlags) -> Self { DeviceFlags { if_flags: df.if_flags.into(), connection_status: df.connection_status.into(), } }}
impl From<Device> for NetDevice { fn from(d: Device) -> Self { NetDevice { name: d.name, desc: d.desc, addresses: d.addresses.into_iter().map(Address::from).collect(), flags: d.flags.into(), } }}
/// ===== Tauri command =====
#[command]pub fn get_net_interfaces() -> Result<Vec<NetDevice>, PcapError> { let devices = Device::list()?; Ok(devices.into_iter().map(NetDevice::from).collect())}
/// ===== Error handling =====
#[derive(Debug, Error)]pub enum PcapError { #[error("Unable to list the network interfaces")] DeviceListError(#[from] pcap::Error),}
// serialize the error as a String toward the UIimpl serde::Serialize for PcapError { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::ser::Serializer, { serializer.serialize_str(self.to_string().as_ref()) }}src-tauri/src/lib.rs:
mod commandes;
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() -> Result<(), tauri::Error> { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![commandes::get_net_interfaces]) .run(tauri::generate_context!())}4) Vue frontend (Options API) — select + detail (TypeScript)
Section titled “4) Vue frontend (Options API) — select + detail (TypeScript)”src/types/NetDevice.ts:
export type Address = { addr: string; netmask?: string | null; broadcast_addr?: string | null; dst_addr?: string | null;};
export type IfFlags = { bits: number };
export type ConnectionStatus = | "Unknown" | "Connected" | "Disconnected" | "NotApplicable";
export type DeviceFlags = { if_flags: IfFlags; connection_status: ConnectionStatus;};
export type NetDevice = { name: string; desc?: string | null; addresses: Address[]; flags: DeviceFlags;};src/components/NetDevicePicker.vue:
<script lang="ts">import { defineComponent } from "vue";import { invoke } from "@tauri-apps/api/core";import type { NetDevice } from "../types/NetDevice";
export default defineComponent({ name: "NetDevicePicker", data() { return { netDevices: [] as NetDevice[], selectedName: "" as string, loading: false, errorMsg: null as string | null, }; }, computed: { selected(): NetDevice | undefined { return this.netDevices.find((d) => d.name === this.selectedName); }, hasDevices(): boolean { return this.netDevices.length > 0; }, }, mounted() { this.refreshDevices(); }, methods: { async refreshDevices() { this.loading = true; this.errorMsg = null; try { const list = await invoke<NetDevice[]>("get_net_interfaces"); this.netDevices = list; if ( !this.selectedName || !this.netDevices.some((d) => d.name === this.selectedName) ) { this.selectedName = this.netDevices[0]?.name ?? ""; } } catch (e: unknown) { this.errorMsg = (e as Error)?.message ?? String(e); this.netDevices = []; this.selectedName = ""; } finally { this.loading = false; } }, },});</script>
<template> <div class="picker"> <h2>Network interfaces</h2>
<div class="row"> <select v-model="selectedName" :disabled="loading || !hasDevices" @click="refreshDevices"> <option v-if="loading" disabled>Loading…</option> <option v-else-if="!hasDevices" disabled>No interface</option> <option v-for="dev in netDevices" :key="dev.name" :value="dev.name"> {{ dev.name }}{{ dev.desc ? ` — ${dev.desc}` : "" }} </option> </select>
<button @click="refreshDevices" :disabled="loading">🔄</button> </div>
<p v-if="errorMsg" class="err">{{ errorMsg }}</p>
<div v-if="selected" class="card"> <h3>{{ selected.name }}</h3> <p v-if="selected.desc" class="muted">{{ selected.desc }}</p>
<details v-if="selected.addresses?.length"> <summary>Addresses ({{ selected.addresses.length }})</summary> <ul> <li v-for="(a, i) in selected.addresses" :key="i"> {{ a.addr }} <span v-if="a.netmask"> / {{ a.netmask }}</span> <span v-if="a.broadcast_addr"> • bcast: {{ a.broadcast_addr }}</span> <span v-if="a.dst_addr"> • dst: {{ a.dst_addr }}</span> </li> </ul> </details>
<details> <summary>Status & flags</summary> <p>Connection: {{ selected.flags.connection_status }}</p> <p>Flags (bits): {{ selected.flags.if_flags.bits }}</p> </details> </div> </div></template>
<style scoped>.picker { max-width: 720px; margin: 24px auto; }.row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }select, button { padding: .55rem .75rem; border-radius: 8px; border: 1px solid #ddd; background: #fff; }button:disabled, select:disabled { opacity: .6; cursor: not-allowed; }.err { color: crimson; margin-top: 8px; }.card { margin-top: 12px; padding: 12px; border: 1px solid #e3e3e3; border-radius: 10px; }.muted { opacity: .7; }</style>5) Bonus: decoding IfFlags.bits into booleans (code pattern)
Section titled “5) Bonus: decoding IfFlags.bits into booleans (code pattern)”You can expose readable flags on the UI side by adding a small backend helper. (Each environment/pcap may have different masks; keep the logic on the Rust side to stay portable.)
#[derive(Debug, Serialize)]pub struct IfFlagsView { pub bits: u32, pub is_up: bool, pub is_running: bool, pub is_loopback: bool, // add other derivations depending on your needs}
impl From<PcapIfFlags> for IfFlagsView { fn from(f: PcapIfFlags) -> Self { let bits = f.bits(); // ⚠️ Generic example: replace MASK_* with the masks suited to your platform/pcap const MASK_UP: u32 = 0x1; const MASK_RUNNING: u32 = 0x40; const MASK_LOOPBACK: u32 = 0x8;
Self { bits, is_up: (bits & MASK_UP) != 0, is_running: (bits & MASK_RUNNING) != 0, is_loopback: (bits & MASK_LOOPBACK) != 0, } }}Tip: start by exposing
bits(as above), and only enable the booleans once you have verified the exact masks on your target (Linux, macOS, Windows/Npcap). You can logbitsper interface to identify the flags present.
6) Running in dev
Section titled “6) Running in dev”deno task tauri devIn summary:
- The orphan rule prevents you from adding
Serializetopcap::Device. - The best practice: create a serializable DTO +
From<pcap::*>conversions. - You keep a clean front-end API, stable and portable.