Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Package to setup system wide HTTP(S) Proxy on Desktop Platforms #190

Merged
merged 76 commits into from
Apr 2, 2024

Conversation

amircybersec
Copy link
Contributor

@amircybersec amircybersec commented Feb 26, 2024

sysproxy package sets up the system wide Web (HTTP/HTTPS) or SOCK proxy settings on:

  • MacOS
  • Linux (Gnome)
  • Windows

The package includes tests that sets the settings parameters and read them back to ensure changes have been properly made in the system settings.

The package exports the following methods:

SetWebProxy(host, port)
DisableWebProxy()
SetSOCKSProxy(host, port)
DisableSOCKSProxy()

@amircybersec amircybersec marked this pull request as ready for review February 26, 2024 21:30
@amircybersec amircybersec requested a review from jyyi1 February 26, 2024 21:30
@amircybersec
Copy link
Contributor Author

@jyyi1 would you be able to review this PR for me whenever you get a chance since Vini is out?

I tested this will x/examples/fyne-proxy example and I am planning to make a separate PR to add the changes to fyne-proxy example; however, if I made both PRs at the same time, the build check for the example PR would fail, since imported sysproxy package would still not be in the SDK before this PR merge.

Here are the changes to fyne-proxy example to set/unset system proxy:
https://github.com/amircybersec/outline-sdk/commit/24377b4a8a77bcbcf3bbc64e1ebb7f139024c87b

Please let me know if you have any thoughts on adding tests to this package...

Copy link
Contributor

@jyyi1 jyyi1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @amircybersec ! This is very helpful, I just added some comments.

x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_darwin.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_darwin.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_windows.go Show resolved Hide resolved
x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
@amircybersec
Copy link
Contributor Author

@jyyi1 Thanks a lot for the feedback. I applied your comments and make the suggested changes. I tested the changes across all platforms again to make sure I didn't break anything. Please let me know if there's any other areas for improvement.

@amircybersec amircybersec requested a review from jyyi1 March 1, 2024 04:17
Copy link
Contributor

@jyyi1 jyyi1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! The general logic looks good, with some additional comments.

x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_darwin.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_darwin.go Outdated Show resolved Hide resolved

