1
//! Configuration logic for tor-ptmgr.
2

            
3
use std::net::SocketAddr;
4

            
5
use derive_builder::Builder;
6
use serde::{Deserialize, Serialize};
7
use tor_config::{impl_standard_builder, ConfigBuildError};
8
use tor_config_path::CfgPath;
9
use tor_linkspec::PtTransportName;
10

            
11
#[cfg(feature = "tor-channel-factory")]
12
use {crate::PtClientMethod, tor_socksproto::SocksVersion};
13

            
14
/// A single pluggable transport.
15
///
16
/// Pluggable transports are programs that transform and obfuscate traffic on
17
/// the network between a Tor client and a Tor bridge, so that an adversary
18
/// cannot recognize it as Tor traffic.
19
///
20
/// A pluggable transport can be either _managed_ (run as an external process
21
/// that we launch and monitor), or _unmanaged_ (running on a local port, not
22
/// controlled by Arti).
23
350
#[derive(Clone, Debug, Builder, Eq, PartialEq)]
24
#[builder(derive(Debug, Serialize, Deserialize))]
25
#[builder(build_fn(error = "ConfigBuildError", validate = "Self::validate"))]
26
pub struct TransportConfig {
27
    /// Names of the transport protocols that we are willing to use from this transport.
28
    ///
29
    /// (These protocols are arbitrary identifiers that describe which protocols
30
    /// we want. They must match names that the binary knows how to provide.)
31
    //
32
    // NOTE(eta): This doesn't use the list builder stuff, because you're not likely to
33
    //            set this field more than once.
34
    pub(crate) protocols: Vec<PtTransportName>,
35

            
36
    /// The path to the binary to run, if any.
37
    ///
38
    /// This needs to be the path to some executable file on disk.
39
    ///
40
    /// Present only for managed transports.
41
    #[builder(default, setter(strip_option))]
42
    pub(crate) path: Option<CfgPath>,
43

            
44
    /// One or more command-line arguments to pass to the binary.
45
    ///
46
    /// Meaningful only for managed transports.
47
    // TODO: Should this be OsString? That's a pain to parse...
48
    //
49
    // NOTE(eta): This doesn't use the list builder stuff, because you're not likely to
50
    //            set this field more than once.
51
    #[builder(default)]
52
    pub(crate) arguments: Vec<String>,
53

            
54
    /// The location at which to contact this transport.
55
    ///
56
    /// Present only for unmanaged transports.
57
    #[builder(default, setter(strip_option))]
58
    pub(crate) proxy_addr: Option<SocketAddr>,
59

            
60
    /// If true, launch this transport on startup.  Otherwise, we launch
61
    /// it on demand.
62
    ///
63
    /// Meaningful only for managed transports.
64
    #[builder(default)]
65
    pub(crate) run_on_startup: bool,
66
}
67

            
68
impl_standard_builder! { TransportConfig: !Default }
69

            
70
impl TransportConfigBuilder {
71
    /// Inspect the list of protocols (ie, transport names)
72
    ///
73
    /// If none have yet been specified, returns an empty list.
74
175
    pub fn get_protocols(&self) -> &[PtTransportName] {
75
175
        self.protocols.as_deref().unwrap_or_default()
76
175
    }
77

            
78
    /// Make sure that this builder is internally consistent.
79
420
    fn validate(&self) -> Result<(), ConfigBuildError> {
80
420
        // `path` can only be set if the `managed-pts` feature is enabled
81
420
        #[cfg(not(feature = "managed-pts"))]
82
420
        if self.path.is_some() {
83
420
            return Err(ConfigBuildError::NoCompileTimeSupport {
84
420
                field: "path".into(),
85
420
                problem:
86
420
                    "Indicates a managed transport, but support is not enabled by cargo features"
87
420
                        .into(),
88
420
            });
89
420
        }
90
420

            
91
420
        match (&self.path, &self.proxy_addr) {
92
35
            (Some(_), Some(_)) => Err(ConfigBuildError::Inconsistent {
93
35
                fields: vec!["path".into(), "proxy_addr".into()],
94
35
                problem: "Cannot provide both path and proxy_addr".into(),
95
35
            }),
96
            // TODO: There is no ConfigBuildError for "one of two fields is missing."
97
            (None, None) => Err(ConfigBuildError::MissingField {
98
                field: "{path or proxy_addr}".into(),
99
            }),
100
            (None, Some(_)) => {
101
105
                if self.arguments.as_ref().is_some_and(|v| !v.is_empty()) {
102
                    Err(ConfigBuildError::Inconsistent {
103
                        fields: vec!["proxy_addr".into(), "arguments".into()],
104
                        problem: "Cannot provide arguments for an unmanaged transport".into(),
105
                    })
106
105
                } else if self.run_on_startup.is_some() {
107
                    Err(ConfigBuildError::Inconsistent {
108
                        fields: vec!["proxy_addr".into(), "run_on_startup".into()],
109
                        problem: "run_on_startup is meaningless for an unmanaged transport".into(),
110
                    })
111
                } else {
112
105
                    Ok(())
113
                }
114
            }
115
280
            (Some(_), None) => Ok(()),
116
        }
117
420
    }
118
}
119

            
120
/// The pluggable transport structure used internally. This is more type-safe than working with
121
/// `TransportConfig` directly, since we can't change `TransportConfig` as it's part of the public
122
/// API.
123
#[derive(Clone, Debug, Eq, PartialEq)]
124
pub(crate) enum TransportOptions {
125
    /// Options for a managed PT transport.
126
    #[cfg(feature = "managed-pts")]
127
    Managed(ManagedTransportOptions),
128
    /// Options for an unmanaged PT transport.
129
    Unmanaged(UnmanagedTransportOptions),
130
}
131

            
132
impl TryFrom<TransportConfig> for TransportOptions {
133
    type Error = tor_error::Bug;
134
    fn try_from(config: TransportConfig) -> Result<Self, Self::Error> {
135
        // We rely on the validation performed in `TransportConfigBuilder::validate` to ensure that
136
        // mutually exclusive options were not set. We could do validation again here, but it would
137
        // be error-prone to duplicate the validation logic. We also couldn't check things like if
138
        // `run_on_startup` was `Some`/`None`, since that's only available to the builder.
139

            
140
        if let Some(path) = config.path {
141
            cfg_if::cfg_if! {
142
                if #[cfg(feature = "managed-pts")] {
143
                    Ok(TransportOptions::Managed(ManagedTransportOptions {
144
                        protocols: config.protocols,
145
                        path,
146
                        arguments: config.arguments,
147
                        run_on_startup: config.run_on_startup,
148
                    }))
149
                } else {
150
                    let _ = path;
151
                    Err(tor_error::internal!(
152
                        "Path is set but 'managed-pts' feature is not enabled. How did this pass builder validation?"
153
                    ))
154
                }
155
            }
156
        } else if let Some(proxy_addr) = config.proxy_addr {
157
            Ok(TransportOptions::Unmanaged(UnmanagedTransportOptions {
158
                protocols: config.protocols,
159
                proxy_addr,
160
            }))
161
        } else {
162
            Err(tor_error::internal!(
163
                "Neither path nor proxy are set. How did this pass builder validation?"
164
            ))
165
        }
166
    }
