From 23b19fd6fb9550456266039f53eaa6cb0fddd879 Mon Sep 17 00:00:00 2001 From: Mathias Peters Date: Mon, 18 Nov 2024 16:45:57 +0100 Subject: [PATCH] Add initial xray implementation --- .gitignore | 5 + Cargo.lock | 425 ++++++++++++++++++++++++++++++++- Cargo.toml | 2 +- neptun-cli/src/main.rs | 2 +- neptun/src/noise/mod.rs | 2 + xray/Cargo.toml | 27 +++ xray/Pipfile | 14 ++ xray/Pipfile.lock | 509 ++++++++++++++++++++++++++++++++++++++++ xray/README.md | 53 +++++ xray/analyze.py | 196 ++++++++++++++++ xray/run.py | 142 +++++++++++ xray/src/client.rs | 214 +++++++++++++++++ xray/src/key_pair.rs | 54 +++++ xray/src/main.rs | 325 +++++++++++++++++++++++++ xray/src/utils.rs | 167 +++++++++++++ 15 files changed, 2123 insertions(+), 14 deletions(-) create mode 100644 xray/Cargo.toml create mode 100644 xray/Pipfile create mode 100644 xray/Pipfile.lock create mode 100644 xray/README.md create mode 100644 xray/analyze.py create mode 100755 xray/run.py create mode 100644 xray/src/client.rs create mode 100644 xray/src/key_pair.rs create mode 100644 xray/src/main.rs create mode 100644 xray/src/utils.rs diff --git a/.gitignore b/.gitignore index b4e53ce..34d561d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ /target **/*.rs.bk .vscode/ + +**/*.csv +**/*.pcap +**/*.sk +**/*.pk diff --git a/Cargo.lock b/Cargo.lock index 06fbd13..debec3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,18 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -187,6 +199,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -377,6 +395,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -389,6 +428,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -442,6 +482,27 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "etherparse" version = "0.12.0" @@ -457,6 +518,95 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -484,6 +634,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "half" version = "2.4.1" @@ -552,6 +708,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is-terminal" version = "0.4.13" @@ -605,6 +770,16 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -659,7 +834,7 @@ name = "neptun" version = "0.6.0" dependencies = [ "aead", - "base64", + "base64 0.13.1", "blake2", "chacha20poly1305", "criterion", @@ -671,7 +846,7 @@ dependencies = [ "ip_network_table", "libc", "mock_instant", - "nix", + "nix 0.28.0", "parking_lot", "rand", "rand_core", @@ -703,12 +878,30 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.1.1", "libc", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -790,12 +983,39 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pcap" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "499125886165f62fbc0c095ead9189b253f48eb1c5fcab49f81a270f2f220652" +dependencies = [ + "bitflags 1.3.2", + "errno", + "libc", + "libloading", + "pkg-config", + "regex", + "windows-sys 0.36.1", +] + [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "plotters" version = "0.3.7" @@ -824,6 +1044,97 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "pnet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -924,7 +1235,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1060,6 +1371,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1178,9 +1498,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "libc", @@ -1429,6 +1749,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1454,13 +1787,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1469,12 +1802,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1487,12 +1832,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1505,6 +1862,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1519,6 +1882,30 @@ checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "xray" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "csv", + "curve25519-dalek", + "futures", + "ipnetwork", + "neptun", + "nix 0.29.0", + "pcap", + "pnet", + "rand", + "thiserror", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", + "x25519-dalek", ] [[package]] @@ -1547,3 +1934,17 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 5148113..3d27503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["neptun", "neptun-cli"] +members = ["neptun", "neptun-cli", "xray"] [profile.release] lto = true # Enable full link-time optimization. diff --git a/neptun-cli/src/main.rs b/neptun-cli/src/main.rs index fac128e..66f46c3 100644 --- a/neptun-cli/src/main.rs +++ b/neptun-cli/src/main.rs @@ -168,7 +168,7 @@ fn main() { use_connected_socket: !matches.get_flag("disable-connected-udp"), #[cfg(target_os = "linux")] use_multi_queue: !matches.get_flag("disable-multi-queue"), - open_uapi_socket: false, + open_uapi_socket: true, protect: Arc::new(neptun::device::MakeExternalNeptunNoop), firewall_process_inbound_callback: None, firewall_process_outbound_callback: None, diff --git a/neptun/src/noise/mod.rs b/neptun/src/noise/mod.rs index 5b91302..3297ee9 100644 --- a/neptun/src/noise/mod.rs +++ b/neptun/src/noise/mod.rs @@ -576,6 +576,8 @@ impl Tunn { if self.packet_queue.len() < MAX_QUEUE_DEPTH { // Drop if too many are already in queue self.packet_queue.push_back(packet.to_vec()); + } else { + println!("Dropping packet"); } } diff --git a/xray/Cargo.toml b/xray/Cargo.toml new file mode 100644 index 0000000..5aca089 --- /dev/null +++ b/xray/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "xray" +description = "Testing the speed of boringtun" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = "0.1.31" +tracing-subscriber = "0.3.9" +tracing-appender = "0.2.1" +ipnetwork = "0.20" +pcap = "2.2.0" +pnet = "0.35" +x25519-dalek = "2.0" +curve25519-dalek = "4.1" +tokio = { version = "1.41", features = ["macros", "rt-multi-thread", "time", "net", "sync"] } +futures = "0.3" +rand = "0.8.5" +thiserror = "1.0" +csv = "1.3.1" +nix = { version = "0.29.0", features = ["fs"] } +base64 = "0.22.1" + +[dependencies.neptun] +version = "0.6.0" +path = "../neptun" +features = ["device"] diff --git a/xray/Pipfile b/xray/Pipfile new file mode 100644 index 0000000..2d3defc --- /dev/null +++ b/xray/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +ruff = "==0.8" +matplotlib = "==3.9" +scapy = "==2.6" + +[dev-packages] + +[requires] +python_version = "3" diff --git a/xray/Pipfile.lock b/xray/Pipfile.lock new file mode 100644 index 0000000..c4f4b5b --- /dev/null +++ b/xray/Pipfile.lock @@ -0,0 +1,509 @@ +{ + "_meta": { + "hash": { + "sha256": "ff245a1a88f62d14e7d481e5cf643e0a661433f2d3fb2911315aad9996567f29" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "contourpy": { + "hashes": [ + "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", + "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", + "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d", + "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509", + "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6", + "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", + "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", + "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", + "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", + "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b", + "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", + "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", + "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec", + "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f", + "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", + "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c", + "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", + "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", + "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", + "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", + "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80", + "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", + "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85", + "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", + "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", + "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", + "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", + "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d", + "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc", + "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", + "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", + "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", + "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", + "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", + "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", + "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", + "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", + "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", + "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", + "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", + "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", + "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", + "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", + "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454", + "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9", + "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", + "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", + "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291", + "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", + "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", + "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e", + "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", + "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9", + "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375" + ], + "markers": "python_version >= '3.10'", + "version": "==1.3.1" + }, + "cycler": { + "hashes": [ + "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", + "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "fonttools": { + "hashes": [ + "sha256:00f7cf55ad58a57ba421b6a40945b85ac7cc73094fb4949c41171d3619a3a47e", + "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c", + "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f", + "sha256:161d1ac54c73d82a3cded44202d0218ab007fde8cf194a23d3dd83f7177a2f03", + "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c", + "sha256:23bbbb49bec613a32ed1b43df0f2b172313cee690c2509f1af8fdedcf0a17438", + "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998", + "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05", + "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60", + "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a", + "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189", + "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c", + "sha256:3f901cef813f7c318b77d1c5c14cf7403bae5cb977cede023e22ba4316f0a8f6", + "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61", + "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd", + "sha256:553bd4f8cc327f310c20158e345e8174c8eed49937fb047a8bda51daf2c353c8", + "sha256:55718e8071be35dff098976bc249fc243b58efa263768c611be17fe55975d40a", + "sha256:61dc0a13451143c5e987dec5254d9d428f3c2789a549a7cf4f815b63b310c1cc", + "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b", + "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6", + "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81", + "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40", + "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71", + "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d", + "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9", + "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c", + "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7", + "sha256:8c9679fc0dd7e8a5351d321d8d29a498255e69387590a86b596a45659a39eb0d", + "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f", + "sha256:a656652e1f5d55b9728937a7e7d509b73d23109cddd4e89ee4f49bde03b736c6", + "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18", + "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6", + "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b", + "sha256:b2e526b325a903868c62155a6a7e24df53f6ce4c5c3160214d8fe1be2c41b478", + "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967", + "sha256:b7ef9068a1297714e6fefe5932c33b058aa1d45a2b8be32a4c6dee602ae22b5c", + "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69", + "sha256:ca7fd6987c68414fece41c96836e945e1f320cda56fc96ffdc16e54a44ec57a2", + "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4", + "sha256:dd2820a8b632f3307ebb0bf57948511c2208e34a4939cf978333bc0a3f11f838", + "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6", + "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880", + "sha256:e8e67974326af6a8879dc2a4ec63ab2910a1c1a9680ccd63e4a690950fceddbe", + "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3", + "sha256:f27526042efd6f67bfb0cc2f1610fa20364396f8b1fc5edb9f45bb815fb090b2", + "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6", + "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246", + "sha256:f50a1f455902208486fbca47ce33054208a4e437b38da49d6721ce2fef732fcf", + "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c", + "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51" + ], + "markers": "python_version >= '3.8'", + "version": "==4.55.0" + }, + "kiwisolver": { + "hashes": [ + "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", + "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95", + "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", + "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", + "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d", + "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", + "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", + "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", + "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", + "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", + "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", + "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", + "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", + "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", + "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", + "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", + "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", + "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", + "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", + "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e", + "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", + "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", + "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", + "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", + "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", + "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", + "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", + "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5", + "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", + "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", + "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", + "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", + "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", + "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", + "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", + "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", + "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3", + "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a", + "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", + "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", + "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", + "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", + "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", + "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", + "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", + "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", + "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", + "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", + "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", + "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", + "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b", + "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", + "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", + "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", + "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", + "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", + "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", + "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", + "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", + "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", + "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", + "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", + "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", + "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933", + "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", + "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", + "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", + "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503", + "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", + "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", + "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", + "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", + "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", + "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", + "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf", + "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d", + "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", + "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", + "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", + "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", + "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2", + "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", + "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade", + "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a", + "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c", + "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", + "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00", + "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", + "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", + "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", + "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", + "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", + "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09", + "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", + "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", + "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89", + "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", + "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", + "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", + "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", + "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", + "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", + "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d", + "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935", + "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", + "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", + "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b", + "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", + "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", + "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", + "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", + "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", + "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", + "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.7" + }, + "matplotlib": { + "hashes": [ + "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38", + "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321", + "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db", + "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888", + "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463", + "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03", + "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56", + "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4", + "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b", + "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b", + "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85", + "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956", + "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb", + "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd", + "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7", + "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89", + "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152", + "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be", + "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e", + "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0", + "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84", + "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674", + "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382", + "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a", + "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5", + "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf", + "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a", + "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d", + "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==3.9.0" + }, + "numpy": { + "hashes": [ + "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", + "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", + "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", + "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", + "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", + "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", + "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", + "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", + "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", + "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", + "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", + "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", + "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", + "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", + "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", + "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", + "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", + "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", + "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", + "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", + "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", + "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", + "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", + "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", + "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", + "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", + "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", + "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", + "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", + "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", + "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", + "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", + "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", + "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", + "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", + "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", + "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", + "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", + "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", + "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", + "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", + "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", + "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", + "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", + "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", + "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", + "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", + "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", + "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", + "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", + "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", + "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", + "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", + "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", + "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4" + ], + "markers": "python_version >= '3.10'", + "version": "==2.1.3" + }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, + "pillow": { + "hashes": [ + "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", + "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", + "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", + "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", + "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", + "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", + "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", + "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", + "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", + "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", + "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", + "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", + "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", + "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", + "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", + "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", + "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", + "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", + "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", + "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", + "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", + "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", + "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", + "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", + "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", + "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", + "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", + "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", + "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", + "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", + "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", + "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", + "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", + "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", + "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", + "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", + "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", + "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", + "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", + "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", + "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", + "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", + "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", + "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", + "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", + "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", + "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", + "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", + "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", + "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", + "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", + "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", + "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", + "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", + "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", + "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", + "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", + "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", + "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", + "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", + "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", + "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", + "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", + "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", + "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", + "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", + "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", + "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", + "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", + "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", + "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", + "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", + "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", + "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", + "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" + ], + "markers": "python_version >= '3.9'", + "version": "==11.0.0" + }, + "pyparsing": { + "hashes": [ + "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", + "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" + ], + "markers": "python_version >= '3.9'", + "version": "==3.2.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "ruff": { + "hashes": [ + "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", + "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", + "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", + "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", + "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", + "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", + "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", + "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", + "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", + "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", + "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", + "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", + "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", + "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", + "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", + "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", + "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", + "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.8.0" + }, + "scapy": { + "hashes": [ + "sha256:6cf241e9b93f3b93bbb48fc259edb1d1f14ba1d88d72c80fb66b3b02eb5493dc", + "sha256:c8f7eb54511c2fab502128ccf18e2e4b98e1f6af6e02ae15b84ad3039143ba3b" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.6.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + } + }, + "develop": {} +} diff --git a/xray/README.md b/xray/README.md new file mode 100644 index 0000000..8318ec9 --- /dev/null +++ b/xray/README.md @@ -0,0 +1,53 @@ +# X-Ray + +A tool to inspect the performance of different wireguard implementations + +## How it works + +The setup consists of two clients and a wireguard interface. One of the clients sends and receives encrypted packets (let's call this the crypto socket), and the other sends and receives plaintext packets (let's call this the plaintext socket). The packets that are being sent contain a sequence number so that the receivning socket can now which packet it is, and a timestamp of when it was sent. + +Execution consists of these steps: +1. The wireguard interface is started +2. `tcpdump` is started, with packets being written to a pcap file +3. The main X-Ray executable is started + 3.1. The crypto client performs a handshake with the wireguard interface + 3.2. The specified number of packets are sent in the specified direction (crypto->wg interface->plaintext, or plaintext->wg interface->crypto) + 3.3. The receiving socket registers the packets it receives that are part of the test + 3.4. The packets are written to a `.csv` file with each row containing the sequence number, the time it was sent and, if it was received, the time it was received +4. `tcpdump` is stopped +5. The wireguard interface is destroyed +6. The `.csv` file and pcap are analyzed + +## Running it + +X-Ray currently only works on linux + +### Support + +Supported adapter types: +- Naitve +- Neptun +- WireguardGo (needs to be installed beforehand) +- Boringtun (needs to be compiled and placed in the `neptun/target/release` folder beforehand) + +### Running the test + +The application is executed with the `run.py` script. I takes some arguments, all of which are optional: + +- **--wg **: which adapter type to use, accepts one of `neptun`, `native`, `wggo` and `boringtun`. Default is `neptun` + +- **--test_type **: which kind of test to run, accepts `crypto` (to send packets from the crypto socket to the plaintext socket) and `pt` (to send packets from the plaintext socket to the crypto socket). Default is `crypto` + +- **--count **: How many packets to send. Default is 10 + +- **--nobuild**: whether or not to build `neptun-cli` before running. Only relevant when the adaptert type is `neptun`. Default is to build `neptun` + +## Known issues + +- The analysis of pcaps is quite limited right now because it doesn't decrypt the packets (this is being worked on) + +- There is currently no proper error handling, with errors being either ignored or `unwrap`-ed. Given the nature of the program, proper error handling is not necessary, but moving to `expect` instead of `unwrap` and adding better diagnostics would make it easier to use + +- There are multiple inefficiencies that could potentially impact the test results, the main one being not reusing buffers when creating and sending packets. Each packet that gets constructed allocates a new buffer when they could all reuse the same one + +- Sometimes the program hangs doing a handshake with the wg interface. When that happens, cancelling the execution with `ctrl+c` and then running again "fixes" it. Sometimes you might need to `ctrl+c` and rerun multiple times, but that has been quite rare so far diff --git a/xray/analyze.py b/xray/analyze.py new file mode 100644 index 0000000..7d2882a --- /dev/null +++ b/xray/analyze.py @@ -0,0 +1,196 @@ +import argparse +import csv +import matplotlib.pyplot as plt # type: ignore +import shlex +import subprocess +from enum import Enum +from functools import reduce +from pathlib import Path +from scapy.all import PcapReader, Packet # type: ignore +from scapy.layers.inet import UDP # type: ignore + +def analyze(csv_path, pcap_path, count, test_type): + analyzer = Analyzer(csv_path, pcap_path, count, test_type) + + +class CsvData: + def __init__(self, csv_path): + self.indices = [] + self.timestamps = [] + self.latencies = [] + self.min_latency = -1 + self.max_latency = -1 + + with open(csv_path, newline="") as csvfile: + reader = csv.reader(csvfile, delimiter=",", quotechar="|") + next(reader) + for row in reader: + self.indices.append(int(row[0])) + send_ts = int(row[1]) + if len(row[2]) > 0 : + recv_ts = int(row[2]) + else: + recv_ts = -1 + self.timestamps.append((send_ts, recv_ts)) + if recv_ts >= 0: + latency = recv_ts - send_ts + self.latencies.append(latency) + if self.min_latency < 0 or latency < self.min_latency: + self.min_latency = latency + if self.max_latency < 0 or latency > self.max_latency: + self.max_latency = latency + +class PcapData: + def __init__(self, pcap_path, test_type): + self.before_wg_packets = [] + self.after_wg_packets = [] + with PcapReader(pcap_path) as pcap_reader: + for pkt in pcap_reader: + if not pkt.haslayer(UDP): + continue + + if test_type == "crypto" and pkt.sport == 63636: + if pkt.dport == 41414: + self.before_wg_packets.append(pkt) + elif pkt.dport == 52525: + self.after_wg_packets.append(pkt) + elif test_type == "pt" and pkt.dport == 63636: + if pkt.sport == 52525: + self.before_wg_packets.append(pkt) + elif pkt.sport == 41414: + self.after_wg_packets.append(pkt) + + +class Analyzer: + def __init__(self, csv_name, pcap_name, count, test_type): + self.count = count + self.csv_data = CsvData(csv_name) + self.pcap_data = PcapData(pcap_name, test_type) + + csv = [self.ordering_pie_chart, self.packet_ordering, self.dropped_packets, self.packet_latency] + pcap = [self.packet_funnel] + + fig, ax = plt.subplots(nrows=max(len(csv), len(pcap)), ncols=2) + fig.tight_layout(pad=1) + + for i, fn in enumerate(csv): + fn(ax[i, 0]) + for i, fn in enumerate(pcap): + fn(ax[i, 1]) + + plt.show() + + + def ordering_pie_chart(self, ax): + in_order = count_ordered(self.csv_data.indices, self.count) + dropped = reduce(lambda count, e: count + (1 if e == 0 else 0), self.csv_data.indices, 0) + reordered = self.count - in_order - dropped + data = [] + labels = [] + if in_order > 0: + data.append(in_order) + labels.append(f"In order ({round((in_order/self.count) * 100, 2)}%)") + if reordered > 0: + data.append(reordered) + labels.append(f"Reordered ({round((reordered/self.count) * 100, 2)}%)") + if dropped > 0: + data.append(dropped) + labels.append(f"Dropped ({round((dropped/self.count) * 100, 2)}%)") + ax.set_title("In order/reordered/dropped") + ax.pie(data, labels=labels) + + def packet_ordering(self, ax): + y_axis = [None] + x_axis = [0] + for iter, index in enumerate(self.csv_data.indices): + if self.csv_data.timestamps[iter][1] >= 0: + y_axis.append(self.csv_data.indices[iter]) + else: + y_axis.append(None) + x_axis.append(iter + 1) + ax.set_title("Packet order") + ax.set_xlabel("Received order") + ax.set_ylabel("Packet index") + ax.plot(x_axis, y_axis) + + def packet_latency(self, ax): + millisec = 1000 + sec = 1000 * millisec + if self.csv_data.min_latency > sec: + divisor = sec + timeunit = "Seconds" + elif self.csv_data.min_latency > millisec: + divisor = millisec + timeunit = "Milliseconds" + else: + divisor = 1 + timeunit = "Microseconds" + + num_buckets = 15 + bucket_size = int((self.csv_data.max_latency - self.csv_data.min_latency) / (num_buckets - 1)) + buckets = [] + for l in self.csv_data.latencies: + bucket_index = int((l - self.csv_data.min_latency) / bucket_size) + buckets.append((self.csv_data.min_latency + (bucket_index * bucket_size)) / divisor) + ax.set_title("Latency") + ax.set_xlabel(f"Latency ({timeunit})") + ax.set_ylabel("Count") + ax.hist(buckets, color="blue", bins=num_buckets) + + def dropped_packets(self, ax): + num_buckets = 100 + bucket_size = int(self.count / (num_buckets - 1)) + buckets = [] + for iter, index in enumerate(self.csv_data.indices): + if self.csv_data.timestamps[iter][1] < 0: + bucket_index = int(iter / bucket_size) + buckets.append(bucket_index * bucket_size) + ax.set_title("Dropped packets") + ax.set_xlabel("Index") + ax.set_ylabel("Count") + ax.hist(buckets, color="blue", bins=num_buckets) + + def dropped_packets2(self, ax): + data = [] + for iter, index in enumerate(self.csv_data.indices): + if self.csv_data.timestamps[iter][1] < 0: + data.append(index) + ax.set_title("Dropped packets 2") + ax.set_xlabel("Packet index") + ax.set_ylabel("Count") + ax.plot(data) + + def packet_funnel(self, ax): + count = self.count + before_wg = len(self.pcap_data.before_wg_packets) + after_wg = len(self.pcap_data.after_wg_packets) + recv = len(list(filter(lambda x: x > 0, self.csv_data.indices))) + categories = [f"Count ({count})", f"before wg ({before_wg})", f"after_wg ({after_wg})", f"Recv ({recv})"] + values = [self.count, before_wg, after_wg, recv] + plt.bar(categories, values, color ='blue', + width = 0.4) + + +# This counts in-order packets by looking at series of successive packets +# the length of the sequence could be considered a number of packest in order +# however, if the first packet of the sequence is not in order, then the length of the sequence - 1 is in order +# this last step also takes care of sequences of length 1 (unless the packet is where it's supposed to be) +def count_ordered(data, count): + if len(data) == 0: + return 0 + ordered = 0 + range_good_start = data[0] == 1 + range_len = 1 + prev = data[0] + for i in range(1, len(data)): + if data[i] == 0: + continue + elif data[i] == prev + 1: + range_len += 1 + else: + ordered += range_len - (0 if range_good_start else 1) + range_good_start = data[i] = i + range_len = 1 + prev = data[i] + ordered += range_len - (0 if range_good_start else 1) + return ordered diff --git a/xray/run.py b/xray/run.py new file mode 100755 index 0000000..5ee2ab6 --- /dev/null +++ b/xray/run.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import matplotlib.pyplot as plt # type: ignore +import os +import shlex +import subprocess +from analyze import analyze +from enum import Enum +from pathlib import Path + +WG_IFC_NAME = "xraywg1" + + +def run_command(cmd, capture_output=False): + args = shlex.split(cmd) + run = subprocess.run(args, capture_output=capture_output) + return (run.stdout, run.stderr) + + +def get_csv_name(wg, test_type, count): + return f"results/xray_metrics_{wg.lower()}_{test_type}_{count}.csv" + + +def get_pcap_name(wg, test_type, count): + return f"results/{WG_IFC_NAME}_{wg.lower()}_{test_type}_{count}.pcap" + + +class Wireguard(Enum): + NepTUN = 1 + WgGo = 2 + Native = 3 + BoringTun = 4 + + def from_str(s): + if s is None or s.lower() == "neptun": + return Wireguard.NepTUN + elif s is not None and s.lower() == "wggo": + return Wireguard.WgGo + elif s is not None and s.lower() == "native": + return Wireguard.Native + elif s is not None and s.lower() == "boringtun": + return Wireguard.BoringTun + else: + raise Exception(f"{s} is not a valid wireguard type") + + +def setup_wireguard(wg, build_neptun): + if wg == Wireguard.Native: + run_command(f"sudo ip link add dev {WG_IFC_NAME} type wireguard") + elif wg == Wireguard.WgGo: + wggo = ( + run_command("which wireguard", capture_output=True)[0] + .strip() + .decode("utf-8") + ) + run_command(f"sudo {wggo} {WG_IFC_NAME}") + elif wg == Wireguard.BoringTun: + run_command(f"sudo ../target/release/boringtun-cli {WG_IFC_NAME}") + else: + if build_neptun: + run_command(f"cargo run --release -p neptun-cli -- {WG_IFC_NAME}") + else: + run_command(f"sudo ../target/release/neptun-cli {WG_IFC_NAME}") + run_command(f"sudo ip link set dev {WG_IFC_NAME} up") + run_command(f"sudo ip link set dev {WG_IFC_NAME} multicast off") # Not strictly necessary but keeps the pcaps a bit cleaner + + +def start_tcpdump(pcap_name): + return subprocess.Popen( + ["sudo", "tcpdump", "-ni", "any", "-w", pcap_name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + +def run_xray(wg, test_type, count): + run_command( + f"cargo run --release -- {wg} {test_type} {count} {get_csv_name(wg, test_type, count)}" + ) + + +def stop_tcpdump(tcpdump): + run_command(f"kill {tcpdump.pid}") + + +def destroy_wireguard(wg): + if wg == Wireguard.NepTUN: + run_command("killall neptun-cli") + elif wg == Wireguard.BoringTun: + run_command("killall boringtun-cli") + else: + run_command(f"sudo ip link delete {WG_IFC_NAME}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--wg") + parser.add_argument("--test_type") + parser.add_argument("--count") + parser.add_argument("--nobuild", action="store_true") + args = parser.parse_args() + + wg = Wireguard.from_str(args.wg) + test_type = args.test_type.lower() if args.test_type is not None else "crypto" + assert test_type in [ + "crypto", + "pt", + ], f"Invalid test type '{test_type}'. Valid options are 'crypto' and 'pt'" + count = int(args.count) if args.count is not None else 10 + assert count > 0, f"Count must be at least one, but got {count}" + build_neptun = args.nobuild is None + + Path("results/").mkdir(parents=True, exist_ok=True) + try: + os.remove(get_csv_name(wg.name, test_type, count)) + os.remove(get_pcap_name(wg.name, test_type, count)) + except: + pass + + setup_wireguard(wg, build_neptun) + tcpdump = start_tcpdump(get_pcap_name(wg.name, test_type, count)) + + succeeded = True + try: + run_xray(wg.name, test_type, count) + except: + succeeded = False + finally: + stop_tcpdump(tcpdump) + destroy_wireguard(wg) + + if succeeded: + analyze( + get_csv_name(wg.name, test_type, count), get_pcap_name(wg.name, test_type, count), count, test_type + ) + + +if __name__ == "__main__": + main() diff --git a/xray/src/client.rs b/xray/src/client.rs new file mode 100644 index 0000000..14c9dd2 --- /dev/null +++ b/xray/src/client.rs @@ -0,0 +1,214 @@ +use std::net::{SocketAddr, SocketAddrV4}; + +use neptun::noise::{Tunn, TunnResult}; +use pnet::packet::{ + ip::IpNextHeaderProtocols, + ipv4::{self, Ipv4Packet, MutableIpv4Packet}, + udp::{MutableUdpPacket, UdpPacket}, + Packet, +}; +use tokio::net::UdpSocket; + +use crate::utils::{RecvType, SendType}; + +pub struct Client { + pub addr: SocketAddrV4, + pub tunn: Option, + pub sock: UdpSocket, + buf: Vec, +} + +impl Client { + pub fn new(addr: SocketAddrV4, tunn: Option, sock: UdpSocket) -> Self { + Self { + addr, + tunn, + sock, + buf: vec![0; 4096], + } + } + + pub async fn do_handshake(&mut self, wg_addr: SocketAddrV4) { + println!("Handshake: starting"); + let tunn = self.tunn.as_mut().unwrap(); + match tunn.format_handshake_initiation(&mut self.buf, true) { + TunnResult::WriteToNetwork(packet) => { + self.sock.send_to(packet, wg_addr).await.unwrap(); + } + unexpected => println!("Received unexpected TunnResult: {unexpected:?}"), + } + let mut handshake_buf = vec![0; 512]; + loop { + let recv_type = self.recv_encrypted(&mut handshake_buf).await; + if matches!(recv_type, RecvType::Handshake) { + break; + } + } + println!("Handshake: done"); + } + + pub async fn send_packet( + &mut self, + sock_dst: SocketAddrV4, + packet_dst: SocketAddrV4, + payload: &[u8], + ) -> SendType { + if self.tunn.is_some() { + self.send_encrypted(sock_dst, packet_dst, payload).await + } else { + self.send_plaintext(sock_dst, payload).await + } + } + + pub async fn recv_packet(&mut self, buf: &mut [u8]) -> RecvType { + if self.tunn.is_some() { + self.recv_encrypted(buf).await + } else { + self.recv_plaintext(buf).await + } + } + + pub async fn tick_timers(&mut self, wg_addr: SocketAddrV4) { + if let Some(ref mut tunn) = &mut self.tunn { + match tunn.update_timers(&mut self.buf) { + TunnResult::Done => (), + TunnResult::WriteToNetwork(packet) => { + if let Err(e) = self.sock.send_to(packet, wg_addr).await { + println!("Update Tunn timers failed to send to socket: {:?}", e); + } + } + TunnResult::Err(e) => { + println!("Failed to update Tunn timers: {:?}", e); + } + unexpected_result => println!( + "Update Tunn timers returned unexpected result: {:?}", + unexpected_result + ), + } + } + } + + async fn send_encrypted( + &mut self, + sock_dst: SocketAddrV4, + packet_dst: SocketAddrV4, + payload: &[u8], + ) -> SendType { + let tunn = &mut self.tunn.as_mut().unwrap(); + let packet = Self::make_udp_packet(self.addr, packet_dst, payload); + let tr = tunn.encapsulate(&packet, &mut self.buf); + let (send_type, packet): (SendType, &[u8]) = match tr { + TunnResult::Done => (SendType::None, &[]), + TunnResult::WriteToNetwork(p) => match p[0] { + 1 => (SendType::Handshake, p), + 4 => (SendType::Data, p), + unexpected => panic!("Unexpected packet type {unexpected}"), + }, + TunnResult::WriteToTunnelV4(p, _) => (SendType::Tunn, p), + unexpected => panic!("Unexpected TunnResult: {unexpected:?}"), + }; + if !matches!(send_type, SendType::None) { + self.sock.send_to(packet, sock_dst).await.unwrap(); + } + send_type + } + + async fn send_plaintext(&mut self, dst: SocketAddrV4, payload: &[u8]) -> SendType { + self.sock.send_to(payload, dst).await.unwrap(); + SendType::Plaintext + } + + async fn recv_encrypted(&mut self, buf: &mut [u8]) -> RecvType { + let (mut bytes_read, from) = self.sock.recv_from(&mut self.buf).await.unwrap(); + let from = match from { + SocketAddr::V4(addr) => addr, + SocketAddr::V6(_) => panic!("IPv6 not supported"), + }; + let mut ret = if self.buf[0] == 2 { + RecvType::Handshake + } else if self.buf[0] == 4 { + RecvType::Data { from, length: 0 } + } else { + panic!("Unexpected packet type {}", self.buf[0]) + }; + let mut decap_buf = vec![0; 1024]; + loop { + let tr = self.tunn.as_mut().unwrap().decapsulate( + None, + &self.buf[0..bytes_read], + &mut decap_buf, + ); + match tr { + TunnResult::Done => break, + TunnResult::WriteToNetwork(p) => { + self.sock.send_to(p, from).await.unwrap(); + bytes_read = 0; + } + TunnResult::WriteToTunnelV4(p, _) => { + let (pfrom, payload_start, payload_end) = Self::parse_udp_packet(p); + assert!(buf.len() >= payload_end - payload_start); + buf.iter_mut() + .zip(p[payload_start..payload_end].iter()) + .for_each(|(b, p)| *b = *p); + ret = RecvType::Data { + from: pfrom, + length: payload_end - payload_start, + }; + break; + } + unexpected => panic!("Unexpected TunnResult: {unexpected:?}"), + } + } + ret + } + + async fn recv_plaintext(&mut self, buf: &mut [u8]) -> RecvType { + let (bytes_read, from) = self.sock.recv_from(buf).await.unwrap(); + let from = match from { + SocketAddr::V4(addr) => addr, + SocketAddr::V6(_) => panic!("IPv6 not supported"), + }; + RecvType::Data { + length: bytes_read, + from, + } + } + + fn make_udp_packet(src: SocketAddrV4, dst: SocketAddrV4, payload: &[u8]) -> Vec { + let len = 28 + payload.len(); // IP header (20 bytes) + UDP heder (8 bytes) + payload length + let mut udp_packet = vec![0; len]; + { + let mut ipv4 = MutableIpv4Packet::new(&mut udp_packet).unwrap(); + ipv4.set_version(4); + ipv4.set_header_length(5); + ipv4.set_total_length(len as u16); + ipv4.set_ttl(20); + ipv4.set_next_level_protocol(IpNextHeaderProtocols::Udp); + ipv4.set_source(*src.ip()); + ipv4.set_destination(*dst.ip()); + ipv4.set_checksum(ipv4::checksum(&ipv4.to_immutable())); + } + { + let mut udp = MutableUdpPacket::new(&mut udp_packet[20..]).unwrap(); + udp.set_source(src.port()); + udp.set_destination(dst.port()); + udp.set_length(8 + payload.len() as u16); + udp.set_payload(payload); + // TODO: Calculate checksum + udp.set_checksum(0); + } + udp_packet + } + + fn parse_udp_packet(packet: &[u8]) -> (SocketAddrV4, usize, usize) { + let ip_packet = Ipv4Packet::new(packet).unwrap(); + let udp_packet = UdpPacket::new(ip_packet.payload()).unwrap(); + let from = SocketAddrV4::new(ip_packet.get_source(), udp_packet.get_source()); + let payload_start = (ip_packet.get_header_length() as usize * 4) + 8; + ( + from, + payload_start, + payload_start + udp_packet.payload().len(), + ) + } +} diff --git a/xray/src/key_pair.rs b/xray/src/key_pair.rs new file mode 100644 index 0000000..4e8e2fa --- /dev/null +++ b/xray/src/key_pair.rs @@ -0,0 +1,54 @@ +use std::{fs::File, io::Write}; + +use base64::prelude::*; +use neptun::x25519::{PublicKey, StaticSecret}; + +pub struct KeyPair { + pub private: StaticSecret, + pub public: PublicKey, +} + +impl KeyPair { + pub fn new() -> Self { + let private = StaticSecret::random_from_rng(rand::thread_rng()); + let public = PublicKey::from(&private); + Self { private, public } + } +} + +pub trait NepTUNKey { + fn bytes(&self) -> &[u8]; + + fn as_hex(&self) -> String { + format_key(self.bytes()) + } + + fn as_b64(&self) -> String { + BASE64_STANDARD.encode(self.bytes()) + } + + fn write_to_file(&self, file_name: &str) { + let mut f = File::create(file_name).unwrap(); + f.write_all(self.as_b64().as_bytes()).unwrap(); + } +} + +impl NepTUNKey for StaticSecret { + fn bytes(&self) -> &[u8] { + self.as_bytes() + } +} + +impl NepTUNKey for PublicKey { + fn bytes(&self) -> &[u8] { + self.as_bytes() + } +} + +fn format_key(key: &[u8]) -> String { + let mut hex_key = String::with_capacity(key.len() * 2); + for b in key { + hex_key.push_str(&format!("{b:02x?}")); + } + hex_key +} diff --git a/xray/src/main.rs b/xray/src/main.rs new file mode 100644 index 0000000..0dfd61b --- /dev/null +++ b/xray/src/main.rs @@ -0,0 +1,325 @@ +mod client; +mod key_pair; +mod utils; + +use std::net::SocketAddrV4; + +use neptun::noise::Tunn; + +use tokio::{ + net::UdpSocket, + sync::mpsc, + time::{Duration, Instant}, +}; + +use crate::{ + client::Client, + key_pair::KeyPair, + utils::{configure_wg, write_to_csv, Packet, RecvType, SendType, TestCmd}, +}; + +struct CliArgs { + packet_count: usize, + csv_name: String, +} + +#[tokio::main] +async fn main() { + let cli_args = std::env::args().collect::>(); + let wg = cli_args + .get(1) + .map(|s| s.to_lowercase()) + .unwrap_or_else(|| "neptun".to_owned()); + let test_type = cli_args + .get(2) + .map(|s| s.to_lowercase()) + .unwrap_or_else(|| "crypto".to_owned()); + let packet_count = cli_args + .get(3) + .map(|s| { + s.parse::() + .expect("Packet count must be a positive integer") + }) + .unwrap_or(10); + let csv_name = cli_args + .get(4) + .cloned() + .unwrap_or_else(|| format!("xray_metrics_{}_{}.csv", wg, test_type)); + + let cli_args = CliArgs { + packet_count, + csv_name, + }; + + let wg_keys = KeyPair::new(); + let peer_keys = KeyPair::new(); + + let wg_name = "xraywg1"; + + let wg_addr: SocketAddrV4 = "100.66.0.1:41414".parse().unwrap(); + let crypto_addr: SocketAddrV4 = "100.66.0.2:63636".parse().unwrap(); + let crypto_sock_addr: SocketAddrV4 = "127.0.0.1:63636".parse().unwrap(); + let pt_addr = SocketAddrV4::new(*wg_addr.ip(), 52525); + + println!("Configuring wireguard with adapter type {wg}"); + configure_wg( + wg.as_str(), + wg_name, + &wg_keys, + &peer_keys, + wg_addr.port(), + &[*wg_addr.ip()], + ) + .await; + + let (cmd_tx, cmd_rx) = mpsc::channel::(100); + let (done_tx, mut done_rx) = mpsc::channel::<()>(1); + + let pt_sock = UdpSocket::bind(pt_addr).await.unwrap(); + let pt_client = Client::new(pt_addr, None, pt_sock); + + let crypto_sock = UdpSocket::bind(crypto_sock_addr).await.unwrap(); + let tunn = Tunn::new(peer_keys.private, wg_keys.public, None, None, 123, None).unwrap(); + let mut crypto_client = Client::new(crypto_addr, Some(tunn), crypto_sock); + crypto_client.do_handshake(wg_addr).await; + + let mut event_loop = + EventLoop::new(cli_args, wg_addr, crypto_client, pt_client, done_tx, cmd_rx); + let task = tokio::task::spawn(async move { + event_loop.run().await; + }); + + println!("Starting {test_type} test with {packet_count} packets"); + for i in 0..packet_count { + if test_type == "crypto" { + cmd_tx + .send(TestCmd::SendEncrypted { + sock_dst: wg_addr, + packet_dst: pt_addr, + index: i as u64, + }) + .await + .unwrap(); + } else { + cmd_tx + .send(TestCmd::SendPlaintext { + dst: crypto_addr, + index: i as u64, + }) + .await + .unwrap(); + } + } + cmd_tx.send(TestCmd::Done).await.unwrap(); + + done_rx.recv().await.unwrap(); + + task.await.unwrap(); +} + +struct EventLoop { + cli_args: CliArgs, + wg_addr: SocketAddrV4, + crypto_client: Client, + pt_client: Client, + done_tx: mpsc::Sender<()>, + cmd_rx: mpsc::Receiver, + packets: Vec, + can_send: bool, + is_done: bool, + crypto_buf: Vec, + pt_buf: Vec, + recv_packet: usize, +} + +impl EventLoop { + fn new( + cli_args: CliArgs, + wg_addr: SocketAddrV4, + crypto_client: Client, + pt_client: Client, + done_tx: mpsc::Sender<()>, + cmd_rx: mpsc::Receiver, + ) -> Self { + let packet_count = cli_args.packet_count; + Self { + cli_args, + wg_addr, + crypto_client, + pt_client, + done_tx, + cmd_rx, + packets: Vec::with_capacity(packet_count), + can_send: true, + is_done: false, + crypto_buf: vec![0; 1024], + pt_buf: vec![0; 1024], + recv_packet: 1, + } + } + + async fn run(&mut self) { + let mut wg_tick_interval = tokio::time::interval(Duration::from_millis(250)); + let finish_timeout = tokio::time::sleep(Duration::from_secs(1)); + tokio::pin!(finish_timeout); + loop { + tokio::select! { + _ = &mut finish_timeout, if self.is_done => { + self.on_finished("Exceeded timeout").await; + break; + }, + _ = wg_tick_interval.tick() => { + self.crypto_client.tick_timers(self.wg_addr).await; + }, + Some(cmd) = self.cmd_rx.recv(), if self.can_send => { + self.on_recv_cmd(cmd, &mut finish_timeout).await; + } + rt = self.crypto_client.recv_packet(&mut self.crypto_buf) => { + let should_break = self.on_recv_crypto_packet(rt).await; + if should_break { + break; + } + } + rt = self.pt_client.recv_packet(&mut self.pt_buf) => { + let should_break = self.on_recv_plaintext_packet(rt).await; + if should_break { + break; + } + } + } + } + } + + async fn on_recv_cmd( + &mut self, + cmd: TestCmd, + finish_timeout: &mut std::pin::Pin<&mut tokio::time::Sleep>, + ) { + match cmd { + TestCmd::Done => { + println!("All packets were sent. Waiting for maximum 10 seconds to receive"); + finish_timeout + .as_mut() + .reset(Instant::now() + Duration::from_secs(10)); + self.is_done = true; + } + TestCmd::SendEncrypted { + sock_dst, + packet_dst, + index, + } => { + if index % (self.cli_args.packet_count / 10) as u64 == 0 { + println!("[Crypto] Sending packet with index {index}"); + } + let mut payload = vec![0; Packet::send_size()]; + let send_ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros(); + let mut packet = Packet { + index, + send_ts, + recv_ts: None, + }; + packet.serialize(&mut payload, 0); + + packet.index = 0; + self.packets.push(packet); + + let sr = self + .crypto_client + .send_packet(sock_dst, packet_dst, &payload) + .await; + if !matches!(sr, SendType::Data) { + println!("Send {:?}", sr); + } + if matches!(sr, SendType::Handshake) { + self.can_send = false; + } + } + TestCmd::SendPlaintext { dst, index } => { + if index % (self.cli_args.packet_count / 10) as u64 == 0 { + println!("[Plaintext] Sending packet with index {index}"); + } + let mut payload = vec![0; Packet::send_size()]; + let send_ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros(); + let mut packet = Packet { + index, + send_ts, + recv_ts: None, + }; + packet.serialize(&mut payload, 0); + + packet.index = 0; + self.packets.push(packet); + + self.pt_client.send_packet(dst, dst, &payload).await; + } + } + } + + async fn on_recv_crypto_packet(&mut self, rt: RecvType) -> bool { + match rt { + RecvType::Handshake => self.can_send = true, + RecvType::Data { + length: bytes_read, .. + } => { + if bytes_read == Packet::send_size() { + if self.recv_packet % (self.cli_args.packet_count / 10) == 0 { + println!("[Crypto] Received {} packets", self.recv_packet); + } + let index = u64::from_le_bytes(self.crypto_buf[0..8].try_into().unwrap()); + let recv_ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros(); + self.packets[index as usize].index = self.recv_packet as u64; + self.packets[index as usize].recv_ts = Some(recv_ts); + self.recv_packet += 1; + } + if self.recv_packet >= self.cli_args.packet_count { + self.on_finished("Got all packets").await; + self.is_done = true; + return true; + } + } + } + false + } + + async fn on_recv_plaintext_packet(&mut self, rt: RecvType) -> bool { + if let RecvType::Data { + length: bytes_read, .. + } = rt + { + if bytes_read == Packet::send_size() { + if self.recv_packet % (self.cli_args.packet_count / 10) == 0 { + println!("[Plaintext] Received {} packets", self.recv_packet); + } + let index = u64::from_le_bytes(self.pt_buf[0..8].try_into().unwrap()); + let recv_ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros(); + self.packets[index as usize].index = self.recv_packet as u64; + self.packets[index as usize].recv_ts = Some(recv_ts); + self.recv_packet += 1; + } + if self.recv_packet >= self.cli_args.packet_count { + self.on_finished("Got all packets").await; + self.is_done = true; + return true; + } + } + false + } + + async fn on_finished(&mut self, msg: &str) { + println!("{msg}. Breaking"); + write_to_csv(&self.cli_args.csv_name, &self.packets); + self.done_tx.send(()).await.unwrap(); + } +} diff --git a/xray/src/utils.rs b/xray/src/utils.rs new file mode 100644 index 0000000..76a72f5 --- /dev/null +++ b/xray/src/utils.rs @@ -0,0 +1,167 @@ +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + process::Command, +}; + +use tokio::net::UnixStream; + +use crate::key_pair::{KeyPair, NepTUNKey}; + +#[derive(Debug)] +pub enum SendType { + Plaintext, + Handshake, + Data, + Tunn, + None, +} + +#[derive(Debug)] +pub enum RecvType { + Handshake, + Data { from: SocketAddrV4, length: usize }, +} + +#[derive(Copy, Clone)] +pub struct Packet { + pub index: u64, + pub send_ts: u128, + pub recv_ts: Option, +} + +impl Packet { + pub const fn send_size() -> usize { + std::mem::size_of::() + std::mem::size_of::() + } + + pub fn serialize(&self, buf: &mut [u8], start: usize) { + buf[start..start + std::mem::size_of::()].copy_from_slice(&self.index.to_le_bytes()); + buf[start + std::mem::size_of::() + ..start + std::mem::size_of::() + std::mem::size_of::()] + .copy_from_slice(&self.send_ts.to_le_bytes()); + } +} + +pub enum TestCmd { + SendEncrypted { + sock_dst: SocketAddrV4, + packet_dst: SocketAddrV4, + index: u64, + }, + SendPlaintext { + dst: SocketAddrV4, + index: u64, + }, + Done, +} + +pub fn run_command(cmd: String) -> Result { + let mut args = cmd.split_ascii_whitespace().collect::>(); + let cmd = args.remove(0); + match Command::new(cmd).args(args).output() { + Ok(output) => { + if output.status.success() { + Ok(format!( + "Command ran successfully with output: {}", + String::from_utf8(output.stdout).unwrap() + )) + } else { + Err(format!( + "Command failed with output: {}", + String::from_utf8(output.stderr).unwrap() + )) + } + } + Err(err) => Err(format!("Failed to run command with error: {err:?}")), + } +} + +pub fn write_to_csv(name: &str, packets: &[Packet]) { + let file = std::fs::File::create(name).unwrap(); + let mut csv = csv::Writer::from_writer(file); + + csv.write_record(["Index", "Send TS", "Recv TS"]).unwrap(); + for info in packets { + csv.write_record([ + info.index.to_string(), + info.send_ts.to_string(), + if let Some(recv_ts) = info.recv_ts { + recv_ts.to_string() + } else { + "".to_owned() + }, + ]) + .unwrap(); + } + csv.flush().unwrap(); +} + +pub async fn configure_wg( + adapter_type: &str, + wg_name: &str, + wg_keys: &KeyPair, + peer_keys: &KeyPair, + wg_port: u16, + ips: &[Ipv4Addr], +) { + for ip in ips { + let ipnet = format!("{}/24", ip); + run_command(format!("ip addr add {ipnet} dev {wg_name}")).unwrap(); + } + + match adapter_type { + "native" | "boringtun" => configure_native_wg(wg_name, wg_keys, peer_keys, wg_port), + "wggo" | "neptun" => configure_userspace_wg(wg_name, wg_keys, peer_keys, wg_port).await, + _ => panic!("Unknown adapter type {adapter_type}"), + } +} + +pub fn configure_native_wg(wg_name: &str, wg_keys: &KeyPair, peer_keys: &KeyPair, wg_port: u16) { + wg_keys.private.write_to_file("wg.sk"); + + let wg_setup = format!("private-key wg.sk listen-port {wg_port}"); + let peer_setup = format!("peer {} allowed-ips 0.0.0.0/0", peer_keys.public.as_b64()); + let uapi_cmd = format!("sudo wg set {wg_name} {wg_setup} {peer_setup}"); + run_command(uapi_cmd).unwrap(); +} + +pub async fn configure_userspace_wg( + wg_name: &str, + wg_keys: &KeyPair, + peer_keys: &KeyPair, + wg_port: u16, +) { + let uapi_cmd = format!( + r#"set=1 +private_key={} +listen_port={wg_port} +public_key={} +allowed_ip=0.0.0.0/0 +"#, + wg_keys.private.as_hex(), + peer_keys.public.as_hex(), + ) + .replace('\"', ""); + + let uapi_sock = UnixStream::connect(format!("/var/run/wireguard/{}.sock", wg_name)) + .await + .unwrap(); + + write_to_uapi(&uapi_sock, &uapi_cmd).await; +} + +pub async fn write_to_uapi(sock: &UnixStream, cmd: &str) { + let cmd = cmd.as_bytes(); + sock.writable().await.unwrap(); + let bytes_written = sock.try_write(cmd).unwrap(); + assert_eq!(bytes_written, cmd.len()); +} + +// pub async fn read_from_uapi(sock: &UnixStream) -> String { +// sock.readable().await.unwrap(); +// let mut buf = vec![0; 1024]; +// let bytes_read = sock.try_read(&mut buf).unwrap(); +// buf.truncate(bytes_read); + +// String::from_utf8(buf).unwrap() +// }