From 081a149077a1144649ce27ec58e63b13794d7c82 Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sat, 1 Aug 2026 04:22:44 -0700 Subject: [PATCH 1/8] Add Windows close-to-tray support On Windows, clicking the console window's close (X) button now keeps Aether running in the background instead of exiting. A tray icon with an Exit item appears in the notification area. The feature is enabled by default on Windows; set AETHER_CLOSE_TO_TRAY=0 to restore the old close-to-exit behavior. - New tray.rs module (Windows-only) with console Ctrl handler + tray icon - Windows-only tray-icon dependency (0.24, common-controls-v6) - Updated README.md and README.fa.md with feature docs - Updated Cargo.lock with tray-icon dependency --- README.fa.md | 3 + README.md | 3 + aether/Cargo.lock | 852 ++++++++++++++++++++++++++++++++++++++++++--- aether/Cargo.toml | 3 + aether/src/main.rs | 5 + aether/src/tray.rs | 150 ++++++++ 6 files changed, 965 insertions(+), 51 deletions(-) create mode 100644 aether/src/tray.rs diff --git a/README.fa.md b/README.fa.md index e33edc6..9ccf984 100644 --- a/README.fa.md +++ b/README.fa.md @@ -19,6 +19,7 @@ Aether یک کلاینت دور زدن سانسور اینترنت است که - استتار ترافیک - اتصال مجدد خودکار، و اتصال سریع به آخرین گیت‌وی سالم برای رد شدن از اسکن دوباره - پراکسی محلی SOCKS5 +- close-to-tray در ویندوز: بستن پنجره‌ی ترمینال تونل را در پس‌زمینه زنده نگه می‌دارد و آیکونی در سینی سیستم نمایش می‌دهد (با `AETHER_CLOSE_TO_TRAY=0` قابل غیرفعال‌سازی است) - پرچم‌های خط فرمان، متغیرهای محیطی، یا سؤال‌های تعاملی — هرکدوم رو که بخوای - پشتیبانی از Linux، Windows، macOS و Android (Termux) @@ -122,6 +123,8 @@ docker run -it -p 127.0.0.1:1819:1819 -v aether-data:/data aether روی ویندوز، به‌جای اون کافیه روی `run-aether.bat` (که همراه zip ریلیز میاد) دابل‌کلیک کنی — یه ترمینال باز می‌کنه، `aether.exe` رو اجرا می‌کنه، و بعدش پنجره رو باز نگه می‌داره تا هر خطایی رو بتونی بخونی. +روی ویندوز، بستن پنجره‌ی ترمینال Aether رو متوقف نمی‌کنه: فرایند از کنسول جدا می‌شه و تونل در پس‌زمینه به کارش ادامه می‌ده، و یک آیکون سینی با گزینه‌ی «Exit» در ناحیه‌ی اعلان ظاهر می‌شه. برای برگردوندن رفتار قبلی (بستن = خروج) مقدار `AETHER_CLOSE_TO_TRAY=0` رو تنظیم کن. + هر سؤال یه پرچم و یه متغیر محیطی معادل داره. برای لیست کامل `./target/release/aether --help` رو بزن، یا به راهنماهای پایین‌تر مراجعه کن. پس از اجرا، پراکسی SOCKS5 روی آدرس زیر در دسترس خواهد بود: diff --git a/README.md b/README.md index 676b142..466ddf9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Unlike traditional VPN clients, Aether is built for environments where Deep Pack - Traffic obfuscation - Automatic reconnection, and quick-reconnect to your last known-good gateway to skip rescanning - Local SOCKS5 proxy +- Close-to-tray on Windows: closing the console keeps the tunnel running in the background with a tray icon (opt out with `AETHER_CLOSE_TO_TRAY=0`) - Command-line flags, environment variables, or interactive prompts — your choice - Linux, Windows, macOS and Android (Termux) @@ -120,6 +121,8 @@ Or skip the prompts with flags: On Windows, double-click `run-aether.bat` (included in the release zip) instead — it opens a terminal, runs `aether.exe`, and keeps the window open afterwards so you can read any errors. +On Windows, closing the terminal window does not stop Aether: the process detaches from the console and keeps the tunnel running in the background, and a tray icon with an "Exit" item appears in the notification area. Set `AETHER_CLOSE_TO_TRAY=0` to restore the old close-to-exit behaviour. + Every prompt has a flag and an environment variable equivalent. Run `./target/release/aether --help` for the full list, or see the guides linked below. After startup, a SOCKS5 proxy will be available at: diff --git a/aether/Cargo.lock b/aether/Cargo.lock index 26d7b12..f0df624 100644 --- a/aether/Cargo.lock +++ b/aether/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -44,10 +50,11 @@ dependencies = [ "serde_json", "smoltcp", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-boring", "toml", + "tray-icon", "x25519-dalek", ] @@ -105,7 +112,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -116,7 +123,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -125,6 +132,29 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -164,7 +194,7 @@ dependencies = [ "regex", "rustc-hash", "shlex 1.3.0", - "syn", + "syn 2.0.118", ] [[package]] @@ -178,6 +208,9 @@ name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] [[package]] name = "blake2" @@ -197,6 +230,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "boring" version = "4.22.0" @@ -264,6 +306,31 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "cc" version = "1.2.65" @@ -283,6 +350,16 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -349,7 +426,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -411,6 +488,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + [[package]] name = "crypto-common" version = "0.1.7" @@ -446,7 +547,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -469,7 +570,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.118", ] [[package]] @@ -480,7 +581,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -518,7 +619,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -527,7 +628,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -547,6 +648,37 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -555,7 +687,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -567,6 +699,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "either" version = "1.16.0" @@ -582,7 +720,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -621,7 +759,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", ] [[package]] @@ -630,12 +777,32 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -660,7 +827,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -750,7 +917,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -782,6 +949,64 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -819,12 +1044,154 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "h2" version = "0.4.15" @@ -869,6 +1236,18 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -1220,7 +1599,7 @@ checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1234,12 +1613,57 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -1256,6 +1680,34 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + [[package]] name = "litemap" version = "0.8.2" @@ -1316,6 +1768,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -1327,6 +1789,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + [[package]] name = "nix" version = "0.25.1" @@ -1364,6 +1847,66 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "octets" version = "0.3.6" @@ -1394,7 +1937,38 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] @@ -1432,12 +2006,31 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "platforms" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9245c6e7c5a6bcdd7977fdf6d1e1c67f4cc2d0d58c041df0ea5940953033e6ca" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -1488,6 +2081,50 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1507,7 +2144,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1583,7 +2220,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1604,7 +2241,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1707,6 +2344,17 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "regex" version = "1.12.4" @@ -1906,7 +2554,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1963,7 +2611,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1988,6 +2636,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "slab" version = "0.4.12" @@ -2021,7 +2675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2048,6 +2702,16 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.118" @@ -2076,7 +2740,35 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] @@ -2085,7 +2777,18 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -2096,7 +2799,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2189,7 +2892,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2217,44 +2920,48 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.20.2", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", - "serde", - "serde_spanned", "toml_datetime", - "toml_write", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_edit" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] [[package]] name = "tower" @@ -2320,7 +3027,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2332,6 +3039,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -2350,6 +3078,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2396,6 +3130,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -2468,7 +3208,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -2553,7 +3293,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2564,7 +3304,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2758,9 +3498,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -2777,6 +3517,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x25519-dalek" version = "2.0.0-rc.3" @@ -2808,7 +3558,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", "synstructure", ] @@ -2829,7 +3579,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2849,7 +3599,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", "synstructure", ] @@ -2870,7 +3620,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2903,7 +3653,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] diff --git a/aether/Cargo.toml b/aether/Cargo.toml index e501933..dcc38aa 100644 --- a/aether/Cargo.toml +++ b/aether/Cargo.toml @@ -39,6 +39,9 @@ http = "1" bytes = "1" tokio-boring = "4.22" +[target.'cfg(windows)'.dependencies] +tray-icon = { version = "0.24", features = ["common-controls-v6"] } + [profile.release] opt-level = 3 lto = "fat" diff --git a/aether/src/main.rs b/aether/src/main.rs index 2db7a11..59c9caf 100644 --- a/aether/src/main.rs +++ b/aether/src/main.rs @@ -23,6 +23,8 @@ mod tunnelping; mod wireguard; mod wg_prober; mod zerotrust; +#[cfg(target_os = "windows")] +mod tray; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -60,6 +62,9 @@ async fn main() -> Result<()> { install_netstack_panic_guard(); + #[cfg(target_os = "windows")] + tray::init(); + let listen: SocketAddr = std::env::var("AETHER_SOCKS") .ok() .and_then(|s| s.parse().ok()) diff --git a/aether/src/tray.rs b/aether/src/tray.rs new file mode 100644 index 0000000..c21a80f --- /dev/null +++ b/aether/src/tray.rs @@ -0,0 +1,150 @@ +//! Windows-only "close to tray" support. +//! +//! Aether is a console application. On Windows, clicking the X on the console +//! window sends `CTRL_CLOSE_EVENT`, after which the system force-terminates +//! the process — taking the tunnel down with it. This module instead detaches +//! the process from its console so the tunnel keeps running in the background, +//! and puts a tray icon with an "Exit" item in the notification area so the +//! user can still quit cleanly afterwards. +//! +//! The whole feature can be disabled with `AETHER_CLOSE_TO_TRAY=0`. + +use std::time::Duration; + +use tray_icon::menu::{Menu, MenuEvent, MenuItem}; +use tray_icon::{Icon, TrayIconBuilder}; + +extern "system" { + fn SetConsoleCtrlHandler(handler: Option i32>, add: i32) -> i32; + fn FreeConsole() -> i32; +} + +const CTRL_C_EVENT: u32 = 0; +const CTRL_BREAK_EVENT: u32 = 1; +const CTRL_CLOSE_EVENT: u32 = 2; +const CTRL_LOGOFF_EVENT: u32 = 5; +const CTRL_SHUTDOWN_EVENT: u32 = 6; + +/// Initializes close-to-tray. Best-effort: every failure is logged and ignored +/// so the app behaves exactly as before on machines where the tray cannot be +/// created (headless sessions, locked-down shells, ...). +pub fn init() { + if !enabled() { + return; + } + if let Err(e) = install_console_handler() { + log::warn!("[close-to-tray] console handler install failed: {e}"); + } + if let Err(e) = spawn_tray() { + log::warn!("[close-to-tray] tray unavailable: {e}"); + } +} + +fn enabled() -> bool { + std::env::var("AETHER_CLOSE_TO_TRAY") + .map(|v| { + !matches!( + v.trim().to_ascii_lowercase().as_str(), + "0" | "false" | "off" | "no" + ) + }) + .unwrap_or(true) +} + +unsafe extern "system" fn console_handler(ctrl_type: u32) -> i32 { + match ctrl_type { + // The console window was closed (X) or the session is ending: detach + // from the console and keep the tunnel alive in the background. + // Detaching removes this process from the console's process list, so + // the ~5s force-termination that follows a close event no longer + // applies to us. + CTRL_CLOSE_EVENT | CTRL_LOGOFF_EVENT | CTRL_SHUTDOWN_EVENT => { + let _ = FreeConsole(); + 1 // TRUE: event handled + } + // Leave Ctrl+C / Ctrl+Break to the default handler so interactive + // shutdown behaviour is unchanged. + CTRL_C_EVENT | CTRL_BREAK_EVENT => 0, + _ => 0, + } +} + +fn install_console_handler() -> std::io::Result<()> { + // SAFETY: the handler has no payload and stays valid for the process + // lifetime; it is never unregistered. + let rc = unsafe { SetConsoleCtrlHandler(Some(console_handler), 1) }; + if rc == 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } +} + +fn spawn_tray() -> Result<(), Box> { + let menu = Menu::new(); + let exit = MenuItem::new("Exit", true, None); + menu.append(&exit)?; + + let icon = Icon::from_rgba(app_icon(), 32, 32)?; + + let tray = TrayIconBuilder::new() + .with_menu(Box::new(menu)) + .with_tooltip("Aether") + .with_icon(icon) + .build()?; + + // The icon must stay alive for the process lifetime; leak it on purpose. + std::mem::forget(tray); + + let receiver = MenuEvent::receiver(); + std::thread::spawn(move || loop { + if let Ok(event) = receiver.try_recv() { + if event.id() == exit.id() { + std::process::exit(0); + } + } + std::thread::sleep(Duration::from_millis(150)); + }); + + log::info!( + "[+] close-to-tray enabled: closing the window keeps Aether running in the background" + ); + Ok(()) +} + +/// 32x32 RGBA tray icon: a cyan "A" on a dark slate background, drawn from a +/// tiny bitmap so the module needs no image-processing dependency. +fn app_icon() -> Vec { + const SIZE: usize = 32; + const GLYPH: [[u8; 5]; 7] = [ + [0, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [0, 1, 0, 1, 0], + [1, 1, 1, 1, 1], + [1, 0, 0, 0, 1], + [1, 0, 0, 0, 1], + [1, 0, 0, 0, 1], + ]; + const BG: (u8, u8, u8, u8) = (30, 41, 59, 255); // slate-800 + const FG: (u8, u8, u8, u8) = (34, 211, 238, 255); // cyan-400 + const OX: usize = 13; + const OY: usize = 6; + + let mut px = vec![0u8; SIZE * SIZE * 4]; + for y in 0..SIZE { + for x in 0..SIZE { + let on = x >= OX + && x < OX + GLYPH[0].len() + && y >= OY + && y < OY + GLYPH.len() + && GLYPH[y - OY][x - OX] == 1; + let (r, g, b, a) = if on { FG } else { BG }; + let i = (y * SIZE + x) * 4; + px[i] = r; + px[i + 1] = g; + px[i + 2] = b; + px[i + 3] = a; + } + } + px +} From 62bb186e0d0c4b40f24e55e4a999807234fda38d Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sun, 2 Aug 2026 05:04:07 -0700 Subject: [PATCH 2/8] Fix Send trait issue: capture MenuItem ID before thread spawn --- aether/src/tray.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aether/src/tray.rs b/aether/src/tray.rs index c21a80f..1f43964 100644 --- a/aether/src/tray.rs +++ b/aether/src/tray.rs @@ -93,13 +93,15 @@ fn spawn_tray() -> Result<(), Box> { .with_icon(icon) .build()?; + // Capture the exit item ID before moving tray into forget (ID is Copy). + let exit_id = exit.id(); // The icon must stay alive for the process lifetime; leak it on purpose. std::mem::forget(tray); let receiver = MenuEvent::receiver(); std::thread::spawn(move || loop { if let Ok(event) = receiver.try_recv() { - if event.id() == exit.id() { + if event.id() == exit_id { std::process::exit(0); } } From 5fa25afaebd1235fdd15213e509492293d3c3c6c Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sun, 2 Aug 2026 05:15:23 -0700 Subject: [PATCH 3/8] Fix lifetime: forget exit MenuItem to keep it alive for closure --- aether/src/tray.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aether/src/tray.rs b/aether/src/tray.rs index 1f43964..0a1579c 100644 --- a/aether/src/tray.rs +++ b/aether/src/tray.rs @@ -83,6 +83,7 @@ fn install_console_handler() -> std::io::Result<()> { fn spawn_tray() -> Result<(), Box> { let menu = Menu::new(); let exit = MenuItem::new("Exit", true, None); + let exit_id = exit.id(); menu.append(&exit)?; let icon = Icon::from_rgba(app_icon(), 32, 32)?; @@ -93,8 +94,8 @@ fn spawn_tray() -> Result<(), Box> { .with_icon(icon) .build()?; - // Capture the exit item ID before moving tray into forget (ID is Copy). - let exit_id = exit.id(); + // Keep exit alive for process lifetime (menu holds a reference to it). + std::mem::forget(exit); // The icon must stay alive for the process lifetime; leak it on purpose. std::mem::forget(tray); From 437d6f3981ae57356ee5ffd7ceb10fe6b45aeb84 Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sun, 2 Aug 2026 05:21:21 -0700 Subject: [PATCH 4/8] Fix: remove unnecessary forget(exit) - closure only uses exit_id (Copy) --- aether/src/tray.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aether/src/tray.rs b/aether/src/tray.rs index 0a1579c..11e3a43 100644 --- a/aether/src/tray.rs +++ b/aether/src/tray.rs @@ -94,8 +94,7 @@ fn spawn_tray() -> Result<(), Box> { .with_icon(icon) .build()?; - // Keep exit alive for process lifetime (menu holds a reference to it). - std::mem::forget(exit); + // exit is kept alive by menu; exit_id is Copy so the closure is fine. // The icon must stay alive for the process lifetime; leak it on purpose. std::mem::forget(tray); From 437b5a1a06f82b3802756b8901ae91ecb8458c8a Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sun, 2 Aug 2026 06:35:51 -0700 Subject: [PATCH 5/8] Cleanup: remove stale comment --- aether/src/tray.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aether/src/tray.rs b/aether/src/tray.rs index 11e3a43..6ab67fc 100644 --- a/aether/src/tray.rs +++ b/aether/src/tray.rs @@ -94,8 +94,8 @@ fn spawn_tray() -> Result<(), Box> { .with_icon(icon) .build()?; - // exit is kept alive by menu; exit_id is Copy so the closure is fine. - // The icon must stay alive for the process lifetime; leak it on purpose. + // Both menu and tray must live for the process lifetime. + // Leak them so the closure can hold exit_id (Copy) safely. std::mem::forget(tray); let receiver = MenuEvent::receiver(); From 1ddebd158d39de4e3dd6a4ce07dbf65b78431ee3 Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sun, 2 Aug 2026 06:48:58 -0700 Subject: [PATCH 6/8] Fix lifetime: leak exit MenuItem for closure --- aether/src/tray.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aether/src/tray.rs b/aether/src/tray.rs index 6ab67fc..7989c4b 100644 --- a/aether/src/tray.rs +++ b/aether/src/tray.rs @@ -94,8 +94,9 @@ fn spawn_tray() -> Result<(), Box> { .with_icon(icon) .build()?; - // Both menu and tray must live for the process lifetime. - // Leak them so the closure can hold exit_id (Copy) safely. + // Keep exit alive for process lifetime so closure can use exit_id. + std::mem::forget(exit); + // The tray must stay alive for the process lifetime. std::mem::forget(tray); let receiver = MenuEvent::receiver(); From 5daa2fe7ea822eb66eea748ecdefa546fd6d1cde Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sun, 2 Aug 2026 07:22:55 -0700 Subject: [PATCH 7/8] Fix lifetime: get exit_id before menu.append --- aether/src/tray.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aether/src/tray.rs b/aether/src/tray.rs index 7989c4b..7a2dcd9 100644 --- a/aether/src/tray.rs +++ b/aether/src/tray.rs @@ -83,6 +83,7 @@ fn install_console_handler() -> std::io::Result<()> { fn spawn_tray() -> Result<(), Box> { let menu = Menu::new(); let exit = MenuItem::new("Exit", true, None); + // Get exit_id BEFORE menu.append so we don't need to borrow exit afterwards. let exit_id = exit.id(); menu.append(&exit)?; @@ -94,9 +95,7 @@ fn spawn_tray() -> Result<(), Box> { .with_icon(icon) .build()?; - // Keep exit alive for process lifetime so closure can use exit_id. - std::mem::forget(exit); - // The tray must stay alive for the process lifetime. + // Menu keeps exit alive; tray must stay alive for process lifetime. std::mem::forget(tray); let receiver = MenuEvent::receiver(); From 4a700b9bd1fdf80fa142a14ba7ed6094ed180d69 Mon Sep 17 00:00:00 2001 From: Psiphon Iran Custom Build <[email protected]> Date: Sun, 2 Aug 2026 08:01:32 -0700 Subject: [PATCH 8/8] Fix lifetime: leak exit MenuItem for closure --- aether/src/tray.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aether/src/tray.rs b/aether/src/tray.rs index 7a2dcd9..cb514a4 100644 --- a/aether/src/tray.rs +++ b/aether/src/tray.rs @@ -95,7 +95,11 @@ fn spawn_tray() -> Result<(), Box> { .with_icon(icon) .build()?; - // Menu keeps exit alive; tray must stay alive for process lifetime. + // Leak exit so the closure can use exit_id (Copy) without lifetime issues. + // The Menu inside tray keeps a reference to exit, so leaking exit ensures + // it stays alive for the process lifetime. + std::mem::forget(exit); + // Tray must stay alive for the process lifetime. std::mem::forget(tray); let receiver = MenuEvent::receiver();