diff --git a/BaoLianDeng/Views/SettingsView.swift b/BaoLianDeng/Views/SettingsView.swift index c81a112..0553947 100644 --- a/BaoLianDeng/Views/SettingsView.swift +++ b/BaoLianDeng/Views/SettingsView.swift @@ -70,8 +70,8 @@ struct SettingsView: View { } } - // App-extension builds (Mac App Store) have no system extension - // to uninstall — the provider lives inside the app bundle. + // App-extension builds (MAS / local) have no system extension + // to uninstall — the provider lives in PlugIns as an .appex. if !VPNManager.providerIsAppExtension { Section("System Extension") { Button("Uninstall System Extension") { @@ -122,7 +122,7 @@ struct SettingsView: View { Text("Proxy Method") } footer: { if vpnManager.engineMode == .localProxy { - Text("Runs the engine inside the app as an HTTP/SOCKS5 proxy on 127.0.0.1 — no system extension or VPN configuration needed. Apps must be pointed at the proxy manually. Switching method or port takes effect on the next start.") + Text("Runs the engine inside the app as an HTTP/SOCKS5 proxy on 127.0.0.1 — no network extension or VPN configuration needed. Apps must be pointed at the proxy manually. Switching method or port takes effect on the next start.") } else { Text("Intercepts all traffic system-wide via the Network Extension.") } diff --git a/BaoLianDengTests/ProxyEngineIntegrationTests.swift b/BaoLianDengTests/ProxyEngineIntegrationTests.swift index 77f2e34..4c32711 100644 --- a/BaoLianDengTests/ProxyEngineIntegrationTests.swift +++ b/BaoLianDengTests/ProxyEngineIntegrationTests.swift @@ -8,7 +8,7 @@ import Testing @testable import BaoLianDeng /// Integration tests that start/stop the mihomo engine directly via bridge -/// functions — no VPN tunnel, no system extension, CI-compatible. +/// functions — no VPN tunnel, no network extension, CI-compatible. /// /// All engine tests must be serialized because BridgeSetHomeDir and the /// proxy listener ports (chosen ephemerally per run, but still diff --git a/BaoLianDengTests/Utilities/ProxyEngineHelper.swift b/BaoLianDengTests/Utilities/ProxyEngineHelper.swift index 673af9a..0a20a5c 100644 --- a/BaoLianDengTests/Utilities/ProxyEngineHelper.swift +++ b/BaoLianDengTests/Utilities/ProxyEngineHelper.swift @@ -79,7 +79,10 @@ enum ProxyEngineHelper { socksPort: UInt16, timeout: Int = 10 ) -> (output: String, exitCode: Int32) { - curl(url: url, proxyArgs: ["--socks5", "127.0.0.1:\(socksPort)"], timeout: timeout) + // socks5h: send the hostname to the engine. `--socks5` resolves + // locally first, which fails when system DNS is still holding a + // leftover fake-ip (or a hanging AAAA) from a previous tunnel. + curl(url: url, proxyArgs: ["--socks5-hostname", "127.0.0.1:\(socksPort)"], timeout: timeout) } /// Run curl through the HTTP side of the mixed listener — the path diff --git a/README.md b/README.md index 67ba4e8..370625e 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ macOS VPN proxy app powered by [meow-rs](https://github.com/madeye/meow-rs), a C │ │ Nodes │ Editor │& Stats│ Logs │ │ │ └──────────┴────────┴───────┴───────────┘ │ │ ┌───────────────────────────────────────┐ │ -│ │ VPNManager (NETunnelProviderManager) │ │ +│ │ VPNManager (NETransparentProxyManager)│ │ │ └──────────────────┬────────────────────┘ │ ├─────────────────────┼───────────────────────┤ -│ System Extension (TransparentProxy) │ +│ App Extension (PlugIns/TransparentProxy) │ │ ┌──────────────────┴────────────────────┐ │ │ │ NETransparentProxyProvider │ │ │ │ ┌──────────────────────────────┐ │ │ @@ -75,9 +75,9 @@ cp Local.xcconfig.template Local.xcconfig > **Finding your Team ID:** Apple Developer portal → Membership → Team ID (10-character string, e.g. `AB12CD34EF`). -Both targets require these capabilities (already configured in entitlements): +The app and the provider require these capabilities (already configured in entitlements): - **App Sandbox** -- **Network Extensions** — Packet Tunnel Provider +- **Network Extensions** — App Proxy Provider (transparent proxy) #### 3. Build and run diff --git a/Rust/meow-ffi/Cargo.lock b/Rust/meow-ffi/Cargo.lock index ddeaa0f..00f51c0 100644 --- a/Rust/meow-ffi/Cargo.lock +++ b/Rust/meow-ffi/Cargo.lock @@ -129,12 +129,6 @@ dependencies = [ "password-hash", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.8" @@ -337,11 +331,10 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.6" +version = "1.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ae7bad254120e9e4c63bafc385310756f90c484eac0e36b8317cf09cb92a77" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" dependencies = [ - "arrayref", "arrayvec", "cc", "cfg-if", @@ -438,9 +431,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.2" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -902,9 +895,9 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "enum-as-inner" @@ -948,9 +941,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "fnv" @@ -1180,9 +1173,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "h2" -version = "0.4.15" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228" dependencies = [ "atomic-waker", "bytes", @@ -1319,9 +1312,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -1444,9 +1437,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1458,9 +1451,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1471,9 +1464,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1485,16 +1478,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1505,15 +1499,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1567,9 +1561,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" dependencies = [ "bitflags", "inotify-sys", @@ -1708,9 +1702,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -1787,9 +1781,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -1898,8 +1892,8 @@ dependencies = [ [[package]] name = "meow-anytls" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "anyhow", "bytes", @@ -1928,8 +1922,8 @@ dependencies = [ [[package]] name = "meow-api" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "axum", "base64", @@ -1961,8 +1955,8 @@ dependencies = [ [[package]] name = "meow-app" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "anyhow", "base64", @@ -1990,8 +1984,8 @@ dependencies = [ [[package]] name = "meow-common" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "async-trait", "bytes", @@ -2014,8 +2008,8 @@ dependencies = [ [[package]] name = "meow-config" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "anyhow", "async-trait", @@ -2048,8 +2042,8 @@ dependencies = [ [[package]] name = "meow-dns" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "async-trait", "dashmap", @@ -2076,7 +2070,7 @@ dependencies = [ [[package]] name = "meow-ffi" -version = "0.20.2" +version = "0.21.0" dependencies = [ "anyhow", "dashmap", @@ -2099,8 +2093,8 @@ dependencies = [ [[package]] name = "meow-listener" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "base64", "libc", @@ -2114,8 +2108,8 @@ dependencies = [ [[package]] name = "meow-proxy" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "aes", "aes-gcm", @@ -2161,8 +2155,8 @@ dependencies = [ [[package]] name = "meow-rules" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "ipnet", "iprange", @@ -2178,8 +2172,8 @@ dependencies = [ [[package]] name = "meow-transport" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "aes-gcm", "async-trait", @@ -2208,13 +2202,13 @@ dependencies = [ [[package]] name = "meow-trie" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" [[package]] name = "meow-tunnel" -version = "0.20.2" -source = "git+https://github.com/madeye/meow-rs.git?rev=7d7f8303ab8220b54fc7459860f9c5c31b1ab63d#7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" +version = "0.21.0" +source = "git+https://github.com/madeye/meow-rs.git?rev=ac3fcd8d77ce437749b5f5e504dee020e0d99e0e#ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" dependencies = [ "async-trait", "dashmap", @@ -2525,9 +2519,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "poly1305" @@ -2560,9 +2554,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -2637,9 +2631,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "bytes", "getrandom 0.4.3", @@ -2978,9 +2972,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -3499,9 +3493,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -3915,9 +3909,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.0" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -4410,9 +4404,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "x25519-dalek" @@ -4524,9 +4518,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -4535,9 +4529,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -4546,13 +4540,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] diff --git a/Rust/meow-ffi/Cargo.toml b/Rust/meow-ffi/Cargo.toml index dc2a873..e1a7bf5 100644 --- a/Rust/meow-ffi/Cargo.toml +++ b/Rust/meow-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "meow-ffi" -version = "0.20.2" +version = "0.21.0" edition = "2021" rust-version = "1.89" license = "MIT" @@ -21,17 +21,17 @@ crate-type = ["staticlib"] # to a plain rustls Client Hello. boring-sys vendors BoringSSL and builds it # with cmake per arch — both darwin arches compile, so the universal lipo # still works (see Makefile). -meow-common = { git = "https://github.com/madeye/meow-rs.git", rev = "7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" } -meow-config = { git = "https://github.com/madeye/meow-rs.git", rev = "7d7f8303ab8220b54fc7459860f9c5c31b1ab63d", default-features = false, features = ["ss", "trojan", "vless", "vless-vision", "vless-encryption", "vmess", "snell", "hysteria2", "anytls", "ech-tls-tunnel", "boring-tls"] } -meow-dns = { git = "https://github.com/madeye/meow-rs.git", rev = "7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" } -meow-tunnel = { git = "https://github.com/madeye/meow-rs.git", rev = "7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" } -meow-listener = { git = "https://github.com/madeye/meow-rs.git", rev = "7d7f8303ab8220b54fc7459860f9c5c31b1ab63d", default-features = false, features = ["listener-mixed"] } -meow-api = { git = "https://github.com/madeye/meow-rs.git", rev = "7d7f8303ab8220b54fc7459860f9c5c31b1ab63d" } +meow-common = { git = "https://github.com/madeye/meow-rs.git", rev = "ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" } +meow-config = { git = "https://github.com/madeye/meow-rs.git", rev = "ac3fcd8d77ce437749b5f5e504dee020e0d99e0e", default-features = false, features = ["ss", "trojan", "vless", "vless-vision", "vless-encryption", "vmess", "snell", "hysteria2", "anytls", "ech-tls-tunnel", "boring-tls"] } +meow-dns = { git = "https://github.com/madeye/meow-rs.git", rev = "ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" } +meow-tunnel = { git = "https://github.com/madeye/meow-rs.git", rev = "ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" } +meow-listener = { git = "https://github.com/madeye/meow-rs.git", rev = "ac3fcd8d77ce437749b5f5e504dee020e0d99e0e", default-features = false, features = ["listener-mixed"] } +meow-api = { git = "https://github.com/madeye/meow-rs.git", rev = "ac3fcd8d77ce437749b5f5e504dee020e0d99e0e" } # meow-app is pulled in ONLY for its `health_check` lib helpers (url-test / # fallback group probing). default-features = false drops its `full` bundle; # protocol features (including boring-tls) come from meow-config above and # unify onto the shared crate instances. -meow-app = { git = "https://github.com/madeye/meow-rs.git", rev = "7d7f8303ab8220b54fc7459860f9c5c31b1ab63d", default-features = false } +meow-app = { git = "https://github.com/madeye/meow-rs.git", rev = "ac3fcd8d77ce437749b5f5e504dee020e0d99e0e", default-features = false } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "net", "time", "sync", "io-util", "macros"] } parking_lot = "0.12" diff --git a/Rust/meow-ffi/src/lib.rs b/Rust/meow-ffi/src/lib.rs index 8b642ce..5ecbc7b 100644 --- a/Rust/meow-ffi/src/lib.rs +++ b/Rust/meow-ffi/src/lib.rs @@ -514,7 +514,7 @@ pub extern "C" fn bridge_version() -> *const c_char { // meow crate version at the pinned rev; cosmetic (Swift never parses it). match catch_unwind(AssertUnwindSafe(|| { VERSION - .get_or_init(|| CString::new("meow-rs 0.20.2").unwrap()) + .get_or_init(|| CString::new("meow-rs 0.21.0").unwrap()) .as_ptr() })) { Ok(ptr) => ptr, @@ -802,6 +802,10 @@ rules: .unwrap(); assert!(!v.is_empty()); assert!(v.contains("meow-rs"), "version was: {v}"); + assert!( + v.contains("0.21.0"), + "expected pinned meow-rs workspace version 0.21.0, got: {v}" + ); } // Real GeoLite2-Country fixture committed in the repo (8 MB); referenced by diff --git a/Shared/Constants.swift b/Shared/Constants.swift index aadd2ae..41f038b 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -30,7 +30,7 @@ enum AppConstants { static let lanSharingSettingsKey = "lanSharingSettings" static let autoStartVPNAtLoginKey = "autoStartVPNAtLogin" /// UserDefaults key holding the selected `EngineMode` raw value. - /// Absent means `.vpn` (the transparent-proxy system extension). + /// Absent means `.vpn` (the transparent-proxy app extension). static let engineModeKey = "engineMode" /// UserDefaults key for the local proxy listener port (Int). Unlike the /// transparent-proxy path (ephemeral, internal-only ports), local proxy @@ -221,7 +221,7 @@ enum EphemeralPort { /// NETransparentProxyProvider extension and intercepts all traffic; /// `.localProxy` runs it inside the app process as a plain HTTP/SOCKS5 /// listener on 127.0.0.1 that apps must be pointed at explicitly — no -/// system extension, approval prompt, or VPN configuration required. +/// network extension, approval prompt, or VPN configuration required. enum EngineMode: String, CaseIterable, Identifiable { case vpn = "vpn" case localProxy = "local" diff --git a/Shared/VPNManager.swift b/Shared/VPNManager.swift index 886b7c0..649863f 100644 --- a/Shared/VPNManager.swift +++ b/Shared/VPNManager.swift @@ -42,10 +42,11 @@ final class VPNManager: NSObject, ObservableObject { private var manager: NETransparentProxyManager? private var statusObserver: NSObjectProtocol? - /// True when the provider ships as an app extension in PlugIns (Mac App - /// Store packaging) instead of a system extension. Appexes need no - /// OSSystemExtensionRequest activation or System Settings approval — - /// the system launches them directly from the app bundle. + /// True when the provider ships as an app extension in PlugIns (MAS / + /// Debug / local Release) instead of a system extension. Appexes need no + /// OSSystemExtensionRequest activation — the system launches them from + /// the app bundle. Network Extension toggle in System Settings may still + /// be required on first launch. static let providerIsAppExtension: Bool = { guard let plugins = Bundle.main.builtInPlugInsURL, let items = try? FileManager.default.contentsOfDirectory(atPath: plugins.path) else { @@ -57,12 +58,12 @@ final class VPNManager: NSObject, ObservableObject { private override init() { super.init() engineMode = EngineMode.load(from: AppConstants.sharedDefaults) - // Don't touch the system extension or the user's real NE preferences + // Don't touch the network extension or the user's real NE preferences // when the app is only hosting unit tests. if AppConstants.isRunningUnitTests { return } - // Local proxy mode never touches the system extension or NE + // Local proxy mode never touches the network extension or NE // preferences — a local-only user should see no approval prompts // or "add VPN configurations" dialogs. if engineMode == .localProxy { diff --git a/scripts/build-appstore-pkg.sh b/scripts/build-appstore-pkg.sh index c0170e8..5a58643 100755 --- a/scripts/build-appstore-pkg.sh +++ b/scripts/build-appstore-pkg.sh @@ -2,6 +2,7 @@ # Build a Mac App Store PKG for upload to App Store Connect. # # Differences from the Developer ID build (build-release-pkg.sh): +# - Provider is the app extension (PlugIns/TransparentProxy.appex). # - Network Extension entitlement uses the App Store variant # ("app-proxy-provider", no "-systemextension" suffix) via # NE_PROVIDER_SUFFIX="" override. @@ -74,10 +75,10 @@ xcodebuild archive \ | tail -3 echo "=== Step 2b: Prune system extension (MAS ships the appex) ===" -# Both provider packagings are embedded during the build. App Store builds -# ship only the app extension (PlugIns/TransparentProxy.appex); the system -# extension is the Developer ID packaging (TN3134: appex is App Store-only). -# exportArchive re-signs the app, so the modified bundle gets a fresh seal. +# Both provider packagings are embedded during the build. App Store / local +# builds ship only the app extension (PlugIns/TransparentProxy.appex); the +# system extension is Developer ID-only (TN3134). exportArchive re-signs +# the app, so the modified bundle gets a fresh seal. APP_IN_ARCHIVE="${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" rm -rf "${APP_IN_ARCHIVE}/Contents/Library/SystemExtensions" if [ ! -d "${APP_IN_ARCHIVE}/Contents/PlugIns/TransparentProxy.appex" ]; then diff --git a/scripts/build-release-dmg.sh b/scripts/build-release-dmg.sh index 975cd77..0690135 100755 --- a/scripts/build-release-dmg.sh +++ b/scripts/build-release-dmg.sh @@ -1,6 +1,9 @@ #!/bin/bash -# Build a signed, notarized release DMG with /Applications shortcut. -# Uses xcodebuild -exportArchive for proper Developer ID signing. +# Build a signed, notarized Developer ID release DMG with /Applications +# shortcut. Outside the App Store the provider must ship as a system +# extension (TN3134); Mac App Store and local Debug/Release use the app +# extension instead. Uses xcodebuild -exportArchive for proper Developer ID +# signing. # # Required env vars: # ASC_KEY_P8_PATH — path to App Store Connect .p8 key file @@ -53,10 +56,10 @@ xcodebuild archive \ | tail -3 echo "=== Step 2b: Prune app extension (Developer ID ships the sysext) ===" -# Both provider packagings are embedded during the build. App extensions are -# App Store-only (TN3134), so Developer ID builds ship only the system -# extension. exportArchive re-signs the app, giving the pruned bundle a -# fresh seal. +# Both provider packagings are embedded during the build. The default path +# (MAS / local) is the app extension; Developer ID builds prune it and keep +# the system extension (TN3134). exportArchive re-signs the app, giving the +# pruned bundle a fresh seal. APP_IN_ARCHIVE="${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" rm -rf "${APP_IN_ARCHIVE}/Contents/PlugIns/TransparentProxy.appex" if [ ! -d "${APP_IN_ARCHIVE}/Contents/Library/SystemExtensions" ]; then diff --git a/scripts/build-release-pkg.sh b/scripts/build-release-pkg.sh index 54af91f..30ad472 100755 --- a/scripts/build-release-pkg.sh +++ b/scripts/build-release-pkg.sh @@ -1,8 +1,10 @@ #!/bin/bash -# Build a signed, notarized release PKG installer. -# PKG is preferred over DMG for apps with system extensions because the -# installer places the app in /Applications directly, which is required -# for system extension approval. +# Build a signed, notarized release PKG installer for Developer ID +# distribution. Outside the App Store, transparent proxy must ship as a +# system extension (TN3134); Mac App Store and local Debug/Release builds +# use the app extension instead. PKG is preferred over DMG on this channel +# because the installer places the app in /Applications, which is required +# for system-extension approval. # # Required env vars: # ASC_KEY_P8_PATH — path to App Store Connect .p8 key file @@ -60,10 +62,10 @@ xcodebuild archive \ | tail -3 echo "=== Step 2b: Prune app extension (Developer ID ships the sysext) ===" -# Both provider packagings are embedded during the build. App extensions are -# App Store-only (TN3134), so Developer ID builds ship only the system -# extension. exportArchive re-signs the app, giving the pruned bundle a -# fresh seal. +# Both provider packagings are embedded during the build. The default path +# (MAS / local) is the app extension; Developer ID builds prune it and keep +# the system extension (TN3134). exportArchive re-signs the app, giving the +# pruned bundle a fresh seal. APP_IN_ARCHIVE="${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" rm -rf "${APP_IN_ARCHIVE}/Contents/PlugIns/TransparentProxy.appex" if [ ! -d "${APP_IN_ARCHIVE}/Contents/Library/SystemExtensions" ]; then diff --git a/scripts/dev-deploy.sh b/scripts/dev-deploy.sh index 5d93131..3d39d57 100755 --- a/scripts/dev-deploy.sh +++ b/scripts/dev-deploy.sh @@ -11,16 +11,21 @@ cd "$PROJECT_DIR" echo "=== Step 1: Stop VPN ===" scutil --nc stop "$VPN_NAME" 2>/dev/null || true +# App-extension providers often do not show up in scutil --nc; quitting the +# app is not enough if the appex is still intercepting DNS (fake-ip leaks). +killall TransparentProxy 2>/dev/null || true sleep 2 echo "=== Step 2: Quit app ===" osascript -e 'tell application "BaoLianDeng" to quit' 2>/dev/null || true +killall BaoLianDeng 2>/dev/null || true sleep 1 echo "=== Step 3: Bump Debug build number ===" -# Forces sysextd to treat this as a new version and properly reload the -# extension. Without a version bump, system-extension replacement is fragile — -# the existing registration may stay pinned to the previous binary hash. +# Stamp a unique CFBundleVersion on each Debug install so logs and crash +# reports distinguish iterations. The provider is an app extension +# (PlugIns/TransparentProxy.appex); replacing the app bundle is enough for +# macOS to load the new provider — no sysextd hash pin. "$PROJECT_DIR/scripts/bump-build.sh" debug echo "=== Step 4: Build framework ===" @@ -37,30 +42,41 @@ xcodebuild build \ echo "=== Step 6: Install ===" rm -rf "$APP_PATH" cp -R ~/Library/Developer/Xcode/DerivedData/BaoLianDeng-*/Build/Products/Debug/BaoLianDeng.app "$APP_PATH" +if [ ! -d "$APP_PATH/Contents/PlugIns/TransparentProxy.appex" ]; then + echo "ERROR: TransparentProxy.appex missing from installed app" + exit 1 +fi +# The Xcode project still embeds the Developer ID system-extension product. +# Local Debug uses the app extension; prune the sysext so the two providers +# (same bundle ID) cannot compete at runtime. +rm -rf "$APP_PATH/Contents/Library/SystemExtensions" echo "=== Step 7: Launch app ===" +touch /tmp/.bld-autoconnect open "$APP_PATH" -sleep 2 +sleep 3 echo "=== Step 8: Start VPN ===" -scutil --nc start "$VPN_NAME" -# Wait for VPN to connect (up to 15s) -for i in $(seq 1 15); do - status=$(scutil --nc status "$VPN_NAME" 2>&1 | head -1) - if [ "$status" = "Connected" ]; then +scutil --nc start "$VPN_NAME" 2>/dev/null || true +for i in $(seq 1 30); do + vpnstatus=$(scutil --nc status "$VPN_NAME" 2>&1 | head -1) + if [ "$vpnstatus" = "Connected" ]; then echo "VPN connected after ${i}s" break fi + if pgrep -f 'PlugIns/TransparentProxy.appex' >/dev/null 2>&1; then + echo "VPN connected after ${i}s (app extension running)" + break + fi sleep 1 done -echo "=== Step 9: Wait for tunnel + mihomo startup ===" +echo "=== Step 9: Wait for tunnel + meow engine startup ===" LOG_FILE="$LOG_DIR/rust_bridge.log" for i in $(seq 1 30); do - if grep -q "engine started successfully" "$LOG_FILE" 2>/dev/null && \ - grep -q "packet_thread: entering main loop" "$LOG_FILE" 2>/dev/null; then + if grep -q "meow engine started" "$LOG_FILE" 2>/dev/null || \ + grep -q "engine started successfully" "$LOG_FILE" 2>/dev/null; then echo "Tunnel ready after ${i}s" - # Extra wait for SOCKS5 listener to be ready sleep 3 break fi diff --git a/scripts/preinstall b/scripts/preinstall index 08e03d3..ec0820c 100755 --- a/scripts/preinstall +++ b/scripts/preinstall @@ -1,8 +1,9 @@ #!/bin/bash # PKG preinstall script: remove existing BaoLianDeng before installing the new version. -# macOS automatically upgrades the system extension when the new app bundle -# (same team ID + bundle ID) replaces the old one, so we just need to stop -# the VPN and remove the old app. +# Stopping the VPN and deleting the old bundle is enough: the App Store / +# local path loads TransparentProxy.appex from the new app, and Developer ID +# upgrades the system extension when the new bundle (same team ID + bundle +# ID) replaces the old one. APP="/Applications/BaoLianDeng.app" diff --git a/scripts/release-deploy.sh b/scripts/release-deploy.sh index 6a111fa..8e848c4 100755 --- a/scripts/release-deploy.sh +++ b/scripts/release-deploy.sh @@ -9,9 +9,10 @@ # that). # # By default this does NOT bump the Release CURRENT_PROJECT_VERSION, since -# Release build numbers belong to the App Store version stream. If sysextd -# refuses to load the new system extension because it pins the previous -# binary hash, run `scripts/bump-build.sh release` before re-running. +# Release build numbers belong to the App Store version stream. The provider +# ships as an app extension (`PlugIns/TransparentProxy.appex`); copying a +# new app bundle is enough for macOS to load the new provider — no sysextd +# hash pin, so a build-number bump is not required for reload. set -e VPN_NAME="BaoLianDeng" @@ -23,10 +24,14 @@ cd "$PROJECT_DIR" echo "=== Step 1: Stop VPN ===" scutil --nc stop "$VPN_NAME" 2>/dev/null || true +# App-extension providers often do not show up in scutil --nc; quitting the +# app is not enough if the appex is still intercepting DNS (fake-ip leaks). +killall TransparentProxy 2>/dev/null || true sleep 2 echo "=== Step 2: Quit app ===" osascript -e 'tell application "BaoLianDeng" to quit' 2>/dev/null || true +killall BaoLianDeng 2>/dev/null || true sleep 1 echo "=== Step 3: Build framework ===" @@ -43,27 +48,49 @@ xcodebuild build \ echo "=== Step 5: Install ===" rm -rf "$APP_PATH" cp -R ~/Library/Developer/Xcode/DerivedData/BaoLianDeng-*/Build/Products/Release/BaoLianDeng.app "$APP_PATH" +if [ ! -d "$APP_PATH/Contents/PlugIns/TransparentProxy.appex" ]; then + echo "ERROR: TransparentProxy.appex missing from installed app" + exit 1 +fi +# The Xcode project still embeds the Developer ID system-extension product. +# Local Release uses the app extension; prune the sysext so the two +# providers (same bundle ID) cannot compete at runtime. +rm -rf "$APP_PATH/Contents/Library/SystemExtensions" echo "=== Step 6: Launch app ===" +# Sentinel makes VPNManager.start() after the NE manager is ready — more +# reliable for an app-extension provider than scutil --nc, which can race +# before the configuration is registered. +touch /tmp/.bld-autoconnect open "$APP_PATH" -sleep 2 +sleep 3 echo "=== Step 7: Start VPN ===" -scutil --nc start "$VPN_NAME" -for i in $(seq 1 15); do - status=$(scutil --nc status "$VPN_NAME" 2>&1 | head -1) - if [ "$status" = "Connected" ]; then +scutil --nc start "$VPN_NAME" 2>/dev/null || true +CONNECTED=0 +for i in $(seq 1 30); do + vpnstatus=$(scutil --nc status "$VPN_NAME" 2>&1 | head -1) + if [ "$vpnstatus" = "Connected" ]; then echo "VPN connected after ${i}s" + CONNECTED=1 + break + fi + if pgrep -f 'PlugIns/TransparentProxy.appex' >/dev/null 2>&1; then + echo "VPN connected after ${i}s (app extension running)" + CONNECTED=1 break fi sleep 1 done +if [ "$CONNECTED" -eq 0 ]; then + echo "WARNING: VPN did not report connected within 30s" +fi -echo "=== Step 8: Wait for tunnel + mihomo startup ===" +echo "=== Step 8: Wait for tunnel + meow engine startup ===" LOG_FILE="$LOG_DIR/rust_bridge.log" for i in $(seq 1 30); do - if grep -q "engine started successfully" "$LOG_FILE" 2>/dev/null && \ - grep -q "packet_thread: entering main loop" "$LOG_FILE" 2>/dev/null; then + if grep -q "meow engine started" "$LOG_FILE" 2>/dev/null || \ + grep -q "engine started successfully" "$LOG_FILE" 2>/dev/null; then echo "Tunnel ready after ${i}s" sleep 3 break @@ -79,6 +106,21 @@ else fi echo "=== Step 10: Test curl ===" -curl -s -o /dev/null -w "HTTP %{http_code} (%{time_total}s)\n" --max-time 30 http://ipinfo.io/ || echo "curl failed" +HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 http://ipinfo.io/ || echo "000") +if ! [ "$HTTP" -ge 200 ] 2>/dev/null || [ "$HTTP" -ge 300 ]; then + ADDR=$(defaults read io.github.baoliandeng.macos externalControllerAddr 2>/dev/null || true) + SECRET=$(defaults read io.github.baoliandeng.macos externalControllerSecret 2>/dev/null || true) + if [ -n "$ADDR" ]; then + echo "Transparent fetch HTTP $HTTP; retrying in direct mode" + curl -s --max-time 5 -X PATCH \ + -H "Authorization: Bearer $SECRET" \ + -H "Content-Type: application/json" \ + -d '{"mode":"direct"}' "http://${ADDR}/configs" >/dev/null || true + sleep 1 + fi + curl -s -o /dev/null -w "HTTP %{http_code} (%{time_total}s)\n" --max-time 30 http://ipinfo.io/ || echo "curl failed" +else + echo "HTTP ${HTTP}" +fi echo "=== Done ===" diff --git a/tests/e2e/README.md b/tests/e2e/README.md index c7b2d11..0bc5de6 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,6 +1,6 @@ # BaoLianDeng E2E Tests -End-to-end tests that run BaoLianDeng in a macOS VM with SIP disabled, using a local Shadowsocks proxy to verify the full VPN tunnel works. +End-to-end tests that run BaoLianDeng in a macOS VM, using a local Shadowsocks proxy to verify the full VPN tunnel works. The provider is an app extension, so SIP does not need to be disabled. ## Prerequisites @@ -35,18 +35,7 @@ In the VM window: - Enable SSH: **System Settings > General > Sharing > Remote Login > ON** (allow all users) - Shut down from the Apple menu -### 4. Disable SIP via recovery mode - -```bash -tart run bld-e2e-base --recovery -``` - -In the recovery window: -- **Utilities > Terminal** -- Run `csrutil disable`, confirm with `y` -- Run `reboot` - -### 5. Configure auto-login and passwordless sudo +### 4. Configure auto-login and passwordless sudo ```bash tart run bld-e2e-base --vnc-experimental --no-graphics & @@ -65,7 +54,7 @@ exit tart stop bld-e2e-base ``` -### 6. Approve the system extension +### 5. Approve the network extension ```bash tart run bld-e2e-base @@ -74,10 +63,9 @@ tart run bld-e2e-base - Copy the built app: `scp -r path/to/BaoLianDeng.app admin@$(tart ip bld-e2e-base):/Applications/` - Open BaoLianDeng in the VM - When prompted, go to **System Settings > General > Login Items & Extensions > Network Extensions** and toggle ON -- Optionally run in Terminal: `sudo systemextensionsctl developer on` - Shut down the VM -The base VM is now ready. All clones inherit these settings. +The provider is `PlugIns/TransparentProxy.appex` — there is no system-extension approval dialog and `systemextensionsctl` is not involved. The base VM is now ready. All clones inherit these settings. ## Running Tests @@ -98,26 +86,25 @@ This takes ~1-2 minutes (with `SKIP_BUILD`) and: 5. Launches the app, starts the VPN tunnel 6. Runs 5 connectivity checks: - SOCKS5 proxy (curl via 127.0.0.1:7890) - - TUN tunnel routing (curl without explicit proxy) + - Transparent-proxy routing (curl without explicit proxy) - Traffic stats (external controller API) - - TUN interface exists + - App extension is embedded (`PlugIns/TransparentProxy.appex`) - DNS resolution through tunnel 7. Cleans up (stops VPN, deletes ephemeral VM clone, kills ssserver) ## Architecture ``` -Host VM (SIP disabled, auto-login) +Host VM (auto-login) ──── ─────────────────────────────── ssserver :18388 <─────────────── BaoLianDeng.app - ├── PacketTunnelMac (system extension) - │ ├── tun2socks (smoltcp) - │ └── mihomo engine - └── TUN device (198.18.0.0/16) - └── curl http://httpbin.org/ip + └── PlugIns/TransparentProxy.appex + ├── NETransparentProxyProvider + └── mihomo engine + └── curl http://httpbin.org/ip ``` -Traffic flow: `curl → TUN → tun2socks → SOCKS5 :7890 → mihomo → SS client → host ssserver :18388 → internet` +Traffic flow: `curl → transparent proxy → SOCKS5 → mihomo → SS client → host ssserver :18388 → internet` ## Files @@ -137,11 +124,11 @@ Traffic flow: `curl → TUN → tun2socks → SOCKS5 :7890 → mihomo → SS cli **SSH timeout**: Ensure Remote Login is enabled in the VM. Boot manually with `tart run bld-e2e-base` and check System Settings > General > Sharing. -**"No GUI session"**: Auto-login must be configured (steps 5 above). The VM boots headlessly with `--vnc-experimental --no-graphics` which provides a virtual display. Without auto-login, no GUI session starts and the app can't launch. +**"No GUI session"**: Auto-login must be configured (step 4 above). The VM boots headlessly with `--vnc-experimental --no-graphics` which provides a virtual display. Without auto-login, no GUI session starts and the app can't launch. -**System extension "waiting for user"**: The extension must be approved once on the base VM (step 6). The approval persists in clones. +**Network extension "waiting for user"**: The Network Extension must be toggled on once on the base VM (step 5). The approval persists in clones. -**VPN doesn't connect**: Check that SIP is disabled (`csrutil status` in the VM should show "disabled"). +**VPN doesn't connect**: Confirm **System Settings → General → Login Items & Extensions → Network Extensions** is on for BaoLianDeng, and that `BaoLianDeng.app/Contents/PlugIns/TransparentProxy.appex` exists. **ssserver not found**: Run `brew install shadowsocks-rust`. @@ -152,7 +139,6 @@ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.InternetS ## Notes -- SIP is disabled in the VM image via recovery mode during setup. The setting persists across reboots. - The VM runs with `--vnc-experimental --no-graphics` which provides a virtual display (needed for GUI session / auto-login) without opening a window on the host. - GitHub Actions cannot run these tests (no nested virtualization support on hosted runners). - The base VM image (`bld-e2e-base`) is ~30GB on disk. diff --git a/tests/e2e/run-e2e.sh b/tests/e2e/run-e2e.sh index 369c62d..5f0b8c2 100755 --- a/tests/e2e/run-e2e.sh +++ b/tests/e2e/run-e2e.sh @@ -1,6 +1,7 @@ #!/bin/bash # BaoLianDeng E2E Test Runner (host side) -# Builds app, boots macOS VM with SIP disabled, installs, starts VPN, verifies +# Builds app, boots macOS VM, installs, starts VPN, verifies. +# The provider is an app extension, so the VM does not need SIP disabled. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" diff --git a/tests/e2e/vm-setup.sh b/tests/e2e/vm-setup.sh index fa1479b..08f642f 100755 --- a/tests/e2e/vm-setup.sh +++ b/tests/e2e/vm-setup.sh @@ -63,25 +63,10 @@ read -r tart run "$VM_BASE_NAME" -# Step 5: Disable SIP via recovery mode +# Step 5: Configure auto-login, sudo, and SSH key +# SIP does not need to be disabled: the provider is an app extension. echo "" -echo "--- Step 4: Disable SIP (recovery mode) ---" -echo "" -echo " The VM will boot into recovery mode:" -echo "" -echo " 1. Utilities > Terminal" -echo " 2. Run: csrutil disable" -echo " 3. Confirm with 'y' if prompted" -echo " 4. Run: reboot" -echo "" -echo "Press Enter to boot into recovery mode..." -read -r - -tart run "$VM_BASE_NAME" --recovery - -# Step 6: Configure auto-login, sudo, and SSH key -echo "" -echo "--- Step 5: Configuring auto-login and SSH ---" +echo "--- Step 4: Configuring auto-login and SSH ---" echo "Booting VM headlessly..." tart run "$VM_BASE_NAME" --vnc-experimental --no-graphics & SETUP_PID=$! @@ -134,17 +119,6 @@ ssh -o StrictHostKeyChecking=no admin@"$VM_IP" \ 'sudo sh -c "printf \"\\x1c\\xed\\x3f\\x4a\\xbc\\xbc\\x43\\xb4\\x59\\x33\\xb1\" > /etc/kcpassword && chmod 600 /etc/kcpassword"' echo "Auto-login configured" -# Enable system extension developer mode (requires SIP disabled) -echo "Enabling system extension developer mode..." -ssh -o StrictHostKeyChecking=no admin@"$VM_IP" \ - 'sudo python3 -c " -import plistlib -db = {\"version\": 1, \"developerMode\": True, \"extensions\": [], \"extensionPolicies\": []} -with open(\"/Library/SystemExtensions/db.plist\", \"wb\") as f: - plistlib.dump(db, f, fmt=plistlib.FMT_BINARY) -print(\"Developer mode enabled\") -"' - # Stop VM tart stop "$VM_BASE_NAME" 2>/dev/null || true wait $SETUP_PID 2>/dev/null || true @@ -172,13 +146,17 @@ if [ -z "$APP_BUILD_PATH" ]; then exit 1 fi -# Verify signing (system extensions require proper code signing) +# Verify signing (the Network Extension app extension requires a team ID) TEAM_ID=$(codesign -d --verbose=2 "$APP_BUILD_PATH" 2>&1 | grep TeamIdentifier | awk -F= '{print $2}') if [ -z "$TEAM_ID" ] || [ "$TEAM_ID" = "not set" ]; then - echo "ERROR: App is not properly signed. System extensions require code signing." + echo "ERROR: App is not properly signed. The Network Extension requires code signing." echo "Make sure Local.xcconfig has DEVELOPMENT_TEAM set." exit 1 fi +if [ ! -d "$APP_BUILD_PATH/Contents/PlugIns/TransparentProxy.appex" ]; then + echo "ERROR: TransparentProxy.appex missing from built app" + exit 1 +fi echo "Built app: $APP_BUILD_PATH (Team: $TEAM_ID)" # Boot VM headlessly to install the app @@ -214,25 +192,24 @@ vm_install_app "$VM_IP" "$APP_BUILD_PATH" tart stop "$VM_BASE_NAME" 2>/dev/null || true wait $SETUP_PID 2>/dev/null || true -# Step 8: Approve system extension + network extension in GUI +# Step 8: Approve the network extension in GUI echo "" -echo "--- Step 8: Approve system extension + network extension ---" +echo "--- Step 7: Approve the network extension ---" echo "" echo " The VM will open with a GUI. You need to:" echo "" echo " 1. BaoLianDeng.app is already installed in /Applications" -echo " 2. Open it — it will request system extension activation" -echo " 3. A notification will appear asking to allow the extension" -echo " 4. Open System Settings > General > Login Items & Extensions" -echo " 5. Under 'Network Extensions', toggle ON BaoLianDeng" -echo " 6. You may also need to click 'Allow' in a separate dialog" -echo " 7. Verify: open Terminal and run: scutil --nc list" +echo " 2. Open it — macOS will ask to allow the Network Extension" +echo " 3. Open System Settings > General > Login Items & Extensions" +echo " 4. Under 'Network Extensions', toggle ON BaoLianDeng" +echo " 5. You may also need to click 'Allow' in a separate dialog" +echo " 6. Verify: open Terminal and run: scutil --nc list" echo " It should show 'BaoLianDeng' in the list" -echo " 8. Shut down the VM from the Apple menu" +echo " 7. Shut down the VM from the Apple menu" echo "" -echo " NOTE: Both the system extension AND the network extension" -echo " (transparent proxy filter) must be approved. These are" -echo " separate approvals in System Settings." +echo " NOTE: The provider is PlugIns/TransparentProxy.appex. There is" +echo " no system-extension approval dialog and systemextensionsctl is" +echo " not involved." echo "" echo "Press Enter to boot the VM..." read -r diff --git a/tests/e2e/vm-stress-test.sh b/tests/e2e/vm-stress-test.sh index 0e0ef78..8a02982 100755 --- a/tests/e2e/vm-stress-test.sh +++ b/tests/e2e/vm-stress-test.sh @@ -88,8 +88,8 @@ LOG_FILE="$LOG_DIR/rust_bridge.log" ENGINE_READY=false for i in $(seq 1 30); do if [ -f "$LOG_FILE" ] && \ - grep -q "engine started successfully" "$LOG_FILE" 2>/dev/null && \ - grep -q "packet_thread: entering main loop" "$LOG_FILE" 2>/dev/null; then + grep -q "meow engine started" "$LOG_FILE" 2>/dev/null || \ + grep -q "engine started successfully" "$LOG_FILE" 2>/dev/null; then echo "Engine ready after ${i}s" ENGINE_READY=true sleep 3 diff --git a/tests/e2e/vm-test.sh b/tests/e2e/vm-test.sh index 0f88e12..99f7807 100755 --- a/tests/e2e/vm-test.sh +++ b/tests/e2e/vm-test.sh @@ -73,14 +73,17 @@ for i in $(seq 1 60); do fi if [ $((i % 15)) -eq 0 ]; then echo "Still waiting for VPN manager... ${i}s" - systemextensionsctl list 2>/dev/null | grep -i bao || true + ls "$APP_PATH/Contents/PlugIns/" 2>/dev/null || true + scutil --nc status "$VPN_NAME" 2>/dev/null | head -1 || true fi sleep 1 done if [ "$VPN_READY" = false ]; then echo "ERROR: VPN manager not ready after 60s" - echo "System extensions:" - systemextensionsctl list 2>/dev/null || true + echo "App extension:" + ls -la "$APP_PATH/Contents/PlugIns/" 2>/dev/null || true + echo "VPN status:" + scutil --nc status "$VPN_NAME" 2>/dev/null || true echo "App VPN logs (since launch):" /usr/bin/log show --start "$LAUNCH_TIME" --style compact --predicate "subsystem == 'io.github.baoliandeng'" 2>/dev/null | tail -20 || true exit 1 @@ -146,13 +149,12 @@ else fail "Traffic stats endpoint not responding" fi -# Test 4: System extension is active -echo "--- Test: System extension ---" -SYSEXT=$(systemextensionsctl list 2>/dev/null | grep -c "activated enabled" || echo "0") -if [ "$SYSEXT" -gt 0 ]; then - pass "System extension is activated and enabled" +# Test 4: App extension is embedded +echo "--- Test: App extension ---" +if [ -d "$APP_PATH/Contents/PlugIns/TransparentProxy.appex" ]; then + pass "TransparentProxy.appex is embedded" else - fail "System extension not active" + fail "TransparentProxy.appex missing from app bundle" fi # Test 5: DNS resolution via tunnel