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}