1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4pub struct ProxyObservation {
5 pub bound_addr: Option<std::net::SocketAddr>,
6 pub exit_code: Option<i32>,
7 pub stdout_lines: Vec<String>,
8 pub stderr_lines: Vec<String>,
9 pub connection_result: ConnectionResult,
10 pub protocol_reply: Option<ProtocolReply>,
11 pub bytes_sent: u64,
12 pub bytes_received: u64,
13 pub auth_result: Option<AuthResult>,
14 pub half_close: Option<HalfCloseBehavior>,
15 pub timing: TimingObservation,
16 pub cleanup: CleanupObservation,
17 pub error: Option<String>,
18}
19
20#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ConnectionResult {
23 Success,
24 Refused,
25 Timeout,
26 Reset,
27 ProxyRejected,
28 #[default]
29 NotAttempted,
30 Partial,
31}
32
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34pub struct ProtocolReply {
35 pub protocol: String,
36 pub status_code: Option<u16>,
37 pub status_text: Option<String>,
38 pub raw_bytes: Option<Vec<u8>>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum AuthResult {
44 Success,
45 Rejected,
46 NotAttempted,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum HalfCloseBehavior {
52 RespondedAfterHalfClose,
53 ClosedAfterHalfClose,
54 Ignored,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58pub struct TimingObservation {
59 pub startup_ms: Option<u64>,
60 pub time_to_first_byte_ms: Option<u64>,
61 pub total_connection_ms: Option<u64>,
62 pub shutdown_ms: Option<u64>,
63}
64
65#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66pub struct CleanupObservation {
67 pub processes_cleaned: bool,
68 pub sockets_removed: bool,
69 pub temp_files_cleaned: bool,
70 pub leftover_artifacts: Vec<String>,
71}
72
73pub fn compare_observations(
74 pproxy: &ProxyObservation,
75 eggress: &ProxyObservation,
76 equivalence: &super::scenario::EquivalenceTarget,
77) -> Vec<super::report::ComparisonResult> {
78 let mut results = Vec::new();
79
80 let pp_cr = serde_json::to_string(&pproxy.connection_result).unwrap_or_default();
81 let eg_cr = serde_json::to_string(&eggress.connection_result).unwrap_or_default();
82 results.push(super::report::make_comparison(
83 "connection_result",
84 &pp_cr,
85 &eg_cr,
86 ));
87
88 match equivalence {
89 super::scenario::EquivalenceTarget::Payload => {
90 let pp_sent = pproxy.bytes_sent.to_string();
91 let eg_sent = eggress.bytes_sent.to_string();
92 results.push(super::report::make_comparison(
93 "bytes_sent",
94 &pp_sent,
95 &eg_sent,
96 ));
97
98 let pp_recv = pproxy.bytes_received.to_string();
99 let eg_recv = eggress.bytes_received.to_string();
100 results.push(super::report::make_comparison(
101 "bytes_received",
102 &pp_recv,
103 &eg_recv,
104 ));
105 }
106 super::scenario::EquivalenceTarget::StatusCode => {
107 if let (Some(pp), Some(eg)) = (&pproxy.protocol_reply, &eggress.protocol_reply) {
108 let pp_sc = pp.status_code.map(|c| c.to_string()).unwrap_or_default();
109 let eg_sc = eg.status_code.map(|c| c.to_string()).unwrap_or_default();
110 results.push(super::report::make_comparison(
111 "status_code",
112 &pp_sc,
113 &eg_sc,
114 ));
115 }
116 }
117 super::scenario::EquivalenceTarget::CoarseResult => {}
118 super::scenario::EquivalenceTarget::BindAddress => {
119 let pp_addr = pproxy.bound_addr.map(|a| a.to_string()).unwrap_or_default();
120 let eg_addr = eggress
121 .bound_addr
122 .map(|a| a.to_string())
123 .unwrap_or_default();
124 results.push(super::report::make_comparison(
125 "bind_address",
126 &pp_addr,
127 &eg_addr,
128 ));
129 }
130 }
131
132 if let (Some(pp_auth), Some(eg_auth)) = (&pproxy.auth_result, &eggress.auth_result) {
133 let pp_s = serde_json::to_string(pp_auth).unwrap_or_default();
134 let eg_s = serde_json::to_string(eg_auth).unwrap_or_default();
135 results.push(super::report::make_comparison("auth_result", &pp_s, &eg_s));
136 }
137
138 results
139}
140
141pub fn observation_from_process(
142 bound_addr: Option<std::net::SocketAddr>,
143 exit_code: Option<i32>,
144 stdout: &[String],
145 stderr: &[String],
146 error: Option<String>,
147) -> ProxyObservation {
148 ProxyObservation {
149 bound_addr,
150 exit_code,
151 stdout_lines: stdout.to_vec(),
152 stderr_lines: stderr.to_vec(),
153 connection_result: if error.is_some() {
154 ConnectionResult::NotAttempted
155 } else {
156 ConnectionResult::Success
157 },
158 protocol_reply: None,
159 bytes_sent: 0,
160 bytes_received: 0,
161 auth_result: None,
162 half_close: None,
163 timing: TimingObservation::default(),
164 cleanup: CleanupObservation::default(),
165 error,
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn observation_serialization_roundtrip() {
175 let obs = ProxyObservation {
176 bound_addr: Some("127.0.0.1:8080".parse().unwrap()),
177 exit_code: None,
178 stdout_lines: vec!["listen on 127.0.0.1:8080".to_string()],
179 stderr_lines: vec![],
180 connection_result: ConnectionResult::Success,
181 protocol_reply: Some(ProtocolReply {
182 protocol: "socks5".to_string(),
183 status_code: Some(0),
184 status_text: Some("success".to_string()),
185 raw_bytes: None,
186 }),
187 bytes_sent: 17,
188 bytes_received: 17,
189 auth_result: None,
190 half_close: None,
191 timing: TimingObservation::default(),
192 cleanup: CleanupObservation::default(),
193 error: None,
194 };
195
196 let json = serde_json::to_string(&obs).unwrap();
197 let parsed: ProxyObservation = serde_json::from_str(&json).unwrap();
198 assert_eq!(parsed.connection_result, ConnectionResult::Success);
199 assert_eq!(parsed.bytes_sent, 17);
200 }
201
202 #[test]
203 fn compare_observations_coarse_pass() {
204 let mut pp = ProxyObservation {
205 ..Default::default()
206 };
207 pp.connection_result = ConnectionResult::Success;
208 let mut eg = ProxyObservation {
209 ..Default::default()
210 };
211 eg.connection_result = ConnectionResult::Success;
212
213 let results = compare_observations(
214 &pp,
215 &eg,
216 &super::super::scenario::EquivalenceTarget::CoarseResult,
217 );
218 assert!(results.iter().all(|r| r.matched));
219 }
220
221 #[test]
222 fn compare_observations_coarse_fail() {
223 let mut pp = ProxyObservation {
224 ..Default::default()
225 };
226 pp.connection_result = ConnectionResult::Success;
227 let mut eg = ProxyObservation {
228 ..Default::default()
229 };
230 eg.connection_result = ConnectionResult::Refused;
231
232 let results = compare_observations(
233 &pp,
234 &eg,
235 &super::super::scenario::EquivalenceTarget::CoarseResult,
236 );
237 assert!(!results.iter().all(|r| r.matched));
238 }
239
240 #[test]
241 fn compare_observations_payload() {
242 let mut pp = ProxyObservation {
243 ..Default::default()
244 };
245 pp.bytes_sent = 17;
246 pp.bytes_received = 17;
247 let mut eg = ProxyObservation {
248 ..Default::default()
249 };
250 eg.bytes_sent = 17;
251 eg.bytes_received = 17;
252
253 let results = compare_observations(
254 &pp,
255 &eg,
256 &super::super::scenario::EquivalenceTarget::Payload,
257 );
258 assert!(results.iter().all(|r| r.matched));
259 }
260
261 #[test]
262 fn observation_from_process_minimal() {
263 let obs = observation_from_process(
264 Some("127.0.0.1:8080".parse().unwrap()),
265 None,
266 &["listen".to_string()],
267 &[],
268 None,
269 );
270 assert_eq!(obs.connection_result, ConnectionResult::Success);
271 assert!(obs.error.is_none());
272 }
273
274 #[test]
275 fn observation_from_process_error() {
276 let obs = observation_from_process(
277 None,
278 Some(1),
279 &[],
280 &["failed".to_string()],
281 Some("startup failed".to_string()),
282 );
283 assert_eq!(obs.connection_result, ConnectionResult::NotAttempted);
284 assert!(obs.error.is_some());
285 }
286}