3. Парсинг ссылок и генерация конфига (vibe-kanban 09e20645)
Реализуй функцию, которая парсит ссылку `vless://`. 1. Извлеки: UUID, адрес сервера, порт, тип передачи (TCP), и SNI (если есть) и другие данные. 2. Сгенерируй JSON-конфиг в файл config.json специально в формате **Sing-box** (не Xray!). 3. В секции `outbounds` укажи `type: vless`. 4. Обязательно добавь секцию `v2ray-transport` внутри `outbound`, если на сервере 3X-UI используются специфические настройки транспорта. 5. Настрой `inbound` на `mixed` (SOCKS5 + HTTP) на порту 1080, чтобы через это ядро мог ходить любой трафик Windows.
This commit is contained in:
BIN
hamy-vpn-client-final.exe
Normal file
BIN
hamy-vpn-client-final.exe
Normal file
Binary file not shown.
BIN
hamy-vpn-client-new.exe
Normal file
BIN
hamy-vpn-client-new.exe
Normal file
Binary file not shown.
304
main.go
304
main.go
@@ -1,14 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"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
|
// runSingBox starts the sing-box process with the given config path
|
||||||
func runSingBox(configPath string) error {
|
func runSingBox(configPath string) error {
|
||||||
singBoxPath := filepath.Join("bin", "sing-box.exe")
|
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() {
|
func main() {
|
||||||
myApp := app.New()
|
myApp := app.New()
|
||||||
myWindow := myApp.NewWindow("Hamy VPN Client")
|
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
|
// Set the window size to 200x300 as requested
|
||||||
myWindow.Resize(fyne.NewSize(200, 300))
|
myWindow.Resize(fyne.NewSize(200, 300))
|
||||||
|
|
||||||
@@ -205,8 +484,23 @@ func main() {
|
|||||||
}
|
}
|
||||||
statusLabel.Refresh()
|
statusLabel.Refresh()
|
||||||
|
|
||||||
// This functionality will be implemented later
|
if isConnected {
|
||||||
dialog.ShowInformation("Info", fmt.Sprintf("Connect/Disconnect functionality will be implemented later using: %s", configs[activeConfig].Title), myWindow)
|
// 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
|
connectButton.Importance = widget.HighImportance
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
|
|||||||
31
test_config.json
Normal file
31
test_config.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user