11//! OS system-proxy integration for the `system` and `pac` proxy modes: point the OS
22//! proxy (or PAC auto-config URL) at the core's local inbound and clear it again.
3- //! Linux drives gsettings (GNOME-family) or kwriteconfig (KDE) by desktop; Windows
4- //! the WinINET registry keys plus an `InternetSetOption` refresh so the change takes
5- //! effect without a sign-out. macOS has no port yet (no-op).
63//!
7- //! These modes never elevate, so the process runs as the logged-in user and the
8- //! gsettings/registry writes land in that user's session — exactly where the OS
9- //! reads its proxy from.
4+ //! Linux has no single system-proxy store, so the apply/clear pipeline is *layered*
5+ //! and the layers are NOT mutually exclusive — each reaches a disjoint set of apps:
6+ //! 1. **gsettings `org.gnome.system.proxy`** (guarded by schema presence) — covers
7+ //! GNOME, MATE, Cinnamon, Budgie, and every GLib/GIO + Chromium-family app on
8+ //! any desktop where the schema is installed (most XFCE/LXQt/LXDE too).
9+ //! 2. **kwriteconfig `kioslaverc`** on KDE/Plasma, followed by a
10+ //! `reparseSlaveConfiguration` D-Bus signal so KIO apps reload.
11+ //! 3. **environment variables** (`~/.config/environment.d` for persistence + a live
12+ //! `systemctl --user`/`dbus-update-activation-environment` push) — the only layer
13+ //! that reaches CLI tools and non-GLib apps, and the only programmatic mechanism
14+ //! for XFCE/LXQt/LXDE. Env vars can't express a PAC URL, so they apply in `system`
15+ //! mode only.
16+ //!
17+ //! Windows uses the WinINET registry keys plus an `InternetSetOption` refresh so the
18+ //! change takes effect without a sign-out. macOS has no port yet (no-op).
19+ //!
20+ //! These modes never elevate, so the process runs as the logged-in user and every
21+ //! write lands in that user's session — exactly where the OS reads its proxy from.
1022
1123#[ cfg( target_os = "linux" ) ]
1224pub use linux:: { clear_system_proxy, set_pac, set_system_proxy} ;
@@ -18,91 +30,154 @@ pub use other::{clear_system_proxy, set_pac, set_system_proxy};
1830
1931#[ cfg( target_os = "linux" ) ]
2032mod linux {
21- use crate :: desktop:: silent;
33+ use std:: path:: PathBuf ;
34+
35+ use crate :: desktop:: { run_out, silent} ;
2236
2337 const HOST : & str = "127.0.0.1" ;
2438 const IGNORE_GNOME : & str = "['localhost', '127.0.0.0/8', '::1']" ;
2539 const IGNORE_KDE : & str = "localhost,127.0.0.1,::1" ;
40+ const NO_PROXY : & str = "localhost,127.0.0.1,::1" ;
2641
27- /// True on a KDE/Plasma session (uses kwriteconfig); everything else takes the
28- /// gsettings path, which the GNOME/XFCE/Cinnamon/MATE/Budgie proxy schema covers.
29- fn is_kde ( ) -> bool {
30- std:: env:: var ( "XDG_CURRENT_DESKTOP" )
31- . unwrap_or_default ( )
32- . to_ascii_lowercase ( )
33- . contains ( "kde" )
42+ /// Proxy env vars, lower- and upper-case (programs disagree on which they read).
43+ const ENV_KEYS : & [ & str ] = & [
44+ "http_proxy" ,
45+ "https_proxy" ,
46+ "all_proxy" ,
47+ "no_proxy" ,
48+ "HTTP_PROXY" ,
49+ "HTTPS_PROXY" ,
50+ "ALL_PROXY" ,
51+ "NO_PROXY" ,
52+ ] ;
53+ /// Persistent per-user env file (systemd user generator reads `environment.d`).
54+ const ENV_FILE_REL : & str = ".config/environment.d/50-kasumi-proxy.conf" ;
55+
56+ // ── apply / clear: run EVERY applicable layer, not just the first match ──
57+
58+ /// Point the OS at the local socks/http inbound. `http_port` carries http + https,
59+ /// `socks_port` the socks proxy (a sing-box mixed inbound passes the same for both).
60+ pub async fn set_system_proxy ( socks_port : u16 , http_port : u16 ) {
61+ gsettings_manual ( socks_port, http_port) . await ;
62+ kde_manual ( socks_port, http_port) . await ;
63+ env_apply ( socks_port, http_port) . await ;
3464 }
3565
36- /// The KDE config writer present on this host (Plasma 6 ships `kwriteconfig6` ,
37- /// Plasma 5 `kwriteconfig5`) .
38- fn kde_writer ( ) -> Option < & ' static str > {
39- [ "kwriteconfig6" , "kwriteconfig5" ]
40- . into_iter ( )
41- . find ( |bin| which ( bin ) )
66+ /// Point the OS at a PAC auto-config URL (`pac` mode). Env vars can't express a PAC ,
67+ /// so the env layer is cleared rather than set .
68+ pub async fn set_pac ( pac_url : & str ) {
69+ gsettings_auto ( pac_url ) . await ;
70+ kde_pac ( pac_url ) . await ;
71+ env_clear ( ) . await ;
4272 }
4373
44- fn which ( bin : & str ) -> bool {
45- std :: env :: var ( "PATH" )
46- . unwrap_or_default ( )
47- . split ( ':' )
48- . any ( |dir| std :: path :: Path :: new ( dir ) . join ( bin ) . exists ( ) )
74+ /// Disable every layer. Idempotent — safe whatever was (or wasn't) set.
75+ pub async fn clear_system_proxy ( ) {
76+ gsettings_none ( ) . await ;
77+ kde_none ( ) . await ;
78+ env_clear ( ) . await ;
4979 }
5080
51- /// Point the OS proxy at the local socks/http inbound. `http_port` carries http +
52- /// https; `socks_port` the socks proxy (for a sing-box mixed inbound the caller
53- /// passes the same port for both).
54- pub async fn set_system_proxy ( socks_port : u16 , http_port : u16 ) {
55- let http = http_port. to_string ( ) ;
56- let socks = socks_port. to_string ( ) ;
57- if is_kde ( ) {
58- let Some ( w) = kde_writer ( ) else { return } ;
59- kde ( w, "ProxyType" , "1" ) . await ;
60- kde ( w, "httpProxy" , & format ! ( "http://{HOST} {http}" ) ) . await ;
61- kde ( w, "httpsProxy" , & format ! ( "http://{HOST} {http}" ) ) . await ;
62- kde ( w, "socksProxy" , & format ! ( "socks://{HOST} {socks}" ) ) . await ;
63- kde ( w, "NoProxyFor" , IGNORE_KDE ) . await ;
81+ // ── layer 1: gsettings (GNOME schema), guarded by schema presence ──
82+
83+ /// Only call gsettings when the proxy schema is installed, so minimal systems
84+ /// (no `gsettings-desktop-schemas`) don't spew errors.
85+ async fn gnome_schema_present ( ) -> bool {
86+ let ( code, out) = run_out ( & [ "gsettings" , "list-schemas" ] ) . await ;
87+ code == 0 && out. lines ( ) . any ( |l| l. trim ( ) == "org.gnome.system.proxy" )
88+ }
89+
90+ async fn gsettings ( args : & [ & str ] ) {
91+ let mut argv = vec ! [ "gsettings" , "set" ] ;
92+ argv. extend_from_slice ( args) ;
93+ silent ( & argv) . await ;
94+ }
95+
96+ async fn gsettings_manual ( socks_port : u16 , http_port : u16 ) {
97+ if !gnome_schema_present ( ) . await {
6498 return ;
6599 }
100+ let http = http_port. to_string ( ) ;
101+ let socks = socks_port. to_string ( ) ;
66102 gsettings ( & [ "org.gnome.system.proxy" , "mode" , "manual" ] ) . await ;
67- for ( schema, port ) in [
68- ( "org.gnome.system.proxy.http" , & http ) ,
69- ( "org.gnome.system.proxy.https" , & http ) ,
103+ for schema in [
104+ "org.gnome.system.proxy.http" ,
105+ "org.gnome.system.proxy.https" ,
70106 ] {
71107 gsettings ( & [ schema, "host" , HOST ] ) . await ;
72- gsettings ( & [ schema, "port" , port ] ) . await ;
108+ gsettings ( & [ schema, "port" , & http ] ) . await ;
73109 }
74110 gsettings ( & [ "org.gnome.system.proxy.socks" , "host" , HOST ] ) . await ;
75111 gsettings ( & [ "org.gnome.system.proxy.socks" , "port" , & socks] ) . await ;
76112 gsettings ( & [ "org.gnome.system.proxy" , "ignore-hosts" , IGNORE_GNOME ] ) . await ;
77113 }
78114
79- /// Point the OS at a PAC auto-config URL (the `pac` mode).
80- pub async fn set_pac ( pac_url : & str ) {
81- if is_kde ( ) {
82- let Some ( w) = kde_writer ( ) else { return } ;
83- kde ( w, "ProxyType" , "2" ) . await ;
84- kde ( w, "Proxy Config Script" , pac_url) . await ;
115+ async fn gsettings_auto ( pac_url : & str ) {
116+ if !gnome_schema_present ( ) . await {
85117 return ;
86118 }
87119 gsettings ( & [ "org.gnome.system.proxy" , "mode" , "auto" ] ) . await ;
88120 gsettings ( & [ "org.gnome.system.proxy" , "autoconfig-url" , pac_url] ) . await ;
89121 }
90122
91- /// Disable the OS proxy. Idempotent — safe to call when nothing was set.
92- pub async fn clear_system_proxy ( ) {
93- if is_kde ( ) {
94- if let Some ( w) = kde_writer ( ) {
95- kde ( w, "ProxyType" , "0" ) . await ;
96- }
123+ async fn gsettings_none ( ) {
124+ if !gnome_schema_present ( ) . await {
97125 return ;
98126 }
99127 gsettings ( & [ "org.gnome.system.proxy" , "mode" , "none" ] ) . await ;
100128 }
101129
102- async fn gsettings ( args : & [ & str ] ) {
103- let mut argv = vec ! [ "gsettings" , "set" ] ;
104- argv. extend_from_slice ( args) ;
105- silent ( & argv) . await ;
130+ // ── layer 2: KDE kioslaverc (+ reparse so KIO apps reload) ──
131+
132+ /// True on a KDE/Plasma session.
133+ fn is_kde ( ) -> bool {
134+ std:: env:: var ( "XDG_CURRENT_DESKTOP" )
135+ . unwrap_or_default ( )
136+ . to_ascii_lowercase ( )
137+ . contains ( "kde" )
138+ }
139+
140+ /// The KDE config writer present on this host (Plasma 6 → `kwriteconfig6`, 5 → 5).
141+ fn kde_writer ( ) -> Option < & ' static str > {
142+ [ "kwriteconfig6" , "kwriteconfig5" ]
143+ . into_iter ( )
144+ . find ( |bin| which ( bin) )
145+ }
146+
147+ fn which ( bin : & str ) -> bool {
148+ std:: env:: var ( "PATH" )
149+ . unwrap_or_default ( )
150+ . split ( ':' )
151+ . any ( |dir| std:: path:: Path :: new ( dir) . join ( bin) . exists ( ) )
152+ }
153+
154+ async fn kde_manual ( socks_port : u16 , http_port : u16 ) {
155+ let Some ( w) = ( is_kde ( ) . then ( kde_writer) ) . flatten ( ) else {
156+ return ;
157+ } ;
158+ kde ( w, "ProxyType" , "1" ) . await ;
159+ kde ( w, "httpProxy" , & format ! ( "http://{HOST} {http_port}" ) ) . await ;
160+ kde ( w, "httpsProxy" , & format ! ( "http://{HOST} {http_port}" ) ) . await ;
161+ kde ( w, "socksProxy" , & format ! ( "socks://{HOST} {socks_port}" ) ) . await ;
162+ kde ( w, "NoProxyFor" , IGNORE_KDE ) . await ;
163+ kde_reparse ( ) . await ;
164+ }
165+
166+ async fn kde_pac ( pac_url : & str ) {
167+ let Some ( w) = ( is_kde ( ) . then ( kde_writer) ) . flatten ( ) else {
168+ return ;
169+ } ;
170+ kde ( w, "ProxyType" , "2" ) . await ;
171+ kde ( w, "Proxy Config Script" , pac_url) . await ;
172+ kde_reparse ( ) . await ;
173+ }
174+
175+ async fn kde_none ( ) {
176+ let Some ( w) = ( is_kde ( ) . then ( kde_writer) ) . flatten ( ) else {
177+ return ;
178+ } ;
179+ kde ( w, "ProxyType" , "0" ) . await ;
180+ kde_reparse ( ) . await ;
106181 }
107182
108183 async fn kde ( writer : & str , key : & str , value : & str ) {
@@ -118,6 +193,71 @@ mod linux {
118193 ] )
119194 . await ;
120195 }
196+
197+ /// Without this signal KIO apps keep the old proxy until restart.
198+ async fn kde_reparse ( ) {
199+ silent ( & [
200+ "dbus-send" ,
201+ "--type=signal" ,
202+ "/KIO/Scheduler" ,
203+ "org.kde.KIO.Scheduler.reparseSlaveConfiguration" ,
204+ "string:" ,
205+ ] )
206+ . await ;
207+ }
208+
209+ // ── layer 3: environment variables (CLI + non-GLib apps; the only XFCE/LXQt path) ──
210+
211+ fn env_file ( ) -> Option < PathBuf > {
212+ std:: env:: var_os ( "HOME" ) . map ( |h| PathBuf :: from ( h) . join ( ENV_FILE_REL ) )
213+ }
214+
215+ /// Persist the proxy env vars (`environment.d`, survives re-login) and push them
216+ /// live into the running session (systemd user manager + D-Bus activation env).
217+ async fn env_apply ( socks_port : u16 , http_port : u16 ) {
218+ let http_url = format ! ( "http://{HOST}:{http_port}" ) ;
219+ let socks_url = format ! ( "socks5://{HOST}:{socks_port}" ) ;
220+ let assigns = [
221+ format ! ( "http_proxy={http_url}" ) ,
222+ format ! ( "https_proxy={http_url}" ) ,
223+ format ! ( "all_proxy={socks_url}" ) ,
224+ format ! ( "no_proxy={NO_PROXY}" ) ,
225+ format ! ( "HTTP_PROXY={http_url}" ) ,
226+ format ! ( "HTTPS_PROXY={http_url}" ) ,
227+ format ! ( "ALL_PROXY={socks_url}" ) ,
228+ format ! ( "NO_PROXY={NO_PROXY}" ) ,
229+ ] ;
230+
231+ if let Some ( path) = env_file ( ) {
232+ if let Some ( dir) = path. parent ( ) {
233+ let _ = std:: fs:: create_dir_all ( dir) ;
234+ }
235+ let _ = std:: fs:: write ( & path, format ! ( "{}\n " , assigns. join( "\n " ) ) ) ;
236+ }
237+
238+ let refs: Vec < & str > = assigns. iter ( ) . map ( String :: as_str) . collect ( ) ;
239+ run_with ( & [ "systemctl" , "--user" , "set-environment" ] , & refs) . await ;
240+ run_with ( & [ "dbus-update-activation-environment" , "--systemd" ] , & refs) . await ;
241+ }
242+
243+ /// Remove the persistent file and unset the vars in the live session. D-Bus can't
244+ /// truly unset, so it gets the vars emptied; systemd does the real unset.
245+ async fn env_clear ( ) {
246+ if let Some ( path) = env_file ( ) {
247+ let _ = std:: fs:: remove_file ( path) ;
248+ }
249+ run_with ( & [ "systemctl" , "--user" , "unset-environment" ] , ENV_KEYS ) . await ;
250+ let empties: Vec < String > = ENV_KEYS . iter ( ) . map ( |k| format ! ( "{k}=" ) ) . collect ( ) ;
251+ let refs: Vec < & str > = empties. iter ( ) . map ( String :: as_str) . collect ( ) ;
252+ run_with ( & [ "dbus-update-activation-environment" , "--systemd" ] , & refs) . await ;
253+ }
254+
255+ /// Run `base` with `extra` args appended (one process, args owned by the caller).
256+ async fn run_with ( base : & [ & str ] , extra : & [ & str ] ) {
257+ let mut argv = base. to_vec ( ) ;
258+ argv. extend_from_slice ( extra) ;
259+ silent ( & argv) . await ;
260+ }
121261}
122262
123263#[ cfg( target_os = "windows" ) ]
0 commit comments