Skip to main content

arti/proxy/
port_info.rs

1//! Record information about where we are listening to a file,
2//! so that other programs can find it without going through RPC.
3//!
4//! ## File format
5//!
6//! The file holds a single json Object, containing the key "ports".
7//!
8//! The "ports" entry contains a list.
9//! Each entry in "ports" is a json Object containing these fields:
10//!
11//! * "protocol" - one of "socks", "http", or "dns_udp".
12//! * "address" - An IPv4 or IPv6 socket address, prefixed with the string "inet:".
13//!
14//! All software using this format MUST ignore:
15//! - unrecognized keys in json Objects,
16//! - entries in the "ports" list with unrecognized "protocol"s
17//! - entries in "ports" whose "address" fields are null.
18//! - entries in "ports" whose "address" fields have an unrecognized prefix (not "inet:").
19//!
20//! (Note that as with other formats, we may break this across Arti major versions,
21//! though we will make our best effort not to do so.)
22//!
23//! ## Liveness
24//!
25//! Arti updates this file whenever on startup, when it binds to its ports.
26//! It does not try to delete the file on shutdown, however,
27//! and on a crash or unexpected SIGKILL,
28//! it will have no opportunity to delete the file.
29//! Therefore, you should not assume that the file will always be up to date,
30//! or that the ports will not be bound by some other program.
31
32use std::path::Path;
33
34use anyhow::{Context as _, anyhow};
35use fs_mistrust::{Mistrust, anon_home::PathExt as _};
36use serde::{Serialize, Serializer};
37use tor_general_addr::general;
38
39/// Information about all the ports we are listening on as a proxy.
40///
41/// (RPC is handled differently; see `tor-rpc-connect-port` for info.)
42#[derive(Clone, Debug, Serialize)]
43#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
44pub(crate) struct PortInfo {
45    /// A list of the ports that we're listening on.
46    pub(crate) ports: Vec<Port>,
47}
48
49impl PortInfo {
50    /// Serialize this port information and write it to a chosen file.
51    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
52    pub(crate) fn write_to_file(&self, mistrust: &Mistrust, path: &Path) -> anyhow::Result<()> {
53        let s = serde_json::to_string(self)?;
54
55        let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) else {
56            return Err(anyhow!(
57                "port_info_file {} is not something we can write to",
58                path.anonymize_home()
59            ));
60        };
61
62        // Create the parent directory if it isn't there.
63        // TODO #2267.
64        let parent = if parent.to_str() == Some("") {
65            Path::new(".")
66        } else {
67            parent
68        };
69        let dir = mistrust
70            .verifier()
71            .permit_readable()
72            .make_secure_dir(parent)
73            .with_context(|| {
74                format!(
75                    "Creating parent directory for port_info_file {}",
76                    path.anonymize_home()
77                )
78            })?;
79
80        dir.write_and_replace(file_name, s)
81            .with_context(|| format!("Unable to write port_info_file {}", path.anonymize_home()))?;
82
83        Ok(())
84    }
85}
86
87/// Representation of a single port in a port_info.json file.
88///
89/// Each port corresponds to a single address, and a protocol that can be spoken at this address.
90#[derive(Clone, Debug, Serialize)]
91#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
92pub(crate) struct Port {
93    /// A protocol that this port expects.
94    ///
95    /// If the address accepts multiple protocols, there will be multiple [`Port`] entries in the [`PortInfo`],
96    /// with the same address.
97    pub(crate) protocol: SupportedProtocol,
98    /// The address we're listening on.
99    ///
100    /// (Right now, this is always an Inet address, but we intend to support AF_UNIX in the future.
101    /// See [arti#1965](https://gitlab.torproject.org/tpo/core/arti/-/issues/1965))
102    #[serde(serialize_with = "serialize_address")]
103    pub(crate) address: general::SocketAddr,
104}
105
106/// Helper: serialize a general::SocketAddr as a string if possible,
107/// or as None if it can't be represented as a string.
108fn serialize_address<S: Serializer>(addr: &general::SocketAddr, ser: S) -> Result<S::Ok, S::Error> {
109    match addr.try_to_string() {
110        Some(string) => ser.serialize_str(&string),
111        None => ser.serialize_none(),
112    }
113}
114
115/// A protocol that a given port supports.
116#[derive(Clone, Debug, Serialize)]
117#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
118#[allow(unused)] // Some of these variants are feature-dependent.
119#[non_exhaustive]
120pub(crate) enum SupportedProtocol {
121    /// SOCKS4, SOCKS4a, and SOCKS5; all with Tor extensions.
122    #[serde(rename = "socks")]
123    Socks,
124    /// HTTP CONNECT with Tor extensions.
125    #[serde(rename = "http")]
126    Http,
127    /// DNS over UDP.
128    #[serde(rename = "dns_udp")]
129    DnsUdp,
130}
131
132#[cfg(test)]
133mod test {
134    // @@ begin test lint list maintained by maint/add_warning @@
135    #![allow(clippy::bool_assert_comparison)]
136    #![allow(clippy::clone_on_copy)]
137    #![allow(clippy::dbg_macro)]
138    #![allow(clippy::mixed_attributes_style)]
139    #![allow(clippy::print_stderr)]
140    #![allow(clippy::print_stdout)]
141    #![allow(clippy::single_char_pattern)]
142    #![allow(clippy::unwrap_used)]
143    #![allow(clippy::unchecked_time_subtraction)]
144    #![allow(clippy::useless_vec)]
145    #![allow(clippy::needless_pass_by_value)]
146    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
147
148    use std::str::FromStr;
149
150    use super::*;
151
152    #[test]
153    fn format() {
154        use SupportedProtocol::*;
155        let pi = PortInfo {
156            ports: vec![Port {
157                protocol: Socks,
158                address: "127.0.0.1:99".parse().unwrap(),
159            }],
160        };
161        let got = serde_json::to_string(&pi).unwrap();
162        let expected = r#"
163        { "ports" : [ {"protocol":"socks", "address":"inet:127.0.0.1:99"} ] }
164        "#;
165        assert_eq!(
166            serde_json::Value::from_str(&got).unwrap(),
167            serde_json::Value::from_str(expected).unwrap()
168        );
169    }
170}