1use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum EquivalenceTarget {
15 Payload,
17 CoarseResult,
19 BindAddress,
21 StatusCode,
23}
24
25#[derive(Debug, Clone, Default)]
27pub struct PlatformRequirements {
28 pub requires_root: bool,
29 pub requires_ipv6: bool,
30 pub requires_python_package: Option<String>,
31 pub required_os: Option<&'static str>,
32}
33
34#[derive(Debug, Clone, Default)]
36pub struct NormalizationRules {
37 pub strip_log_prefixes: bool,
39 pub normalize_ports: bool,
41 pub normalize_line_endings: bool,
43 pub strip_versions: bool,
45}
46
47#[derive(Debug, Clone)]
49pub struct OracleScenario {
50 pub id: &'static str,
52 pub capability_ids: Vec<&'static str>,
54 pub description: &'static str,
56 pub pproxy_args: Vec<&'static str>,
58 pub eggress_toml: &'static str,
60 pub expected_equivalence: EquivalenceTarget,
62 pub normalization: NormalizationRules,
64 pub platform: PlatformRequirements,
66 pub timeout: Duration,
68 pub category: ScenarioCategory,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ScenarioCategory {
76 CliDefaults,
78 HttpSocksTcp,
80 Chains,
82 Rules,
84 Udp,
86}
87
88pub fn all_scenarios() -> Vec<OracleScenario> {
90 let mut scenarios = Vec::new();
91 scenarios.extend(cli_defaults_scenarios());
92 scenarios.extend(http_socks_tcp_scenarios());
93 scenarios.extend(chain_scenarios());
94 scenarios.extend(rule_scenarios());
95 scenarios.extend(udp_scenarios());
96 scenarios
97}
98
99pub fn scenarios_for_category(category: ScenarioCategory) -> Vec<OracleScenario> {
101 all_scenarios()
102 .into_iter()
103 .filter(|s| s.category == category)
104 .collect()
105}
106
107pub fn find_scenario(id: &str) -> Option<OracleScenario> {
109 all_scenarios().into_iter().find(|s| s.id == id)
110}
111
112fn cli_defaults_scenarios() -> Vec<OracleScenario> {
115 vec![
116 OracleScenario {
117 id: "cli.socks5_default",
118 capability_ids: vec!["cli.socks5_default", "uri.socks5"],
119 description: "SOCKS5 listener on default port with direct routing",
120 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
121 eggress_toml: r#"
122version = 1
123
124[[listeners]]
125name = "test"
126bind = "127.0.0.1:{PORT}"
127protocols = ["socks5"]
128"#,
129 expected_equivalence: EquivalenceTarget::CoarseResult,
130 normalization: NormalizationRules {
131 strip_log_prefixes: true,
132 normalize_ports: true,
133 ..Default::default()
134 },
135 platform: PlatformRequirements::default(),
136 timeout: Duration::from_secs(10),
137 category: ScenarioCategory::CliDefaults,
138 },
139 OracleScenario {
140 id: "cli.socks4_default",
141 capability_ids: vec!["cli.socks4_default", "uri.socks4"],
142 description: "SOCKS4 listener on default port with direct routing",
143 pproxy_args: vec!["-l", "socks4://127.0.0.1:{PORT}", "-r", "direct"],
144 eggress_toml: r#"
145version = 1
146
147[[listeners]]
148name = "test"
149bind = "127.0.0.1:{PORT}"
150protocols = ["socks4"]
151"#,
152 expected_equivalence: EquivalenceTarget::CoarseResult,
153 normalization: NormalizationRules {
154 strip_log_prefixes: true,
155 normalize_ports: true,
156 ..Default::default()
157 },
158 platform: PlatformRequirements::default(),
159 timeout: Duration::from_secs(10),
160 category: ScenarioCategory::CliDefaults,
161 },
162 OracleScenario {
163 id: "cli.http_default",
164 capability_ids: vec!["cli.http_default", "uri.http"],
165 description: "HTTP CONNECT listener on default port with direct routing",
166 pproxy_args: vec!["-l", "http://127.0.0.1:{PORT}", "-r", "direct"],
167 eggress_toml: r#"
168version = 1
169
170[[listeners]]
171name = "test"
172bind = "127.0.0.1:{PORT}"
173protocols = ["http"]
174"#,
175 expected_equivalence: EquivalenceTarget::CoarseResult,
176 normalization: NormalizationRules {
177 strip_log_prefixes: true,
178 normalize_ports: true,
179 ..Default::default()
180 },
181 platform: PlatformRequirements::default(),
182 timeout: Duration::from_secs(10),
183 category: ScenarioCategory::CliDefaults,
184 },
185 OracleScenario {
186 id: "cli.https_default",
187 capability_ids: vec!["cli.https_default", "uri.https"],
188 description: "HTTPS (TLS) CONNECT listener with direct routing",
189 pproxy_args: vec!["-l", "https://127.0.0.1:{PORT}", "-r", "direct"],
190 eggress_toml: r#"
191version = 1
192
193[[listeners]]
194name = "test"
195bind = "127.0.0.1:{PORT}"
196protocols = ["http"]
197
198[listeners.tls]
199cert = "tests/fixtures/cert.pem"
200key = "tests/fixtures/key.pem"
201"#,
202 expected_equivalence: EquivalenceTarget::CoarseResult,
203 normalization: NormalizationRules {
204 strip_log_prefixes: true,
205 normalize_ports: true,
206 ..Default::default()
207 },
208 platform: PlatformRequirements::default(),
209 timeout: Duration::from_secs(10),
210 category: ScenarioCategory::CliDefaults,
211 },
212 OracleScenario {
213 id: "cli.ss_default",
214 capability_ids: vec!["cli.ss_default", "uri.ss"],
215 description: "Shadowsocks listener with direct routing",
216 pproxy_args: vec![
217 "-l",
218 "ss://127.0.0.1:{PORT}#testuser:testpass",
219 "-r",
220 "direct",
221 ],
222 eggress_toml: r#"
223version = 1
224
225[[listeners]]
226name = "test"
227bind = "127.0.0.1:{PORT}"
228protocols = ["shadowsocks"]
229
230[listeners.shadowsocks]
231password = "testpass"
232method = "aes-256-gcm"
233"#,
234 expected_equivalence: EquivalenceTarget::CoarseResult,
235 normalization: NormalizationRules {
236 strip_log_prefixes: true,
237 normalize_ports: true,
238 ..Default::default()
239 },
240 platform: PlatformRequirements::default(),
241 timeout: Duration::from_secs(10),
242 category: ScenarioCategory::CliDefaults,
243 },
244 OracleScenario {
245 id: "cli.trojan_default",
246 capability_ids: vec!["cli.trojan_default", "uri.trojan"],
247 description: "Trojan listener with direct routing",
248 pproxy_args: vec!["-l", "trojan://127.0.0.1:{PORT}", "-r", "direct"],
249 eggress_toml: r#"
250version = 1
251
252[[listeners]]
253name = "test"
254bind = "127.0.0.1:{PORT}"
255protocols = ["trojan"]
256
257[listeners.trojan]
258password = "password"
259
260[listeners.tls]
261cert = "tests/fixtures/cert.pem"
262key = "tests/fixtures/key.pem"
263"#,
264 expected_equivalence: EquivalenceTarget::CoarseResult,
265 normalization: NormalizationRules {
266 strip_log_prefixes: true,
267 normalize_ports: true,
268 ..Default::default()
269 },
270 platform: PlatformRequirements::default(),
271 timeout: Duration::from_secs(10),
272 category: ScenarioCategory::CliDefaults,
273 },
274 OracleScenario {
275 id: "cli.mixed_default",
276 capability_ids: vec!["cli.mixed_default"],
277 description: "Mixed SOCKS5+HTTP listener on single port",
278 pproxy_args: vec![
279 "-l",
280 "socks5://127.0.0.1:{PORT}",
281 "-l",
282 "http://127.0.0.1:{PORT2}",
283 "-r",
284 "direct",
285 ],
286 eggress_toml: r#"
287version = 1
288
289[[listeners]]
290name = "test"
291bind = "127.0.0.1:{PORT}"
292protocols = ["socks5", "http"]
293"#,
294 expected_equivalence: EquivalenceTarget::CoarseResult,
295 normalization: NormalizationRules {
296 strip_log_prefixes: true,
297 normalize_ports: true,
298 ..Default::default()
299 },
300 platform: PlatformRequirements::default(),
301 timeout: Duration::from_secs(10),
302 category: ScenarioCategory::CliDefaults,
303 },
304 ]
305}
306
307fn http_socks_tcp_scenarios() -> Vec<OracleScenario> {
310 vec![
311 OracleScenario {
312 id: "tcp.http_connect",
313 capability_ids: vec!["protocol.http_connect"],
314 description: "HTTP CONNECT to TCP echo server",
315 pproxy_args: vec!["-l", "http://127.0.0.1:{PORT}", "-r", "direct"],
316 eggress_toml: r#"
317version = 1
318
319[[listeners]]
320name = "test"
321bind = "127.0.0.1:{PORT}"
322protocols = ["http"]
323"#,
324 expected_equivalence: EquivalenceTarget::Payload,
325 normalization: NormalizationRules {
326 strip_log_prefixes: true,
327 normalize_ports: true,
328 ..Default::default()
329 },
330 platform: PlatformRequirements::default(),
331 timeout: Duration::from_secs(10),
332 category: ScenarioCategory::HttpSocksTcp,
333 },
334 OracleScenario {
335 id: "tcp.socks4_connect",
336 capability_ids: vec!["protocol.socks4_connect"],
337 description: "SOCKS4 CONNECT to TCP echo server",
338 pproxy_args: vec!["-l", "socks4://127.0.0.1:{PORT}", "-r", "direct"],
339 eggress_toml: r#"
340version = 1
341
342[[listeners]]
343name = "test"
344bind = "127.0.0.1:{PORT}"
345protocols = ["socks4"]
346"#,
347 expected_equivalence: EquivalenceTarget::Payload,
348 normalization: NormalizationRules {
349 strip_log_prefixes: true,
350 normalize_ports: true,
351 ..Default::default()
352 },
353 platform: PlatformRequirements::default(),
354 timeout: Duration::from_secs(10),
355 category: ScenarioCategory::HttpSocksTcp,
356 },
357 OracleScenario {
358 id: "tcp.socks4a_connect",
359 capability_ids: vec!["protocol.socks4a_connect"],
360 description: "SOCKS4a CONNECT (domain name) to TCP echo server",
361 pproxy_args: vec!["-l", "socks4a://127.0.0.1:{PORT}", "-r", "direct"],
362 eggress_toml: r#"
363version = 1
364
365[[listeners]]
366name = "test"
367bind = "127.0.0.1:{PORT}"
368protocols = ["socks4"]
369"#,
370 expected_equivalence: EquivalenceTarget::Payload,
371 normalization: NormalizationRules {
372 strip_log_prefixes: true,
373 normalize_ports: true,
374 ..Default::default()
375 },
376 platform: PlatformRequirements::default(),
377 timeout: Duration::from_secs(10),
378 category: ScenarioCategory::HttpSocksTcp,
379 },
380 OracleScenario {
381 id: "tcp.socks5_connect",
382 capability_ids: vec!["protocol.socks5_connect"],
383 description: "SOCKS5 CONNECT to TCP echo server",
384 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
385 eggress_toml: r#"
386version = 1
387
388[[listeners]]
389name = "test"
390bind = "127.0.0.1:{PORT}"
391protocols = ["socks5"]
392"#,
393 expected_equivalence: EquivalenceTarget::Payload,
394 normalization: NormalizationRules {
395 strip_log_prefixes: true,
396 normalize_ports: true,
397 ..Default::default()
398 },
399 platform: PlatformRequirements::default(),
400 timeout: Duration::from_secs(10),
401 category: ScenarioCategory::HttpSocksTcp,
402 },
403 OracleScenario {
404 id: "tcp.socks5_auth",
405 capability_ids: vec!["protocol.socks5_auth"],
406 description: "SOCKS5 CONNECT with username/password auth",
407 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}#user:pass", "-r", "direct"],
408 eggress_toml: r#"
409version = 1
410
411[[listeners]]
412name = "test"
413bind = "127.0.0.1:{PORT}"
414protocols = ["socks5"]
415
416[listeners.auth]
417type = "password"
418username = "user"
419password = "pass"
420"#,
421 expected_equivalence: EquivalenceTarget::Payload,
422 normalization: NormalizationRules {
423 strip_log_prefixes: true,
424 normalize_ports: true,
425 ..Default::default()
426 },
427 platform: PlatformRequirements::default(),
428 timeout: Duration::from_secs(10),
429 category: ScenarioCategory::HttpSocksTcp,
430 },
431 OracleScenario {
432 id: "tcp.socks5_connect_domain",
433 capability_ids: vec!["protocol.socks5_connect_domain"],
434 description: "SOCKS5 CONNECT via domain name target",
435 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
436 eggress_toml: r#"
437version = 1
438
439[[listeners]]
440name = "test"
441bind = "127.0.0.1:{PORT}"
442protocols = ["socks5"]
443"#,
444 expected_equivalence: EquivalenceTarget::Payload,
445 normalization: NormalizationRules {
446 strip_log_prefixes: true,
447 normalize_ports: true,
448 ..Default::default()
449 },
450 platform: PlatformRequirements::default(),
451 timeout: Duration::from_secs(10),
452 category: ScenarioCategory::HttpSocksTcp,
453 },
454 OracleScenario {
455 id: "tcp.socks5_refused",
456 capability_ids: vec!["protocol.socks5_refused"],
457 description: "SOCKS5 CONNECT to refused port (negative case)",
458 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
459 eggress_toml: r#"
460version = 1
461
462[[listeners]]
463name = "test"
464bind = "127.0.0.1:{PORT}"
465protocols = ["socks5"]
466"#,
467 expected_equivalence: EquivalenceTarget::CoarseResult,
468 normalization: NormalizationRules {
469 strip_log_prefixes: true,
470 normalize_ports: true,
471 ..Default::default()
472 },
473 platform: PlatformRequirements::default(),
474 timeout: Duration::from_secs(10),
475 category: ScenarioCategory::HttpSocksTcp,
476 },
477 OracleScenario {
478 id: "tcp.http_forward_get",
479 capability_ids: vec!["protocol.http_forward_get"],
480 description: "HTTP forward proxy GET request",
481 pproxy_args: vec!["-l", "http://127.0.0.1:{PORT}", "-r", "direct"],
482 eggress_toml: r#"
483version = 1
484
485[[listeners]]
486name = "test"
487bind = "127.0.0.1:{PORT}"
488protocols = ["http"]
489"#,
490 expected_equivalence: EquivalenceTarget::Payload,
491 normalization: NormalizationRules {
492 strip_log_prefixes: true,
493 normalize_ports: true,
494 normalize_line_endings: true,
495 ..Default::default()
496 },
497 platform: PlatformRequirements::default(),
498 timeout: Duration::from_secs(10),
499 category: ScenarioCategory::HttpSocksTcp,
500 },
501 OracleScenario {
502 id: "tcp.http_forward_post",
503 capability_ids: vec!["protocol.http_forward_post"],
504 description: "HTTP forward proxy POST request",
505 pproxy_args: vec!["-l", "http://127.0.0.1:{PORT}", "-r", "direct"],
506 eggress_toml: r#"
507version = 1
508
509[[listeners]]
510name = "test"
511bind = "127.0.0.1:{PORT}"
512protocols = ["http"]
513"#,
514 expected_equivalence: EquivalenceTarget::Payload,
515 normalization: NormalizationRules {
516 strip_log_prefixes: true,
517 normalize_ports: true,
518 normalize_line_endings: true,
519 ..Default::default()
520 },
521 platform: PlatformRequirements::default(),
522 timeout: Duration::from_secs(10),
523 category: ScenarioCategory::HttpSocksTcp,
524 },
525 OracleScenario {
526 id: "tcp.socks5_auth_failure",
527 capability_ids: vec!["protocol.socks5_auth_failure"],
528 description: "SOCKS5 with wrong credentials (negative case)",
529 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}#user:pass", "-r", "direct"],
530 eggress_toml: r#"
531version = 1
532
533[[listeners]]
534name = "test"
535bind = "127.0.0.1:{PORT}"
536protocols = ["socks5"]
537
538[listeners.auth]
539type = "password"
540username = "user"
541password = "pass"
542"#,
543 expected_equivalence: EquivalenceTarget::CoarseResult,
544 normalization: NormalizationRules {
545 strip_log_prefixes: true,
546 normalize_ports: true,
547 ..Default::default()
548 },
549 platform: PlatformRequirements::default(),
550 timeout: Duration::from_secs(10),
551 category: ScenarioCategory::HttpSocksTcp,
552 },
553 ]
554}
555
556fn chain_scenarios() -> Vec<OracleScenario> {
559 vec![
560 OracleScenario {
561 id: "chain.socks5_to_socks5",
562 capability_ids: vec!["routing.chain_socks5_socks5"],
563 description: "SOCKS5 chained through another SOCKS5 upstream",
564 pproxy_args: vec![
565 "-l",
566 "socks5://127.0.0.1:{PORT}",
567 "-r",
568 "socks5://127.0.0.1:{UPSTREAM_PORT}",
569 ],
570 eggress_toml: r#"
571version = 1
572
573[[listeners]]
574name = "test"
575bind = "127.0.0.1:{PORT}"
576protocols = ["socks5"]
577
578[[upstreams]]
579id = "upstream-0"
580uri = "socks5://127.0.0.1:{UPSTREAM_PORT}"
581
582[[upstream_groups]]
583id = "chain-group"
584members = ["upstream-0"]
585
586[[rules]]
587id = "route-all"
588upstream_group = "chain-group"
589"#,
590 expected_equivalence: EquivalenceTarget::Payload,
591 normalization: NormalizationRules {
592 strip_log_prefixes: true,
593 normalize_ports: true,
594 ..Default::default()
595 },
596 platform: PlatformRequirements::default(),
597 timeout: Duration::from_secs(15),
598 category: ScenarioCategory::Chains,
599 },
600 OracleScenario {
601 id: "chain.http_to_socks5",
602 capability_ids: vec!["routing.chain_http_socks5"],
603 description: "HTTP CONNECT chained through SOCKS5 upstream",
604 pproxy_args: vec![
605 "-l",
606 "http://127.0.0.1:{PORT}",
607 "-r",
608 "socks5://127.0.0.1:{UPSTREAM_PORT}",
609 ],
610 eggress_toml: r#"
611version = 1
612
613[[listeners]]
614name = "test"
615bind = "127.0.0.1:{PORT}"
616protocols = ["http"]
617
618[[upstreams]]
619id = "upstream-0"
620uri = "socks5://127.0.0.1:{UPSTREAM_PORT}"
621
622[[upstream_groups]]
623id = "chain-group"
624members = ["upstream-0"]
625
626[[rules]]
627id = "route-all"
628upstream_group = "chain-group"
629"#,
630 expected_equivalence: EquivalenceTarget::Payload,
631 normalization: NormalizationRules {
632 strip_log_prefixes: true,
633 normalize_ports: true,
634 ..Default::default()
635 },
636 platform: PlatformRequirements::default(),
637 timeout: Duration::from_secs(15),
638 category: ScenarioCategory::Chains,
639 },
640 OracleScenario {
641 id: "chain.socks5_to_http",
642 capability_ids: vec!["routing.chain_socks5_http"],
643 description: "SOCKS5 chained through HTTP upstream",
644 pproxy_args: vec![
645 "-l",
646 "socks5://127.0.0.1:{PORT}",
647 "-r",
648 "http://127.0.0.1:{UPSTREAM_PORT}",
649 ],
650 eggress_toml: r#"
651version = 1
652
653[[listeners]]
654name = "test"
655bind = "127.0.0.1:{PORT}"
656protocols = ["socks5"]
657
658[[upstreams]]
659id = "upstream-0"
660uri = "http://127.0.0.1:{UPSTREAM_PORT}"
661
662[[upstream_groups]]
663id = "chain-group"
664members = ["upstream-0"]
665
666[[rules]]
667id = "route-all"
668upstream_group = "chain-group"
669"#,
670 expected_equivalence: EquivalenceTarget::Payload,
671 normalization: NormalizationRules {
672 strip_log_prefixes: true,
673 normalize_ports: true,
674 ..Default::default()
675 },
676 platform: PlatformRequirements::default(),
677 timeout: Duration::from_secs(15),
678 category: ScenarioCategory::Chains,
679 },
680 OracleScenario {
681 id: "chain.socks5_auth_to_socks5",
682 capability_ids: vec!["routing.chain_auth"],
683 description: "SOCKS5 with auth chained through SOCKS5 upstream",
684 pproxy_args: vec![
685 "-l",
686 "socks5://127.0.0.1:{PORT}#user:pass",
687 "-r",
688 "socks5://127.0.0.1:{UPSTREAM_PORT}",
689 ],
690 eggress_toml: r#"
691version = 1
692
693[[listeners]]
694name = "test"
695bind = "127.0.0.1:{PORT}"
696protocols = ["socks5"]
697
698[listeners.auth]
699type = "password"
700username = "user"
701password = "pass"
702
703[[upstreams]]
704id = "upstream-0"
705uri = "socks5://127.0.0.1:{UPSTREAM_PORT}"
706
707[[upstream_groups]]
708id = "chain-group"
709members = ["upstream-0"]
710
711[[rules]]
712id = "route-all"
713upstream_group = "chain-group"
714"#,
715 expected_equivalence: EquivalenceTarget::Payload,
716 normalization: NormalizationRules {
717 strip_log_prefixes: true,
718 normalize_ports: true,
719 ..Default::default()
720 },
721 platform: PlatformRequirements::default(),
722 timeout: Duration::from_secs(15),
723 category: ScenarioCategory::Chains,
724 },
725 OracleScenario {
726 id: "chain.ss_to_socks5",
727 capability_ids: vec!["routing.chain_ss_socks5"],
728 description: "Shadowsocks chained through SOCKS5 upstream",
729 pproxy_args: vec![
730 "-l",
731 "ss://127.0.0.1:{PORT}#testuser:testpass",
732 "-r",
733 "socks5://127.0.0.1:{UPSTREAM_PORT}",
734 ],
735 eggress_toml: r#"
736version = 1
737
738[[listeners]]
739name = "test"
740bind = "127.0.0.1:{PORT}"
741protocols = ["shadowsocks"]
742
743[listeners.shadowsocks]
744password = "testpass"
745method = "aes-256-gcm"
746
747[[upstreams]]
748id = "upstream-0"
749uri = "socks5://127.0.0.1:{UPSTREAM_PORT}"
750
751[[upstream_groups]]
752id = "chain-group"
753members = ["upstream-0"]
754
755[[rules]]
756id = "route-all"
757upstream_group = "chain-group"
758"#,
759 expected_equivalence: EquivalenceTarget::Payload,
760 normalization: NormalizationRules {
761 strip_log_prefixes: true,
762 normalize_ports: true,
763 ..Default::default()
764 },
765 platform: PlatformRequirements::default(),
766 timeout: Duration::from_secs(15),
767 category: ScenarioCategory::Chains,
768 },
769 ]
770}
771
772fn rule_scenarios() -> Vec<OracleScenario> {
775 vec![
776 OracleScenario {
777 id: "rules.reject_ip",
778 capability_ids: vec!["routing.reject_ip"],
779 description: "Rule rejecting connections to specific IP",
780 pproxy_args: vec![
781 "-l",
782 "socks5://127.0.0.1:{PORT}",
783 "-r",
784 "direct",
785 "-b",
786 "127.0.0.2",
787 ],
788 eggress_toml: r#"
789version = 1
790
791[[listeners]]
792name = "test"
793bind = "127.0.0.1:{PORT}"
794protocols = ["socks5"]
795
796[[rules]]
797id = "reject-ip"
798host_regex = "127\\.0\\.0\\.2"
799reject = "blocked"
800"#,
801 expected_equivalence: EquivalenceTarget::CoarseResult,
802 normalization: NormalizationRules {
803 strip_log_prefixes: true,
804 normalize_ports: true,
805 ..Default::default()
806 },
807 platform: PlatformRequirements::default(),
808 timeout: Duration::from_secs(10),
809 category: ScenarioCategory::Rules,
810 },
811 OracleScenario {
812 id: "rules.reject_domain",
813 capability_ids: vec!["routing.reject_domain"],
814 description: "Rule rejecting connections to specific domain",
815 pproxy_args: vec![
816 "-l",
817 "socks5://127.0.0.1:{PORT}",
818 "-r",
819 "direct",
820 "-b",
821 "blocked.example.com",
822 ],
823 eggress_toml: r#"
824version = 1
825
826[[listeners]]
827name = "test"
828bind = "127.0.0.1:{PORT}"
829protocols = ["socks5"]
830
831[[rules]]
832id = "reject-domain"
833host_regex = "blocked\\.example\\.com"
834reject = "blocked"
835"#,
836 expected_equivalence: EquivalenceTarget::CoarseResult,
837 normalization: NormalizationRules {
838 strip_log_prefixes: true,
839 normalize_ports: true,
840 ..Default::default()
841 },
842 platform: PlatformRequirements::default(),
843 timeout: Duration::from_secs(10),
844 category: ScenarioCategory::Rules,
845 },
846 OracleScenario {
847 id: "rules.allow_all",
848 capability_ids: vec!["routing.allow_all"],
849 description: "Rule allowing all connections (default behavior)",
850 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
851 eggress_toml: r#"
852version = 1
853
854[[listeners]]
855name = "test"
856bind = "127.0.0.1:{PORT}"
857protocols = ["socks5"]
858"#,
859 expected_equivalence: EquivalenceTarget::Payload,
860 normalization: NormalizationRules {
861 strip_log_prefixes: true,
862 normalize_ports: true,
863 ..Default::default()
864 },
865 platform: PlatformRequirements::default(),
866 timeout: Duration::from_secs(10),
867 category: ScenarioCategory::Rules,
868 },
869 OracleScenario {
870 id: "rules.block_reject",
871 capability_ids: vec!["routing.block_reject"],
872 description: "Block action rejecting connections",
873 pproxy_args: vec![
874 "-l",
875 "socks5://127.0.0.1:{PORT}",
876 "-r",
877 "direct",
878 "-b",
879 "127.0.0.3",
880 ],
881 eggress_toml: r#"
882version = 1
883
884[[listeners]]
885name = "test"
886bind = "127.0.0.1:{PORT}"
887protocols = ["socks5"]
888
889[[rules]]
890id = "reject-b"
891host_regex = "127\\.0\\.0\\.3"
892reject = "blocked"
893"#,
894 expected_equivalence: EquivalenceTarget::CoarseResult,
895 normalization: NormalizationRules {
896 strip_log_prefixes: true,
897 normalize_ports: true,
898 ..Default::default()
899 },
900 platform: PlatformRequirements::default(),
901 timeout: Duration::from_secs(10),
902 category: ScenarioCategory::Rules,
903 },
904 OracleScenario {
905 id: "rules.multiple_reject",
906 capability_ids: vec!["routing.multiple_reject"],
907 description: "Multiple reject rules with different targets",
908 pproxy_args: vec![
909 "-l",
910 "socks5://127.0.0.1:{PORT}",
911 "-r",
912 "direct",
913 "-b",
914 "127.0.0.4",
915 "-b",
916 "127.0.0.5",
917 ],
918 eggress_toml: r#"
919version = 1
920
921[[listeners]]
922name = "test"
923bind = "127.0.0.1:{PORT}"
924protocols = ["socks5"]
925
926[[rules]]
927id = "reject-c"
928host_regex = "127\\.0\\.0\\.4"
929reject = "blocked"
930
931[[rules]]
932id = "reject-d"
933host_regex = "127\\.0\\.0\\.5"
934reject = "blocked"
935"#,
936 expected_equivalence: EquivalenceTarget::CoarseResult,
937 normalization: NormalizationRules {
938 strip_log_prefixes: true,
939 normalize_ports: true,
940 ..Default::default()
941 },
942 platform: PlatformRequirements::default(),
943 timeout: Duration::from_secs(10),
944 category: ScenarioCategory::Rules,
945 },
946 ]
947}
948
949fn udp_scenarios() -> Vec<OracleScenario> {
952 vec![
953 OracleScenario {
954 id: "udp.socks5_associate",
955 capability_ids: vec!["protocol.socks5_udp_associate"],
956 description: "SOCKS5 UDP ASSOCIATE lifecycle",
957 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
958 eggress_toml: r#"
959version = 1
960
961[[listeners]]
962name = "test"
963bind = "127.0.0.1:{PORT}"
964protocols = ["socks5"]
965"#,
966 expected_equivalence: EquivalenceTarget::CoarseResult,
967 normalization: NormalizationRules {
968 strip_log_prefixes: true,
969 normalize_ports: true,
970 ..Default::default()
971 },
972 platform: PlatformRequirements::default(),
973 timeout: Duration::from_secs(10),
974 category: ScenarioCategory::Udp,
975 },
976 OracleScenario {
977 id: "udp.socks5_relay",
978 capability_ids: vec!["protocol.socks5_udp_relay"],
979 description: "SOCKS5 UDP relay with echo payload",
980 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
981 eggress_toml: r#"
982version = 1
983
984[[listeners]]
985name = "test"
986bind = "127.0.0.1:{PORT}"
987protocols = ["socks5"]
988"#,
989 expected_equivalence: EquivalenceTarget::Payload,
990 normalization: NormalizationRules {
991 strip_log_prefixes: true,
992 normalize_ports: true,
993 ..Default::default()
994 },
995 platform: PlatformRequirements::default(),
996 timeout: Duration::from_secs(10),
997 category: ScenarioCategory::Udp,
998 },
999 OracleScenario {
1000 id: "udp.standalone",
1001 capability_ids: vec!["protocol.standalone_udp"],
1002 description: "Standalone UDP relay (pproxy-compatible mode)",
1003 pproxy_args: vec!["-l", "udp://127.0.0.1:{PORT}", "-r", "direct"],
1004 eggress_toml: r#"
1005version = 1
1006
1007[[listeners]]
1008name = "test"
1009bind = "127.0.0.1:{PORT}"
1010protocols = ["socks5"]
1011
1012[listeners.udp]
1013mode = "standalone_pproxy_udp"
1014"#,
1015 expected_equivalence: EquivalenceTarget::Payload,
1016 normalization: NormalizationRules {
1017 strip_log_prefixes: true,
1018 normalize_ports: true,
1019 ..Default::default()
1020 },
1021 platform: PlatformRequirements::default(),
1022 timeout: Duration::from_secs(10),
1023 category: ScenarioCategory::Udp,
1024 },
1025 OracleScenario {
1026 id: "udp.echo_roundtrip",
1027 capability_ids: vec!["protocol.udp_echo_roundtrip"],
1028 description: "UDP echo roundtrip through SOCKS5 proxy",
1029 pproxy_args: vec!["-l", "socks5://127.0.0.1:{PORT}", "-r", "direct"],
1030 eggress_toml: r#"
1031version = 1
1032
1033[[listeners]]
1034name = "test"
1035bind = "127.0.0.1:{PORT}"
1036protocols = ["socks5"]
1037"#,
1038 expected_equivalence: EquivalenceTarget::Payload,
1039 normalization: NormalizationRules {
1040 strip_log_prefixes: true,
1041 normalize_ports: true,
1042 ..Default::default()
1043 },
1044 platform: PlatformRequirements::default(),
1045 timeout: Duration::from_secs(10),
1046 category: ScenarioCategory::Udp,
1047 },
1048 ]
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::*;
1054
1055 #[test]
1056 fn all_scenarios_have_unique_ids() {
1057 let scenarios = all_scenarios();
1058 let mut ids: Vec<&str> = scenarios.iter().map(|s| s.id).collect();
1059 ids.sort();
1060 ids.dedup();
1061 assert_eq!(ids.len(), scenarios.len(), "duplicate scenario IDs found");
1062 }
1063
1064 #[test]
1065 fn all_scenarios_have_nonempty_capabilities() {
1066 for scenario in all_scenarios() {
1067 assert!(
1068 !scenario.capability_ids.is_empty(),
1069 "scenario {} has no capability IDs",
1070 scenario.id
1071 );
1072 }
1073 }
1074
1075 #[test]
1076 fn scenario_count_by_category() {
1077 assert_eq!(cli_defaults_scenarios().len(), 7);
1078 assert_eq!(http_socks_tcp_scenarios().len(), 10);
1079 assert_eq!(chain_scenarios().len(), 5);
1080 assert_eq!(rule_scenarios().len(), 5);
1081 assert_eq!(udp_scenarios().len(), 4);
1082 assert_eq!(all_scenarios().len(), 31);
1083 }
1084
1085 #[test]
1086 fn find_scenario_by_id() {
1087 assert!(find_scenario("cli.socks5_default").is_some());
1088 assert!(find_scenario("nonexistent").is_none());
1089 }
1090
1091 #[test]
1092 fn scenarios_for_category_filter() {
1093 let cli = scenarios_for_category(ScenarioCategory::CliDefaults);
1094 assert_eq!(cli.len(), 7);
1095 for s in &cli {
1096 assert_eq!(s.category, ScenarioCategory::CliDefaults);
1097 }
1098 }
1099
1100 #[test]
1109 fn scenario_eggress_toml_parses() {
1110 let port_substitution = |scenario: &OracleScenario, port: u16| -> String {
1111 scenario
1112 .eggress_toml
1113 .replace("{PORT}", &port.to_string())
1114 .replace("{PORT2}", &(port + 1).to_string())
1115 .replace("{ECHO_PORT}", &(port + 100).to_string())
1116 .replace("{UPSTREAM_PORT}", &(port + 100).to_string())
1117 };
1118
1119 for scenario in all_scenarios() {
1120 let toml = port_substitution(&scenario, 18080);
1121
1122 let parsed: Result<eggress_config::model::ConfigFile, _> = toml::from_str(&toml);
1123 let config = match parsed {
1124 Ok(c) => c,
1125 Err(e) => panic!(
1126 "scenario {} produced TOML that failed to parse: {e}\nTOML was:\n{toml}",
1127 scenario.id
1128 ),
1129 };
1130
1131 if let Err(errors) = eggress_config::validate::validate_config(&config) {
1132 let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
1133 panic!(
1134 "scenario {} produced TOML that failed validation: {}\nTOML was:\n{toml}",
1135 scenario.id,
1136 messages.join("; ")
1137 );
1138 }
1139 }
1140 }
1141}