1044 lines
32 KiB
Go
1044 lines
32 KiB
Go
package nativeui
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
fyneapp "fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/storage"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
coreapp "socksrevivepc/internal/app"
|
|
"socksrevivepc/internal/config"
|
|
"socksrevivepc/internal/crash"
|
|
)
|
|
|
|
type UI struct {
|
|
core *coreapp.App
|
|
app fyne.App
|
|
win fyne.Window
|
|
|
|
profiles []config.Profile
|
|
current config.Profile
|
|
lastLog int64
|
|
logText string
|
|
|
|
profileList *widget.List
|
|
status *widget.Label
|
|
subStatus *widget.Label
|
|
connectBtn *widget.Button
|
|
modeContent *fyne.Container
|
|
logView *widget.Label
|
|
logScroller *container.Scroll
|
|
|
|
name *widget.Entry
|
|
mode *widget.Select
|
|
sshHost *widget.Entry
|
|
sshPort *widget.Entry
|
|
sshUser *widget.Entry
|
|
sshPass *widget.Entry
|
|
keepAlive *widget.Entry
|
|
handshake *widget.Entry
|
|
|
|
proxyHost *widget.Entry
|
|
proxyPort *widget.Entry
|
|
|
|
payloadText *widget.Entry
|
|
payloadWait *widget.Check
|
|
payloadAny *widget.Check
|
|
payloadReply *widget.Entry
|
|
payloadSplit *widget.Entry
|
|
|
|
tlsHost *widget.Entry
|
|
tlsPort *widget.Entry
|
|
tlsSNI *widget.Entry
|
|
tlsInsecure *widget.Check
|
|
|
|
dnsttEmbedded *widget.Check
|
|
dnsttResolverType *widget.Select
|
|
dnsttResolverAddr *widget.Entry
|
|
dnsttDomain *widget.Entry
|
|
dnsttPublicKey *widget.Entry
|
|
dnsttUTLS *widget.Entry
|
|
dnsttExe *widget.Entry
|
|
dnsttArgs *widget.Entry
|
|
dnsttLocalHost *widget.Entry
|
|
dnsttLocalPort *widget.Entry
|
|
|
|
xrayExe *widget.Entry
|
|
xrayArgs *widget.Entry
|
|
xrayConfig *widget.Entry
|
|
xraySocksHost *widget.Entry
|
|
xraySocksPort *widget.Entry
|
|
|
|
udpgwEnabled *widget.Check
|
|
udpgwProtocol *widget.Select
|
|
udpgwHost *widget.Entry
|
|
udpgwPort *widget.Entry
|
|
|
|
localSocksHost *widget.Entry
|
|
localSocksPort *widget.Entry
|
|
|
|
reconnectEnabled *widget.Check
|
|
reconnectDelay *widget.Entry
|
|
reconnectMax *widget.Entry
|
|
reconnectCheck *widget.Entry
|
|
|
|
tunEnabled *widget.Check
|
|
tunDevice *widget.Entry
|
|
tunIface *widget.Entry
|
|
tunMTU *widget.Entry
|
|
tunCIDR *widget.Entry
|
|
tunDNS *widget.Entry
|
|
tunRoute *widget.Check
|
|
tunIPv6Enabled *widget.Check
|
|
tunIPv6CIDR *widget.Entry
|
|
tunIPv6DNS *widget.Entry
|
|
tunBlockIPv6Leak *widget.Check
|
|
}
|
|
|
|
func Run(core *coreapp.App) {
|
|
a := fyneapp.NewWithID("com.socksrevive.pc")
|
|
a.Settings().SetTheme(theme.DarkTheme())
|
|
w := a.NewWindow("SocksRevive PC")
|
|
w.Resize(fyne.NewSize(1220, 780))
|
|
|
|
u := &UI{core: core, app: a, win: w}
|
|
u.build()
|
|
u.loadProfiles()
|
|
u.refreshStatus()
|
|
u.startLogPoller()
|
|
|
|
w.SetCloseIntercept(func() {
|
|
u.core.Engine.Stop()
|
|
w.Close()
|
|
})
|
|
w.CenterOnScreen()
|
|
w.ShowAndRun()
|
|
}
|
|
|
|
func (u *UI) build() {
|
|
u.profileList = widget.NewList(
|
|
func() int { return len(u.profiles) },
|
|
func() fyne.CanvasObject {
|
|
name := widget.NewLabel("Profile")
|
|
name.TextStyle = fyne.TextStyle{Bold: true}
|
|
name.Truncation = fyne.TextTruncateEllipsis
|
|
mode := widget.NewLabel("Tunnel mode")
|
|
mode.Truncation = fyne.TextTruncateEllipsis
|
|
return container.NewVBox(name, mode)
|
|
},
|
|
func(id widget.ListItemID, obj fyne.CanvasObject) {
|
|
if id < 0 || id >= len(u.profiles) {
|
|
return
|
|
}
|
|
p := u.profiles[id]
|
|
box := obj.(*fyne.Container)
|
|
name := box.Objects[0].(*widget.Label)
|
|
mode := box.Objects[1].(*widget.Label)
|
|
prefix := ""
|
|
if p.ID == u.current.ID {
|
|
prefix = "● "
|
|
}
|
|
name.SetText(prefix + p.Name)
|
|
mode.SetText(modeLabel(p.Mode))
|
|
},
|
|
)
|
|
u.profileList.OnSelected = func(id widget.ListItemID) {
|
|
if id < 0 || id >= len(u.profiles) {
|
|
return
|
|
}
|
|
p, err := u.core.Store.Load(u.profiles[id].ID)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
u.setProfile(p)
|
|
}
|
|
|
|
u.status = widget.NewLabel("Disconnected")
|
|
u.status.TextStyle = fyne.TextStyle{Bold: true}
|
|
u.subStatus = widget.NewLabel("No active tunnel")
|
|
u.subStatus.Truncation = fyne.TextTruncateEllipsis
|
|
u.connectBtn = widget.NewButtonWithIcon("Connect", theme.MediaPlayIcon(), u.toggleConnection)
|
|
|
|
side := fixedWidth(315, sidebarPanel(u.sidebar()))
|
|
center := minSize(860, 620, container.NewBorder(u.topBar(), nil, nil, nil, u.profileEditor()))
|
|
u.win.SetContent(container.NewBorder(nil, nil, side, nil, center))
|
|
}
|
|
|
|
func (u *UI) sidebar() fyne.CanvasObject {
|
|
title := widget.NewLabel("SocksRevive PC")
|
|
title.TextStyle = fyne.TextStyle{Bold: true}
|
|
subtitle := widget.NewLabel("Offline profiles")
|
|
subtitle.Importance = widget.LowImportance
|
|
|
|
newBtn := widget.NewButtonWithIcon("New", theme.ContentAddIcon(), u.newProfile)
|
|
deleteBtn := widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), u.deleteProfile)
|
|
importBtn := widget.NewButtonWithIcon("Import", theme.FolderOpenIcon(), u.importProfile)
|
|
exportBtn := widget.NewButtonWithIcon("Export", theme.DocumentSaveIcon(), u.exportProfile)
|
|
buttons := container.NewGridWithColumns(2, newBtn, deleteBtn, importBtn, exportBtn)
|
|
|
|
emptyHint := widget.NewLabel("Create or import a .srpc profile to start.")
|
|
emptyHint.Wrapping = fyne.TextWrapWord
|
|
emptyHint.Importance = widget.LowImportance
|
|
|
|
top := container.NewVBox(title, subtitle, buttons, widget.NewSeparator())
|
|
body := container.NewBorder(top, container.NewVBox(widget.NewSeparator(), emptyHint), nil, nil, u.profileList)
|
|
return container.NewPadded(body)
|
|
}
|
|
|
|
func (u *UI) topBar() fyne.CanvasObject {
|
|
brand := widget.NewLabel("Tunnel Control")
|
|
brand.TextStyle = fyne.TextStyle{Bold: true}
|
|
statusBlock := container.NewVBox(u.status, u.subStatus)
|
|
actions := container.NewHBox(
|
|
widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), u.saveProfile),
|
|
u.connectBtn,
|
|
)
|
|
content := container.NewBorder(nil, nil, container.NewVBox(brand), actions, statusBlock)
|
|
return softPanel(container.NewPadded(content))
|
|
}
|
|
|
|
func (u *UI) profileEditor() fyne.CanvasObject {
|
|
u.name = widget.NewEntry()
|
|
u.name.SetPlaceHolder("My server")
|
|
u.mode = widget.NewSelect(modeLabels(), func(_ string) { u.updateModePanel() })
|
|
|
|
u.sshHost = widget.NewEntry()
|
|
u.sshHost.SetPlaceHolder("ssh.example.com")
|
|
u.sshPort = widget.NewEntry()
|
|
u.sshPort.SetPlaceHolder("22")
|
|
u.sshUser = widget.NewEntry()
|
|
u.sshUser.SetPlaceHolder("username")
|
|
u.sshPass = widget.NewPasswordEntry()
|
|
u.sshPass.SetPlaceHolder("password")
|
|
u.keepAlive = widget.NewEntry()
|
|
u.handshake = widget.NewEntry()
|
|
|
|
u.proxyHost = widget.NewEntry()
|
|
u.proxyHost.SetPlaceHolder("proxy1.com#proxy2.com#52.85.78.104")
|
|
u.proxyPort = widget.NewEntry()
|
|
u.proxyPort.SetPlaceHolder("80 or 443")
|
|
|
|
u.payloadText = widget.NewMultiLineEntry()
|
|
u.payloadText.SetMinRowsVisible(8)
|
|
u.payloadText.Wrapping = fyne.TextWrapOff
|
|
u.payloadText.SetPlaceHolder("CONNECT [host]:[port] HTTP/1.1[crlf]Host: [host][crlf][crlf]")
|
|
u.payloadWait = widget.NewCheck("Wait for proxy response", nil)
|
|
u.payloadAny = widget.NewCheck("Accept any HTTP status", nil)
|
|
u.payloadReply = widget.NewEntry()
|
|
u.payloadSplit = widget.NewEntry()
|
|
|
|
u.tlsHost = widget.NewEntry()
|
|
u.tlsHost.SetPlaceHolder("cdn.example.com")
|
|
u.tlsPort = widget.NewEntry()
|
|
u.tlsSNI = widget.NewEntry()
|
|
u.tlsSNI.SetPlaceHolder("server name / SNI")
|
|
u.tlsInsecure = widget.NewCheck("Allow insecure TLS / skip certificate verify", nil)
|
|
|
|
u.dnsttEmbedded = widget.NewCheck("Use embedded DNSTT engine / no external EXE", nil)
|
|
u.dnsttResolverType = widget.NewSelect([]string{"doh", "dot", "udp"}, nil)
|
|
u.dnsttResolverAddr = widget.NewEntry()
|
|
u.dnsttResolverAddr.SetPlaceHolder("DoH URL, DoT host:853, or UDP DNS host:53")
|
|
u.dnsttDomain = widget.NewEntry()
|
|
u.dnsttDomain.SetPlaceHolder("t.example.com")
|
|
u.dnsttPublicKey = widget.NewEntry()
|
|
u.dnsttPublicKey.SetPlaceHolder("server public key hex")
|
|
u.dnsttUTLS = widget.NewEntry()
|
|
u.dnsttExe = widget.NewEntry()
|
|
u.dnsttArgs = widget.NewMultiLineEntry()
|
|
u.dnsttArgs.SetMinRowsVisible(3)
|
|
u.dnsttLocalHost = widget.NewEntry()
|
|
u.dnsttLocalPort = widget.NewEntry()
|
|
|
|
u.xrayExe = widget.NewEntry()
|
|
u.xrayExe.SetPlaceHolder("tools/xray/xray.exe")
|
|
u.xrayArgs = widget.NewEntry()
|
|
u.xrayConfig = widget.NewEntry()
|
|
u.xrayConfig.SetPlaceHolder("configs/xray.json")
|
|
u.xraySocksHost = widget.NewEntry()
|
|
u.xraySocksPort = widget.NewEntry()
|
|
|
|
u.udpgwEnabled = widget.NewCheck("Enable UDPGW for real UDP in SSH modes", nil)
|
|
u.udpgwProtocol = widget.NewSelect([]string{"badvpn", "legacy"}, nil)
|
|
u.udpgwProtocol.SetSelected("badvpn")
|
|
u.udpgwHost = widget.NewEntry()
|
|
u.udpgwHost.SetPlaceHolder("127.0.0.1")
|
|
u.udpgwPort = widget.NewEntry()
|
|
u.udpgwPort.SetPlaceHolder("7400")
|
|
|
|
u.localSocksHost = widget.NewEntry()
|
|
u.localSocksPort = widget.NewEntry()
|
|
|
|
u.reconnectEnabled = widget.NewCheck("Auto reconnect if the tunnel is lost", nil)
|
|
u.reconnectDelay = widget.NewEntry()
|
|
u.reconnectDelay.SetPlaceHolder("3")
|
|
u.reconnectMax = widget.NewEntry()
|
|
u.reconnectMax.SetPlaceHolder("0 = forever")
|
|
u.reconnectCheck = widget.NewEntry()
|
|
u.reconnectCheck.SetPlaceHolder("10")
|
|
|
|
u.tunEnabled = widget.NewCheck("Enable full-device TUN mode", nil)
|
|
u.tunDevice = widget.NewEntry()
|
|
u.tunDevice.SetPlaceHolder("Windows: wintun | Linux: tun://socksrevive0")
|
|
u.tunIface = widget.NewEntry()
|
|
u.tunIface.SetPlaceHolder("Windows: wintun | Linux: socksrevive0")
|
|
u.tunMTU = widget.NewEntry()
|
|
u.tunCIDR = widget.NewEntry()
|
|
u.tunCIDR.SetPlaceHolder("198.18.0.1/15")
|
|
u.tunDNS = widget.NewEntry()
|
|
u.tunDNS.SetPlaceHolder("1.1.1.1, 8.8.8.8")
|
|
u.tunRoute = widget.NewCheck("Route all IPv4 traffic", nil)
|
|
u.tunIPv6Enabled = widget.NewCheck("Enable IPv6 through tunnel", nil)
|
|
u.tunIPv6CIDR = widget.NewEntry()
|
|
u.tunIPv6CIDR.SetPlaceHolder("fd00:534f:434b::1/64")
|
|
u.tunIPv6DNS = widget.NewEntry()
|
|
u.tunIPv6DNS.SetPlaceHolder("2606:4700:4700::1111, 2001:4860:4860::8888")
|
|
u.tunBlockIPv6Leak = widget.NewCheck("Block IPv6 leaks when IPv6 tunnel is off", nil)
|
|
|
|
u.logView = widget.NewLabel("No logs yet.")
|
|
u.logView.Wrapping = fyne.TextWrapWord
|
|
u.logView.TextStyle = fyne.TextStyle{Monospace: true}
|
|
u.logScroller = container.NewVScroll(u.logView)
|
|
u.logScroller.SetMinSize(fyne.NewSize(760, 430))
|
|
|
|
u.modeContent = container.NewVBox()
|
|
u.updateModePanel()
|
|
return container.NewPadded(u.modeContent)
|
|
}
|
|
|
|
func (u *UI) updateModePanel() {
|
|
if u.modeContent == nil || u.mode == nil {
|
|
return
|
|
}
|
|
mode := modeValue(u.mode.Selected)
|
|
tabs := container.NewAppTabs(
|
|
tabPage("Main", u.mainSection()),
|
|
)
|
|
switch mode {
|
|
case config.ModeDirect:
|
|
tabs.Append(tabPage("SSH", u.sshSection(true)))
|
|
case config.ModePayload:
|
|
tabs.Append(tabPage("SSH", u.sshSection(true)))
|
|
tabs.Append(tabPage("Proxy", u.proxySection()))
|
|
tabs.Append(tabPage("Payload", u.payloadSection()))
|
|
case config.ModeSSL:
|
|
tabs.Append(tabPage("SSH", u.sshSection(true)))
|
|
tabs.Append(tabPage("SSL/SNI", u.tlsSection()))
|
|
case config.ModePayloadSSL:
|
|
tabs.Append(tabPage("SSH", u.sshSection(true)))
|
|
tabs.Append(tabPage("SSL/SNI", u.tlsSection()))
|
|
tabs.Append(tabPage("Payload", u.payloadSection()))
|
|
case config.ModeDNSTT:
|
|
tabs.Append(tabPage("DNSTT", u.dnsttSection()))
|
|
tabs.Append(tabPage("SSH", u.sshSection(false)))
|
|
case config.ModeXray:
|
|
tabs.Append(tabPage("Xray", u.xraySection()))
|
|
default:
|
|
tabs.Append(tabPage("SSH", u.sshSection(true)))
|
|
}
|
|
if mode != config.ModeXray {
|
|
tabs.Append(tabPage("UDPGW", u.udpgwSection()))
|
|
}
|
|
tabs.Append(tabPage("TUN", u.tunSection()))
|
|
tabs.Append(tabPage("Logs", u.logsSection()))
|
|
tabs.SetTabLocation(container.TabLocationTop)
|
|
u.modeContent.Objects = []fyne.CanvasObject{tabs}
|
|
u.modeContent.Refresh()
|
|
}
|
|
|
|
func (u *UI) mainSection() fyne.CanvasObject {
|
|
return section("Profile", "Choose the tunnel mode first. Only the tabs needed for that mode are shown.", widget.NewForm(
|
|
widget.NewFormItem("Profile name", u.name),
|
|
widget.NewFormItem("Tunnel mode", u.mode),
|
|
widget.NewFormItem("Local SOCKS host", u.localSocksHost),
|
|
widget.NewFormItem("Local SOCKS port", u.localSocksPort),
|
|
widget.NewFormItem("Reconnect", u.reconnectEnabled),
|
|
widget.NewFormItem("Reconnect delay seconds", u.reconnectDelay),
|
|
widget.NewFormItem("Reconnect max retries", u.reconnectMax),
|
|
widget.NewFormItem("Reconnect check seconds", u.reconnectCheck),
|
|
))
|
|
}
|
|
|
|
func (u *UI) sshSection(includeHost bool) fyne.CanvasObject {
|
|
items := []*widget.FormItem{}
|
|
if includeHost {
|
|
items = append(items,
|
|
widget.NewFormItem("SSH host", u.sshHost),
|
|
widget.NewFormItem("SSH port", u.sshPort),
|
|
)
|
|
}
|
|
items = append(items,
|
|
widget.NewFormItem("Username", u.sshUser),
|
|
widget.NewFormItem("Password", u.sshPass),
|
|
widget.NewFormItem("Keep alive seconds", u.keepAlive),
|
|
widget.NewFormItem("Handshake timeout ms", u.handshake),
|
|
)
|
|
subtitle := "Credentials used after the transport is ready."
|
|
if includeHost {
|
|
subtitle = "SSH server and login credentials."
|
|
}
|
|
return section("SSH", subtitle, widget.NewForm(items...))
|
|
}
|
|
|
|
func (u *UI) proxySection() fyne.CanvasObject {
|
|
return section("Optional proxy / rotate", "Leave empty to connect directly. For rotation, separate hosts with #, comma, semicolon, or new lines. The app tries each proxy until payload + SSH succeeds.", widget.NewForm(
|
|
widget.NewFormItem("Proxy host(s)", u.proxyHost),
|
|
widget.NewFormItem("Proxy port", u.proxyPort),
|
|
))
|
|
}
|
|
|
|
func (u *UI) payloadSection() fyne.CanvasObject {
|
|
settings := widget.NewForm(
|
|
widget.NewFormItem("Response timeout ms", u.payloadReply),
|
|
widget.NewFormItem("Split delay ms", u.payloadSplit),
|
|
)
|
|
content := container.NewVBox(u.payloadText, u.payloadWait, u.payloadAny, settings)
|
|
return section("HTTP payload", "Supports [host], [port], [crlf], [lf], [rotate=a;b;c] or [rotate=a#b#c], [split] and [instant_split].", content)
|
|
}
|
|
|
|
func (u *UI) tlsSection() fyne.CanvasObject {
|
|
return section("SSL/SNI", "TLS front host and SNI used before SSH or payload injection.", widget.NewForm(
|
|
widget.NewFormItem("TLS/front host", u.tlsHost),
|
|
widget.NewFormItem("TLS port", u.tlsPort),
|
|
widget.NewFormItem("SNI/server name", u.tlsSNI),
|
|
widget.NewFormItem("Security", u.tlsInsecure),
|
|
))
|
|
}
|
|
|
|
func (u *UI) dnsttSection() fyne.CanvasObject {
|
|
resolver := widget.NewForm(
|
|
widget.NewFormItem("Embedded engine", u.dnsttEmbedded),
|
|
widget.NewFormItem("Resolver type", u.dnsttResolverType),
|
|
widget.NewFormItem("Resolver", u.dnsttResolverAddr),
|
|
widget.NewFormItem("Tunnel domain", u.dnsttDomain),
|
|
widget.NewFormItem("Server public key", u.dnsttPublicKey),
|
|
widget.NewFormItem("uTLS profile", u.dnsttUTLS),
|
|
widget.NewFormItem("Local SSH host", u.dnsttLocalHost),
|
|
widget.NewFormItem("Local SSH port", u.dnsttLocalPort),
|
|
)
|
|
fallback := widget.NewAccordion(widget.NewAccordionItem("External DNSTT fallback", widget.NewForm(
|
|
widget.NewFormItem("Executable", u.dnsttExe),
|
|
widget.NewFormItem("Arguments", u.dnsttArgs),
|
|
)))
|
|
return section("DNSTT", "Embedded client is used by default. External executable is only for legacy fallback.", container.NewVBox(resolver, fallback))
|
|
}
|
|
|
|
func (u *UI) xraySection() fyne.CanvasObject {
|
|
return section("Xray", "Place xray.exe in tools/xray or browse to your own executable path.", widget.NewForm(
|
|
widget.NewFormItem("Executable", u.xrayExe),
|
|
widget.NewFormItem("Arguments", u.xrayArgs),
|
|
widget.NewFormItem("Config file", u.xrayConfig),
|
|
widget.NewFormItem("Xray SOCKS host", u.xraySocksHost),
|
|
widget.NewFormItem("Xray SOCKS port", u.xraySocksPort),
|
|
))
|
|
}
|
|
|
|
func (u *UI) udpgwSection() fyne.CanvasObject {
|
|
return section("UDPGW / UDP support", "Optional UDP gateway for SSH-based modes. Use badvpn for Android-compatible badvpn-udpgw servers and IPv6 UDP. Use legacy only for the old experimental PC frame.", widget.NewForm(
|
|
widget.NewFormItem("Enabled", u.udpgwEnabled),
|
|
widget.NewFormItem("Protocol", u.udpgwProtocol),
|
|
widget.NewFormItem("Remote UDPGW host", u.udpgwHost),
|
|
widget.NewFormItem("Remote UDPGW port", u.udpgwPort),
|
|
))
|
|
}
|
|
|
|
func (u *UI) tunSection() fyne.CanvasObject {
|
|
return section("TUN / full device mode", "Optional OpenVPN-style routing through the active SOCKS tunnel. IPv6 is optional; enable it only when your server/tunnel has IPv6 egress. Keep leak blocking on if you do not want Windows to expose IPv6 outside the VPN.", widget.NewForm(
|
|
widget.NewFormItem("Enabled", u.tunEnabled),
|
|
widget.NewFormItem("Device", u.tunDevice),
|
|
widget.NewFormItem("Interface name", u.tunIface),
|
|
widget.NewFormItem("MTU", u.tunMTU),
|
|
widget.NewFormItem("IPv4 CIDR", u.tunCIDR),
|
|
widget.NewFormItem("IPv4 DNS", u.tunDNS),
|
|
widget.NewFormItem("IPv4 routing", u.tunRoute),
|
|
widget.NewFormItem("IPv6 support", u.tunIPv6Enabled),
|
|
widget.NewFormItem("IPv6 CIDR", u.tunIPv6CIDR),
|
|
widget.NewFormItem("IPv6 DNS", u.tunIPv6DNS),
|
|
widget.NewFormItem("IPv6 leak protection", u.tunBlockIPv6Leak),
|
|
))
|
|
}
|
|
|
|
func (u *UI) logsSection() fyne.CanvasObject {
|
|
return section("Logs", "Runtime messages from SSH, DNSTT, Xray, TUN and routing.", u.logScroller)
|
|
}
|
|
|
|
func (u *UI) newProfile() {
|
|
p := config.Profile{
|
|
Name: "New Profile",
|
|
Mode: config.ModeDirect,
|
|
Payload: config.PayloadConfig{Text: "CONNECT [host]:[port] HTTP/1.1[crlf]Host: [host][crlf]User-Agent: SocksRevivePC[crlf][crlf]", WaitForResponse: true, AcceptAnyStatus: true},
|
|
TLS: config.TLSConfig{Port: 443},
|
|
DNSTT: config.DNSTTConfig{UseEmbedded: true, ResolverType: "doh"},
|
|
UDPGW: config.UDPGWConfig{Host: "127.0.0.1", Port: 7400, Protocol: "badvpn"},
|
|
Reconnect: config.ReconnectConfig{Enabled: true, DelaySeconds: 3, MaxRetries: 0, CheckIntervalSeconds: 10},
|
|
Tun: config.TunConfig{RouteAll: true},
|
|
}
|
|
config.ApplyDefaults(&p)
|
|
u.setProfile(p)
|
|
}
|
|
|
|
func (u *UI) loadProfiles() {
|
|
profiles, err := u.core.Store.List()
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
u.profiles = profiles
|
|
u.profileList.Refresh()
|
|
if u.current.ID == "" && len(profiles) > 0 {
|
|
p, err := u.core.Store.Load(profiles[0].ID)
|
|
if err == nil {
|
|
u.setProfile(p)
|
|
}
|
|
}
|
|
if len(profiles) == 0 {
|
|
u.newProfile()
|
|
}
|
|
}
|
|
|
|
func (u *UI) setProfile(p config.Profile) {
|
|
config.ApplyDefaults(&p)
|
|
u.current = p
|
|
u.name.SetText(p.Name)
|
|
u.mode.SetSelected(modeLabel(p.Mode))
|
|
u.sshHost.SetText(p.SSH.Host)
|
|
u.sshPort.SetText(itoa(p.SSH.Port))
|
|
u.sshUser.SetText(p.SSH.Username)
|
|
u.sshPass.SetText(p.SSH.Password)
|
|
u.keepAlive.SetText(itoa(p.SSH.KeepAliveSeconds))
|
|
u.handshake.SetText(itoa(p.SSH.HandshakeTimeoutMs))
|
|
u.proxyHost.SetText(p.Proxy.Host)
|
|
u.proxyPort.SetText(itoa(p.Proxy.Port))
|
|
u.payloadText.SetText(p.Payload.Text)
|
|
u.payloadWait.SetChecked(p.Payload.WaitForResponse)
|
|
u.payloadAny.SetChecked(p.Payload.AcceptAnyStatus)
|
|
u.payloadReply.SetText(itoa(p.Payload.ResponseTimeoutMs))
|
|
u.payloadSplit.SetText(itoa(p.Payload.SplitDelayMs))
|
|
u.tlsHost.SetText(p.TLS.Host)
|
|
u.tlsPort.SetText(itoa(p.TLS.Port))
|
|
u.tlsSNI.SetText(p.TLS.ServerName)
|
|
u.tlsInsecure.SetChecked(p.TLS.InsecureSkipVerify)
|
|
u.dnsttEmbedded.SetChecked(p.DNSTT.UseEmbedded)
|
|
u.dnsttResolverType.SetSelected(p.DNSTT.ResolverType)
|
|
u.dnsttResolverAddr.SetText(p.DNSTT.ResolverAddress)
|
|
u.dnsttDomain.SetText(p.DNSTT.Domain)
|
|
u.dnsttPublicKey.SetText(p.DNSTT.PublicKey)
|
|
u.dnsttUTLS.SetText(p.DNSTT.UTLSDistribution)
|
|
u.dnsttExe.SetText(p.DNSTT.Executable)
|
|
u.dnsttArgs.SetText(strings.Join(p.DNSTT.Args, " "))
|
|
u.dnsttLocalHost.SetText(p.DNSTT.LocalSSHHost)
|
|
u.dnsttLocalPort.SetText(itoa(p.DNSTT.LocalSSHPort))
|
|
u.xrayExe.SetText(p.Xray.Executable)
|
|
u.xrayArgs.SetText(strings.Join(p.Xray.Args, " "))
|
|
u.xrayConfig.SetText(p.Xray.ConfigPath)
|
|
u.xraySocksHost.SetText(p.Xray.LocalSocksHost)
|
|
u.xraySocksPort.SetText(itoa(p.Xray.LocalSocksPort))
|
|
u.udpgwEnabled.SetChecked(p.UDPGW.Enabled)
|
|
u.udpgwProtocol.SetSelected(p.UDPGW.Protocol)
|
|
u.udpgwHost.SetText(p.UDPGW.Host)
|
|
u.udpgwPort.SetText(itoa(p.UDPGW.Port))
|
|
u.localSocksHost.SetText(p.Local.SocksHost)
|
|
u.localSocksPort.SetText(itoa(p.Local.SocksPort))
|
|
u.reconnectEnabled.SetChecked(p.Reconnect.Enabled)
|
|
u.reconnectDelay.SetText(itoa(p.Reconnect.DelaySeconds))
|
|
u.reconnectMax.SetText(itoa(p.Reconnect.MaxRetries))
|
|
u.reconnectCheck.SetText(itoa(p.Reconnect.CheckIntervalSeconds))
|
|
u.tunEnabled.SetChecked(p.Tun.Enabled)
|
|
u.tunDevice.SetText(p.Tun.Device)
|
|
u.tunIface.SetText(p.Tun.InterfaceName)
|
|
u.tunMTU.SetText(itoa(p.Tun.MTU))
|
|
u.tunCIDR.SetText(p.Tun.CIDR)
|
|
u.tunDNS.SetText(strings.Join(p.Tun.DNS, ", "))
|
|
u.tunRoute.SetChecked(p.Tun.RouteAll)
|
|
u.tunIPv6Enabled.SetChecked(p.Tun.IPv6Enabled)
|
|
u.tunIPv6CIDR.SetText(p.Tun.IPv6CIDR)
|
|
u.tunIPv6DNS.SetText(strings.Join(p.Tun.IPv6DNS, ", "))
|
|
u.tunBlockIPv6Leak.SetChecked(!p.Tun.AllowIPv6Leak)
|
|
u.updateModePanel()
|
|
u.profileList.Refresh()
|
|
}
|
|
|
|
func (u *UI) readProfileFromForm() (config.Profile, error) {
|
|
p := u.current
|
|
p.Name = strings.TrimSpace(u.name.Text)
|
|
p.Mode = modeValue(u.mode.Selected)
|
|
p.SSH.Host = strings.TrimSpace(u.sshHost.Text)
|
|
p.SSH.Port = mustInt(u.sshPort.Text, 22)
|
|
p.SSH.Username = strings.TrimSpace(u.sshUser.Text)
|
|
p.SSH.Password = u.sshPass.Text
|
|
p.SSH.KeepAliveSeconds = mustInt(u.keepAlive.Text, 20)
|
|
p.SSH.HandshakeTimeoutMs = mustInt(u.handshake.Text, 15000)
|
|
p.Proxy.Host = strings.TrimSpace(u.proxyHost.Text)
|
|
p.Proxy.Port = mustInt(u.proxyPort.Text, 0)
|
|
p.Payload.Text = u.payloadText.Text
|
|
p.Payload.WaitForResponse = u.payloadWait.Checked
|
|
p.Payload.AcceptAnyStatus = u.payloadAny.Checked
|
|
p.Payload.ResponseTimeoutMs = mustInt(u.payloadReply.Text, 8000)
|
|
p.Payload.SplitDelayMs = mustInt(u.payloadSplit.Text, 120)
|
|
p.TLS.Enabled = p.Mode == config.ModeSSL || p.Mode == config.ModePayloadSSL || u.tlsHost.Text != "" || u.tlsSNI.Text != ""
|
|
p.TLS.Host = strings.TrimSpace(u.tlsHost.Text)
|
|
p.TLS.Port = mustInt(u.tlsPort.Text, 443)
|
|
p.TLS.ServerName = strings.TrimSpace(u.tlsSNI.Text)
|
|
p.TLS.InsecureSkipVerify = u.tlsInsecure.Checked
|
|
p.DNSTT.Enabled = p.Mode == config.ModeDNSTT
|
|
p.DNSTT.UseEmbedded = u.dnsttEmbedded.Checked
|
|
p.DNSTT.ResolverType = strings.TrimSpace(u.dnsttResolverType.Selected)
|
|
p.DNSTT.ResolverAddress = strings.TrimSpace(u.dnsttResolverAddr.Text)
|
|
p.DNSTT.Domain = strings.TrimSpace(u.dnsttDomain.Text)
|
|
p.DNSTT.PublicKey = strings.TrimSpace(u.dnsttPublicKey.Text)
|
|
p.DNSTT.UTLSDistribution = strings.TrimSpace(u.dnsttUTLS.Text)
|
|
p.DNSTT.Executable = strings.TrimSpace(u.dnsttExe.Text)
|
|
p.DNSTT.Args = splitArgs(u.dnsttArgs.Text)
|
|
p.DNSTT.LocalSSHHost = strings.TrimSpace(u.dnsttLocalHost.Text)
|
|
p.DNSTT.LocalSSHPort = mustInt(u.dnsttLocalPort.Text, 2222)
|
|
p.Xray.Executable = strings.TrimSpace(u.xrayExe.Text)
|
|
p.Xray.Args = splitArgs(u.xrayArgs.Text)
|
|
p.Xray.ConfigPath = strings.TrimSpace(u.xrayConfig.Text)
|
|
p.Xray.LocalSocksHost = strings.TrimSpace(u.xraySocksHost.Text)
|
|
p.Xray.LocalSocksPort = mustInt(u.xraySocksPort.Text, 10808)
|
|
p.UDPGW.Enabled = u.udpgwEnabled.Checked
|
|
p.UDPGW.Protocol = strings.TrimSpace(u.udpgwProtocol.Selected)
|
|
p.UDPGW.Host = strings.TrimSpace(u.udpgwHost.Text)
|
|
p.UDPGW.Port = mustInt(u.udpgwPort.Text, 7400)
|
|
p.Local.SocksHost = strings.TrimSpace(u.localSocksHost.Text)
|
|
p.Local.SocksPort = mustInt(u.localSocksPort.Text, 10809)
|
|
p.Reconnect.Enabled = u.reconnectEnabled.Checked
|
|
p.Reconnect.DelaySeconds = mustInt(u.reconnectDelay.Text, 3)
|
|
p.Reconnect.MaxRetries = mustInt(u.reconnectMax.Text, 0)
|
|
p.Reconnect.CheckIntervalSeconds = mustInt(u.reconnectCheck.Text, 10)
|
|
p.Tun.Enabled = u.tunEnabled.Checked
|
|
p.Tun.Device = strings.TrimSpace(u.tunDevice.Text)
|
|
p.Tun.InterfaceName = strings.TrimSpace(u.tunIface.Text)
|
|
p.Tun.MTU = mustInt(u.tunMTU.Text, 1500)
|
|
p.Tun.CIDR = strings.TrimSpace(u.tunCIDR.Text)
|
|
p.Tun.DNS = splitCSV(u.tunDNS.Text)
|
|
p.Tun.RouteAll = u.tunRoute.Checked
|
|
p.Tun.IPv6Enabled = u.tunIPv6Enabled.Checked
|
|
p.Tun.IPv6CIDR = strings.TrimSpace(u.tunIPv6CIDR.Text)
|
|
p.Tun.IPv6DNS = splitCSV(u.tunIPv6DNS.Text)
|
|
p.Tun.AllowIPv6Leak = !u.tunBlockIPv6Leak.Checked
|
|
config.ApplyDefaults(&p)
|
|
return p, config.Validate(p)
|
|
}
|
|
|
|
func (u *UI) saveProfile() {
|
|
p, err := u.readProfileFromForm()
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
saved, err := u.core.Store.Save(p)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
u.current = saved
|
|
u.loadProfiles()
|
|
u.setProfile(saved)
|
|
dialog.ShowInformation("Saved", "Profile saved as .srpc", u.win)
|
|
}
|
|
|
|
func (u *UI) deleteProfile() {
|
|
if u.current.ID == "" {
|
|
return
|
|
}
|
|
dialog.ShowConfirm("Delete profile", "Delete this offline profile?", func(ok bool) {
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := u.core.Store.Delete(u.current.ID); err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
u.current = config.Profile{}
|
|
u.loadProfiles()
|
|
}, u.win)
|
|
}
|
|
|
|
func (u *UI) toggleConnection() {
|
|
s := u.core.Engine.Status()
|
|
if s.Running || s.Connecting {
|
|
u.disconnect()
|
|
return
|
|
}
|
|
u.connect()
|
|
}
|
|
|
|
func (u *UI) connect() {
|
|
p, err := u.readProfileFromForm()
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
saved, err := u.core.Store.Save(p)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
u.current = saved
|
|
u.loadProfiles()
|
|
u.setProfile(saved)
|
|
u.status.SetText("Connecting...")
|
|
u.subStatus.SetText("Starting tunnel. Logs will keep updating while proxy rotation is tested.")
|
|
u.connectBtn.SetText("Cancel")
|
|
u.connectBtn.SetIcon(theme.MediaStopIcon())
|
|
crash.Go(u.core.Root, func() {
|
|
err := u.core.Engine.Start(saved)
|
|
fyne.Do(func() {
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
dialog.ShowError(err, u.win)
|
|
}
|
|
u.refreshStatus()
|
|
})
|
|
})
|
|
}
|
|
|
|
func (u *UI) disconnect() {
|
|
u.status.SetText("Disconnecting...")
|
|
u.subStatus.SetText("Stopping tunnel and restoring routes.")
|
|
u.connectBtn.Disable()
|
|
crash.Go(u.core.Root, func() {
|
|
u.core.Engine.Stop()
|
|
fyne.Do(func() {
|
|
u.connectBtn.Enable()
|
|
u.refreshStatus()
|
|
})
|
|
})
|
|
}
|
|
|
|
func (u *UI) importProfile() {
|
|
fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
if r == nil {
|
|
return
|
|
}
|
|
defer r.Close()
|
|
b, err := io.ReadAll(r)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
p, err := config.DecodeProfileFile(b)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
p.ID = ""
|
|
saved, err := u.core.Store.Save(p)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
u.loadProfiles()
|
|
u.setProfile(saved)
|
|
}, u.win)
|
|
fd.SetFilter(storage.NewExtensionFileFilter([]string{config.ProfileExtension}))
|
|
fd.Show()
|
|
}
|
|
|
|
func (u *UI) exportProfile() {
|
|
p, err := u.readProfileFromForm()
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
p, err = u.core.Store.Save(p)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
data, err := config.EncodeProfileFile(p)
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
fd := dialog.NewFileSave(func(w fyne.URIWriteCloser, err error) {
|
|
if err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
if w == nil {
|
|
return
|
|
}
|
|
defer w.Close()
|
|
if _, err := w.Write(data); err != nil {
|
|
dialog.ShowError(err, u.win)
|
|
return
|
|
}
|
|
}, u.win)
|
|
fd.SetFileName(safeFileName(p.Name) + config.ProfileExtension)
|
|
fd.SetFilter(storage.NewExtensionFileFilter([]string{config.ProfileExtension}))
|
|
fd.Show()
|
|
}
|
|
|
|
func (u *UI) refreshStatus() {
|
|
s := u.core.Engine.Status()
|
|
if s.Running {
|
|
u.status.SetText("Connected")
|
|
u.subStatus.SetText(fmt.Sprintf("Mode: %s | SOCKS: %s | TUN: %v", s.Mode, s.SocksAddr, s.Tun))
|
|
if u.connectBtn != nil {
|
|
u.connectBtn.Enable()
|
|
u.connectBtn.SetText("Disconnect")
|
|
u.connectBtn.SetIcon(theme.MediaStopIcon())
|
|
}
|
|
} else if s.Connecting {
|
|
u.status.SetText("Connecting...")
|
|
if s.SocksAddr != "" {
|
|
u.subStatus.SetText(fmt.Sprintf("Mode: %s | SOCKS starting: %s | TUN: %v", s.Mode, s.SocksAddr, s.Tun))
|
|
} else {
|
|
u.subStatus.SetText(fmt.Sprintf("Mode: %s | trying connection attempts...", s.Mode))
|
|
}
|
|
if u.connectBtn != nil {
|
|
u.connectBtn.Enable()
|
|
u.connectBtn.SetText("Cancel")
|
|
u.connectBtn.SetIcon(theme.MediaStopIcon())
|
|
}
|
|
} else {
|
|
u.status.SetText("Disconnected")
|
|
u.subStatus.SetText("No active tunnel")
|
|
if u.connectBtn != nil {
|
|
u.connectBtn.Enable()
|
|
u.connectBtn.SetText("Connect")
|
|
u.connectBtn.SetIcon(theme.MediaPlayIcon())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (u *UI) startLogPoller() {
|
|
crash.Go(u.core.Root, func() {
|
|
ticker := time.NewTicker(900 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
logs := u.core.Engine.LogsSince(u.lastLog)
|
|
if len(logs) == 0 {
|
|
fyne.Do(u.refreshStatus)
|
|
continue
|
|
}
|
|
var b strings.Builder
|
|
old := u.logText
|
|
if old != "" {
|
|
b.WriteString(old)
|
|
if !strings.HasSuffix(old, "\n") {
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
for _, l := range logs {
|
|
u.lastLog = l.ID
|
|
b.WriteString("[")
|
|
b.WriteString(l.Time)
|
|
b.WriteString("] ")
|
|
b.WriteString(strings.ToUpper(l.Level))
|
|
b.WriteString(" ")
|
|
b.WriteString(l.Message)
|
|
b.WriteByte('\n')
|
|
}
|
|
text := b.String()
|
|
if len(text) > 30000 {
|
|
text = text[len(text)-30000:]
|
|
}
|
|
u.logText = text
|
|
fyne.Do(func() {
|
|
u.logView.SetText(text)
|
|
if u.logScroller != nil {
|
|
u.logScroller.ScrollToBottom()
|
|
}
|
|
u.refreshStatus()
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func sidebarPanel(obj fyne.CanvasObject) fyne.CanvasObject {
|
|
bg := canvas.NewRectangle(color.NRGBA{R: 16, G: 17, B: 21, A: 255})
|
|
return container.NewStack(bg, obj)
|
|
}
|
|
|
|
func softPanel(obj fyne.CanvasObject) fyne.CanvasObject {
|
|
bg := canvas.NewRectangle(color.NRGBA{R: 24, G: 25, B: 31, A: 255})
|
|
return container.NewStack(bg, obj)
|
|
}
|
|
|
|
func section(title, subtitle string, content fyne.CanvasObject) fyne.CanvasObject {
|
|
titleLabel := widget.NewLabel(title)
|
|
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
|
items := []fyne.CanvasObject{titleLabel}
|
|
if strings.TrimSpace(subtitle) != "" {
|
|
sub := widget.NewLabel(subtitle)
|
|
sub.Wrapping = fyne.TextWrapWord
|
|
sub.Importance = widget.LowImportance
|
|
items = append(items, sub)
|
|
}
|
|
items = append(items, widget.NewSeparator(), content)
|
|
return softPanel(container.NewPadded(container.NewVBox(items...)))
|
|
}
|
|
|
|
func helpCard(title, text string) fyne.CanvasObject {
|
|
label := widget.NewLabel(text)
|
|
label.Wrapping = fyne.TextWrapWord
|
|
label.Importance = widget.LowImportance
|
|
return section(title, "", label)
|
|
}
|
|
|
|
func tabPage(title string, content fyne.CanvasObject) *container.TabItem {
|
|
scroll := container.NewVScroll(container.NewPadded(content))
|
|
scroll.SetMinSize(fyne.NewSize(820, 560))
|
|
return container.NewTabItem(title, scroll)
|
|
}
|
|
|
|
type fixedWidthLayout struct {
|
|
width float32
|
|
}
|
|
|
|
func (l *fixedWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
|
for _, obj := range objects {
|
|
obj.Move(fyne.NewPos(0, 0))
|
|
obj.Resize(fyne.NewSize(l.width, size.Height))
|
|
}
|
|
}
|
|
|
|
func (l *fixedWidthLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
|
height := float32(0)
|
|
for _, obj := range objects {
|
|
min := obj.MinSize()
|
|
if min.Height > height {
|
|
height = min.Height
|
|
}
|
|
}
|
|
return fyne.NewSize(l.width, height)
|
|
}
|
|
|
|
func fixedWidth(width float32, obj fyne.CanvasObject) fyne.CanvasObject {
|
|
return container.New(&fixedWidthLayout{width: width}, obj)
|
|
}
|
|
|
|
type minSizeLayout struct {
|
|
width float32
|
|
height float32
|
|
}
|
|
|
|
func (l *minSizeLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
|
for _, obj := range objects {
|
|
obj.Move(fyne.NewPos(0, 0))
|
|
obj.Resize(size)
|
|
}
|
|
}
|
|
|
|
func (l *minSizeLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
|
width := l.width
|
|
height := l.height
|
|
for _, obj := range objects {
|
|
min := obj.MinSize()
|
|
if min.Width > width {
|
|
width = min.Width
|
|
}
|
|
if min.Height > height {
|
|
height = min.Height
|
|
}
|
|
}
|
|
return fyne.NewSize(width, height)
|
|
}
|
|
|
|
func minSize(width, height float32, obj fyne.CanvasObject) fyne.CanvasObject {
|
|
return container.New(&minSizeLayout{width: width, height: height}, obj)
|
|
}
|
|
|
|
func modeLabels() []string {
|
|
return []string{"Direct SSH", "Payload SSH", "SSL/TLS SSH", "Payload + SSL", "DNSTT + SSH", "Xray Core"}
|
|
}
|
|
|
|
func modeLabel(m config.Mode) string {
|
|
switch m {
|
|
case config.ModePayload:
|
|
return "Payload SSH"
|
|
case config.ModeSSL:
|
|
return "SSL/TLS SSH"
|
|
case config.ModePayloadSSL:
|
|
return "Payload + SSL"
|
|
case config.ModeDNSTT:
|
|
return "DNSTT + SSH"
|
|
case config.ModeXray:
|
|
return "Xray Core"
|
|
default:
|
|
return "Direct SSH"
|
|
}
|
|
}
|
|
|
|
func modeValue(label string) config.Mode {
|
|
switch label {
|
|
case "Payload SSH":
|
|
return config.ModePayload
|
|
case "SSL/TLS SSH":
|
|
return config.ModeSSL
|
|
case "Payload + SSL":
|
|
return config.ModePayloadSSL
|
|
case "DNSTT + SSH":
|
|
return config.ModeDNSTT
|
|
case "Xray Core":
|
|
return config.ModeXray
|
|
default:
|
|
return config.ModeDirect
|
|
}
|
|
}
|
|
|
|
func itoa(v int) string {
|
|
if v == 0 {
|
|
return ""
|
|
}
|
|
return strconv.Itoa(v)
|
|
}
|
|
|
|
func mustInt(s string, def int) int {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return def
|
|
}
|
|
v, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return v
|
|
}
|
|
|
|
func splitArgs(s string) []string {
|
|
return strings.Fields(strings.TrimSpace(s))
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
parts := strings.FieldsFunc(s, func(r rune) bool { return r == ',' || r == '\n' || r == ';' })
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func safeFileName(s string) string {
|
|
s = strings.TrimSpace(strings.ToLower(s))
|
|
if s == "" {
|
|
return "profile"
|
|
}
|
|
replacer := strings.NewReplacer("/", "-", "\\", "-", ":", "-", "*", "-", "?", "-", "\"", "-", "<", "-", ">", "-", "|", "-")
|
|
s = replacer.Replace(s)
|
|
s = strings.ReplaceAll(s, " ", "-")
|
|
return s
|
|
}
|