diff --git a/hamy-vpn-client-proxy.exe b/hamy-vpn-client-proxy.exe new file mode 100644 index 0000000..1d60dab Binary files /dev/null and b/hamy-vpn-client-proxy.exe differ diff --git a/main.go b/main.go index 435be25..a93e822 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "syscall" + "unsafe" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" @@ -23,6 +24,8 @@ import ( "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "fyne.io/fyne/v2" + + "golang.org/x/sys/windows/registry" ) // Config represents a VPN configuration @@ -405,6 +408,82 @@ func generateConfigFromVLESSURL(vlessURL, outputPath string) error { return nil } +// setSystemProxy enables the system proxy with the specified server and port +func setSystemProxy(proxyServer string) error { + key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE) + if err != nil { + return fmt.Errorf("failed to open registry key: %v", err) + } + defer key.Close() + + // Set ProxyEnable to 1 to enable proxy + err = key.SetDWordValue("ProxyEnable", 1) + if err != nil { + return fmt.Errorf("failed to set ProxyEnable: %v", err) + } + + // Set ProxyServer to the specified server and port + err = key.SetStringValue("ProxyServer", proxyServer) + if err != nil { + return fmt.Errorf("failed to set ProxyServer: %v", err) + } + + // Optionally, set ProxyOverride to bypass proxy for local addresses + err = key.SetStringValue("ProxyOverride", "localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*") + if err != nil { + return fmt.Errorf("failed to set ProxyOverride: %v", err) + } + + // Notify Windows that the proxy settings have changed + notifyProxyChange() + + return nil +} + +// disableSystemProxy disables the system proxy +func disableSystemProxy() error { + key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE) + if err != nil { + return fmt.Errorf("failed to open registry key: %v", err) + } + defer key.Close() + + // Set ProxyEnable to 0 to disable proxy + err = key.SetDWordValue("ProxyEnable", 0) + if err != nil { + return fmt.Errorf("failed to set ProxyEnable: %v", err) + } + + // Notify Windows that the proxy settings have changed + notifyProxyChange() + + return nil +} + +// notifyProxyChange notifies Windows that proxy settings have changed +func notifyProxyChange() { + // Use Windows API to notify about proxy change + // Call InternetSetOption to refresh proxy settings + procInternetSetOption := syscall.NewLazyDLL("wininet.dll").NewProc("InternetSetOptionW") + procInternetSetOption.Call( + 0, // hInternet = NULL + 39, // INTERNET_OPTION_SETTINGS_CHANGED + 0, // lpBuffer = NULL + 0, // dwBufferLength = 0 + ) + + // Also send WM_SETTINGCHANGE to broadcast the change + procSendMessageTimeout := syscall.NewLazyDLL("user32.dll").NewProc("SendMessageTimeoutW") + procSendMessageTimeout.Call( + 0xFFFF, // HWND_BROADCAST + 0x001A, // WM_SETTINGCHANGE + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Proxy"))), + 0, + 0x0002, // SMTO_ABORTIFHUNG + 5000, // timeout + ) +} + func main() { myApp := app.New() myWindow := myApp.NewWindow("Hamy VPN Client") @@ -469,7 +548,42 @@ func main() { } // Toggle connection state - isConnected = !isConnected + if !isConnected { + // Connect - generate config and run sing-box + err := generateAndRunSingBox(configs[activeConfig].URL) + if err != nil { + dialog.ShowError(fmt.Errorf("failed to start connection: %v", err), myWindow) + // Revert connection state on failure + isConnected = false + updateConnectionButtonText(connectButton) + statusLabel.Text = "Отключено" + statusLabel.Color = color.RGBA{R: 128, G: 128, B: 128, A: 255} + statusLabel.Refresh() + return + } + + // Set proxy to route traffic through VPN + err = setSystemProxy("127.0.0.1:1080") + if err != nil { + log.Printf("Failed to set system proxy: %v", err) + dialog.ShowError(fmt.Errorf("failed to set system proxy: %v", err), myWindow) + } + + // Update connection state after successful connection + isConnected = true + } else { + // Disconnect - kill the current process + killCurrentProcess() + + // Disable proxy when disconnecting from VPN + err := disableSystemProxy() + if err != nil { + log.Printf("Failed to disable system proxy: %v", err) + } + + // Update connection state after disconnection + isConnected = false + } // Update button text updateConnectionButtonText(connectButton) @@ -483,24 +597,6 @@ func main() { statusLabel.Color = color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for disconnected } statusLabel.Refresh() - - if isConnected { - // Disconnect - kill the current process - killCurrentProcess() - } else { - // Connect - generate config and run sing-box - err := generateAndRunSingBox(configs[activeConfig].URL) - if err != nil { - dialog.ShowError(fmt.Errorf("failed to start connection: %v", err), myWindow) - // Revert connection state - isConnected = false - updateConnectionButtonText(connectButton) - statusLabel.Text = "Отключено" - statusLabel.Color = color.RGBA{R: 128, G: 128, B: 128, A: 255} - statusLabel.Refresh() - return - } - } }) connectButton.Importance = widget.HighImportance if len(configs) == 0 { @@ -646,6 +742,20 @@ func main() { // Create a top-level container mainContainer := container.NewStack(contentContainer) + // Register an app lifecycle listener to handle cleanup when the app exits + myApp.Lifecycle().SetOnStopped(func() { + // Kill the current process if running + if currentProcess != nil && currentProcess.Process != nil { + currentProcess.Process.Kill() + } + + // Disable proxy when the application exits + err := disableSystemProxy() + if err != nil { + log.Printf("Failed to disable system proxy on exit: %v", err) + } + }) + // Set the content and show the window myWindow.SetContent(mainContainer)