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

            
32
use std::path::Path;
33

            
34
use anyhow::{Context as _, anyhow};
35
use fs_mistrust::{Mistrust, anon_home::PathExt as _};
36
use serde::{Serialize, Serializer};
37
use 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))]
44
pub(crate) struct PortInfo {
45
    /// A list of the ports that we're listening on.
46
    pub(crate) ports: Vec<Port>,
47
}
48

            
49
impl 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))]
92
pub(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.
108
2
fn serialize_address<S: Serializer>(addr: &general::SocketAddr, ser: S) -> Result<S::Ok, S::Error> {
109
2
    match addr.try_to_string() {
110
2
        Some(string) => ser.serialize_str(&string),
111
        None => ser.serialize_none(),
112
    }
113
2
}
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]
120
pub(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)]
133
mod 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
}