func UnsetProxy() error {
// Execute Linux specific commands to unset proxy
return execCommand("gsettings", "set", "org.gnome.system.proxy", "mode", "none")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it support other modes? If so, do we need to backup the previous settings and restore it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jyyi1 The other modes are auto, and manual. Choosing manual, the system uses the last used proxy settings that can be manually overwritten. I can potentially backup the previous state before setting the proxy to the new settings and restore the system settings back to the previous state when UnsetProxy is called.

auto uses a link to .pac file that contains the proxy settings.

defer key.Close()

values := map[string]interface{}{
"MigrateProxy": 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this option mean? Do we need to unset it in our UnsetProxy function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jyyi1 Documentation for MigrateProxy DWORD was sparse. From what I understood by reading up one some resources I found (1, 2), It appears that this is related to how Internet Explore / Edge migrate their proxy settings.

I am a bit conflicted now since the docs say that IE sets this value to 1 after migrating the settings. BTW, I followed the way GreenTunnel was setting up Windows proxy params and tested the code on Windows successfully. But there could be corner and untested cases that I missed.

If you recall in the office, we looked at Chrome proxy settings was in sync with system proxy settings and we could see the settings switching in the Browser on Windows. I did not however test this with IE. I am looking further into this...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the information. I think we can ignore IE for now because Microsoft has already ended its support on June 15, 2022.

For the Edge, since it's using Chromium, I believe it should behave the same as Chrome.

Let's remove this setting and make sure everything works fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jyyi1 It seems like "ProxyHttp1.1": 0, setting is also related to IE according to this. I am going to remove this one too and test the changes on Windows.

x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
return fmt.Errorf("unsupported value type")
}
if err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to unset the registry here, so we don't leave it in an unusable state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. That's definitely a much safer approach. I will make a copy of settings param before making changes and revert then back to previous state at UnsetProxy.

x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
Copy link
Contributor

@fortuna fortuna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exciting!

x/sysproxy/doc.go Outdated Show resolved Hide resolved
// getActiveNetworkInterface finds the active network interface using shell commands.
// https://keith.github.io/xcode-man-pages/networksetup.8.html#listnetworkserviceorder
func getActiveNetworkInterface() (string, error) {
cmd := "networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep 'interface' | cut -d ':' -f2` -B 1 | head -n 1 | cut -d ' ' -f2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer $() over backticks: https://google.github.io/styleguide/shellguide.html#s6.2-command-substitution

Suggested change
cmd := "networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep 'interface' | cut -d ':' -f2` -B 1 | head -n 1 | cut -d ' ' -f2"
cmd := "networksetup -listnetworkserviceorder | grep "$(route -n get 0.0.0.0 | grep 'interface' | cut -d ':' -f2)" -B 1 | head -n 1 | cut -d ' ' -f2"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, can you put that on a separate getDefaultInterface()?

And then provide a getNetworkServiceForInterface(interfaceName string) that does the networksetup -listnetworkserviceorder call?

That will be a lot more readable.

x/sysproxy/sysproxy_darwin.go Outdated Show resolved Hide resolved
Comment on lines 83 to 84
cmdStr := fmt.Sprintf("networksetup -set%sproxy \"%s\" %s %s", ptype, interfaceName, ip, port)
cmd := exec.Command("bash", "-c", cmdStr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call networksetup directly instead. There's no need for bash here.

// https://keith.github.io/xcode-man-pages/networksetup.8.html#setwebproxystate
func removeProxyCommand(ptype ProxyType, interfaceName string) error {
cmdStr := fmt.Sprintf("networksetup -set%sproxystate \"%s\" off", ptype, interfaceName)
cmd := exec.Command("bash", "-c", cmdStr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto


func UnsetProxy() error {
// Execute Linux specific commands to unset proxy
return execCommand("gsettings", "set", "org.gnome.system.proxy", "mode", "none")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also erase the individual proxy entries we added, in case someone else enables the manual mode later.

httpsProxyType ProxyType = "secureweb"
)

func SetProxy(ip string, port string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably return not supported on iOS. I think you can check GOOS.

ftpProxyType ProxyType = "ftp"
)

func SetProxy(ip string, port string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably return not supported on Android. I think you can check GOOS?

// setProxyCommand sets the specified type of proxy on the given network interface.
// https://keith.github.io/xcode-man-pages/networksetup.8.html#getsecurewebproxy
func setProxyCommand(ptype ProxyType, interfaceName string, ip string, port string) error {
cmdStr := fmt.Sprintf("networksetup -set%sproxy \"%s\" %s %s", ptype, interfaceName, ip, port)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do a switch based on the type, instead of interpolating the command?
Like

switch proxyType {
  case ProxyTypeHTTP:
    cmd = exec.Command("networksetup", "-setwebproxy", interfaceName, host, port)
...
}

That will be a lot cleaner to read.

x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
amircybersec and others added 8 commits March 5, 2024 17:46
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
redundant likes that got pushed in accidentally

Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
remove print statement

Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
remove orphan line

Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
redundant code

Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
| --- | ----------- | ------ | ------ |
| HTTP | Yes | Yes | Yes
| HTTPS | Yes | Yes | Yes
| SOCKS | No | Yes | Yes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you do support Windows, but it's SOCKS4?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this socks server package that implements sock4, 4a, and 5:

https://github.com/cybozu-go/usocksd?tab=readme-ov-file

I wanted to test socks proxy on all platforms and see what versions they support in reality. I have tested linux and mac with socks 5 and they work fine. I could not get windows working with socks5 and it only worked with sock4 server in my tests. I have not tested mac and linux against socks4 though...Maybe we can do this later as TODO item.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated doc.go to reflect this in the documentation language

| SOCKS | No | Yes | Yes


`SetupWebProxy` implementation in this package setups both HTTP and HTTPS proxy when they distinguished by the platform. `SetupSOCKSProxy` sets up SOCKS proxy that proxies all TCP-based connections.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to explain what setting a HTTP proxy and setting a HTTPS proxy mean.

x/sysproxy/README.md Outdated Show resolved Hide resolved
x/sysproxy/README.md Outdated Show resolved Hide resolved

On Windows, The client only supports SOCKS4 spec and cannot connecto SOCKS5 proxy.

Support for FTP Proxy setting was not included due lack of adoption and usage. Username and password authentication was not included due to potential unreliability and untestd behavior. Please note that in SOCKS and HTTP proxy, credentials are communicated in plain text.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that authentication is not supported because the intended usage it to connect to a proxy running on localhost.

}
}

func getProxySettings(p ProxyType, interfaceName string) (*proxySettings, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is dead code. We can remove the get stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna I am using getProxySettings in tests to ensure parameters are properly set by the setProxy action.


func ClearWebProxy() error {
// clear settings
if err := setProxySettings(proxyTypeHTTP, "127.0.0.1", "0"); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same: can we use empty strings? It's more obvious.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I will test it first to make sure it works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna it seems like setting the strings to empty value has unintended consequences. For example, on macos, if I do

networksetup -setwebproxy Wi-Fi ""  ""

and then get the setting values, the port is returned as 0 but address is empty:

networksetup -getwebproxy Wi-Fi     
Enabled: Yes
Server: 
Port: 0
Authenticated Proxy Enabled: 0

This breaks my tests since set and read-back values do not match.

I am concerned changing this causes some stability issues if not throughly tested on all platforms. Since my tests are currently passing on all platforms and I have also tested the proxy behavior on all platforms end-to-end, let's use a default IP and port instead of empty string, if that is okay with you

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is moot since we are not clearing the settings anymore.

}

// Unset the web proxy and secure web proxy
if err := disableProxy(proxyTypeHTTP, activeInterface); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disable first, then clear the proxy. Otherwise someone may try to use it with the bogus address. Same for the other platforms.

Copy link
Contributor Author

@amircybersec amircybersec Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna I was doing that before (disabling first) but setting proxy parameters automatically change the proxy state to manual and turn it on. I had to change the order otherwise the proxy stayed in ON state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I saw this odd behavior on Linux, we might be able to disable proxy on Windows and MacOS before clearing the settings. Need to investigate this again...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna I confirm both on MacOS and Linux changing the proxy server and port automatically turns on the proxy. On Windows, we have to explicitly enable and disable the proxy settings for changes to take effect. This means we must clear the settings before disabling the proxy - the reverse order would leave the proxy state ON.

If we do not clear / change the setting, we can just disable the proxy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for testing. If we can't clear the settings, perhaps we just disable it then?

We would have to rename the method to DisableXPRoxy()

x/sysproxy/sysproxy_linux.go Show resolved Hide resolved

For example, on MacOS, if you have HTTP Proxy setup and visit a website over HTTPS, your traffic does NOT go through the proxy.

However, if only HTTPS proxy is setup, both HTTP and HTTPS requests go through the HTTPS proxy.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you set HTTPS, HTTP will use it? That's very counter intuitive.
Does it work the same in the other platforms? It would help to characterize that.

Copy link
Contributor Author

@amircybersec amircybersec Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna The proxy server running locally is HTTP proxy server in both cases.

In the first experiment, I only setup system's client HTTP proxy settings. In this case, only HTTP requests get routed through the proxy. If I setup system's client HTTPS proxy settings, both HTTP and HTTPS requests get routed through the proxy.

I believe this is just the way the system decides which type of request to route to the proxy. HTTPS is basically the super set in this design.

In our case, we don't care much about this since we are settings up both at the same time on all platforms and call it WebProxy which ends up routing both HTTP and HTTPS requests through the proxy.

amircybersec and others added 6 commits March 25, 2024 15:17
Co-authored-by: Vinicius Fortuna <fortuna@users.noreply.github.com>
Co-authored-by: Vinicius Fortuna <fortuna@users.noreply.github.com>
@amircybersec amircybersec requested a review from fortuna March 26, 2024 17:01
x/sysproxy/doc.go Outdated Show resolved Hide resolved
}

// Unset the web proxy and secure web proxy
if err := disableProxy(proxyTypeHTTP, activeInterface); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for testing. If we can't clear the settings, perhaps we just disable it then?

We would have to rename the method to DisableXPRoxy()

@amircybersec amircybersec requested a review from fortuna April 1, 2024 19:26
Copy link
Contributor

@jyyi1 jyyi1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few tweaks

x/sysproxy/doc.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_test.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_windows.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_darwin.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_other.go Outdated Show resolved Hide resolved
x/sysproxy/sysproxy_test.go Outdated Show resolved Hide resolved
amircybersec and others added 8 commits April 1, 2024 16:07
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
Co-authored-by: J. Yi <93548144+jyyi1@users.noreply.github.com>
@amircybersec
Copy link
Contributor Author

@fortuna & @jyyi1 Thanks a lot for all your feedback! I applied the latest round of suggestions and look forward to have this PR merged :)

Copy link
Contributor

@fortuna fortuna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

@amircybersec amircybersec merged commit 48d8a48 into Jigsaw-Code:main Apr 2, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants