Skip to main content

eggress_testkit/oracle/
observations.rs

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}