Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Put changes for the upcoming release here!
- Changed (Rust API, lang bindings): the `TS_RS_EXPERIMENT` environment variable is no longer required to use the
library. The library logs a warning during initialization, as a reminder that it's still work-in-progress software.
- Updated MSRV to 1.97.
- Added (ts_netmon): support for macOS. This brings macOS support for direct peer-to-peer connections to parity with
Linux and Windows. [#396](https://github.com/tailscale/tailscale-rs/pull/396)

## [0.5.0](https://github.com/tailscale/tailscale-rs/releases/tag/v0.5.0) - 2026-08-14

Expand Down
29 changes: 21 additions & 8 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion ts_cli_util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub fn init_tracing() {
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy();

let fmt_layer = tracing_subscriber::fmt::layer();
let fmt_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);

let fmt_layer = if std::env::var("TS_RS_LOG_PRETTY") == Ok("1".into()) {
fmt_layer.pretty().boxed()
Expand Down
16 changes: 15 additions & 1 deletion ts_netmon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,30 @@ license.workspace = true
rust-version.workspace = true

[dependencies]
bytes.workspace = true
cfg-if.workspace = true
flume.workspace = true
futures-util.workspace = true
ipnet.workspace = true
pin-project-lite.workspace = true
regex = "1.13"
smallvec.workspace = true
static_assertions.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
tracing.workspace = true

ts_future_util.workspace = true
ts_hexdump.workspace = true

[dev-dependencies]
ts_cli_util.workspace = true

bstr = "1.13"
clap.workspace = true
proptest.workspace = true
tracing-test = { version = "0.2", features = ["no-env-filter"] }

[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = [
"Win32_NetworkManagement_IpHelper",
Expand All @@ -35,9 +46,12 @@ windows = { version = "0.62", features = [
rtnetlink = "0.21"

[target.'cfg(target_os = "macos")'.dependencies]
nix = "0.31"
bitflags = "2.13"
libc = "0.2"
nom = "8.0"
socket2 = "0.6"
tokio = { workspace = true, features = ["net"] }
zerocopy.workspace = true

[lints]
workspace = true
152 changes: 152 additions & 0 deletions ts_netmon/examples/macos_dump_route.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! Dump route tables on macOS.

#[cfg(target_os = "macos")]
mod _mac {
use std::{io::Read, path::PathBuf};

use nom::combinator::ParserIterator;
use ts_netmon::{
FamilyOrBoth,
bsd::{net_table, net_table::DumpType},
};

#[derive(clap::Parser)]
pub struct Args {
/// Rather than querying the OS for a RIB, read the file instead.
#[arg(short = 'i', long, conflicts_with_all = ["out_file", "interface2", "interface", "route2", "route"])]
pub in_file: Option<PathBuf>,

/// Write out the RIB data received from the OS to the specified file.
#[arg(short = 'o', long, conflicts_with("in_file"))]
pub out_file: Option<PathBuf>,

#[command(flatten)]
pub ty: Ty,
}

#[derive(clap::Args, Debug)]
#[group(multiple = false)]
pub struct Ty {
/// Fetch the IFMIB in `NET_RT_IFLIST2` format.
#[clap(long = "if2")]
pub interface2: bool,

/// Fetch the IFMIB in `NET_RT_IFLIST` format.
#[clap(long = "if")]
pub interface: bool,

/// Fetch the RIB in `NET_RT_DUMP2` format.
#[clap(long = "rt2")]
pub route2: bool,

/// Fetch the RIB in `NET_RT_DUMP` RIB format.
#[clap(long = "rt")]
pub route: bool,
}

impl Ty {
pub fn get(&self) -> Option<DumpType> {
if self.interface2 {
Some(DumpType::Interface2)
} else if self.interface {
Some(DumpType::Interface)
} else if self.route2 {
Some(DumpType::Route2)
} else if self.route {
Some(DumpType::Route)
} else {
None
}
}
}

pub fn load_rib(args: &Args) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
if let Some(in_file) = &args.in_file {
let mut rib = vec![];

let mut f = std::fs::File::open(in_file)?;
f.read_to_end(&mut rib)?;

Ok(rib)
} else {
let rib = net_table::dump(
FamilyOrBoth::Both,
args.ty.get().unwrap_or(DumpType::Interface2),
0,
)?;

if let Some(out_file) = &args.out_file {
use std::io::Write;
let mut f = std::fs::File::create(out_file)?;
f.write_all(&rib)?;
}

Ok(rib)
}
}

pub fn finish_iter<'i, F>(
iter: ParserIterator<&'i [u8], nom::error::Error<&'i [u8]>, F>,
) -> Result<&'i [u8], nom::Err<nom::error::Error<Vec<u8>>>> {
match iter.finish() {
Err(ref e @ (nom::Err::Error(ref inner) | nom::Err::Failure(ref inner)))
if !inner.input.is_empty() =>
{
panic!("{e}");
}
Err(e) => {
tracing::warn!("{e}");
Ok(&[])
}
x => x.map(|x| x.0).map_err(|e| e.to_owned()),
}
}
}

#[cfg(target_os = "macos")]
use _mac::*;

#[cfg(target_os = "macos")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
use ts_netmon::bsd::{
net_table,
net_table::{Address, MessageHeader},
};

ts_cli_util::init_tracing();

let args = Args::parse();

let rib = load_rib(&args)?;
tracing::debug!(rib_len = rib.len());

let mut iter = nom::combinator::iterator(
rib.as_slice(),
nom::combinator::complete(net_table::msg_chunk()),
);
for chunk in &mut iter {
let (rest, (_ty, hdr)) = MessageHeader::parse(chunk).map_err(|e| format!("{e}"))?;

tracing::info!(?hdr);

let mut iter = nom::combinator::iterator(rest, nom::combinator::complete(Address::parse()));

for (addr, flag) in (&mut iter).zip(hdr.addrs().iter()) {
tracing::info!(?flag, ?addr, "ADDR");
}

let rest = finish_iter(iter)?;
assert!(rest.is_empty());
}

let rest = finish_iter(iter)?;
assert!(rest.is_empty());

Ok(())
}

#[cfg(not(target_os = "macos"))]
fn main() {
eprintln!("error: this example only runs on macOS")
}
45 changes: 45 additions & 0 deletions ts_netmon/examples/macos_raw_monitor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Dump route tables on macOS.

#[cfg(target_os = "macos")]
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
use futures_util::StreamExt;
use ts_netmon::bsd::{
RouteSocket,
net_table::{Address, MessageHeader},
};
use zerocopy::IntoBytes;

#[derive(clap::Parser)]
struct Args {}

ts_cli_util::init_tracing();
let _args = Args::parse();

let sock = RouteSocket::new()?;
let mut raw_stream = sock.raw_msg_stream();

while let Some(msg) = raw_stream.next().await {
let msg = msg?;

let (rest, (ty, msg)) = MessageHeader::parse(msg.as_bytes())
.map_err(|e| std::io::Error::other(e.to_string()))?;

let mut iter = nom::combinator::iterator(rest, Address::parse::<_, nom::error::Error<_>>());

let addrs = msg.addrs().into_iter().zip(&mut iter).collect::<Vec<_>>();
iter.finish()
.map_err(|e| e.to_string())
.map_err(std::io::Error::other)?;

tracing::info!(?ty, ?msg, ?addrs);
}

Ok(())
}

#[cfg(not(target_os = "macos"))]
fn main() {
eprintln!("error: this example only runs on macOS")
}
Loading