diff --git a/.gitignore b/.gitignore index 8874511..7a42323 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ xcuserdata ._* *.vcxproj.user .vs + +/keys \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c4ef8d6..9d58e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -185,6 +195,12 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -203,7 +219,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools", @@ -217,12 +233,27 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -245,6 +276,30 @@ dependencies = [ "piper", ] +[[package]] +name = "boringtun" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751787b019c674b9ac353f4eaa285e6711c21badb421cd8c199bf2c83b727f29" +dependencies = [ + "aead", + "base64 0.13.1", + "blake2", + "chacha20poly1305", + "hex", + "hmac", + "ip_network", + "ip_network_table", + "libc", + "nix 0.25.1", + "parking_lot", + "rand_core 0.6.4", + "ring 0.16.20", + "tracing", + "untrusted 0.9.0", + "x25519-dalek", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -335,6 +390,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.42" @@ -349,6 +428,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -510,9 +600,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.10" @@ -555,6 +673,7 @@ dependencies = [ "block-buffer", "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -632,6 +751,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -663,7 +788,7 @@ dependencies = [ "futures-core", "futures-sink", "nanorand", - "spin", + "spin 0.9.8", ] [[package]] @@ -805,7 +930,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c016cebf305060d144de015c98191ede05c210af588857bc2d4f8611c04663" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", "windows-sys 0.59.0", ] @@ -853,6 +978,21 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.3.1" @@ -920,7 +1060,7 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1049,7 +1189,7 @@ version = "0.1.48" dependencies = [ "async-stream", "async_zip", - "base64", + "base64 0.22.1", "byteorder", "bytes", "chrono", @@ -1100,6 +1240,8 @@ dependencies = [ name = "idevice-tools" version = "0.1.0" dependencies = [ + "base64 0.22.1", + "boringtun", "clap", "futures-util", "idevice", @@ -1109,8 +1251,10 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "tun-rs", "ureq", "uuid", + "x25519-dalek", ] [[package]] @@ -1146,6 +1290,37 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + [[package]] name = "ipnet" version = "2.11.0" @@ -1215,7 +1390,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -1376,7 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.10.0", "byteorder", "libc", "log", @@ -1407,13 +1582,25 @@ dependencies = [ "log", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -1426,7 +1613,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -1542,6 +1729,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parking" version = "2.2.1" @@ -1656,13 +1849,19 @@ dependencies = [ "spki", ] +[[package]] +name = "platforms" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f21de1852251c849a53467e0ce8b97cca9d11fd4efa3930145c5d5f02f24447" + [[package]] name = "plist" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ - "base64", + "base64 0.22.1", "indexmap", "quick-xml", "serde", @@ -1682,6 +1881,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1813,7 +2023,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -1851,7 +2061,7 @@ version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "http", @@ -1877,6 +2087,21 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -1887,7 +2112,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1932,13 +2157,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -1954,7 +2188,7 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1977,9 +2211,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -2000,6 +2234,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2148,6 +2388,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -2462,7 +2708,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2604,6 +2850,22 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -2616,7 +2878,7 @@ version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "percent-encoding", @@ -2633,7 +2895,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ - "base64", + "base64 0.22.1", "http", "httparse", "log", @@ -3163,6 +3425,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x25519-dalek" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "x509-cert" version = "0.2.5" diff --git a/em_proxy b/em_proxy new file mode 160000 index 0000000..f710414 --- /dev/null +++ b/em_proxy @@ -0,0 +1 @@ +Subproject commit f7104143278a907305103de848dadbb4189fa7ea diff --git a/tools/Cargo.toml b/tools/Cargo.toml index b88a6c8..9d4cf16 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -143,12 +143,19 @@ path = "src/notifications.rs" name = "installcoordination_proxy" path = "src/installcoordination_proxy.rs" +[[bin]] +name = "em_proxy" +path = "src/em_proxy.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } tracing = { version = "0.1.41" } tracing-subscriber = { version = "0.3", features = ["env-filter"] } -# tun-rs = { version = "1.5", features = ["async"] } +tun-rs = { version = "2.7.4", features = ["async_tokio"], optional = true } +boringtun = { version = "0.6.0", optional = true } +x25519-dalek = { version = "=2.0.0-rc.3", optional = true } +base64 = { version = "0.22.1", optional = true } sha2 = { version = "0.10" } ureq = { version = "3" } clap = { version = "4.5" } @@ -158,6 +165,8 @@ uuid = "1.16" futures-util = { version = "0.3" } [features] -default = ["aws-lc"] +default = ["aws-lc", "core-device"] aws-lc = ["idevice/aws-lc"] ring = ["idevice/ring"] +wireguard = ["dep:boringtun", "dep:x25519-dalek", "dep:base64"] +core-device = ["dep:tun-rs"] diff --git a/tools/src/core_device_proxy_tun.rs b/tools/src/core_device_proxy_tun.rs index a6df7b1..0fe0d48 100644 --- a/tools/src/core_device_proxy_tun.rs +++ b/tools/src/core_device_proxy_tun.rs @@ -5,13 +5,12 @@ use idevice::{ core_device_proxy::{self}, IdeviceService, }; -use tun_rs::AbstractDevice; mod common; #[tokio::main] async fn main() { - env_logger::init(); + tracing_subscriber::fmt::init(); let matches = Command::new("core_device_proxy_tun") .about("Start a tunnel") .arg( @@ -63,30 +62,48 @@ async fn main() { .await .expect("Unable to connect"); - let dev = tun_rs::create(&tun_rs::Configuration::default()).unwrap(); - dev.add_address_v6( - tun_proxy - .handshake - .client_parameters - .address - .parse() - .unwrap(), - 32, - ) - .unwrap(); + // Create TUN interface + use tun_rs::DeviceBuilder; + let dev = DeviceBuilder::new() + .mtu(tun_proxy.handshake.client_parameters.mtu) + .build_sync() + .expect("Failed to create TUN interface"); + + // Make TUN interface with addresses from handshake + let client_ip: std::net::Ipv6Addr = tun_proxy + .handshake + .client_parameters + .address + .parse() + .expect("Failed to parse client IP (must be IPv6)"); + + // Set MTU dev.set_mtu(tun_proxy.handshake.client_parameters.mtu) - .unwrap(); - dev.set_network_address( - tun_proxy.handshake.client_parameters.address.clone(), - tun_proxy - .handshake - .client_parameters - .netmask - .parse() - .unwrap(), - Some(tun_proxy.handshake.server_address.parse().unwrap()), - ) - .unwrap(); + .expect("Failed to set MTU"); + + // convert netmask to prefix length + let netmask_str = &tun_proxy.handshake.client_parameters.netmask; + let prefix_len = if let Ok(netmask_ipv6) = netmask_str.parse::() { + // Count leading 1s in the netmask to get prefix length + let octets = netmask_ipv6.octets(); + let mut prefix = 0; + for &byte in &octets { + if byte == 0xFF { + prefix += 8; + } else { + // Count bits in partial byte + prefix += byte.leading_ones(); + break; + } + } + prefix as u8 + } else { + // Default to /64 for IPv6 if parsing fails + 64 + }; + + dev.add_address_v6(client_ip, prefix_len) + .expect("Failed to add IPv6 address"); let async_dev = tun_rs::AsyncDevice::new(dev).unwrap(); async_dev.enabled(true).unwrap(); diff --git a/tools/src/em_proxy.rs b/tools/src/em_proxy.rs new file mode 100644 index 0000000..0c9e2fe --- /dev/null +++ b/tools/src/em_proxy.rs @@ -0,0 +1,629 @@ +// Jackson Coxson & SternXD +// Emotional Mangling Proxy - A transparent interface level proxy for TCP packets +// Based on the original implementation by jkcoxson (https://github.com/jkcoxson/em_proxy) + +use clap::{Arg, Command}; +use idevice::{IdeviceService, core_device_proxy::CoreDeviceProxy}; + +mod common; + +#[derive(Debug, Clone)] +enum TunnelMode { + WireGuard, + CoreDeviceProxy, +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + let matches = Command::new("em_proxy") + .about("Emotional Mangling Proxy - Transparent TCP packet proxy") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("wireguard") + .long("wireguard") + .help("Use WireGuard tunnel (default)") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("core-device") + .long("core-device") + .help("Use CoreDeviceProxy tunnel") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("wg-interface") + .long("wg-interface") + .value_name("INTERFACE") + .help("WireGuard interface name (default: wg0)"), + ) + .arg( + Arg::new("wg-server-key") + .long("wg-server-key") + .value_name("KEY_OR_PATH") + .help("WireGuard server private key (base64 string or path to key file)"), + ) + .arg( + Arg::new("wg-client-key") + .long("wg-client-key") + .value_name("KEY_OR_PATH") + .help("WireGuard client public key (base64 string or path to key file)"), + ) + .arg( + Arg::new("wg-keys-dir") + .long("wg-keys-dir") + .value_name("DIR") + .help("Directory containing server_privatekey and client_publickey files (default: ./keys)"), + ) + .arg( + Arg::new("wg-bind") + .long("wg-bind") + .value_name("ADDRESS") + .help("WireGuard UDP bind address (default: 127.0.0.1:51820)"), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("em_proxy - Emotional Mangling Proxy"); + println!("A transparent interface-level proxy for TCP packets"); + return; + } + + // Determine tunnel mode + let tunnel_mode = if matches.get_flag("core-device") { + TunnelMode::CoreDeviceProxy + } else { + TunnelMode::WireGuard + }; + + let wg_interface = matches + .get_one::("wg-interface") + .map(|s| s.as_str()) + .unwrap_or("wg0"); + + let wg_server_key = matches.get_one::("wg-server-key"); + let wg_client_key = matches.get_one::("wg-client-key"); + let wg_keys_dir = matches.get_one::("wg-keys-dir"); + let wg_bind = matches.get_one::("wg-bind"); + + match tunnel_mode { + TunnelMode::WireGuard => { + println!("Using WireGuard tunnel (interface: {})", wg_interface); + run_wireguard_proxy( + wg_interface, + wg_server_key, + wg_client_key, + wg_keys_dir, + wg_bind, + ) + .await; + } + TunnelMode::CoreDeviceProxy => { + #[cfg(feature = "core-device")] + { + println!("Using CoreDeviceProxy tunnel"); + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = + match common::get_provider(udid, host, pairing_file, "em_proxy-jkcoxson").await + { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + run_core_device_proxy(provider).await; + } + #[cfg(not(feature = "core-device"))] + { + eprintln!( + "CoreDeviceProxy support requires the 'core-device' feature to be enabled" + ); + eprintln!("Build with: cargo build --features core-device"); + eprintln!(); + eprintln!("Alternatively, use --wireguard for WireGuard tunnel"); + } + } + } +} + +/// Loads a key from either a file path or uses the string directly as base64 +#[cfg(feature = "wireguard")] +fn load_key( + key_or_path: Option<&String>, + default_file: &str, + keys_dir: Option<&String>, +) -> Option { + if let Some(key) = key_or_path { + // Check if it's a file path (contains / or starts with .) + if key.contains('/') || key.starts_with('.') || std::path::Path::new(key).exists() { + // It's a file path + match std::fs::read_to_string(key) { + Ok(content) => { + // Trim whitespace and newlines + Some(content.trim().to_string()) + } + Err(e) => { + eprintln!("Failed to read key file '{}': {e:?}", key); + None + } + } + } else { + // It's a direct key string + Some(key.clone()) + } + } else if let Some(dir) = keys_dir { + // Try to load from keys directory + let path = std::path::Path::new(dir).join(default_file); + match std::fs::read_to_string(&path) { + Ok(content) => Some(content.trim().to_string()), + Err(_) => None, + } + } else { + // First try current dir + let current_dir_path = std::path::Path::new("keys").join(default_file); + if let Ok(content) = std::fs::read_to_string(¤t_dir_path) { + return Some(content.trim().to_string()); + } + + // Second try user config dir + if let Some(home) = std::env::var_os("HOME") { + let config_path = std::path::Path::new(&home) + .join(".config") + .join("em_proxy") + .join("keys") + .join(default_file); + if let Ok(content) = std::fs::read_to_string(&config_path) { + return Some(content.trim().to_string()); + } + } + + None + } +} + +#[cfg(feature = "wireguard")] +async fn run_wireguard_proxy( + wg_interface: &str, + wg_server_key: Option<&String>, + wg_client_key: Option<&String>, + wg_keys_dir: Option<&String>, + wg_bind: Option<&String>, +) { + use boringtun::noise::Tunn; + use std::net::SocketAddrV4; + use std::str::FromStr; + use x25519_dalek::{PublicKey, StaticSecret}; + + // Default WireGuard UDP port + let bind_addr_str = wg_bind.map(|s| s.as_str()).unwrap_or("127.0.0.1:51820"); + let bind_addr = match SocketAddrV4::from_str(bind_addr_str) { + Ok(addr) => addr, + Err(e) => { + eprintln!("Invalid bind address '{}': {e:?}", bind_addr_str); + eprintln!("Expected format: IP:PORT (e.g., 127.0.0.1:51820)"); + return; + } + }; + + // Get keys from CLI, files, or use placeholders + let server_private_str = load_key(wg_server_key, "server_privatekey", wg_keys_dir) + .unwrap_or_else(|| { + eprintln!("Warning: No server private key provided"); + eprintln!("Provide key via:"); + eprintln!(" --wg-server-key "); + eprintln!(" --wg-server-key /path/to/key/file"); + eprintln!(" --wg-keys-dir /path/to/keys (looks for server_privatekey)"); + eprintln!(" Or place server_privatekey in one of:"); + eprintln!(" - ./keys/ (current directory)"); + eprintln!(" - ~/.config/em_proxy/keys/ (user config)"); + "00000000000000000000000000000000000000000000000000".to_string() // 44 chars base64 + }); + + let client_public_str = load_key(wg_client_key, "client_publickey", wg_keys_dir) + .unwrap_or_else(|| { + eprintln!("Warning: No client public key provided"); + eprintln!("Provide key via:"); + eprintln!(" --wg-client-key "); + eprintln!(" --wg-client-key /path/to/key/file"); + eprintln!(" --wg-keys-dir /path/to/keys (looks for client_publickey)"); + eprintln!(" Or place client_publickey in one of:"); + eprintln!(" - ./keys/ (current directory)"); + eprintln!(" - ~/.config/em_proxy/keys/ (user config)"); + "00000000000000000000000000000000000000000000000000".to_string() // 44 chars base64 + }); + + // Parse base64 keys to bytes, then to StaticSecret/PublicKey + use base64::{Engine as _, engine::general_purpose}; + + let server_private_bytes = match general_purpose::STANDARD.decode(server_private_str.trim()) { + Ok(bytes) if bytes.len() == 32 => bytes, + Ok(_) => { + eprintln!("Server private key must be 32 bytes when decoded"); + return; + } + Err(e) => { + eprintln!("Failed to decode server private key (must be base64): {e:?}"); + return; + } + }; + + let client_public_bytes = match general_purpose::STANDARD.decode(client_public_str.trim()) { + Ok(bytes) if bytes.len() == 32 => bytes, + Ok(_) => { + eprintln!("Client public key must be 32 bytes when decoded"); + return; + } + Err(e) => { + eprintln!("Failed to decode client public key (must be base64): {e:?}"); + return; + } + }; + + let server_private = + StaticSecret::from(<[u8; 32]>::try_from(server_private_bytes.as_slice()).unwrap()); + let client_public = + PublicKey::from(<[u8; 32]>::try_from(client_public_bytes.as_slice()).unwrap()); + + let tun = match Tunn::new(server_private, client_public, None, None, 0, None) { + Ok(t) => t, + Err(e) => { + eprintln!("Failed to create WireGuard tunnel: {e:?}"); + return; + } + }; + + println!("-----------------------------"); + println!("WireGuard Emotional Mangling Proxy"); + println!("Listening on: {}", bind_addr); + println!("Interface: {}", wg_interface); + println!("-----------------------------"); + println!("em_proxy is now intercepting and modifying packets"); + println!("Packets will have source/destination IPs swapped (bytes 12-15 <-> 16-19)"); + + // Bind to UDP socket for WireGuard + let socket = match tokio::net::UdpSocket::bind(bind_addr).await { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to bind to {}: {e:?}", bind_addr); + eprintln!("Note: The address may be in use. Try a different port."); + return; + } + }; + + let mut buf = [0_u8; 2048]; + let mut unencrypted_buf = [0_u8; 2176]; + let tun = std::sync::Arc::new(tokio::sync::Mutex::new(tun)); + + loop { + match socket.recv_from(&mut buf).await { + Ok((size, endpoint)) => { + let tun_clone = tun.clone(); + let mut tun_guard = tun_clone.lock().await; + + // Decapsulate WireGuard packet + let result = + tun_guard.decapsulate(Some(endpoint.ip()), &buf[..size], &mut unencrypted_buf); + + match result { + boringtun::noise::TunnResult::WriteToTunnelV4(packet, _addr) => { + // This is the "emotional mangling" - swap source and destination IP + // Bytes 12-15 (source IP) <-> bytes 16-19 (destination IP) + let mut mangled_packet = packet.to_vec(); + if mangled_packet.len() >= 20 { + emotional_mangle_ip_packet(&mut mangled_packet); + } + + // Re-encapsulate and send back + let mut send_buf = [0_u8; 2048]; + match tun_guard.encapsulate(&mangled_packet, &mut send_buf) { + boringtun::noise::TunnResult::WriteToNetwork(data) => { + if let Err(e) = socket.send_to(data, endpoint).await { + eprintln!("Failed to send packet: {e:?}"); + } + } + _ => { + eprintln!("Unexpected result from encapsulate"); + } + } + } + boringtun::noise::TunnResult::WriteToTunnelV6(_, _) => { + eprintln!("IPv6 not supported"); + } + boringtun::noise::TunnResult::WriteToNetwork(data) => { + // Forward WireGuard control packets + if let Err(e) = socket.send_to(data, endpoint).await { + eprintln!("Failed to send control packet: {e:?}"); + } + } + boringtun::noise::TunnResult::Done => { + // Handshake complete or keep alive + } + boringtun::noise::TunnResult::Err(e) => { + eprintln!("WireGuard error: {e:?}"); + } + } + } + Err(e) => { + eprintln!("Error receiving from socket: {e:?}"); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + } + } +} + +#[cfg(not(feature = "wireguard"))] +async fn run_wireguard_proxy( + _wg_interface: &str, + _wg_server_key: Option<&String>, + _wg_client_key: Option<&String>, + _wg_keys_dir: Option<&String>, + _wg_bind: Option<&String>, +) { + eprintln!("WireGuard support requires the 'wireguard' feature to be enabled"); + eprintln!("Build with: cargo build --features wireguard"); + eprintln!(); + eprintln!("Alternatively, use --core-device for CoreDeviceProxy tunnel"); +} + +#[cfg(feature = "core-device")] +async fn run_core_device_proxy(provider: Box) { + let mut tun_proxy = match CoreDeviceProxy::connect(&*provider).await { + Ok(p) => p, + Err(e) => { + eprintln!("Unable to connect to CoreDeviceProxy: {e:?}"); + return; + } + }; + + // Create TUN interface + use tun_rs::DeviceBuilder; + let dev = match DeviceBuilder::new() + .mtu(tun_proxy.handshake.client_parameters.mtu) + .build_sync() + { + Ok(d) => d, + Err(e) => { + eprintln!("Failed to create TUN interface: {e:?}"); + eprintln!("Note: You may need to run with sudo/root privileges"); + return; + } + }; + + // Configure TUN interface with addresses from handshake + let client_ip: std::net::Ipv6Addr = match tun_proxy.handshake.client_parameters.address.parse() + { + Ok(ip) => ip, + Err(e) => { + eprintln!("Failed to parse client IP (must be IPv6): {e:?}"); + return; + } + }; + + let server_ip: std::net::Ipv6Addr = match tun_proxy.handshake.server_address.parse() { + Ok(ip) => ip, + Err(e) => { + eprintln!("Failed to parse server IP (must be IPv6): {e:?}"); + return; + } + }; + + // Set MTU + if let Err(e) = dev.set_mtu(tun_proxy.handshake.client_parameters.mtu) { + eprintln!("Failed to set MTU: {e:?}"); + return; + } + + // convert netmask to prefix length + let netmask_str = &tun_proxy.handshake.client_parameters.netmask; + let prefix_len = if let Ok(netmask_ipv6) = netmask_str.parse::() { + // Count leading 1s in the netmask to get prefix length + let octets = netmask_ipv6.octets(); + let mut prefix = 0; + for &byte in &octets { + if byte == 0xFF { + prefix += 8; + } else { + // Count bits in partial byte + prefix += byte.leading_ones(); + break; + } + } + prefix as u8 + } else { + // Default to /64 for IPv6 if parsing fails + 64 + }; + + if let Err(e) = dev.add_address_v6(client_ip, prefix_len) { + eprintln!("Failed to add IPv6 address: {e:?}"); + return; + } + + let async_dev = match tun_rs::AsyncDevice::new(dev) { + Ok(d) => d, + Err(e) => { + eprintln!("Failed to create async TUN device: {e:?}"); + return; + } + }; + + if let Err(e) = async_dev.enabled(true) { + eprintln!("Failed to enable TUN device: {e:?}"); + return; + } + + println!("-----------------------------"); + println!("TUN device created: {:?}", async_dev.name()); + println!( + "Client address: {}", + tun_proxy.handshake.client_parameters.address + ); + println!("Server address: {}", tun_proxy.handshake.server_address); + println!("RSD port: {}", tun_proxy.handshake.server_rsd_port); + println!("-----------------------------"); + println!("em_proxy is now intercepting and modifying TCP packets"); + println!("Packets are being retransmitted through CoreDeviceProxy tunnel"); + + // intercept packets, modify them, and retransmit + let mut buf = vec![0; 20_000]; // Large buffer for XPC packets + loop { + tokio::select! { + Ok(len) = async_dev.recv(&mut buf) => { + // Packet received from TUN interface + // Translate localhost (127.0.0.1) to tunnel IPs + let modified_packet = modify_packet_for_loopback(&buf[..len], &client_ip, &server_ip); + + // Send modified packet through CoreDeviceProxy tunnel + if let Err(e) = tun_proxy.send(&modified_packet).await { + eprintln!("Failed to send packet through tunnel: {e:?}"); + } + } + Ok(res) = tun_proxy.recv() => { + // Packet received from device through tunnel + // Translate tunnel IPs back to localhost + let modified_packet = modify_packet_from_device(&res, &client_ip, &server_ip); + + if let Err(e) = async_dev.send(&modified_packet).await { + eprintln!("Failed to send packet to TUN: {e:?}"); + } + } + } + } +} + +/// Emotional mangling: swaps source and destination IP addresses +/// Used for WireGuard mode - swaps bytes 12-15 with 16-19 in the IP header +#[cfg(feature = "wireguard")] +fn emotional_mangle_ip_packet(packet: &mut [u8]) { + // bytes 12-15 are source IP, 16-19 are destination IP + // Swap them to route through tunnel instead of localhost + if packet.len() >= 20 { + packet.swap(12, 16); + packet.swap(13, 17); + packet.swap(14, 18); + packet.swap(15, 19); + } +} + +/// Modifies a packet to work around loopback limitations for CoreDeviceProxy +/// Translates localhost (::1) addresses to tunnel IPs +fn modify_packet_for_loopback( + packet: &[u8], + client_ip: &std::net::Ipv6Addr, + server_ip: &std::net::Ipv6Addr, +) -> Vec { + let mut modified = packet.to_vec(); + translate_localhost_to_tunnel(&mut modified, client_ip, server_ip); + modified +} + +/// Translates localhost addresses to tunnel IPs +/// Replaces ::1 (IPv6 localhost) with tunnel IPs so CoreDeviceProxy can route them +fn translate_localhost_to_tunnel( + packet: &mut [u8], + client_ip: &std::net::Ipv6Addr, + server_ip: &std::net::Ipv6Addr, +) { + // IPv6 header is at least 40 bytes, IP addresses start at offset 8 (source) and 24 (destination) + if packet.len() < 40 { + return; // Not a valid IPv6 packet + } + + // Check IPv6 version (first 4 bits should be 6) + if (packet[0] >> 4) != 6 { + return; // Not an IPv6 packet + } + + // Check if source IP is ::1 (IPv6 localhost: all zeros except last byte is 1) + let is_localhost_src = packet[8..15] == [0, 0, 0, 0, 0, 0, 0] && packet[15] == 1; + // Check if destination IP is ::1 + let is_localhost_dst = packet[24..31] == [0, 0, 0, 0, 0, 0, 0] && packet[31] == 1; + + if is_localhost_src { + // Replace source IP with client tunnel IP + let ip_bytes = client_ip.octets(); + packet[8..16].copy_from_slice(&ip_bytes); + } + if is_localhost_dst { + // Replace destination IP with server tunnel IP + let ip_bytes = server_ip.octets(); + packet[24..32].copy_from_slice(&ip_bytes); + } +} + +/// Modifies a packet received from the device to route back to localhost +/// Translates tunnel IPs back to ::1 (IPv6 localhost) +fn modify_packet_from_device( + packet: &[u8], + client_ip: &std::net::Ipv6Addr, + server_ip: &std::net::Ipv6Addr, +) -> Vec { + let mut modified = packet.to_vec(); + translate_tunnel_to_localhost(&mut modified, client_ip, server_ip); + modified +} + +/// Translates tunnel IPs back to localhost addresses (::1) +fn translate_tunnel_to_localhost( + packet: &mut [u8], + client_ip: &std::net::Ipv6Addr, + server_ip: &std::net::Ipv6Addr, +) { + // IPv6 header is at least 40 bytes, IP addresses start at offset 8 (source) and 24 (destination) + if packet.len() < 40 { + return; // Not a valid IPv6 packet + } + + // Check IPv6 version (first 4 bits should be 6) + if (packet[0] >> 4) != 6 { + return; // Not an IPv6 packet + } + + let client_bytes = client_ip.octets(); + let server_bytes = server_ip.octets(); + + // Check if source IP matches server tunnel IP (packet coming from device) + let is_server_src = packet[8..16] == server_bytes; + + // Check if destination IP matches client tunnel IP (packet going to Mac) + let is_client_dst = packet[24..32] == client_bytes; + + if is_server_src { + // Replace server IP with ::1 (IPv6 localhost) + packet[8..16].fill(0); + packet[15] = 1; + } + if is_client_dst { + // Replace client IP with ::1 (IPv6 localhost) + packet[24..32].fill(0); + packet[31] = 1; + } +}