mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Previously, traffic was only classified as private if the source IP was in a known interface subnet. This prevented private access from VPNs on different VLANs. Now all RFC 1918 IPv4 and ULA/link-local IPv6 addresses are treated as private, and DNS resolution for private domains works for these sources by returning IPs from all interfaces.
288 lines
8.6 KiB
Rust
288 lines
8.6 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
|
|
use std::path::Path;
|
|
|
|
use async_stream::try_stream;
|
|
use color_eyre::eyre::eyre;
|
|
use futures::stream::BoxStream;
|
|
use futures::{StreamExt, TryStreamExt};
|
|
use imbl_value::InternedString;
|
|
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
|
|
use nix::net::if_::if_nametoindex;
|
|
use tokio::net::{TcpListener, TcpStream};
|
|
use tokio::process::Command;
|
|
|
|
use crate::GatewayId;
|
|
use crate::db::model::public::{IpInfo, NetworkInterfaceType};
|
|
use crate::prelude::*;
|
|
use crate::util::Invoke;
|
|
|
|
pub async fn load_ip_info() -> Result<BTreeMap<GatewayId, IpInfo>, Error> {
|
|
let output = String::from_utf8(
|
|
Command::new("ip")
|
|
.arg("-o")
|
|
.arg("addr")
|
|
.arg("show")
|
|
.invoke(crate::ErrorKind::Network)
|
|
.await?,
|
|
)?;
|
|
|
|
let err_fn = || {
|
|
Error::new(
|
|
eyre!("malformed output from `ip`"),
|
|
crate::ErrorKind::Network,
|
|
)
|
|
};
|
|
|
|
let mut res = BTreeMap::<GatewayId, IpInfo>::new();
|
|
|
|
for line in output.lines() {
|
|
let split = line.split_ascii_whitespace().collect::<Vec<_>>();
|
|
let iface = GatewayId::from(InternedString::from(*split.get(1).ok_or_else(&err_fn)?));
|
|
let subnet: IpNet = split.get(3).ok_or_else(&err_fn)?.parse()?;
|
|
let ip_info = res.entry(iface.clone()).or_default();
|
|
ip_info.name = iface.into();
|
|
ip_info.scope_id = split
|
|
.get(0)
|
|
.ok_or_else(&err_fn)?
|
|
.strip_suffix(":")
|
|
.ok_or_else(&err_fn)?
|
|
.parse()?;
|
|
ip_info.subnets.insert(subnet);
|
|
}
|
|
|
|
for (id, ip_info) in res.iter_mut() {
|
|
ip_info.device_type = probe_iface_type(id.as_str()).await;
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
pub fn ipv6_is_link_local(addr: Ipv6Addr) -> bool {
|
|
(addr.segments()[0] & 0xffc0) == 0xfe80
|
|
}
|
|
|
|
pub fn ipv6_is_local(addr: Ipv6Addr) -> bool {
|
|
addr.is_loopback() || (addr.segments()[0] & 0xfe00) == 0xfc00 || ipv6_is_link_local(addr)
|
|
}
|
|
|
|
pub fn is_private_ip(addr: IpAddr) -> bool {
|
|
match addr {
|
|
IpAddr::V4(v4) => v4.is_private() || v4.is_loopback() || v4.is_link_local(),
|
|
IpAddr::V6(v6) => ipv6_is_local(v6),
|
|
}
|
|
}
|
|
|
|
fn parse_iface_ip(output: &str) -> Result<Vec<&str>, Error> {
|
|
let output = output.trim();
|
|
if output.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
let mut res = Vec::new();
|
|
for line in output.lines() {
|
|
if let Some(ip) = line.split_ascii_whitespace().nth(3) {
|
|
res.push(ip)
|
|
} else {
|
|
return Err(Error::new(
|
|
eyre!("malformed output from `ip`"),
|
|
crate::ErrorKind::Network,
|
|
));
|
|
}
|
|
}
|
|
Ok(res)
|
|
}
|
|
|
|
pub async fn get_iface_ipv4_addr(iface: &str) -> Result<Option<(Ipv4Addr, Ipv4Net)>, Error> {
|
|
Ok(parse_iface_ip(&String::from_utf8(
|
|
Command::new("ip")
|
|
.arg("-4")
|
|
.arg("-o")
|
|
.arg("addr")
|
|
.arg("show")
|
|
.arg(iface)
|
|
.invoke(crate::ErrorKind::Network)
|
|
.await?,
|
|
)?)?
|
|
.into_iter()
|
|
.map(|s| Ok::<_, Error>((s.split("/").next().unwrap().parse()?, s.parse()?)))
|
|
.next()
|
|
.transpose()?)
|
|
}
|
|
|
|
pub async fn get_iface_ipv6_addr(iface: &str) -> Result<Option<(Ipv6Addr, Ipv6Net)>, Error> {
|
|
Ok(parse_iface_ip(&String::from_utf8(
|
|
Command::new("ip")
|
|
.arg("-6")
|
|
.arg("-o")
|
|
.arg("addr")
|
|
.arg("show")
|
|
.arg(iface)
|
|
.invoke(crate::ErrorKind::Network)
|
|
.await?,
|
|
)?)?
|
|
.into_iter()
|
|
.find(|ip| !ip.starts_with("fe80::"))
|
|
.map(|s| Ok::<_, Error>((s.split("/").next().unwrap().parse()?, s.parse()?)))
|
|
.transpose()?)
|
|
}
|
|
|
|
pub async fn probe_iface_type(iface: &str) -> Option<NetworkInterfaceType> {
|
|
match tokio::fs::read_to_string(Path::new("/sys/class/net").join(iface).join("uevent"))
|
|
.await
|
|
.ok()?
|
|
.lines()
|
|
.find_map(|l| l.strip_prefix("DEVTYPE="))
|
|
{
|
|
Some("wlan") => Some(NetworkInterfaceType::Wireless),
|
|
Some("bridge") => Some(NetworkInterfaceType::Bridge),
|
|
Some("wireguard") => Some(NetworkInterfaceType::Wireguard),
|
|
None if iface_is_physical(iface).await => Some(NetworkInterfaceType::Ethernet),
|
|
None if iface_is_loopback(iface).await => Some(NetworkInterfaceType::Loopback),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub async fn iface_is_physical(iface: &str) -> bool {
|
|
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("device"))
|
|
.await
|
|
.is_ok()
|
|
}
|
|
|
|
pub async fn iface_is_wireless(iface: &str) -> bool {
|
|
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("wireless"))
|
|
.await
|
|
.is_ok()
|
|
}
|
|
|
|
pub async fn iface_is_bridge(iface: &str) -> bool {
|
|
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("bridge"))
|
|
.await
|
|
.is_ok()
|
|
}
|
|
|
|
pub async fn iface_is_loopback(iface: &str) -> bool {
|
|
tokio::fs::read_to_string(Path::new("/sys/class/net").join(iface).join("type"))
|
|
.await
|
|
.ok()
|
|
.map_or(false, |x| x.trim() == "772")
|
|
}
|
|
|
|
pub fn list_interfaces() -> BoxStream<'static, Result<String, Error>> {
|
|
try_stream! {
|
|
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;
|
|
while let Some(iface) = ifaces.next_entry().await? {
|
|
if let Some(iface) = iface.file_name().into_string().ok() {
|
|
yield iface;
|
|
}
|
|
}
|
|
}
|
|
.boxed()
|
|
}
|
|
|
|
pub async fn find_wifi_iface() -> Result<Option<String>, Error> {
|
|
let mut ifaces = list_interfaces();
|
|
while let Some(iface) = ifaces.try_next().await? {
|
|
if iface_is_wireless(&iface).await {
|
|
return Ok(Some(iface));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn find_eth_iface() -> Result<String, Error> {
|
|
let mut ifaces = list_interfaces();
|
|
while let Some(iface) = ifaces.try_next().await? {
|
|
if iface_is_physical(&iface).await && !iface_is_wireless(&iface).await {
|
|
return Ok(iface);
|
|
}
|
|
}
|
|
Err(Error::new(
|
|
eyre!("Could not detect ethernet interface"),
|
|
crate::ErrorKind::Network,
|
|
))
|
|
}
|
|
|
|
pub async fn all_socket_addrs_for(port: u16) -> Result<Vec<(InternedString, SocketAddr)>, Error> {
|
|
let mut res = Vec::new();
|
|
|
|
let raw = String::from_utf8(
|
|
Command::new("ip")
|
|
.arg("-o")
|
|
.arg("addr")
|
|
.arg("show")
|
|
.invoke(ErrorKind::ParseSysInfo)
|
|
.await?,
|
|
)?;
|
|
let err = |item: &str, lineno: usize, line: &str| {
|
|
Error::new(
|
|
eyre!("failed to parse ip info ({item}[line:{lineno}]) from {line:?}"),
|
|
ErrorKind::ParseSysInfo,
|
|
)
|
|
};
|
|
for (idx, line) in raw
|
|
.lines()
|
|
.map(|l| l.trim())
|
|
.enumerate()
|
|
.filter(|(_, l)| !l.is_empty())
|
|
{
|
|
let mut split = line.split_whitespace();
|
|
let _num = split.next();
|
|
let ifname = split.next().ok_or_else(|| err("ifname", idx, line))?;
|
|
let _kind = split.next();
|
|
let ipnet_str = split.next().ok_or_else(|| err("ipnet", idx, line))?;
|
|
let ipnet = ipnet_str
|
|
.parse::<IpNet>()
|
|
.with_ctx(|_| (ErrorKind::ParseSysInfo, err("ipnet", idx, ipnet_str)))?;
|
|
match ipnet.addr() {
|
|
IpAddr::V4(ip4) => res.push((ifname.into(), SocketAddr::new(ip4.into(), port))),
|
|
IpAddr::V6(ip6) => res.push((
|
|
ifname.into(),
|
|
SocketAddr::V6(SocketAddrV6::new(
|
|
ip6,
|
|
port,
|
|
0,
|
|
if_nametoindex(ifname)
|
|
.with_ctx(|_| (ErrorKind::ParseSysInfo, "reading scope_id"))?,
|
|
)),
|
|
)),
|
|
}
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
pub struct TcpListeners {
|
|
listeners: Vec<TcpListener>,
|
|
}
|
|
impl TcpListeners {
|
|
pub fn new(listeners: impl IntoIterator<Item = TcpListener>) -> Self {
|
|
Self {
|
|
listeners: listeners.into_iter().collect(),
|
|
}
|
|
}
|
|
|
|
pub async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
|
|
futures::future::select_all(self.listeners.iter().map(|l| Box::pin(l.accept())))
|
|
.await
|
|
.0
|
|
}
|
|
}
|
|
// impl hyper::server::accept::Accept for TcpListeners {
|
|
// type Conn = TcpStream;
|
|
// type Error = std::io::Error;
|
|
|
|
// fn poll_accept(
|
|
// self: std::pin::Pin<&mut Self>,
|
|
// cx: &mut std::task::Context<'_>,
|
|
// ) -> std::task::Poll<Option<Result<Self::Conn, Self::Error>>> {
|
|
// for listener in self.listeners.iter() {
|
|
// let poll = listener.poll_accept(cx);
|
|
// if poll.is_ready() {
|
|
// return poll.map(|a| a.map(|a| a.0)).map(Some);
|
|
// }
|
|
// }
|
|
// std::task::Poll::Pending
|
|
// }
|
|
// }
|
|
// TODO
|