167
}
168

            
169
/// A pluggable transport that is run as an external process that we launch and monitor.
170
#[cfg(feature = "managed-pts")]
171
#[derive(Clone, Debug, Eq, PartialEq)]
172
pub(crate) struct ManagedTransportOptions {
173
    /// See [TransportConfig::protocols].
174
    pub(crate) protocols: Vec<PtTransportName>,
175

            
176
    /// See [TransportConfig::path].
177
    pub(crate) path: CfgPath,
178

            
179
    /// See [TransportConfig::arguments].
180
    pub(crate) arguments: Vec<String>,
181

            
182
    /// See [TransportConfig::run_on_startup].
183
    pub(crate) run_on_startup: bool,
184
}
185

            
186
/// A pluggable transport running on a local port, not controlled by Arti.
187
#[derive(Clone, Debug, Eq, PartialEq)]
188
pub(crate) struct UnmanagedTransportOptions {
189
    /// See [TransportConfig::protocols].
190
    pub(crate) protocols: Vec<PtTransportName>,
191

            
192
    /// See [TransportConfig::proxy_addr].
193
    pub(crate) proxy_addr: SocketAddr,
194
}
195

            
196
impl UnmanagedTransportOptions {
197
    /// A client method that can be used to contact this transport.
198
    #[cfg(feature = "tor-channel-factory")]
199
    pub(crate) fn cmethod(&self) -> PtClientMethod {
200
        PtClientMethod {
201
            // TODO: Someday we might want to support other protocols;
202
            // but for now, let's see if we can get away with just socks5.
203
            kind: SocksVersion::V5,
204
            endpoint: self.proxy_addr,
205
        }
206
    }
207

            
208
    /// Return true if this transport is configured on localhost.
209
    pub(crate) fn is_localhost(&self) -> bool {
210
        self.proxy_addr.ip().is_loopback()
211
    }
212
}