diff --git a/hamy-vpn-client-final.exe b/hamy-vpn-client-final.exe new file mode 100644 index 0000000..52faf88 Binary files /dev/null and b/hamy-vpn-client-final.exe differ diff --git a/hamy-vpn-client-new.exe b/hamy-vpn-client-new.exe new file mode 100644 index 0000000..29228a0 Binary files /dev/null and b/hamy-vpn-client-new.exe differ diff --git a/main.go b/main.go index d590c71..435be25 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,17 @@ package main import ( + "encoding/json" "errors" "fmt" "image/color" + "log" "net/url" "os" "os/exec" "path/filepath" "regexp" + "strconv" "strings" "syscall" @@ -91,6 +94,21 @@ func removeConfig(index int) { } } +// generateAndRunSingBox generates a Sing-box config from VLESS URL and runs sing-box +func generateAndRunSingBox(vlessURL string) error { + // Generate unique config filename based on timestamp or hash + configPath := "config.json" + + // Generate the Sing-box configuration from the VLESS URL + err := generateConfigFromVLESSURL(vlessURL, configPath) + if err != nil { + return fmt.Errorf("failed to generate config: %v", err) + } + + // Run sing-box with the generated config + return runSingBox(configPath) +} + // runSingBox starts the sing-box process with the given config path func runSingBox(configPath string) error { singBoxPath := filepath.Join("bin", "sing-box.exe") @@ -123,13 +141,274 @@ func killCurrentProcess() { } } +// VLESSConfig represents parsed VLESS URL data +type VLESSConfig struct { + UUID string + ServerAddr string + ServerPort int + Type string + SNI string + Network string + Path string + Host string + Fingerprint string + Flow string + AllowInsecure bool +} + +// parseVLESS parses a VLESS URL and extracts relevant information +func parseVLESS(vlessURL string) (*VLESSConfig, error) { + u, err := url.Parse(vlessURL) + if err != nil { + return nil, err + } + + // Extract UUID from the user part of the URL + uuid := u.User.Username() + + // Extract server address and port + serverAddr := u.Hostname() + portStr := u.Port() + if portStr == "" { + return nil, fmt.Errorf("port not specified in VLESS URL") + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, err + } + + // Parse query parameters + params := u.Query() + + vlessConfig := &VLESSConfig{ + UUID: uuid, + ServerAddr: serverAddr, + ServerPort: port, + Type: params.Get("type"), + SNI: params.Get("sni"), + Network: params.Get("network"), + Path: params.Get("path"), + Host: params.Get("host"), + Fingerprint: params.Get("fp"), + Flow: params.Get("flow"), + AllowInsecure: params.Get("allowInsecure") == "true", + } + + // Set default network type if not specified + if vlessConfig.Network == "" { + vlessConfig.Network = "tcp" + } + + return vlessConfig, nil +} + +// SingBoxOutbound represents an outbound configuration for Sing-box +type SingBoxOutbound struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Server string `json:"server"` + ServerPort int `json:"server_port"` + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network string `json:"network,omitempty"` + TLS *SingBoxTLS `json:"tls,omitempty"` + Transport map[string]interface{} `json:"transport,omitempty"` + V2RayTransport *V2RayTransport `json:"v2ray_transport,omitempty"` +} + +// SingBoxTLS represents TLS configuration for Sing-box +type SingBoxTLS struct { + Enabled bool `json:"enabled"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + UTLS *UTLS `json:"utls,omitempty"` +} + +// UTLS represents uTLS configuration +type UTLS struct { + Enabled bool `json:"enabled"` + Fingerprint string `json:"fingerprint"` +} + +// V2RayTransport represents V2Ray transport configuration +type V2RayTransport struct { + Type string `json:"type"` + Headers map[string]string `json:"headers,omitempty"` + Host string `json:"host,omitempty"` + Method string `json:"method,omitempty"` + Path string `json:"path,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// SingBoxInbound represents an inbound configuration for Sing-box +type SingBoxInbound struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Listen string `json:"listen"` + ListenPort int `json:"listen_port"` + SetSystemProxy bool `json:"set_system_proxy,omitempty"` +} + +// SingBoxConfig represents the full Sing-box configuration +type SingBoxConfig struct { + Log map[string]interface{} `json:"log"` + Inbounds []SingBoxInbound `json:"inbounds"` + Outbounds []SingBoxOutbound `json:"outbounds"` +} + +// generateSingBoxConfig generates a Sing-box configuration from VLESSConfig +func generateSingBoxConfig(vlessConfig *VLESSConfig, configPath string) error { + // Create TLS configuration if SNI is present + var tlsConfig *SingBoxTLS + if vlessConfig.SNI != "" || vlessConfig.Fingerprint != "" { + tlsConfig = &SingBoxTLS{ + Enabled: true, + ServerName: vlessConfig.SNI, + Insecure: vlessConfig.AllowInsecure, + } + + if vlessConfig.Fingerprint != "" { + tlsConfig.Fingerprint = vlessConfig.Fingerprint + // Enable uTLS if fingerprint is specified + tlsConfig.UTLS = &UTLS{ + Enabled: true, + Fingerprint: vlessConfig.Fingerprint, + } + } + } + + // Create V2Ray transport if specific transport settings are present + var v2rayTransport *V2RayTransport + if vlessConfig.Path != "" || vlessConfig.Host != "" { + v2rayTransport = &V2RayTransport{ + Type: vlessConfig.Network, + } + + if vlessConfig.Path != "" { + v2rayTransport.Path = vlessConfig.Path + } + + if vlessConfig.Host != "" { + v2rayTransport.Host = vlessConfig.Host + } + + // For HTTP headers + if vlessConfig.Host != "" { + v2rayTransport.Headers = map[string]string{ + "Host": vlessConfig.Host, + } + } + } + + // Create the outbound configuration + outbound := SingBoxOutbound{ + Type: "vless", + Server: vlessConfig.ServerAddr, + ServerPort: vlessConfig.ServerPort, + UUID: vlessConfig.UUID, + Flow: vlessConfig.Flow, + Network: vlessConfig.Network, + } + + if tlsConfig != nil { + outbound.TLS = tlsConfig + } + + if v2rayTransport != nil { + outbound.V2RayTransport = v2rayTransport + } + + // Special handling for different transport types + switch vlessConfig.Network { + case "ws": + // WebSocket transport + transport := map[string]interface{}{ + "type": vlessConfig.Network, + } + + if vlessConfig.Path != "" { + transport["path"] = vlessConfig.Path + } + + if vlessConfig.Host != "" { + headers := map[string]interface{}{ + "Host": vlessConfig.Host, + } + transport["headers"] = headers + } + + outbound.Transport = transport + case "grpc": + // gRPC transport + transport := map[string]interface{}{ + "type": vlessConfig.Network, + } + + if vlessConfig.Path != "" { + transport["service_name"] = vlessConfig.Path + } + + outbound.Transport = transport + } + + // Create inbounds - mixed (SOCKS5 + HTTP) on port 1080 + inbounds := []SingBoxInbound{ + { + Type: "mixed", + Listen: "127.0.0.1", + ListenPort: 1080, + SetSystemProxy: true, + }, + } + + // Create the full configuration + config := SingBoxConfig{ + Log: map[string]interface{}{ + "level": "info", + }, + Inbounds: inbounds, + Outbounds: []SingBoxOutbound{outbound}, + } + + // Convert to JSON + jsonData, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + // Write to file + err = os.WriteFile(configPath, jsonData, 0644) + if err != nil { + return err + } + + log.Printf("Sing-box configuration written to %s", configPath) + return nil +} + +// generateConfigFromVLESSURL parses a VLESS URL and generates a Sing-box config file +func generateConfigFromVLESSURL(vlessURL, outputPath string) error { + // Parse the VLESS URL + vlessConfig, err := parseVLESS(vlessURL) + if err != nil { + return err + } + + // Generate the Sing-box configuration + err = generateSingBoxConfig(vlessConfig, outputPath) + if err != nil { + return err + } + + return nil +} + func main() { myApp := app.New() myWindow := myApp.NewWindow("Hamy VPN Client") - // Set the window to not resizable and fixed size - myWindow.SetFixedSize(true) - // Set the window size to 200x300 as requested myWindow.Resize(fyne.NewSize(200, 300)) @@ -205,8 +484,23 @@ func main() { } statusLabel.Refresh() - // This functionality will be implemented later - dialog.ShowInformation("Info", fmt.Sprintf("Connect/Disconnect functionality will be implemented later using: %s", configs[activeConfig].Title), myWindow) + 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 { diff --git a/test_config.json b/test_config.json new file mode 100644 index 0000000..e8fce13 --- /dev/null +++ b/test_config.json @@ -0,0 +1,31 @@ +{ + "log": { + "level": "info" + }, + "inbounds": [ + { + "type": "mixed", + "listen": "127.0.0.1", + "listen_port": 1080, + "set_system_proxy": true + } + ], + "outbounds": [ + { + "type": "vless", + "server": "myserver.com", + "server_port": 443, + "uuid": "de3338a1-1234-5678-abcd-1234567890ab", + "network": "tcp", + "tls": { + "enabled": true, + "server_name": "myserver.com", + "fingerprint": "chrome", + "utls": { + "enabled": true, + "fingerprint": "chrome" + } + } + } + ] +} \ No newline at end of file