use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Mutex;
use ipnetwork::{Ipv4Network, Ipv6Network};
use log::{debug, warn};
use tun_rs::{AsyncDevice, DeviceBuilder};
use crate::config::TunConfig;
pub struct TunDevice {
inner: AsyncDevice,
programmed_ip6: Mutex<Option<IpAddr>>,
}
impl TunDevice {
pub fn create(cfg: &TunConfig) -> std::io::Result<Self> {
let mut builder = DeviceBuilder::new()
.mtu(cfg.mtu)
.ipv4(cfg.ip, cfg.netmask, Some(cfg.peer_ip));
if let Some(ip6) = cfg.ip6 {
builder = builder.ipv6(ip6.ip(), ip6.prefix());
}
if let Some(name) = &cfg.name {
builder = builder.name(name.clone());
}
let inner = builder.build_async()?;
Ok(Self {
inner,
programmed_ip6: Mutex::new(cfg.ip6.map(|n| n.ip().into())),
})
}
pub fn create_unaddressed(name: Option<&str>, mtu: u16) -> std::io::Result<Self> {
let mut builder = DeviceBuilder::new().mtu(mtu);
if let Some(name) = name {
builder = builder.name(name);
}
let inner = builder.build_async()?;
Ok(Self {
inner,
programmed_ip6: Mutex::new(None),
})
}
pub fn apply_assignment(
&self,
ip: Ipv4Addr,
netmask: Ipv4Addr,
peer: Ipv4Addr,
ip6: Option<Ipv6Network>,
) -> std::io::Result<()> {
self.inner
.set_network_address(ip, netmask, assignment_destination(peer))?;
if let Some(old) = peek_programmed_ip6(&self.programmed_ip6) {
forget_if_removed(&self.programmed_ip6, self.inner.remove_address(old))?;
}
if let Some(n) = ip6 {
self.inner.add_address_v6(n.ip(), n.prefix())?;
record_programmed_ip6(&self.programmed_ip6, n.ip());
}
Ok(())
}
pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.recv(buf).await
}
pub async fn send(&self, packet: &[u8]) -> std::io::Result<usize> {
self.inner.send(packet).await
}
pub fn name(&self) -> std::io::Result<String> {
self.inner.name()
}
pub fn mtu(&self) -> std::io::Result<u16> {
self.inner.mtu()
}
pub fn local_ip(cfg: &TunConfig) -> Ipv4Addr {
cfg.ip
}
}
fn assignment_destination(peer: Ipv4Addr) -> Option<Ipv4Addr> {
#[cfg(windows)]
{
let _ = peer;
None
}
#[cfg(not(windows))]
Some(peer)
}
fn peek_programmed_ip6(slot: &Mutex<Option<IpAddr>>) -> Option<IpAddr> {
*slot.lock().unwrap()
}
fn take_programmed_ip6(slot: &Mutex<Option<IpAddr>>) -> Option<IpAddr> {
slot.lock().unwrap().take()
}
fn record_programmed_ip6(slot: &Mutex<Option<IpAddr>>, ip: Ipv6Addr) {
*slot.lock().unwrap() = Some(IpAddr::V6(ip));
}
fn address_already_gone(err: &io::Error) -> bool {
if matches!(
err.kind(),
io::ErrorKind::NotFound | io::ErrorKind::AddrNotAvailable
) {
return true;
}
err.kind() == io::ErrorKind::Other && netsh_address_gone(err)
}
fn netsh_address_gone(err: &io::Error) -> bool {
let msg = err.to_string();
let Some(out) = msg.split("out=").nth(1) else {
return false;
};
let out = out.to_ascii_lowercase();
out.contains("not present") || out.contains("element not found")
}
fn forget_if_removed(slot: &Mutex<Option<IpAddr>>, remove: io::Result<()>) -> io::Result<()> {
match remove {
Ok(()) => {
take_programmed_ip6(slot);
Ok(())
}
Err(e) if address_already_gone(&e) => {
take_programmed_ip6(slot);
Ok(())
}
Err(e) => Err(e),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct InstalledPrefix {
dst: IpAddr,
prefix: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PrefixAction {
Refresh,
Replace { old: Option<InstalledPrefix> },
}
fn prefix_action(current: Option<InstalledPrefix>, next: InstalledPrefix) -> PrefixAction {
match current {
Some(old) if old == next => PrefixAction::Refresh,
other => PrefixAction::Replace { old: other },
}
}
fn v4_connected(assigned: Ipv4Addr, netmask: Ipv4Addr) -> io::Result<InstalledPrefix> {
let net = Ipv4Network::with_netmask(assigned, netmask)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
Ok(InstalledPrefix {
dst: IpAddr::V4(net.network()),
prefix: net.prefix(),
})
}
fn ip6_connected(n: Ipv6Network) -> InstalledPrefix {
InstalledPrefix {
dst: IpAddr::V6(n.network()),
prefix: n.prefix(),
}
}
pub struct SubnetRouteGuard {
ifindex: u32,
tun_ip: Ipv4Addr,
v4: Option<InstalledPrefix>,
v6: Option<InstalledPrefix>,
}
impl SubnetRouteGuard {
pub fn new(tun_name: &str) -> io::Result<Self> {
Ok(Self {
ifindex: crate::policy::route::interface_index(tun_name)?,
tun_ip: Ipv4Addr::UNSPECIFIED,
v4: None,
v6: None,
})
}
pub fn apply(
&mut self,
assigned_ip: Ipv4Addr,
netmask: Ipv4Addr,
ip6: Option<Ipv6Network>,
) -> io::Result<()> {
self.tun_ip = assigned_ip;
self.sync_prefix(true, v4_connected(assigned_ip, netmask)?)?;
match ip6 {
Some(n) => self.sync_prefix(false, ip6_connected(n))?,
None => {
if let Some(old) = self.v6.take() {
self.delete_prefix(old, false);
}
}
}
Ok(())
}
fn sync_prefix(&mut self, is_v4: bool, next: InstalledPrefix) -> io::Result<()> {
let current = if is_v4 { self.v4 } else { self.v6 };
match prefix_action(current, next) {
PrefixAction::Refresh => {
if is_v4 {
crate::policy::route::modify_route(
self.ifindex,
self.tun_ip,
next.dst,
next.prefix,
true,
)?;
}
}
PrefixAction::Replace { old } => {
if is_v4 {
self.v4 = None;
} else {
self.v6 = None;
}
if let Some(old) = old {
self.delete_prefix(old, false);
}
crate::policy::route::modify_route(
self.ifindex,
self.tun_ip,
next.dst,
next.prefix,
true,
)?;
}
}
if is_v4 {
self.v4 = Some(next);
} else {
self.v6 = Some(next);
}
Ok(())
}
fn delete_prefix(&self, p: InstalledPrefix, on_shutdown: bool) {
if let Err(e) =
crate::policy::route::modify_route(self.ifindex, self.tun_ip, p.dst, p.prefix, false)
{
if on_shutdown {
debug!(
"failed to remove subnet route {}/{} on shutdown: {e}",
p.dst, p.prefix
);
} else {
warn!("failed to remove subnet route {}/{}: {e}", p.dst, p.prefix);
}
}
}
}
impl Drop for SubnetRouteGuard {
fn drop(&mut self) {
if let Some(p) = self.v4.take() {
self.delete_prefix(p, true);
}
if let Some(p) = self.v6.take() {
self.delete_prefix(p, true);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assignment_destination_is_not_a_windows_gateway() {
let peer = Ipv4Addr::new(10, 9, 0, 1);
let dest = assignment_destination(peer);
#[cfg(windows)]
assert_eq!(dest, None);
#[cfg(not(windows))]
assert_eq!(dest, Some(peer));
}
#[test]
fn reapply_leaves_a_single_programmed_v6() {
let create_v6: Ipv6Addr = "fd07:7::1".parse().unwrap();
let first: Ipv6Network = "fd07:7::a09:25/64".parse().unwrap();
let second: Ipv6Network = "fd07:7::a09:26/64".parse().unwrap();
let slot = Mutex::new(Some(IpAddr::V6(create_v6)));
let old = take_programmed_ip6(&slot);
record_programmed_ip6(&slot, first.ip());
assert_eq!(old, Some(IpAddr::V6(create_v6)));
assert_eq!(*slot.lock().unwrap(), Some(IpAddr::V6(first.ip())));
let old = take_programmed_ip6(&slot);
record_programmed_ip6(&slot, first.ip());
assert_eq!(old, Some(IpAddr::V6(first.ip())));
assert_eq!(*slot.lock().unwrap(), Some(IpAddr::V6(first.ip())));
let old = take_programmed_ip6(&slot);
record_programmed_ip6(&slot, second.ip());
assert_eq!(old, Some(IpAddr::V6(first.ip())));
assert_eq!(*slot.lock().unwrap(), Some(IpAddr::V6(second.ip())));
}
#[test]
fn failed_remove_keeps_programmed_ip6() {
let ip: Ipv6Addr = "fd07:7::1".parse().unwrap();
let slot = Mutex::new(Some(IpAddr::V6(ip)));
let err = io::Error::new(io::ErrorKind::PermissionDenied, "busy");
assert!(forget_if_removed(&slot, Err(err)).is_err());
assert_eq!(*slot.lock().unwrap(), Some(IpAddr::V6(ip)));
}
#[test]
fn gone_remove_forgets_programmed_ip6() {
let ip: Ipv6Addr = "fd07:7::1".parse().unwrap();
let slot = Mutex::new(Some(IpAddr::V6(ip)));
let err = io::Error::new(io::ErrorKind::NotFound, "gone");
assert!(forget_if_removed(&slot, Err(err)).is_ok());
assert!(slot.lock().unwrap().is_none());
}
#[test]
fn windows_netsh_gone_forgets_programmed_ip6() {
let ip: Ipv6Addr = "fd07:7::1".parse().unwrap();
let slot = Mutex::new(Some(IpAddr::V6(ip)));
let err = io::Error::other(
r#"cmd="netsh interface ipv6 delete address 12 fd07:7::1",out="The specified address is not present on the interface.""#,
);
assert!(address_already_gone(&err));
assert!(forget_if_removed(&slot, Err(err)).is_ok());
assert!(slot.lock().unwrap().is_none());
}
#[test]
fn windows_netsh_denied_keeps_programmed_ip6() {
let ip: Ipv6Addr = "fd07:7::1".parse().unwrap();
let slot = Mutex::new(Some(IpAddr::V6(ip)));
let err = io::Error::other(
r#"cmd="netsh interface ipv6 delete address 12 fd07:7::1",out="Access is denied.""#,
);
assert!(!address_already_gone(&err));
assert!(forget_if_removed(&slot, Err(err)).is_err());
assert_eq!(*slot.lock().unwrap(), Some(IpAddr::V6(ip)));
}
#[test]
fn unaddressed_apply_then_reapply_leaves_a_single_v6() {
let slot = Mutex::new(None);
let n: Ipv6Network = "fd07:7::a09:25/64".parse().unwrap();
assert!(take_programmed_ip6(&slot).is_none());
record_programmed_ip6(&slot, n.ip());
assert_eq!(*slot.lock().unwrap(), Some(IpAddr::V6(n.ip())));
let old = take_programmed_ip6(&slot);
record_programmed_ip6(&slot, n.ip());
assert_eq!(old, Some(IpAddr::V6(n.ip())));
assert_eq!(*slot.lock().unwrap(), Some(IpAddr::V6(n.ip())));
}
#[test]
fn v4_connected_is_the_network_not_the_host() {
let p = v4_connected(Ipv4Addr::new(10, 9, 0, 37), Ipv4Addr::new(255, 255, 255, 0)).unwrap();
assert_eq!(p.dst, IpAddr::V4(Ipv4Addr::new(10, 9, 0, 0)));
assert_eq!(p.prefix, 24);
}
#[test]
fn ip6_connected_is_the_prefix_not_a_host_route() {
let n: Ipv6Network = "fd07:7::a09:25/64".parse().unwrap();
let p = ip6_connected(n);
assert_eq!(p.dst, IpAddr::V6("fd07:7::".parse().unwrap()));
assert_eq!(p.prefix, 64);
}
#[test]
fn same_prefix_refreshes_different_prefix_replaces() {
let a = v4_connected(Ipv4Addr::new(10, 9, 0, 37), Ipv4Addr::new(255, 255, 255, 0)).unwrap();
let same =
v4_connected(Ipv4Addr::new(10, 9, 0, 38), Ipv4Addr::new(255, 255, 255, 0)).unwrap();
let other =
v4_connected(Ipv4Addr::new(10, 8, 0, 1), Ipv4Addr::new(255, 255, 255, 0)).unwrap();
assert_eq!(prefix_action(None, a), PrefixAction::Replace { old: None });
assert_eq!(prefix_action(Some(a), same), PrefixAction::Refresh);
assert_eq!(
prefix_action(Some(a), other),
PrefixAction::Replace { old: Some(a) }
);
let v6a = ip6_connected("fd07:7::a09:25/64".parse().unwrap());
let v6b = ip6_connected("fd07:7::a09:26/64".parse().unwrap());
let v6c = ip6_connected("fd08:8::1/64".parse().unwrap());
assert_eq!(prefix_action(Some(v6a), v6b), PrefixAction::Refresh);
assert_eq!(
prefix_action(Some(v6a), v6c),
PrefixAction::Replace { old: Some(v6a) }
);
}
}