diff --git a/tailscale-localapi/Cargo.toml b/tailscale-localapi/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ebeb974f3d7d53b86d321d92f24a1877b46e4de4 --- /dev/null +++ b/tailscale-localapi/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tailscale-localapi" +description = "Client for the Tailscale local API" +version = "0.3.0" +authors = ["John Downey"] +edition = "2021" +license = "MIT" +documentation = "https://docs.rs/tailscale-localapi" +homepage = "https://github.com/jtdowney/tailscale-localapi" + +[dependencies] +async-trait = "0.1.73" +base64 = "0.21.2" +chrono = { version = "0.4.19", features = ["serde"] } +http = "0.2.6" +hyper = { version = "0.14.18", features = ["client", "http1"] } +rustls-pemfile = "1" +serde = { version = "1", features = ["derive"] } +serde-aux = "4" +serde_json = "1" +thiserror = "1" +tokio = { version = "1", features = ["net", "rt"] } +cidr = {version = "0.2.2", features = ["serde"]} + +[dev-dependencies] +libc = "0.2.147" diff --git a/tailscale-localapi/LICENSE b/tailscale-localapi/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ad4a20d473e39187146d5e821f1de06a0b71372f --- /dev/null +++ b/tailscale-localapi/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 John Downey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tailscale-localapi/README.md b/tailscale-localapi/README.md new file mode 100644 index 0000000000000000000000000000000000000000..78f72322422b07fcf1d067c41dcc633c2a1e0b1f --- /dev/null +++ b/tailscale-localapi/README.md @@ -0,0 +1,19 @@ +# tailscale-localapi + +This is a rust crate designed to interact with the [Tailscale](https://tailscale.com) local API. On Linux and other Unix-like systems, this is through a unix socket. On macOS and Windows, this is through a local TCP port and a password. The Tailscale localapi is large but so far this crate does: + +1. Get the status of the node and the tailnet (similar to `tailscale status`) +2. Get a certificate and key for the node (similar to `tailscale cert`) +3. Get whois information for a given IP address in the tailnet + +## Limitations + +This crate uses hyper and requires tokio and async rust. + +## Example + +```rust +let socket_path = "/var/run/tailscale/tailscaled.sock"; +let client = tailscale_localapi::LocalApi::new_with_socket_path(socket_path); +dbg!(client.status().await.unwrap()); +``` diff --git a/tailscale-localapi/src/adapters/mod.rs b/tailscale-localapi/src/adapters/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..31c4e41f94356e4ff77e4e2437bc8b6f6dac1d9e --- /dev/null +++ b/tailscale-localapi/src/adapters/mod.rs @@ -0,0 +1,15 @@ +use async_trait::async_trait; +use http::{Response, Uri}; +use hyper::Body; + +use crate::error::Error; + +pub mod tcp; +pub mod unix; + +/// Abstract trait for the tailscale API client +#[async_trait] +pub trait LocalApiAdapter: Clone { + async fn get(&self, uri: Uri) -> Result<Response<Body>, Error>; + async fn patch(&self, uri: Uri, body: Body) -> Result<Response<Body>, Error>; +} diff --git a/tailscale-localapi/src/adapters/tcp.rs b/tailscale-localapi/src/adapters/tcp.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9da945ced3f40d23c1ce999c2fefeda70ae35d5 --- /dev/null +++ b/tailscale-localapi/src/adapters/tcp.rs @@ -0,0 +1,89 @@ +use std::net::Ipv4Addr; + +use async_trait::async_trait; +use base64::Engine; +use http::{ + header::{AUTHORIZATION, HOST}, + Request, Response, Uri, +}; +use hyper::Body; +use tokio::net::TcpSocket; + +use crate::error::Error; + +use super::LocalApiAdapter; + +/// Client that connects to the local tailscaled over TCP with a password. This +/// is used on Windows and macOS when sandboxing is enabled. +#[derive(Clone)] +pub struct TcpWithPasswordAdapter { + port: u16, + password: String, +} + +impl TcpWithPasswordAdapter { + pub fn new<S: Into<String>>(port: u16, password: S) -> Self { + TcpWithPasswordAdapter { + port, + password: password.into(), + } + } + + async fn request(&self, request: Request<Body>) -> Result<Response<Body>, Error> { + let stream = TcpSocket::new_v4()? + .connect((Ipv4Addr::LOCALHOST, self.port).into()) + .await?; + let (mut request_sender, connection) = hyper::client::conn::handshake(stream).await?; + + tokio::spawn(async move { + if let Err(e) = connection.await { + eprintln!("Error in connection: {}", e); + } + }); + + let response = request_sender.send_request(request).await?; + if response.status() == 200 { + Ok(response) + } else { + Err(Error::UnprocessableEntity) + } + } + + fn get_request_builder(&self) -> http::request::Builder { + Request::builder() + .header(HOST, "local-tailscaled.sock") + .header( + AUTHORIZATION, + format!( + "Basic {}", + base64::engine::general_purpose::STANDARD_NO_PAD + .encode(format!(":{}", self.password)) + ), + ) + } +} + +#[async_trait] +impl LocalApiAdapter for TcpWithPasswordAdapter { + async fn get(&self, uri: Uri) -> Result<Response<Body>, Error> { + let request = self + .get_request_builder() + .method("GET") + .uri(uri) + .body(Body::empty())?; + + let response = self.request(request).await?; + Ok(response) + } + + async fn patch(&self, uri: Uri, body: Body) -> Result<Response<Body>, Error> { + let request = self + .get_request_builder() + .method("PATCH") + .uri(uri) + .body(body)?; + + let response = self.request(request).await?; + Ok(response) + } +} diff --git a/tailscale-localapi/src/adapters/unix.rs b/tailscale-localapi/src/adapters/unix.rs new file mode 100644 index 0000000000000000000000000000000000000000..8578aad2fd321b4566c780576de0ff4430ee6e6e --- /dev/null +++ b/tailscale-localapi/src/adapters/unix.rs @@ -0,0 +1,72 @@ +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; +use http::{header::HOST, Request, Response, Uri}; +use hyper::Body; +use tokio::net::UnixStream; + +use crate::error::Error; + +use super::LocalApiAdapter; + +/// Client that connects to the local tailscaled over a unix socket. This is +/// used on Linux and other Unix-like systems. +#[derive(Clone)] +pub struct UnixStreamAdapter { + socket_path: PathBuf, +} + +impl UnixStreamAdapter { + pub fn new<P: AsRef<Path>>(socket_path: P) -> Self { + let socket_path = socket_path.as_ref().to_path_buf(); + + UnixStreamAdapter { socket_path } + } + + async fn request(&self, request: Request<Body>) -> Result<Response<Body>, Error> { + let stream = UnixStream::connect(&self.socket_path).await?; + let (mut request_sender, connection) = hyper::client::conn::handshake(stream).await?; + + tokio::spawn(async move { + if let Err(e) = connection.await { + eprintln!("Error in connection: {}", e); + } + }); + + let response = request_sender.send_request(request).await?; + if response.status() == 200 { + Ok(response) + } else { + Err(Error::UnprocessableEntity) + } + } + + fn get_request_builder(&self) -> http::request::Builder { + Request::builder().header(HOST, "local-tailscaled.sock") + } +} + +#[async_trait] +impl LocalApiAdapter for UnixStreamAdapter { + async fn get(&self, uri: Uri) -> Result<Response<Body>, Error> { + let request = self + .get_request_builder() + .method("GET") + .uri(uri) + .body(Body::empty())?; + + let response = self.request(request).await?; + Ok(response) + } + + async fn patch(&self, uri: Uri, body: Body) -> Result<Response<Body>, Error> { + let request = self + .get_request_builder() + .method("PATCH") + .uri(uri) + .body(body)?; + + let response = self.request(request).await?; + Ok(response) + } +} diff --git a/tailscale-localapi/src/error.rs b/tailscale-localapi/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..6e2f6eed607b031bd22a683aa61acd8bd392e1aa --- /dev/null +++ b/tailscale-localapi/src/error.rs @@ -0,0 +1,17 @@ + +/// Error type for this crate +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("connection failed")] + IoError(#[from] std::io::Error), + #[error("request failed")] + HyperError(#[from] hyper::Error), + #[error("http error")] + HttpError(#[from] hyper::http::Error), + #[error("unprocessible entity")] + UnprocessableEntity, + #[error("unable to parse json")] + ParsingError(#[from] serde_json::Error), + #[error("unable to parse certificate or key")] + UnknownCertificateOrKey, +} diff --git a/tailscale-localapi/src/lib.rs b/tailscale-localapi/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..0d23a4c7aa87c8abe105a127b8973ecb33ef9553 --- /dev/null +++ b/tailscale-localapi/src/lib.rs @@ -0,0 +1,149 @@ +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] + +use std::{io::Read, net::SocketAddr, path::Path}; + +use adapters::{tcp::TcpWithPasswordAdapter, unix::UnixStreamAdapter, LocalApiAdapter}; +use error::Error; +use http::Uri; +use hyper::{body::Buf, Body}; +pub use types::*; + +pub mod adapters; +/// Definitions of types used in the tailscale API +pub mod error; +pub mod types; + +/// Client for the local tailscaled socket +#[derive(Clone)] +pub struct LocalApi<T: LocalApiAdapter> { + /// Path to the tailscaled socket + client: T, +} + +impl LocalApi<UnixStreamAdapter> { + /// Create a new client for the local tailscaled from the path to the + /// socket. + pub fn new_with_socket_path<P: AsRef<Path>>(socket_path: P) -> Self { + Self { + client: UnixStreamAdapter::new(socket_path), + } + } +} + +impl LocalApi<TcpWithPasswordAdapter> { + /// Create a new client for the local tailscaled from the TCP port and + /// password. + pub fn new_with_port_and_password<S: Into<String>>(port: u16, password: S) -> Self { + Self { + client: TcpWithPasswordAdapter::new(port, password), + } + } +} + +impl<T: LocalApiAdapter> LocalApi<T> { + /// Get the certificate and key for a domain. The domain should be one of + /// the valid domains for the local node. + pub async fn certificate_pair( + &self, + domain: &str, + ) -> Result<(PrivateKey, Vec<Certificate>), Error> { + let response = self + .client + .get( + format!("/localapi/v0/cert/{domain}?type=pair") + .parse() + .unwrap(), + ) + .await?; + + let body = hyper::body::aggregate(response.into_body()).await?; + let items = rustls_pemfile::read_all(&mut body.reader())?; + let (certificates, mut private_keys) = items + .into_iter() + .map(|item| match item { + rustls_pemfile::Item::ECKey(data) + | rustls_pemfile::Item::PKCS8Key(data) + | rustls_pemfile::Item::RSAKey(data) => Ok((false, data)), + rustls_pemfile::Item::X509Certificate(data) => Ok((true, data)), + _ => Err(Error::UnknownCertificateOrKey), + }) + .collect::<Result<Vec<_>, Error>>()? + .into_iter() + .partition::<Vec<(bool, Vec<u8>)>, _>(|&(cert, _)| cert); + + let certificates = certificates + .into_iter() + .map(|(_, data)| Certificate(data)) + .collect(); + let (_, private_key_data) = private_keys.pop().ok_or(Error::UnknownCertificateOrKey)?; + let private_key = PrivateKey(private_key_data); + + Ok((private_key, certificates)) + } + + /// Get the status of the local node. + pub async fn status(&self) -> Result<Status, Error> { + let response = self + .client + .get(Uri::from_static("/localapi/v0/status")) + .await?; + let body = hyper::body::aggregate(response.into_body()).await?; + let status = serde_json::de::from_reader(body.reader())?; + + Ok(status) + } + + pub async fn get_prefs(&self) -> Result<Prefs, Error> { + let response = self + .client + .get(Uri::from_static("/localapi/v0/prefs")) + .await?; + let body = hyper::body::aggregate(response.into_body()).await?; + + let mut temp = String::new(); + body.reader().read_to_string(&mut temp)?; + println!("{}", temp); + + let response = self + .client + .get(Uri::from_static("/localapi/v0/prefs")) + .await?; + let body = hyper::body::aggregate(response.into_body()).await?; + + let prefs = serde_json::de::from_reader(body.reader())?; + + Ok(prefs) + } + + pub async fn edit_prefs(&self, prefs: &MaskedPrefs) -> Result<Prefs, Error> { + let response = self + .client + .patch( + Uri::from_static("/localapi/v0/prefs"), + Body::from(serde_json::ser::to_string(prefs)?), + ) + .await?; + let body = hyper::body::aggregate(response.into_body()).await?; + + let prefs = serde_json::de::from_reader(body.reader())?; + + Ok(prefs) + } + + /// Request whois information for an address in the tailnet. + pub async fn whois(&self, address: SocketAddr) -> Result<Whois, Error> { + let response = self + .client + .get( + format!("/localapi/v0/whois?addr={address}") + .parse() + .unwrap(), + ) + .await?; + let body = hyper::body::aggregate(response.into_body()).await?; + let whois = serde_json::de::from_reader(body.reader())?; + + Ok(whois) + } +} diff --git a/tailscale-localapi/src/types.rs b/tailscale-localapi/src/types.rs new file mode 100644 index 0000000000000000000000000000000000000000..38e425d47557344f03048aa15058a15160cb7fee --- /dev/null +++ b/tailscale-localapi/src/types.rs @@ -0,0 +1,359 @@ +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, +}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde::de::{Error}; +use serde_aux::prelude::*; + +/// State of the backend +#[derive(Deserialize, Debug)] +#[non_exhaustive] +pub enum BackendState { + NoState, + NeedsLogin, + NeedsMachineAuth, + Stopped, + Starting, + Running, +} + +/// Status of a peer +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct PeerStatus { + #[serde(rename = "ID")] + pub id: String, + pub public_key: String, + #[serde(rename = "HostName")] + pub hostname: String, + #[serde(rename = "DNSName")] + pub dnsname: String, + #[serde(rename = "OS")] + pub os: String, + #[serde(rename = "UserID")] + pub user_id: i64, + #[serde( + rename = "TailscaleIPs", + deserialize_with = "deserialize_default_from_null" + )] + pub tailscale_ips: Vec<IpAddr>, + #[serde(default, deserialize_with = "deserialize_default_from_null")] + pub tags: Vec<String>, + #[serde(default, deserialize_with = "deserialize_default_from_null")] + pub primary_routes: Vec<String>, + #[serde(deserialize_with = "deserialize_default_from_null")] + pub addrs: Vec<String>, + pub cur_addr: String, + pub relay: String, + pub rx_bytes: i64, + pub tx_bytes: i64, + pub created: DateTime<Utc>, + pub last_write: DateTime<Utc>, + pub last_seen: DateTime<Utc>, + pub last_handshake: DateTime<Utc>, + pub online: bool, + #[serde(default)] + pub keep_alive: bool, + pub exit_node: bool, + pub exit_node_option: bool, + pub active: bool, + #[serde( + rename = "PeerAPIURL", + deserialize_with = "deserialize_default_from_null" + )] + pub peer_api_url: Vec<String>, + #[serde(default, deserialize_with = "deserialize_default_from_null")] + pub capabilities: Vec<String>, + #[serde( + default, + rename = "sshHostKeys", + deserialize_with = "deserialize_default_from_null" + )] + pub ssh_hostkeys: Vec<String>, + #[serde(default)] + pub sharee_node: bool, + pub in_network_map: bool, + pub in_magic_sock: bool, + pub in_engine: bool, +} + +/// Status of the current tailnet. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct TailnetStatus { + pub name: String, + #[serde(rename = "MagicDNSSuffix")] + pub magic_dns_suffix: String, + #[serde(rename = "MagicDNSEnabled")] + pub magic_dns_enabled: bool, +} + +/// Status of the local tailscaled. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct Status { + pub version: String, + pub backend_state: BackendState, + #[serde(rename = "AuthURL")] + pub auth_url: String, + #[serde(rename = "TailscaleIPs")] + pub tailscale_ips: Vec<IpAddr>, + #[serde(rename = "Self")] + pub self_status: PeerStatus, + #[serde(deserialize_with = "deserialize_default_from_null")] + pub health: Vec<String>, + pub current_tailnet: Option<TailnetStatus>, + #[serde(deserialize_with = "deserialize_default_from_null")] + pub cert_domains: Vec<String>, + #[serde(deserialize_with = "deserialize_default_from_null")] + pub peer: HashMap<String, PeerStatus>, + pub user: HashMap<i64, UserProfile>, +} + +/// Service protocol +#[derive(Deserialize, Debug, Copy, Clone)] +#[non_exhaustive] +pub enum ServiceProto { + #[serde(rename = "tcp")] + Tcp, + #[serde(rename = "udp")] + Udp, + #[serde(rename = "peerapi4")] + PeerAPI4, + #[serde(rename = "peerapi6")] + PeerAPI6, + #[serde(rename = "peerapi-dns-proxy")] + PeerAPIDNS, +} + +/// Service running on a node +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Service { + pub proto: ServiceProto, + pub port: u16, + pub description: Option<String>, +} + +/// Host information +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Hostinfo { + #[serde(rename = "OS")] + pub os: Option<String>, + #[serde(rename = "OSVersion")] + pub os_version: Option<String>, + pub hostname: Option<String>, + pub services: Option<Vec<Service>>, + #[serde(default, rename = "sshHostKeys")] + pub ssh_hostkeys: Option<Vec<String>>, +} + +/// Node in the tailnet +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Node { + #[serde(rename = "ID")] + pub id: i64, + #[serde(rename = "StableID")] + pub stable_id: String, + pub name: String, + pub user: i64, + pub sharer: Option<i64>, + pub key: String, + pub key_expiry: DateTime<Utc>, + pub machine: String, + pub disco_key: String, + pub addresses: Vec<String>, + #[serde(rename = "AllowedIPs")] + pub allowed_ips: Vec<String>, + pub endpoints: Option<Vec<SocketAddr>>, + #[serde(rename = "DERP")] + pub derp: Option<String>, + pub hostinfo: Hostinfo, + pub created: DateTime<Utc>, + #[serde(default)] + pub tags: Vec<String>, + #[serde(default)] + pub primary_routes: Vec<String>, + pub last_seen: Option<DateTime<Utc>>, + pub online: Option<bool>, + pub keep_alive: Option<bool>, + pub machine_authorized: Option<bool>, // TODO: Check the upstream code if this has changed to MachineStatus + #[serde(default)] + pub capabilities: Vec<String>, + #[serde(deserialize_with = "deserialize_default_from_null")] + pub computed_name: String, + #[serde(deserialize_with = "deserialize_default_from_null")] + pub computed_name_with_host: String, +} + +/// User profile. +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct UserProfile { + #[serde(rename = "ID")] + pub id: i64, + pub login_name: String, + pub display_name: String, + #[serde(rename = "ProfilePicURL")] + pub profile_pic_url: String, +} + +/// Whois response +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Whois { + pub node: Node, + pub user_profile: UserProfile, + #[serde(default)] + pub caps: Vec<String>, +} + +/// DER encoded X.509 certificate for the node. This can either be the leaf +/// certificate or part of the certificate chain. +pub struct Certificate(pub Vec<u8>); + +/// DER encoded private key for the node +pub struct PrivateKey(pub Vec<u8>); + +type NetfilterMode = u8; +type StableNodeID = String; + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[serde(rename_all = "PascalCase")] +pub struct Prefs { + // ControlURL is the URL of the control server to use. + #[serde(rename = "ControlURL")] + pub control_url: String, + // RouteAll specifies whether to accept subnets advertised by other nodes on the Tailscale network. + pub route_all: bool, + // AllowSingleHosts specifies whether to install routes for each node IP on the tailscale network. + pub allow_single_hosts: bool, + // ExitNodeID and ExitNodeIP specify the node that should be used as an exit node for internet traffic. + #[serde(rename = "ExitNodeID")] + pub exit_node_id: Option<StableNodeID>, + #[serde(rename = "ExitNodeIP", deserialize_with = "deserialize_ip_option")] + pub exit_node_ip: Option<std::net::IpAddr>, + // ExitNodeAllowLANAccess indicates whether locally accessible subnets should be routed directly or via the exit node. + #[serde(rename = "ExitNodeAllowLANAccess")] + pub exit_node_allow_lan_access: bool, + // CorpDNS specifies whether to install the Tailscale network's DNS configuration, if it exists. + #[serde(rename = "CorpDNS")] + pub corp_dns: bool, + // RunSSH is whether this node should run an SSH server. + #[serde(rename = "RunSSH")] + pub run_ssh: bool, + // RunWebClient is whether this node should run a web client. + pub run_web_client: Option<bool>, + // WantRunning indicates whether networking should be active on this node. + pub want_running: bool, + // LoggedOut indicates whether the user intends to be logged out. + pub logged_out: bool, + // ShieldsUp indicates whether to block all incoming connections. + pub shields_up: bool, + // AdvertiseTags specifies groups that this node wants to join, for purposes of ACL enforcement. + pub advertise_tags: Option<Vec<String>>, + // Hostname is the hostname to use for identifying the node. + pub hostname: String, + // NotepadURLs is a debugging setting that opens OAuth URLs in notepad.exe on Windows. + #[serde(rename = "NotepadURLs")] + pub notepad_urls: bool, + // ForceDaemon specifies whether a platform should keep running after the GUI ends and/or the user logs out. + pub force_daemon: Option<bool>, + // Egg is an optional debug flag. + pub egg: Option<bool>, + // The following block of options only have an effect on Linux. + // AdvertiseRoutes specifies CIDR prefixes to advertise into the Tailscale network. + pub advertise_routes: Option<Vec<cidr::IpCidr>>, + // NoSNAT specifies whether to source NAT traffic going to destinations in AdvertiseRoutes. + #[serde(rename = "NoSNAT")] + pub no_snat: Option<bool>, + // NetfilterMode specifies how much to manage netfilter rules for Tailscale. + pub netfilter_mode: NetfilterMode, + // OperatorUser is the local machine user name who is allowed to operate tailscaled without being root or using sudo. + pub operator_user: Option<String>, + // ProfileName is the desired name of the profile. + pub profile_name: Option<String>, + // AutoUpdate sets the auto-update preferences for the node agent. + pub auto_update: Option<AutoUpdatePrefs>, + // AppConnector sets the app connector preferences for the node agent. + pub app_connector: Option<AppConnectorPrefs>, + // PostureChecking enables the collection of information used for device posture checks. + pub posture_checking: Option<bool>, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[serde(rename_all = "PascalCase")] +pub struct AutoUpdatePrefs { + // Check specifies whether background checks for updates are enabled. + check: bool, + // Apply specifies whether background auto-updates are enabled. + apply: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[serde(rename_all = "PascalCase")] +pub struct AppConnectorPrefs { + // Advertise specifies whether the app connector subsystem is advertising this node as a connector. + advertise: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[serde(rename_all = "PascalCase")] +pub struct MaskedPrefs { + #[serde(flatten)] + pub prefs: Prefs, + + #[serde(rename = "ControlURLSet")] + pub control_url_set: Option<bool>, + pub route_all_set: Option<bool>, + pub allow_single_hosts_set: Option<bool>, + #[serde(rename = "ExitNodeIDSet")] + pub exit_node_id_set: Option<bool>, + #[serde(rename = "ExitNodeIPSet")] + pub exit_node_ip_set: Option<bool>, + #[serde(rename = "ExitNodeAllowLANAccessSet")] + pub exit_node_allow_lan_access_set: Option<bool>, + #[serde(rename = "CorpDNSSet")] + pub corp_dns_set: Option<bool>, + #[serde(rename = "RunSSHSet")] + pub run_ssh_set: Option<bool>, + pub run_web_client_set: Option<bool>, + pub want_running_set: Option<bool>, + pub logged_out_set: Option<bool>, + pub shields_up_set: Option<bool>, + pub advertise_tags_set: Option<bool>, + pub hostname_set: Option<bool>, + #[serde(rename = "NotepadURLsSet")] + pub notepad_urls_set: Option<bool>, + pub force_daemon_set: Option<bool>, + pub egg_set: Option<bool>, + pub advertise_routes_set: Option<bool>, + #[serde(rename = "NoSNATSet")] + pub no_snat_set: Option<bool>, + pub netfilter_mode_set: Option<bool>, + pub operator_user_set: Option<bool>, + pub profile_name_set: Option<bool>, + pub auto_update_set: Option<bool>, + pub app_connector_set: Option<bool>, + pub posture_checking_set: Option<bool>, +} + +fn deserialize_ip_option<'de, D>(deserializer: D) -> Result<Option<IpAddr>, D::Error> +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + if s.is_empty() { + Ok(None) + } else { + match s.parse() { + Ok(ip) => Ok(Some(ip)), + Err(_) => Err(Error::custom("Error deserializng IP")), + } + } +}