Repository: tauri-apps/wry (language: rust)

Issue:
Expose WebkitGTK Proxy Feature on Linux
Currently wry does not expose or support upstream proxies for the webview on Linux.
WebkitGTK supports this out of the box since version `2.16` but it is not exposed in a uniform interface in wry.

I would like to have an exposed wry proxy configuration, where it is possible to set the webview proxy independent
of the underlying WebkitGTK method. Possibly this interface could be used for `Webview2` by adding custom parameters
to the window creation by default.
I would like to have a patch for older wry versions, so we could use this feature in Tauri `1.x` instead of just in the `2.x` releases.

The most recent supported interface is exposed in the [`WebsiteDataManager.set_network_proxy_settings`](https://webkitgtk.org/reference/webkit2gtk/stable/method.WebsiteDataManager.set_network_proxy_settings.html)
and the deprecated version is exposed via [`WebContext.set_network_proxy_settings`](https://webkitgtk.org/reference/webkit2gtk/stable/method.WebContext.set_network_proxy_settings.html
).

There are no alternatives present and using this for auditing or compliance would require a custom built version of Tauri and Wry.
The functionality is needed in corporate environments where there are upstream proxies to be honored and when auditing Tauri applications it is helpful to use common security tooling in the process.

The proxy feature is exposed on windows via the custom webview arguments (`--proxy-server 127.0.0.1:8080`) and on MacOs in a new [beta version](https://developer.apple.com/documentation/network/nwparameters/privacycontext/4156642-proxyconfigurations).

Discussed this shortly with @wusyong before opening the issue here.


Discussion / hints:
https://developer.apple.com/videos/play/wwdc2023/10002/


This might help us to understand `proxyConfigurations` on macOS 
WebKit documentation for setting up the proxy configuration:
https://developer.apple.com/documentation/webkit/wkwebsitedatastore/4245799-proxyconfigurations?language=objc
Surveying WebView2 proxy feature:
- [How to use Microsoft Edge command-line options to configure proxy settings](https://learn.microsoft.com/en-us/deployedge/edge-learnmore-cmdline-options-proxy-settings)
- https://github.com/MicrosoftEdge/WebView2Feedback/issues/132
- https://github.com/MicrosoftEdge/WebView2Feedback/issues/193

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index de7d0f341..58c2387b5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@ devtools = [ ]
 transparent = [ ]
 fullscreen = [ ]
 linux-body = [ "webkit2gtk/v2_40" ]
+mac-proxy = [ ]
 
 [dependencies]
 libc = "0.2"
diff --git a/examples/proxy.rs b/examples/proxy.rs
new file mode 100644
index 000000000..f411b0278
--- /dev/null
+++ b/examples/proxy.rs
@@ -0,0 +1,44 @@
+// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use wry::webview::{ProxyConfig, ProxyEndpoint};
+
+fn main() -> wry::Result<()> {
+  use wry::{
+    application::{
+      event::{Event, StartCause, WindowEvent},
+      event_loop::{ControlFlow, EventLoop},
+      window::WindowBuilder,
+    },
+    webview::WebViewBuilder,
+  };
+
+  let event_loop = EventLoop::new();
+  let window = WindowBuilder::new()
+    .with_title("Proxy Test")
+    .build(&event_loop)?;
+
+  let http_proxy = ProxyConfig::Http(ProxyEndpoint {
+    host: "localhost".to_string(),
+    port: "3128".to_string(),
+  });
+
+  let _webview = WebViewBuilder::new(window)?
+    .with_proxy_config(http_proxy)
+    .with_url("https://www.myip.com/")?
+    .build()?;
+
+  event_loop.run(move |event, _, control_flow| {
+    *control_flow = ControlFlow::Wait;
+
+    match event {
+      Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
+      Event::WindowEvent {
+        event: WindowEvent::CloseRequested,
+        ..
+      } => *control_flow = ControlFlow::Exit,
+      _ => (),
+    }
+  });
+}
diff --git a/src/lib.rs b/src/lib.rs
index 16526fd24..37ddb0505 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -165,4 +165,6 @@ pub enum Error {
   #[cfg(target_os = "android")]
   #[error(transparent)]
   JniError(#[from] tao::platform::android::ndk_glue::jni::errors::Error),
+  #[error("Failed to create proxy endpoint")]
+  ProxyEndpointCreationFailed,
 }
diff --git a/src/webview/mod.rs b/src/webview/mod.rs
index d4abc7bf7..a8ba6faea 100644
--- a/src/webview/mod.rs
+++ b/src/webview/mod.rs
@@ -4,6 +4,7 @@
 
 //! [`WebView`] struct and associated types.
 
+mod proxy;
 mod web_context;
 
 pub use web_context::WebContext;
@@ -50,6 +51,7 @@ use windows::{Win32::Foundation::HWND, Win32::UI::WindowsAndMessaging::DestroyWi
 
 use std::{borrow::Cow, path::PathBuf, rc::Rc};
 
+pub use proxy::{ProxyConfig, ProxyEndpoint};
 pub use url::Url;
 
 #[cfg(target_os = "windows")]
@@ -235,6 +237,12 @@ pub struct WebViewAttributes {
 
   /// Set a handler closure to process page load events.
   pub on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent, String)>>,
+
+  /// Set a proxy configuration for the webview. Supports HTTP CONNECT and SOCKSv5 proxies
+  ///
+  /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled.
+  /// - **Android / iOS:** Not supported.
+  pub proxy_config: Option<ProxyConfig>,
 }
 
 impl Default for WebViewAttributes {
@@ -267,6 +275,7 @@ impl Default for WebViewAttributes {
       incognito: false,
       autoplay: true,
       on_page_load_handler: None,
+      proxy_config: None,
     }
   }
 }
@@ -655,6 +664,16 @@ impl<'a> WebViewBuilder<'a> {
     self
   }
 
+  /// Set a proxy configuration for the webview.
+  ///
+  /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled. Supports HTTP CONNECT and SOCKSv5 proxies.
+  /// - **Windows / Linux**: Supports HTTP CONNECT and SOCKSv5 proxies.
+  /// - **Android / iOS:** Not supported.
+  pub fn with_proxy_config(mut self, configuration: ProxyConfig) -> Self {
+    self.webview.proxy_config = Some(configuration);
+    self
+  }
+
   /// Consume the builder and create the [`WebView`].
   ///
   /// Platform-specific behavior:
@@ -682,7 +701,8 @@ pub trait WebViewBuilderExtWindows {
   /// ## Warning
   ///
   /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection`
-  /// and `--autoplay-policy=no-user-gesture-required` if autoplay is enabled
+  /// `--autoplay-policy=no-user-gesture-required` if autoplay is enabled
+  /// and `--proxy-server=<scheme>://<host>:<port>` if a proxy is set.
   /// so if you use this method, you have to add these arguments yourself if you want to keep the same behavior.
   fn with_additional_browser_args<S: Into<String>>(self, additional_args: S) -> Self;
 
diff --git a/src/webview/proxy.rs b/src/webview/proxy.rs
new file mode 100644
index 000000000..c7a426070
--- /dev/null
+++ b/src/webview/proxy.rs
@@ -0,0 +1,15 @@
+#[derive(Debug, Clone)]
+pub struct ProxyEndpoint {
+  /// Proxy server host (e.g. 192.168.0.100, localhost, example.com, etc.)
+  pub host: String,
+  /// Proxy server port (e.g. 1080, 3128, etc.)
+  pub port: String,
+}
+
+#[derive(Debug, Clone)]
+pub enum ProxyConfig {
+  /// Connect to proxy server via HTTP CONNECT
+  Http(ProxyEndpoint),
+  /// Connect to proxy server via SOCKSv5
+  Socks5(ProxyEndpoint),
+}
diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs
index 93fa6fc7c..85ed28117 100644
--- a/src/webview/webkitgtk/mod.rs
+++ b/src/webview/webkitgtk/mod.rs
@@ -15,9 +15,9 @@ use std::{
 };
 use url::Url;
 use webkit2gtk::{
-  traits::*, AutoplayPolicy, LoadEvent, NavigationPolicyDecision, PolicyDecisionType, SettingsExt,
-  URIRequest, UserContentInjectedFrames, UserScript, UserScriptInjectionTime, WebView,
-  WebViewBuilder, WebsitePoliciesBuilder,
+  traits::*, AutoplayPolicy, LoadEvent, NavigationPolicyDecision, NetworkProxyMode,
+  NetworkProxySettings, PolicyDecisionType, SettingsExt, URIRequest, UserContentInjectedFrames,
+  UserScript, UserScriptInjectionTime, WebView, WebViewBuilder, WebsitePoliciesBuilder,
 };
 use webkit2gtk_sys::{
   webkit_get_major_version, webkit_get_micro_version, webkit_get_minor_version,
@@ -29,7 +29,7 @@ pub use web_context::WebContextImpl;
 
 use crate::{
   application::{platform::unix::*, window::Window},
-  webview::{web_context::WebContext, PageLoadEvent, WebViewAttributes, RGBA},
+  webview::{proxy::ProxyConfig, web_context::WebContext, PageLoadEvent, WebViewAttributes, RGBA},
   Error, Result,
 };
 
@@ -71,7 +71,20 @@ impl InnerWebView {
         }
       }
     };
-
+    if let Some(proxy_setting) = attributes.proxy_config {
+      let proxy_uri = match proxy_setting {
+        ProxyConfig::Http(endpoint) => format!("http://{}:{}", endpoint.host, endpoint.port),
+        ProxyConfig::Socks5(endpoint) => {
+          format!("socks5://{}:{}", endpoint.host, endpoint.port)
+        }
+      };
+      use webkit2gtk::WebContextExt;
+      if let Some(website_data_manager) = web_context.context().website_data_manager() {
+        let mut settings = NetworkProxySettings::new(Some(&proxy_uri.as_str()), &[]);
+        website_data_manager
+          .set_network_proxy_settings(NetworkProxyMode::Custom, Some(&mut settings));
+      }
+    }
     let webview = {
       let mut webview = WebViewBuilder::new();
       webview = webview.user_content_manager(web_context.manager());
diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs
index 9781cb963..439008061 100644
--- a/src/webview/webview2/mod.rs
+++ b/src/webview/webview2/mod.rs
@@ -5,7 +5,7 @@
 mod file_drop;
 
 use crate::{
-  webview::{PageLoadEvent, WebContext, WebViewAttributes, RGBA},
+  webview::{proxy::ProxyConfig, PageLoadEvent, WebContext, WebViewAttributes, RGBA},
   Error, Result,
 };
 
@@ -13,8 +13,14 @@ use file_drop::FileDropController;
 use url::Url;
 
 use std::{
-  collections::HashSet, fmt::Write, iter::once, mem::MaybeUninit, os::windows::prelude::OsStrExt,
-  path::PathBuf, rc::Rc, sync::mpsc, sync::Arc,
+  collections::HashSet,
+  fmt::Write,
+  iter::once,
+  mem::MaybeUninit,
+  os::windows::prelude::OsStrExt,
+  path::PathBuf,
+  rc::Rc,
+  sync::{mpsc, Arc},
 };
 
 use once_cell::unsync::OnceCell;
@@ -74,7 +80,7 @@ impl InnerWebView {
     let file_drop_handler = attributes.file_drop_handler.take();
     let file_drop_window = window.clone();
 
-    let env = Self::create_environment(&web_context, pl_attrs.clone(), attributes.autoplay)?;
+    let env = Self::create_environment(&web_context, pl_attrs.clone(), &attributes)?;
     let controller = Self::create_controller(hwnd, &env, attributes.incognito)?;
     let webview = Self::init_webview(window, hwnd, attributes, &env, &controller, pl_attrs)?;
 
@@ -95,7 +101,7 @@ impl InnerWebView {
   fn create_environment(
     web_context: &Option<&mut WebContext>,
     pl_attrs: super::PlatformSpecificWebViewAttributes,
-    autoplay: bool,
+    attributes: &WebViewAttributes,
   ) -> webview2_com::Result<ICoreWebView2Environment> {
     let (tx, rx) = mpsc::channel();
 
@@ -105,6 +111,35 @@ impl InnerWebView {
       .and_then(|path| path.to_str())
       .map(String::from);
 
+    let argument = PCWSTR::from_raw(
+      encode_wide(pl_attrs.additional_browser_args.unwrap_or_else(|| {
+        // remove "mini menu" - See https://github.com/tauri-apps/wry/issues/535
+        // and "smart screen" - See https://github.com/tauri-apps/tauri/issues/1345
+        format!(
+          "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection{}{}",
+          if attributes.autoplay {
+            " --autoplay-policy=no-user-gesture-required"
+          } else {
+            ""
+          },
+          if let Some(proxy_setting) = &attributes.proxy_config {
+            match proxy_setting {
+              ProxyConfig::Http(endpoint) => {
+                format!(" --proxy-server=http://{}:{}", endpoint.host, endpoint.port)
+              }
+              ProxyConfig::Socks5(endpoint) => format!(
+                " --proxy-server=socks5://{}:{}",
+                endpoint.host, endpoint.port
+              ),
+            }
+          } else {
+            "".to_string()
+          }
+        )
+      }))
+      .as_ptr(),
+    );
+
     CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation(
       Box::new(move |environmentcreatedhandler| unsafe {
         let options = {
@@ -126,21 +161,7 @@ impl InnerWebView {
           options
         };
 
-        let _ = options.SetAdditionalBrowserArguments(PCWSTR::from_raw(
-          encode_wide(pl_attrs.additional_browser_args.unwrap_or_else(|| {
-            // remove "mini menu" - See https://github.com/tauri-apps/wry/issues/535
-            // and "smart screen" - See https://github.com/tauri-apps/tauri/issues/1345
-            format!(
-              "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection{}",
-              if autoplay {
-                " --autoplay-policy=no-user-gesture-required"
-              } else {
-                ""
-              }
-            )
-          }))
-          .as_ptr(),
-        ));
+        let _ = options.SetAdditionalBrowserArguments(argument);
 
         if let Some(data_directory) = data_directory {
           CreateCoreWebView2EnvironmentWithOptions(
diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs
index 6696aaf54..70c2131ed 100644
--- a/src/webview/wkwebview/mod.rs
+++ b/src/webview/wkwebview/mod.rs
@@ -6,6 +6,8 @@ mod download;
 #[cfg(target_os = "macos")]
 mod file_drop;
 mod navigation;
+#[cfg(feature = "mac-proxy")]
+mod proxy;
 #[cfg(target_os = "macos")]
 mod synthetic_mouse_events;
 
@@ -43,6 +45,14 @@ use file_drop::{add_file_drop_methods, set_file_drop_handler};
 #[cfg(target_os = "ios")]
 use crate::application::platform::ios::WindowExtIOS;
 
+#[cfg(feature = "mac-proxy")]
+use crate::webview::{
+  proxy::ProxyConfig,
+  wkwebview::proxy::{
+    nw_endpoint_t, nw_proxy_config_create_http_connect, nw_proxy_config_create_socksv5,
+  },
+};
+
 use crate::{
   application::{
     dpi::{LogicalSize, PhysicalSize},
@@ -314,6 +324,23 @@ impl InnerWebView {
       let _preference: id = msg_send![config, preferences];
       let _yes: id = msg_send![class!(NSNumber), numberWithBool:1];
 
+      #[cfg(feature = "mac-proxy")]
+      if let Some(proxy_config) = attributes.proxy_config {
+        let proxy_config = match proxy_config {
+          ProxyConfig::Http(endpoint) => {
+            let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap();
+            nw_proxy_config_create_http_connect(nw_endpoint, nil)
+          }
+          ProxyConfig::Socks5(endpoint) => {
+            let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap();
+            nw_proxy_config_create_socksv5(nw_endpoint)
+          }
+        };
+
+        let proxies: id = msg_send![class!(NSArray), arrayWithObject: proxy_config];
+        let () = msg_send![data_store, setProxyConfigurations: proxies];
+      }
+
       #[cfg(target_os = "macos")]
       (*webview).set_ivar(ACCEPT_FIRST_MOUSE, attributes.accept_first_mouse);
 
@@ -1123,6 +1150,14 @@ impl NSString {
     }
   }
 
+  #[allow(dead_code)] // only used when `mac-proxy` feature is enabled
+  fn to_cstr(&self) -> *const c_char {
+    unsafe {
+      let utf_8_string = msg_send![self.0, UTF8String];
+      utf_8_string
+    }
+  }
+
   fn as_ptr(&self) -> id {
     self.0
   }
diff --git a/src/webview/wkwebview/proxy.rs b/src/webview/wkwebview/proxy.rs
new file mode 100644
index 000000000..b2c1f46a2
--- /dev/null
+++ b/src/webview/wkwebview/proxy.rs
@@ -0,0 +1,67 @@
+// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use cocoa::base::nil;
+use libc::c_char;
+
+use crate::{webview::proxy::ProxyEndpoint, Error};
+
+use super::NSString;
+
+#[allow(non_camel_case_types)]
+pub type nw_endpoint_t = *mut objc::runtime::Object;
+#[allow(non_camel_case_types)]
+pub type nw_relay_hop_t = *mut objc::runtime::Object;
+#[allow(non_camel_case_types)]
+pub type nw_protocol_options_t = *mut objc::runtime::Object;
+#[allow(non_camel_case_types)]
+pub type nw_proxy_config_t = *mut objc::runtime::Object;
+
+#[link(name = "Network", kind = "framework")]
+extern "C" {
+  #[allow(dead_code)]
+  fn nw_endpoint_create_url(url: *const c_char) -> nw_endpoint_t;
+  #[allow(dead_code)]
+  fn nw_endpoint_get_url(endpoint: nw_endpoint_t) -> *const c_char;
+  fn nw_endpoint_create_host(host: *const c_char, port: *const c_char) -> nw_endpoint_t;
+  #[allow(dead_code)]
+  fn nw_proxy_config_set_username_and_password(
+    proxy_config: nw_proxy_config_t,
+    username: *const c_char,
+    password: *const c_char,
+  );
+  #[allow(dead_code)]
+  fn nw_relay_hop_create(
+    http3_relay_endpoint: nw_endpoint_t,
+    http2_relay_endpoint: nw_endpoint_t,
+    relay_tls_options: nw_protocol_options_t,
+  ) -> nw_relay_hop_t;
+  #[allow(dead_code)]
+  fn nw_proxy_config_create_relay(
+    first_hop: nw_relay_hop_t,
+    second_hop: nw_relay_hop_t,
+  ) -> nw_proxy_config_t;
+  pub fn nw_proxy_config_create_socksv5(proxy_endpoint: nw_endpoint_t) -> nw_proxy_config_t;
+  pub fn nw_proxy_config_create_http_connect(
+    proxy_endpoint: nw_endpoint_t,
+    proxy_tls_options: nw_protocol_options_t,
+  ) -> nw_proxy_config_t;
+}
+
+impl TryFrom<ProxyEndpoint> for nw_endpoint_t {
+  type Error = Error;
+  fn try_from(endpoint: ProxyEndpoint) -> Result<Self, Error> {
+    unsafe {
+      let endpoint_host = NSString::new(&endpoint.host).to_cstr();
+      let endpoint_port = NSString::new(&endpoint.port).to_cstr();
+      let endpoint = nw_endpoint_create_host(endpoint_host, endpoint_port);
+
+      match endpoint {
+        #[allow(non_upper_case_globals)]
+        nil => Err(Error::ProxyEndpointCreationFailed),
+        _ => Ok(endpoint),
+      }
+    }
+  }
+}


Test patch: