use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
time::Duration,
};
#[doc(inline)]
pub use config::Config;
#[doc(inline)]
pub use error::{Error, InternalErrorKind};
pub use secrecy::SecretString;
#[doc(inline)]
pub use ts_control::ExitNodeSelector;
#[doc(inline)]
pub use ts_control::Node as NodeInfo;
#[doc(inline)]
pub use ts_control::tls::{CertifiedKey, TlsAcceptor, TlsStream};
#[doc(inline)]
pub use ts_control::{CertError, MISSING_CERT_RPC, ServeConfig, ServeState, ServeTarget};
#[doc(inline)]
pub use ts_control::{DnsConfig, DnsResolver, ExtraRecord};
#[doc(inline)]
pub use ts_control::{ExitProxyConfig, ExitProxyScheme};
pub use ts_control::{
IdTokenError, LogoutError, ServiceError, ServiceMode, SetDnsError, SetDnsInternalErrorKind,
SshAccept, SshAction, SshConnIdentity, SshDecision, SshDenyReason, SshPolicy, SshPrincipal,
SshRule, StableNodeId,
};
pub use ts_control::{TransportMode, TunConfig};
#[doc(inline)]
pub use ts_netstack_smoltcp::PingError;
use ts_netstack_smoltcp::{CreateSocket, netcore::Channel};
#[doc(inline)]
pub use ts_runtime::fallback_tcp::{
FallbackConnFuture, FallbackConnHandler, FallbackDecision, FallbackTcpHandle,
};
#[doc(inline)]
pub use ts_runtime::taildrop::WaitingFile;
#[doc(inline)]
pub use ts_runtime::{
DeviceState, FileTarget, NetcheckReport, RegionLatency, RegistrationError, Status, StatusNode,
WhoIs,
};
#[doc(inline)]
pub use url::Url;
#[cfg(feature = "axum")]
pub mod axum;
pub mod config;
mod dial;
mod error;
mod loopback;
#[cfg(feature = "ssh")]
pub mod ssh;
#[doc(inline)]
pub use dial::{ConnectedUdpSocket, DialConn};
#[doc(inline)]
pub use loopback::LoopbackHandle;
pub struct Device {
runtime: ts_runtime::Runtime,
channel: Option<Channel>,
enable_ipv6: bool,
serve: std::sync::Mutex<Option<ts_runtime::serve::ServeManager>>,
funnel: std::sync::Mutex<Option<ts_runtime::funnel::FunnelManager>>,
}
fn taildrop_err(e: ts_runtime::taildrop::TaildropError) -> Error {
use ts_runtime::taildrop::TaildropError;
match e {
TaildropError::InvalidFileName => Error::Internal(InternalErrorKind::BadRequest),
TaildropError::FileExists => Error::Internal(InternalErrorKind::AlreadyExists),
TaildropError::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
Error::Internal(InternalErrorKind::NotFound)
}
TaildropError::Io(_) => Error::Internal(InternalErrorKind::Io),
}
}
fn taildrop_send_err(e: ts_runtime::taildrop_send::TaildropSendError) -> Error {
use ts_runtime::taildrop_send::TaildropSendError;
match e {
TaildropSendError::Connect | TaildropSendError::Timeout => Error::Timeout,
TaildropSendError::InvalidName
| TaildropSendError::Forbidden
| TaildropSendError::Conflict
| TaildropSendError::UnexpectedStatus(_) => Error::Internal(InternalErrorKind::BadRequest),
TaildropSendError::Io => Error::Internal(InternalErrorKind::Io),
}
}
#[cfg(feature = "identity-federation")]
async fn resolve_auth_key(
config: &Config,
auth_key: Option<String>,
) -> Result<Option<String>, Error> {
let wif = ts_control::WifConfig {
auth_key,
client_id: config.client_id.clone(),
client_secret: config.client_secret.clone(),
id_token: config.id_token.clone(),
audience: config.audience.clone(),
tags: config.requested_tags.clone(),
};
ts_control::resolve_auth_key(&wif, &config.control_server_url)
.await
.map_err(|e| {
tracing::error!(error = %e, "resolving auth key via workload-identity federation");
Error::Internal(InternalErrorKind::BadRequest)
})
}
#[cfg(not(feature = "identity-federation"))]
async fn resolve_auth_key(
_config: &Config,
auth_key: Option<String>,
) -> Result<Option<String>, Error> {
Ok(auth_key)
}
impl Device {
pub async fn new(config: &Config, auth_key: Option<String>) -> Result<Self, Error> {
check_magic_env()?;
let auth_key = auth_key.or_else(|| config.auth_key.clone());
let auth_key = resolve_auth_key(config, auth_key).await?;
let rt =
ts_runtime::Runtime::spawn(config.into(), auth_key, (&config.key_state).into()).await?;
let channel = match rt.channel().await {
Ok(c) => Some(c),
Err(e) if e.kind == ts_runtime::ErrorKind::UnsupportedInTunMode => None,
Err(e) => return Err(e.into()),
};
Ok(Self {
runtime: rt,
channel,
enable_ipv6: config.enable_ipv6,
serve: std::sync::Mutex::new(None),
funnel: std::sync::Mutex::new(None),
})
}
pub async fn new_with_secret(
config: &Config,
auth_key: Option<SecretString>,
) -> Result<Self, Error> {
use secrecy::ExposeSecret as _;
let plain = auth_key.map(|s| s.expose_secret().to_string());
Self::new(config, plain).await
}
fn channel(&self) -> Result<&Channel, Error> {
self.channel
.as_ref()
.ok_or(Error::Internal(InternalErrorKind::UnsupportedInTunMode))
}
pub async fn ipv4_addr(&self) -> Result<Ipv4Addr, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::Ipv4)
.await
.map_err(ts_runtime::Error::from)?
.ok_or(Error::Internal(InternalErrorKind::Actor))
}
pub async fn ipv6_addr(&self) -> Result<Ipv6Addr, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::Ipv6)
.await
.map_err(ts_runtime::Error::from)?
.ok_or(Error::Internal(InternalErrorKind::Actor))
}
pub async fn tailscale_ips(&self) -> Result<(Ipv4Addr, Option<Ipv6Addr>), Error> {
let me = self.self_node().await?;
let v4 = me.tailnet_address.ipv4.addr();
let v6 = me.tailnet_address.ipv6.addr();
let v6 = (self.enable_ipv6 && !v6.is_unspecified()).then_some(v6);
Ok((v4, v6))
}
pub async fn udp_bind(&self, socket_addr: SocketAddr) -> Result<netstack::UdpSocket, Error> {
self.channel()?
.udp_bind(socket_addr)
.await
.map_err(Into::into)
}
pub async fn tcp_listen(
&self,
socket_addr: SocketAddr,
) -> Result<netstack::TcpListener, Error> {
self.channel()?
.tcp_listen(socket_addr)
.await
.map_err(Into::into)
}
pub fn register_fallback_tcp_handler<F>(&self, cb: F) -> Result<FallbackTcpHandle, Error>
where
F: Fn(SocketAddr, SocketAddr) -> FallbackDecision + Send + Sync + 'static,
{
self.runtime
.register_fallback_tcp_handler(std::sync::Arc::new(cb))
.map_err(Into::into)
}
pub async fn resolve(&self, name: &str) -> Result<Option<Ipv4Addr>, Error> {
if let Some(peer) = self.peer_by_name(name).await? {
return Ok(Some(peer.tailnet_address.ipv4.addr()));
}
let me = self.self_node().await?;
if me.matches_name(name) {
return Ok(Some(me.tailnet_address.ipv4.addr()));
}
Ok(None)
}
pub async fn connect_by_name(
&self,
name: &str,
port: u16,
) -> Result<netstack::TcpStream, Error> {
let addr = self
.resolve(name)
.await?
.ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
self.tcp_connect((addr, port).into()).await
}
async fn resolve_dial_addr(
&self,
network: dial::Network,
addr: &str,
) -> Result<SocketAddr, Error> {
let (host, port) = dial::split_host_port(addr)?;
let ip: IpAddr = if let Ok(ip) = host.parse::<IpAddr>() {
ip
} else {
self.resolve(host)
.await?
.ok_or(Error::Internal(InternalErrorKind::BadRequest))?
.into()
};
dial::check_family(network.family, ip)?;
if ip.is_ipv6() && !self.enable_ipv6 {
return Err(Error::Internal(InternalErrorKind::BadRequest));
}
Ok((ip, port).into())
}
pub async fn dial(&self, network: &str, addr: &str) -> Result<DialConn, Error> {
let net = dial::parse_network(network)?;
let remote = self.resolve_dial_addr(net, addr).await?;
match net.transport {
dial::Transport::Tcp => Ok(DialConn::Tcp(self.tcp_connect(remote).await?)),
dial::Transport::Udp => {
let local_ip: IpAddr = if remote.is_ipv6() {
self.ipv6_addr().await?.into()
} else {
self.ipv4_addr().await?.into()
};
let sock = self.udp_bind((local_ip, 0).into()).await?;
Ok(DialConn::Udp(ConnectedUdpSocket::new(sock, remote)))
}
}
}
pub async fn dial_tcp(&self, addr: &str) -> Result<netstack::TcpStream, Error> {
let remote = self
.resolve_dial_addr(
dial::Network {
transport: dial::Transport::Tcp,
family: dial::Family::Any,
},
addr,
)
.await?;
self.tcp_connect(remote).await
}
pub async fn dial_udp(&self, addr: &str) -> Result<ConnectedUdpSocket, Error> {
let remote = self
.resolve_dial_addr(
dial::Network {
transport: dial::Transport::Udp,
family: dial::Family::Any,
},
addr,
)
.await?;
let local_ip: IpAddr = if remote.is_ipv6() {
self.ipv6_addr().await?.into()
} else {
self.ipv4_addr().await?.into()
};
let sock = self.udp_bind((local_ip, 0).into()).await?;
Ok(ConnectedUdpSocket::new(sock, remote))
}
pub async fn listen_packet(
&self,
network: &str,
addr: &str,
) -> Result<netstack::UdpSocket, Error> {
let net = dial::parse_network(network)?;
if net.transport != dial::Transport::Udp {
return Err(Error::Internal(InternalErrorKind::BadRequest));
}
let (host, port) = dial::split_host_port(addr)?;
let ip: IpAddr = host
.parse()
.map_err(|_| Error::Internal(InternalErrorKind::BadRequest))?;
dial::check_family(net.family, ip)?;
if ip.is_ipv6() && !self.enable_ipv6 {
return Err(Error::Internal(InternalErrorKind::BadRequest));
}
let bind_ip: IpAddr = if ip.is_unspecified() {
if ip.is_ipv6() {
self.ipv6_addr().await?.into()
} else {
self.ipv4_addr().await?.into()
}
} else {
ip
};
self.udp_bind((bind_ip, port).into()).await
}
pub async fn tcp_connect(&self, remote: SocketAddr) -> Result<netstack::TcpStream, Error> {
let channel = self.channel()?;
let ip: IpAddr = match remote.is_ipv4() {
true => self.ipv4_addr().await?.into(),
false => self.ipv6_addr().await?.into(),
};
let ephemeral_port = rand::random_range(49152..=u16::MAX);
channel
.tcp_connect((ip, ephemeral_port).into(), remote)
.await
.map_err(Into::into)
}
pub async fn loopback(&self) -> Result<(std::net::SocketAddr, String, LoopbackHandle), Error> {
let channel = self.channel()?.clone();
let self_ipv4 = self.ipv4_addr().await?;
let control = self.runtime.control.clone();
let peer_tracker = self.runtime.peer_tracker.clone();
let resolve: loopback::Resolver = std::sync::Arc::new(move |name: String| {
let control = control.clone();
let peer_tracker = peer_tracker.clone();
Box::pin(async move {
let pt = peer_tracker
.upgrade()
.ok_or(Error::Internal(InternalErrorKind::Actor))?;
let peer = pt
.ask(ts_runtime::peer_tracker::PeerByName { name: name.clone() })
.await
.map_err(ts_runtime::Error::from)?;
if let Some(peer) = peer {
return Ok(Some(peer.tailnet_address.ipv4.addr()));
}
let me = control
.ask(ts_runtime::control_runner::SelfNode)
.await
.map_err(ts_runtime::Error::from)?
.ok_or(Error::Internal(InternalErrorKind::Actor))?;
if me.matches_name(&name) {
Ok(Some(me.tailnet_address.ipv4.addr()))
} else {
Ok(None)
}
}) as std::pin::Pin<Box<dyn std::future::Future<Output = _> + Send>>
});
let dialer = loopback::OverlayDialer::new(channel, self_ipv4, resolve);
loopback::start(dialer).await
}
pub async fn self_node(&self) -> Result<NodeInfo, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::SelfNode)
.await
.map_err(ts_runtime::Error::from)?
.ok_or(Error::Internal(InternalErrorKind::Actor))
}
pub async fn cert_domains(&self) -> Result<Vec<String>, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::CertDomains)
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn dns_config(&self) -> Result<Option<DnsConfig>, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::DnsConfig)
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn pop_browser_url(&self) -> Result<Option<Url>, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::PopBrowserUrl)
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn netcheck(&self) -> Result<NetcheckReport, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::Netcheck)
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn self_key_expiry_unix(&self) -> Result<Option<i64>, Error> {
Ok(self.self_node().await?.key_expiry_unix())
}
pub async fn self_key_expired(&self) -> Result<bool, Error> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(i64::MAX);
Ok(self.self_node().await?.key_expired_at_unix(now))
}
pub async fn ssh_policy(&self) -> Result<Option<ts_control::SshPolicy>, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::CurrentSshPolicy)
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn peer_by_name(&self, name: &str) -> Result<Option<NodeInfo>, Error> {
let pt = self
.runtime
.peer_tracker
.upgrade()
.ok_or(Error::Internal(InternalErrorKind::Actor))?;
pt.ask(ts_runtime::peer_tracker::PeerByName {
name: name.to_string(),
})
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn peer_by_tailnet_ip(&self, ip: IpAddr) -> Result<Option<NodeInfo>, Error> {
let pt = self
.runtime
.peer_tracker
.upgrade()
.ok_or(Error::Internal(InternalErrorKind::Actor))?;
pt.ask(ts_runtime::peer_tracker::PeerByTailnetIp { ip })
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn peers_with_route(&self, ip: IpAddr) -> Result<Vec<NodeInfo>, Error> {
let pt = self
.runtime
.peer_tracker
.upgrade()
.ok_or(Error::Internal(InternalErrorKind::Actor))?;
pt.ask(ts_runtime::peer_tracker::PeerByAcceptedRoute { ip })
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub fn taildrop_waiting_files(&self) -> Result<Vec<WaitingFile>, Error> {
let Some(store) = self.runtime.taildrop_store() else {
return Ok(Vec::new());
};
store
.waiting_files()
.map_err(|_| Error::Internal(InternalErrorKind::Actor))
}
pub fn taildrop_open_file(&self, name: &str) -> Result<(std::fs::File, u64), Error> {
let store = self
.runtime
.taildrop_store()
.ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
store.open_file(name).map_err(taildrop_err)
}
pub fn taildrop_delete_file(&self, name: &str) -> Result<(), Error> {
let store = self
.runtime
.taildrop_store()
.ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
store.delete_file(name).map_err(taildrop_err)
}
pub async fn send_file<R>(
&self,
peer: &NodeInfo,
name: &str,
content_length: u64,
reader: R,
) -> Result<(), Error>
where
R: tokio::io::AsyncRead + Unpin,
{
let channel = self.channel()?;
let dst = peer
.peerapi_addr()
.ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
if !ts_control::is_tailscale_ip(dst.ip()) {
return Err(Error::Internal(InternalErrorKind::BadRequest));
}
let self_ipv4 = self.ipv4_addr().await?;
ts_runtime::taildrop_send::send_file(channel, self_ipv4, dst, name, content_length, reader)
.await
.map_err(taildrop_send_err)
}
pub async fn file_targets(&self) -> Result<Vec<FileTarget>, Error> {
self.runtime.file_targets().await.map_err(Into::into)
}
pub async fn capture_pcap<W>(&self, writer: W) -> Result<(), Error>
where
W: std::io::Write + Send + 'static,
{
let sink = std::sync::Arc::new(std::sync::Mutex::new(
ts_runtime::capture::PcapSink::new(writer)
.map_err(|_| Error::Internal(InternalErrorKind::Io))?,
));
let hook: ts_runtime::CaptureHook = std::sync::Arc::new(move |path, pkt: &[u8]| {
if let Ok(mut sink) = sink.lock() {
drop(sink.log_packet(path.code(), pkt));
}
});
self.runtime.install_capture(Some(hook)).await?;
Ok(())
}
pub async fn stop_capture(&self) -> Result<(), Error> {
self.runtime.install_capture(None).await?;
Ok(())
}
pub async fn status(&self) -> Result<Status, Error> {
self.runtime.status().await.map_err(Into::into)
}
pub async fn tka_status(&self) -> Result<Option<ts_control::TkaStatus>, Error> {
self.runtime
.control
.ask(ts_runtime::control_runner::CurrentTkaStatus)
.await
.map_err(ts_runtime::Error::from)
.map_err(Into::into)
}
pub async fn fetch_id_token(&self, audience: &str) -> Result<String, ts_control::IdTokenError> {
self.runtime.fetch_id_token(audience.to_string()).await
}
pub async fn set_dns(&self, name: &str, value: &str) -> Result<(), ts_control::SetDnsError> {
self.runtime
.set_dns(name.to_string(), value.to_string())
.await
}
pub async fn logout(&self) -> Result<(), ts_control::LogoutError> {
self.runtime.logout().await
}
pub fn metrics(&self) -> String {
ts_metrics::write_prometheus()
}
pub async fn whois(&self, addr: SocketAddr) -> Result<Option<WhoIs>, Error> {
self.runtime.whois(addr).await.map_err(Into::into)
}
pub async fn set_exit_node(&self, exit_node: Option<ExitNodeSelector>) -> Result<(), Error> {
self.runtime
.set_exit_node(exit_node)
.await
.map_err(Into::into)
}
pub fn exit_node(&self) -> Option<ExitNodeSelector> {
self.runtime.exit_node()
}
pub async fn set_advertise_routes(&self, routes: Vec<ipnet::IpNet>) -> Result<(), Error> {
self.runtime
.set_advertise_routes(routes)
.await
.map_err(Into::into)
}
pub async fn rebind(&self) -> Result<(), Error> {
self.runtime.rebind().await.map_err(Into::into)
}
pub fn active_exit_node(&self) -> Option<ts_control::StableNodeId> {
self.runtime.active_exit_node()
}
pub async fn watch_netmap(
&self,
) -> Result<tokio::sync::watch::Receiver<Vec<StatusNode>>, Error> {
self.runtime.watch_netmap().await.map_err(Into::into)
}
pub fn device_state(&self) -> DeviceState {
self.runtime.device_state()
}
pub fn watch_state(&self) -> tokio::sync::watch::Receiver<DeviceState> {
self.runtime.watch_state()
}
pub async fn wait_until_running(
&self,
timeout: Option<Duration>,
) -> Result<(), RegistrationError> {
self.runtime.wait_until_running(timeout).await
}
pub async fn ping(&self, dst: IpAddr, timeout: Duration) -> Result<Duration, PingError> {
let channel = self.channel().map_err(|_| PingError::Timeout)?;
let src = self.ipv4_addr().await.map_err(|_| PingError::Timeout)?;
ts_netstack_smoltcp::ping(channel, src, dst, timeout).await
}
#[cfg(not(feature = "acme"))]
pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
ts_control::get_certificate(name).await
}
#[cfg(feature = "acme")]
pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
self.runtime.get_certificate(name.to_string()).await
}
pub async fn listen_tls(
&self,
cfg: &ts_control::ServeConfig,
) -> Result<TlsAcceptor, ts_control::CertError> {
cfg.validate()?;
let cert = self.get_certificate(&cfg.name).await?;
ts_control::tls_acceptor(cert)
}
pub fn get_serve_config(&self) -> ts_control::ServeState {
match &*self.serve.lock().unwrap_or_else(|e| e.into_inner()) {
Some(mgr) => mgr.get(),
None => ts_control::ServeState::default(),
}
}
pub async fn set_serve_config(
&self,
state: ts_control::ServeState,
) -> Result<ts_runtime::serve::ServeAcceptedReceiver, Error> {
state
.validate()
.map_err(|_| Error::Internal(InternalErrorKind::BadRequest))?;
let mut resolved = std::collections::BTreeMap::new();
for (port, target) in &state.ports {
let acceptor = if target.terminates_tls() {
let cfg = ts_control::ServeConfig {
name: state.name.clone(),
port: *port,
target: target.clone(),
};
Some(self.listen_tls(&cfg).await.map_err(|_| {
Error::Internal(InternalErrorKind::BadRequest)
})?)
} else {
None
};
resolved.insert(
*port,
ts_runtime::serve::ResolvedPort {
target: target.clone(),
acceptor,
},
);
}
let self_ipv4 = self.ipv4_addr().await?;
let channel = self.channel()?.clone();
let mut slot = self.serve.lock().unwrap_or_else(|e| e.into_inner());
let mgr =
slot.get_or_insert_with(|| ts_runtime::serve::ServeManager::new(channel, self_ipv4));
Ok(mgr.set(state, resolved))
}
pub async fn listen_funnel(
&self,
cfg: &ts_control::ServeConfig,
opts: ts_control::FunnelOptions,
) -> Result<ts_runtime::funnel::FunnelAcceptedReceiver, ts_control::FunnelError> {
let me = self
.self_node()
.await
.map_err(|_| ts_control::FunnelError::NotAllowed)?;
cfg.validate()?;
ts_control::funnel_access(&me, cfg.port)?;
let cert = self
.get_certificate(&cfg.name)
.await
.map_err(ts_control::FunnelError::Cert)?;
let acceptor = ts_control::tls_acceptor(cert).map_err(ts_control::FunnelError::Cert)?;
let _ = opts;
let (manager, sink, receiver) = ts_runtime::funnel::FunnelManager::new(acceptor);
{
let slot = self.runtime.funnel_ingress_slot();
*slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(sink);
}
self.runtime
.ingress_active_flag()
.store(true, std::sync::atomic::Ordering::Relaxed);
let old = {
let mut held = self.funnel.lock().unwrap_or_else(|e| e.into_inner());
held.replace(manager)
};
drop(old);
Ok(receiver)
}
pub async fn listen_service(
&self,
name: &str,
mode: ts_control::ServiceMode,
) -> Result<netstack::TcpListener, ts_control::ServiceError> {
let me = self
.self_node()
.await
.map_err(|e| ts_control::ServiceError::Listen(e.to_string()))?;
let listen_addr = ts_control::resolve_service_listen(&me, name, mode, self.enable_ipv6)?;
self.tcp_listen(listen_addr)
.await
.map_err(|e| ts_control::ServiceError::Listen(e.to_string()))
}
pub async fn shutdown(self, timeout: Option<Duration>) -> bool {
self.runtime.graceful_shutdown(timeout).await
}
}
pub mod netstack {
#[doc(inline)]
pub use ts_netstack_smoltcp::netcore::Error;
#[doc(inline)]
pub use ts_netstack_smoltcp::netcore::InternalErrorKind;
#[doc(inline)]
pub use ts_netstack_smoltcp::netsock::{TcpListener, TcpStream, UdpSocket};
}
pub mod geneve {
#[doc(inline)]
pub use ts_packet::geneve::{
GENEVE_FIXED_HEADER_LEN, GENEVE_PROTOCOL_DISCO, GENEVE_PROTOCOL_WIREGUARD, GeneveError,
GeneveHeader,
};
}
pub mod tka {
#[doc(inline)]
pub use ts_tka::{
AumHash, AumKind, Authority, Key, KeyKind, NodeKeySignature, SigKind, State, TkaError,
aum_hash,
};
}
pub mod keys {
#[doc(inline)]
pub use ts_keys::{
DiscoKeyPair, DiscoPrivateKey, DiscoPublicKey, MachineKeyPair, MachinePrivateKey,
MachinePublicKey, NetworkLockKeyPair, NetworkLockPrivateKey, NetworkLockPublicKey,
NodeKeyPair, NodePrivateKey, NodePublicKey, NodeState, PersistState,
};
}
const ENV_MAGIC_VAR: &str = "TS_RS_EXPERIMENT";
const ENV_MAGIC_VALUE: &str = "this_is_unstable_software";
fn check_magic_env() -> Result<(), Error> {
if std::env::var(ENV_MAGIC_VAR).as_deref() != Ok(ENV_MAGIC_VALUE) {
let warning = format!(
"
check failed: set {ENV_MAGIC_VAR}={ENV_MAGIC_VALUE} to acknowledge that tailscale-rs is early-days
experimental software containing bugs, unvalidated cryptography, and no stability or compatibility
guarantees.
"
);
eprintln!("{}", warning.trim());
return Err(Error::UnstableEnvVar);
};
Ok(())
}
#[cfg(test)]
mod tests {
use secrecy::ExposeSecret as _;
use super::*;
const SAMPLE_KEY: &str = "tskey-auth-koCgSLP5R811CNTRL-EXAMPLEEXAMPLEEXAMPLEEXAMPLE";
#[test]
fn secret_exposes_to_identical_string() {
let plain = Some(SAMPLE_KEY.to_string());
let from_secret =
Some(SecretString::from(SAMPLE_KEY)).map(|s| s.expose_secret().to_string());
assert_eq!(from_secret, plain);
let none_secret: Option<SecretString> = None;
assert_eq!(
none_secret.map(|s| s.expose_secret().to_string()),
None::<String>
);
}
#[tokio::test]
async fn new_with_secret_resolves_same_as_new() {
let config = Config::default();
let via_plain = resolve_auth_key(&config, Some(SAMPLE_KEY.to_string()))
.await
.expect("plain auth key resolves");
let exposed = Some(SecretString::from(SAMPLE_KEY)).map(|s| s.expose_secret().to_string());
let via_secret = resolve_auth_key(&config, exposed)
.await
.expect("secret-derived auth key resolves");
assert_eq!(via_plain, via_secret);
#[cfg(not(feature = "identity-federation"))]
assert_eq!(via_secret, Some(SAMPLE_KEY.to_string()));
}
}