use std::net::{Ipv4Addr, Ipv6Addr};
use anyhow::{Context, Result, bail};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tun::{Configuration, DeviceReader, DeviceWriter};
const TUN_MTU: u16 = 1280;
pub struct TunReader {
reader: DeviceReader,
}
pub struct TunWriter {
writer: DeviceWriter,
}
fn is_cgnat(ip: Ipv4Addr) -> bool {
let octets = ip.octets();
octets[0] == 100 && (octets[1] & 0xC0) == 64
}
pub fn check_cgnat_conflict() -> Result<()> {
let output = std::process::Command::new("ifconfig").output();
let output = match output {
Ok(o) => o,
Err(_) => return Ok(()),
};
let stdout = String::from_utf8_lossy(&output.stdout);
let mut current_iface = String::new();
for line in stdout.lines() {
if !line.starts_with('\t')
&& !line.starts_with(' ')
&& let Some(name) = line.split(':').next()
{
current_iface = name.to_string();
}
if line.contains("inet ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(pos) = parts.iter().position(|&p| p == "inet")
&& let Some(ip_str) = parts.get(pos + 1)
&& let Ok(ip) = ip_str.parse::<Ipv4Addr>()
&& is_cgnat(ip)
{
bail!(
"interface {} already has CGNAT address {} — another VPN \
(e.g. Tailscale) is using the 100.64.0.0/10 range. \
Disable it before starting rayfish.",
current_iface,
ip
);
}
}
}
Ok(())
}
pub async fn create(v4: Ipv4Addr, v6: Ipv6Addr) -> Result<(TunReader, TunWriter, String)> {
let gateway = Ipv4Addr::new(100, 64, 0, 1);
let mut config = Configuration::default();
config
.address(v4)
.destination(gateway)
.netmask((255, 192, 0, 0)) .mtu(TUN_MTU)
.up();
#[cfg(target_os = "linux")]
config.platform_config(|p| {
p.ensure_root_privileges(true);
});
let device = tun::create_as_async(&config)?;
let tun_name = device
.as_ref()
.tun_name()
.unwrap_or_else(|_| "unknown".to_string());
tracing::info!(addr = %v4, ipv6 = %v6, tun = %tun_name, "TUN device created");
if let Err(e) = configure_ipv6(&tun_name, v6).await {
tracing::warn!(error = %e, "failed to configure IPv6 on TUN (IPv6 routing will not work)");
}
let (writer, reader) = device.split()?;
Ok((TunReader { reader }, TunWriter { writer }, tun_name))
}
#[cfg(target_os = "linux")]
async fn configure_ipv6(tun_name: &str, addr: Ipv6Addr) -> Result<()> {
use futures::TryStreamExt;
use std::net::IpAddr;
let (connection, handle, _) = rtnetlink::new_connection().context("open netlink socket")?;
let conn = tokio::spawn(connection);
let result = async {
let index = handle
.link()
.get()
.match_name(tun_name.to_owned())
.execute()
.try_next()
.await
.context("query TUN link")?
.with_context(|| format!("TUN link {tun_name} not found"))?
.header
.index;
handle
.address()
.add(index, IpAddr::V6(addr), 128)
.replace()
.execute()
.await
.context("add IPv6 address via netlink")?;
Ok(())
}
.await;
conn.abort();
result
}
#[cfg(target_os = "macos")]
async fn configure_ipv6(tun_name: &str, addr: Ipv6Addr) -> Result<()> {
let status = std::process::Command::new("ifconfig")
.args([tun_name, "inet6", &addr.to_string(), "prefixlen", "128"])
.status()
.context("run ifconfig")?;
anyhow::ensure!(status.success(), "ifconfig inet6 failed with {status}");
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn route_peer_range(tun_name: &str) -> Result<()> {
use futures::TryStreamExt;
use rtnetlink::RouteMessageBuilder;
let (connection, handle, _) = rtnetlink::new_connection().context("open netlink socket")?;
let conn = tokio::spawn(connection);
let result = async {
let index = handle
.link()
.get()
.match_name(tun_name.to_owned())
.execute()
.try_next()
.await
.context("query TUN link")?
.with_context(|| format!("TUN link {tun_name} not found"))?
.header
.index;
let route = RouteMessageBuilder::<Ipv6Addr>::new()
.destination_prefix(Ipv6Addr::new(0x0200, 0, 0, 0, 0, 0, 0, 0), 7)
.output_interface(index)
.build();
handle
.route()
.add(route)
.replace()
.execute()
.await
.context("add 200::/7 route via netlink")?;
Ok(())
}
.await;
conn.abort();
result
}
#[cfg(target_os = "macos")]
pub async fn route_peer_range(tun_name: &str) -> Result<()> {
for (family, net) in [("-inet", "100.64.0.0/10"), ("-inet6", "200::/7")] {
let _ = std::process::Command::new("route")
.args(["-n", "delete", family, "-net", net, "-interface", tun_name])
.status();
let status = std::process::Command::new("route")
.args(["-n", "add", family, "-net", net, "-interface", tun_name])
.status()
.with_context(|| format!("run route add {family} {net}"))?;
anyhow::ensure!(status.success(), "route add {family} {net} failed with {status}");
}
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn route_magic_dns(tun_name: &str) -> Result<()> {
use futures::TryStreamExt;
use rtnetlink::RouteMessageBuilder;
let (connection, handle, _) = rtnetlink::new_connection().context("open netlink socket")?;
let conn = tokio::spawn(connection);
let result = async {
let index = handle
.link()
.get()
.match_name(tun_name.to_owned())
.execute()
.try_next()
.await
.context("query TUN link")?
.with_context(|| format!("TUN link {tun_name} not found"))?
.header
.index;
let route = RouteMessageBuilder::<Ipv4Addr>::new()
.destination_prefix(crate::dns::MAGIC_DNS_V4, 32)
.output_interface(index)
.build();
handle
.route()
.add(route)
.replace()
.execute()
.await
.context("add magic-DNS /32 route via netlink")?;
Ok(())
}
.await;
conn.abort();
result
}
#[cfg(target_os = "macos")]
pub async fn route_magic_dns(tun_name: &str) -> Result<()> {
let ip = crate::dns::MAGIC_DNS_V4.to_string();
let _ = std::process::Command::new("route")
.args(["-n", "delete", "-inet", "-host", &ip, "-interface", tun_name])
.status();
let status = std::process::Command::new("route")
.args(["-n", "add", "-inet", "-host", &ip, "-interface", tun_name])
.status()
.context("run route add magic dns")?;
anyhow::ensure!(status.success(), "route add magic dns failed with {status}");
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub async fn route_magic_dns(_tun_name: &str) -> Result<()> {
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn route_self_loopback(v4: Ipv4Addr, v6: Ipv6Addr) -> Result<()> {
for (family, addr) in [("-inet", v4.to_string()), ("-inet6", v6.to_string())] {
let _ = std::process::Command::new("route")
.args(["-n", "delete", family, "-host", &addr, "-interface", "lo0"])
.status();
let status = std::process::Command::new("route")
.args(["-n", "add", family, "-host", &addr, "-interface", "lo0"])
.status()
.context("run route add (loopback self-route)")?;
anyhow::ensure!(
status.success(),
"route add {family} -host {addr} via lo0 failed with {status}"
);
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub async fn route_self_loopback(_v4: Ipv4Addr, _v6: Ipv6Addr) -> Result<()> {
Ok(())
}
pub fn set_link_up(tun_name: &str) -> Result<()> {
set_link_state(tun_name, true)
}
pub fn set_link_down(tun_name: &str) -> Result<()> {
set_link_state(tun_name, false)
}
fn set_link_state(tun_name: &str, up: bool) -> Result<()> {
#[cfg(target_os = "macos")]
{
let state = if up { "up" } else { "down" };
let status = std::process::Command::new("ifconfig")
.args([tun_name, state])
.status()
.context("run ifconfig")?;
anyhow::ensure!(status.success(), "ifconfig {state} failed with {status}");
}
#[cfg(target_os = "linux")]
{
let state = if up { "up" } else { "down" };
let status = std::process::Command::new("ip")
.args(["link", "set", tun_name, state])
.status()
.context("run ip link set")?;
anyhow::ensure!(status.success(), "ip link set {state} failed with {status}");
}
Ok(())
}
impl TunReader {
pub async fn read_into(&mut self, buf: &mut bytes::BytesMut) -> Result<usize> {
let n = self.reader.read_buf(buf).await?;
Ok(n)
}
}
impl TunWriter {
pub async fn write_packet(&mut self, packet: &[u8]) -> Result<()> {
self.writer.write_all(packet).await?;
Ok(())
}
}