Skip to content

Commit

Permalink
feat: allow filtering of resolved records by network interfaces ✨ (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
hrzlgnm authored Mar 17, 2024
1 parent 98698a5 commit 59dba43
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 70 deletions.
4 changes: 2 additions & 2 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ tauri = { version = "1", features = [ "window-all", "process-relaunch", "process
] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
mdns-sd = "0.10.4"
mdns-sd = { version = "0.10.4", features = ["log"] }
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
log = "^0.4.21"
if-addrs = "0.11.1"
if-addrs = { version = "0.11.1", features = ["link-local"] }
network-interface = "1.1.1"

[features]
Expand Down
181 changes: 117 additions & 64 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use if_addrs::get_if_addrs;
use if_addrs::{get_if_addrs, IfAddr, Interface};
use log::LevelFilter;
use mdns_sd::{IfKind, ServiceDaemon, ServiceEvent};
use mdns_sd::{ServiceDaemon, ServiceEvent};
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
use serde::Serialize;
use std::{
borrow::Borrow,
collections::HashMap,
collections::HashSet,
net::IpAddr,
sync::{Arc, Mutex},
};
use tauri::Manager;
use tauri::State;
use tauri_plugin_log::LogTarget;

type SharedServiceDaemon = Arc<Mutex<ServiceDaemon>>;

struct Daemon {
struct MdnsState {
shared: SharedServiceDaemon,
enabled_interfaces: Arc<Mutex<Vec<Interface>>>,
}

impl MdnsState {
fn new() -> Self {
Self {
shared: get_shared_daemon(),
enabled_interfaces: Arc::new(Mutex::new(get_all_interfaces_except_loopback())),
}
}
}

fn get_shared_daemon() -> SharedServiceDaemon {
Expand All @@ -33,17 +44,17 @@ struct TxtRecord {
}

#[derive(Serialize, Clone, Debug)]
struct ResolvedService {
pub struct ResolvedService {
instance_name: String,
hostname: String,
port: u16,
addresses: Vec<IpAddr>,
pub addresses: Vec<IpAddr>,
subtype: Option<String>,
txt: Vec<TxtRecord>,
}

#[tauri::command]
fn resolve_service(service_type: String, state: State<Daemon>) -> Vec<ResolvedService> {
fn resolve_service(service_type: String, state: State<MdnsState>) -> Vec<ResolvedService> {
log::info!("Resolving {}", service_type);
let mdns = state.shared.lock().unwrap();
let mut service_type = service_type;
Expand Down Expand Up @@ -94,11 +105,69 @@ fn resolve_service(service_type: String, state: State<Daemon>) -> Vec<ResolvedSe
_ => {}
}
}
result.values().cloned().collect()

filter_resolved_service_by_interfaces_addresses(
result.values().cloned().collect(),
state.enabled_interfaces.lock().unwrap().clone(),
)
}

fn valid_ip_on_interface(addr: &IpAddr, interface: &Interface) -> bool {
match (addr, &interface.addr) {
(IpAddr::V4(addr), IfAddr::V4(interface_address)) => {
let netmask = u32::from(interface_address.netmask);
let interface_net = u32::from(interface_address.ip) & netmask;
let addr_net = u32::from(*addr) & netmask;
addr_net == interface_net
}
(IpAddr::V6(addr), IfAddr::V6(interface_address)) => {
let netmask = u128::from(interface_address.netmask);
let interface_net = u128::from(interface_address.ip) & netmask;
let addr_net = u128::from(*addr) & netmask;
addr_net == interface_net
}
_ => false,
}
}

fn get_addresses_on_interface(addr: &Vec<IpAddr>, interface: &Interface) -> Vec<IpAddr> {
addr.iter()
.filter(|a| valid_ip_on_interface(a, interface))
.copied()
.collect()
}

fn filter_resolved_service_by_interfaces_addresses(
resolved_services: Vec<ResolvedService>,
interfaces: Vec<Interface>,
) -> Vec<ResolvedService> {
let mut result = Vec::<ResolvedService>::new();
for resolved_service in resolved_services.iter() {
let mut unique_addresses = HashSet::<IpAddr>::new();
for interface in interfaces.iter() {
unique_addresses.extend(get_addresses_on_interface(
&resolved_service.addresses,
&interface,
));
}
let mut addresses = unique_addresses.into_iter().collect::<Vec<_>>();
if !addresses.is_empty() {
addresses.sort();
result.push(ResolvedService {
instance_name: resolved_service.instance_name.clone(),
hostname: resolved_service.hostname.clone(),
port: resolved_service.port,
addresses,
subtype: resolved_service.subtype.clone(),
txt: resolved_service.txt.clone(),
});
}
}
result
}

#[tauri::command]
fn enum_service_types(state: State<Daemon>) -> Vec<String> {
fn enum_service_types(state: State<MdnsState>) -> Vec<String> {
let mut found = vec![];
if let Ok(mdns) = state.shared.lock() {
let meta_service = "_services._dns-sd._udp.local.";
Expand Down Expand Up @@ -128,79 +197,65 @@ fn enum_service_types(state: State<Daemon>) -> Vec<String> {
log::debug!("Metrics {:#?}", metrics);
}
found.sort();
log::debug!("Found service types: {:#?}", found);
log::debug!("Found service types: {:?}", found);
}
found
}

fn get_all_interface_names_except_loopback() -> Vec<(String, String)> {
let ifaces = get_if_addrs().unwrap();
fn get_all_interfaces_except_loopback() -> Vec<Interface> {
let interface_addresses = get_if_addrs().unwrap();

// if_addrs unfortunately shows the GUID of the interface as name,
// so we workaround here by using network_interface in addition, as mdns_sd expects name from
// if_addrs
let nwifs = NetworkInterface::show().unwrap();
// so we work around here by using network_interface in addition to get user friendly names
let network_interfaces = NetworkInterface::show().unwrap();
let mut index_to_name = HashMap::new();
for nwif in nwifs.iter() {
index_to_name.insert(nwif.index, nwif.name.clone());
for network_interface in network_interfaces.iter() {
index_to_name.insert(network_interface.index, network_interface.name.clone());
}
ifaces

interface_addresses
.into_iter()
.filter(|itf| !itf.is_loopback())
.map(|itf| {
let idx = itf.index.unwrap();
(itf.name, index_to_name.get(&idx).unwrap().clone())
.map(|itf| Interface {
name: index_to_name.get(&itf.index.unwrap()).unwrap().clone(),
addr: itf.addr,
index: itf.index,
})
.collect()
}

#[tauri::command]
fn get_interfaces() -> Vec<String> {
let itfs = get_all_interface_names_except_loopback();

log::debug!("Got interfacs: {:#?}", itfs);
fn get_all_interface_names_except_loopback() -> Vec<String> {
let interface_addresses = get_all_interfaces_except_loopback();

itfs.into_iter().map(|i| i.1).collect()
interface_addresses
.into_iter()
.map(|interface| interface.name)
.collect::<HashSet<_>>()
.into_iter()
.collect()
}

#[tauri::command]
fn set_interfaces(itfs: Vec<String>, state: State<Daemon>) {
if let Ok(mdns) = state.shared.lock() {
let all_ifpairs = get_all_interface_names_except_loopback();
fn get_interfaces() -> Vec<String> {
let mut interface_names = get_all_interface_names_except_loopback();
interface_names.sort();

let itfs_to_disable = all_ifpairs
.clone()
.into_iter()
.filter(|p| !itfs.contains(&p.1))
.map(|p| p.0)
.collect::<Vec<_>>();
interface_names
}

let itfs_to_enable = all_ifpairs
.into_iter()
.filter(|p| itfs.contains(&p.1))
.map(|p| p.0)
.collect::<Vec<_>>();
#[tauri::command]
fn set_interfaces(interfaces: Vec<String>, state: State<MdnsState>) {
let interface_names = get_all_interface_names_except_loopback();

log::debug!(
"Enabling interfaces: {:#?}, disabling interfaces {:#?}",
itfs_to_enable,
itfs_to_disable
);
mdns.enable_interface(
itfs_to_enable
.into_iter()
.map(IfKind::Name)
.collect::<Vec<_>>(),
)
.expect("to enable interfaces");
mdns.disable_interface(
itfs_to_disable
.into_iter()
.map(IfKind::Name)
.collect::<Vec<_>>(),
)
.expect("to disable interfaces");
}
let enabled_interface_names = interface_names
.into_iter()
.filter(|name| interfaces.contains(name))
.collect::<Vec<_>>();
let enabled_interfaces = get_all_interfaces_except_loopback()
.into_iter()
.filter(|interface| enabled_interface_names.contains(&interface.name))
.collect::<Vec<_>>();
*state.enabled_interfaces.lock().unwrap() = enabled_interfaces;
}

#[cfg(target_os = "linux")]
Expand Down Expand Up @@ -235,9 +290,7 @@ fn main() {
.expect("title to be set");
Ok(())
})
.manage(Daemon {
shared: get_shared_daemon(),
})
.manage(MdnsState::new())
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(
tauri_plugin_log::Builder::default()
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"package": {
"productName": "mdns-browser",
"version": "0.2.1"
"version": "0.2.2"
},
"tauri": {
"allowlist": {
Expand Down
6 changes: 3 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ async fn get_interfaces() -> Interfaces {

#[derive(Serialize, Deserialize)]
struct SetInterfacesArgs {
itfs: Vec<String>,
interfaces: Vec<String>,
}

async fn set_interfaces(itfs: Interfaces) {
let args = to_value(&SetInterfacesArgs { itfs }).unwrap();
async fn set_interfaces(interfaces: Interfaces) {
let args = to_value(&SetInterfacesArgs { interfaces }).unwrap();
invoke("set_interfaces", args).await;
}

Expand Down

0 comments on commit 59dba43

Please sign in to comment.