From 691bc0bca60aa3b16aaf4b0930d73d14441de9bc Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Fri, 10 Apr 2026 20:53:30 +0300 Subject: [PATCH 01/28] feature: Simple UI Implementation --- build_ui.sh | 26 ++++++++++++++++ ui/config.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ ui/go.mod | 40 ++++++++++++++++++++++++ ui/go.sum | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ ui/logger.go | 11 +++++++ ui/main.go | 66 +++++++++++++++++++++++++++++++++++++++ ui/process.go | 52 +++++++++++++++++++++++++++++++ ui/ui.go | 70 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 430 insertions(+) create mode 100644 build_ui.sh create mode 100644 ui/config.go create mode 100644 ui/go.mod create mode 100644 ui/go.sum create mode 100644 ui/logger.go create mode 100644 ui/main.go create mode 100644 ui/process.go create mode 100644 ui/ui.go diff --git a/build_ui.sh b/build_ui.sh new file mode 100644 index 0000000..b4a8d45 --- /dev/null +++ b/build_ui.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e + +echo "=== Building OlcRTC ===" +echo "" + +# Build olcrtc binary +echo "[1/2] Building olcrtc binary..." +cd "$(dirname "$0")" +go build -o olcrtc ./cmd/olcrtc/main.go +echo "✓ olcrtc binary built: ./olcrtc" + +# Build UI binary +echo "" +echo "[2/2] Building UI binary..." +cd ui +go build -o ../ui . +cd .. +echo "✓ UI binary built: ./ui" + +echo "" +echo "=== Build Complete ===" +echo "Binaries ready:" +echo " - ./olcrtc" +echo " - ./ui" diff --git a/ui/config.go b/ui/config.go new file mode 100644 index 0000000..2aa4fa5 --- /dev/null +++ b/ui/config.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" +) + +type Config struct { + DNS string `json:"dns"` // на потом + EncryptionKey string `json:"encryption_key"` + SocksPort string `json:"socks_port"` + ConferenceID string `json:"conference_id"` +} + +func getConfigPath() string { + home, err := os.UserHomeDir() + if err != nil { + log("WARNING: Could not get home directory: %v", err) + return "./olcrtc_config.json" + } + configDir := filepath.Join(home, ".olcrtc") + if err := os.MkdirAll(configDir, 0755); err != nil { + log("WARNING: Could not create config directory: %v", err) + } + return filepath.Join(configDir, "config.json") +} + +func loadConfig() *Config { + configPath := getConfigPath() + log("Loading config from: %s", configPath) + + cfg := &Config{ + DNS: "1.1.1.1", + EncryptionKey: "", + SocksPort: "1080", + ConferenceID: "", + } + + data, err := ioutil.ReadFile(configPath) + if err != nil { + if os.IsNotExist(err) { + log("Config file not found. Using default configuration.") + } else { + log("WARNING: Could not read config file: %v", err) + } + return cfg + } + + if err := json.Unmarshal(data, cfg); err != nil { + log("WARNING: Could not parse config file: %v", err) + return cfg + } + + log("Config loaded successfully") + return cfg +} + +func (p *Program) saveConfig(dns, encryptionKey, socksPort, conferenceID string) { + log("Saving configuration...") + + p.Config = &Config{ + DNS: dns, + EncryptionKey: encryptionKey, + SocksPort: socksPort, + ConferenceID: conferenceID, + } + + configPath := getConfigPath() + data, err := json.MarshalIndent(p.Config, "", " ") + if err != nil { + log("ERROR: Could not marshal config: %v", err) + p.showError(err) + return + } + + if err := ioutil.WriteFile(configPath, data, 0644); err != nil { + log("ERROR: Could not write config file: %v", err) + p.showError(err) + return + } + + log("Configuration saved to: %s", configPath) +} diff --git a/ui/go.mod b/ui/go.mod new file mode 100644 index 0000000..020ef0e --- /dev/null +++ b/ui/go.mod @@ -0,0 +1,40 @@ +module github.com/openlibrecommunity/olcrtc/ui + +go 1.26.1 + +require fyne.io/fyne/v2 v2.7.3 + +require ( + fyne.io/systray v1.12.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fredbi/uri v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fyne-io/gl-js v0.2.0 // indirect + github.com/fyne-io/glfw-js v0.3.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.2.0 // indirect + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect + github.com/go-text/render v0.2.0 // indirect + github.com/go-text/typesetting v0.3.3 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rymdport/portal v0.4.2 // indirect + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/ui/go.sum b/ui/go.sum new file mode 100644 index 0000000..7988b55 --- /dev/null +++ b/ui/go.sum @@ -0,0 +1,80 @@ +fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE= +fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw= +fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= +fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko= +github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= +github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= +github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= +github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= +github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= +github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= +github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8= +github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= +github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= +github.com/go-text/typesetting v0.3.3 h1:ihGNJU9KzdK2QRDy1Bm7FT5RFQoYb+3n3EIhI/4eaQc= +github.com/go-text/typesetting v0.3.3/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts= +github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs= +github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= +github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= +github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= +github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= +github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= +github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ui/logger.go b/ui/logger.go new file mode 100644 index 0000000..508bef7 --- /dev/null +++ b/ui/logger.go @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func log(msg string, args ...interface{}) { + if len(args) > 0 { + fmt.Printf(msg+"\n", args...) + } else { + fmt.Println(msg) + } +} diff --git a/ui/main.go b/ui/main.go new file mode 100644 index 0000000..9c0c8b1 --- /dev/null +++ b/ui/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "os/exec" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type Program struct { + App fyne.App + ParentWindow fyne.Window + RunString string + Config *Config + Cmd *exec.Cmd +} + +func main() { + log("Starting application...") + program := NewProgram() + program.Run() +} + +func NewProgram() *Program { + log("Initializing program...") + cfg := loadConfig() + p := &Program{ + App: app.New(), + Config: cfg, + } + p.buildRunString(cfg.ConferenceID, cfg.EncryptionKey, cfg.SocksPort, cfg.DNS) + return p +} + +func (p *Program) Run() { + log("Creating main window...") + w := p.App.NewWindow("OlcRTC") + w.CenterOnScreen() + w.Resize(fyne.NewSize(1280, 700)) + p.ParentWindow = w + + settingsBtn := widget.NewButtonWithIcon("Settings", theme.SettingsIcon(), func() { + log("Settings button clicked") + p.settingsWindow() + }) + + runCheck := widget.NewCheck("Run", func(b bool) { + if b { + log("Run enabled") + p.olcrtcRun() + } else { + log("Run disabled") + p.olcrtcStop() + } + }) + + w.SetContent(container.NewBorder( + settingsBtn, + runCheck, nil, nil, + )) + log("Window created and running...") + w.ShowAndRun() +} diff --git a/ui/process.go b/ui/process.go new file mode 100644 index 0000000..c17d51b --- /dev/null +++ b/ui/process.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "time" +) + +func (p *Program) olcrtcRun() { + log("%s - Starting olcrtc process...", time.Now().Format("2006-01-02 15:04:05")) + if p.RunString == "" { + log("ERROR: Run string is empty. Please configure settings first.") + p.showError(fmt.Errorf("run string is empty - please configure settings")) + return + } + + p.Cmd = exec.Command("sh", "-c", p.RunString) + err := p.Cmd.Start() + if err != nil { + log("ERROR: Failed to start olcrtc: %v", err) + p.showError(err) + p.Cmd = nil + } else { + log("olcrtc process started (PID: %d)", p.Cmd.Process.Pid) + go func() { + err := p.Cmd.Wait() + if err != nil { + log("olcrtc process exited with error: %v", err) + } else { + log("olcrtc process exited successfully") + } + p.Cmd = nil + }() + } +} + +func (p *Program) olcrtcStop() { + log("%s - Stopping olcrtc process...", time.Now().Format("2006-01-02 15:04:05")) + if p.Cmd == nil || p.Cmd.Process == nil { + log("WARNING: No active olcrtc process to stop") + return + } + + err := p.Cmd.Process.Signal(os.Interrupt) + if err != nil { + log("ERROR: Failed to signal olcrtc: %v", err) + p.showError(err) + } else { + log("olcrtc process termination signal sent (PID: %d)", p.Cmd.Process.Pid) + } +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..81b3b47 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +func (p *Program) settingsWindow() { + log("Opening settings dialog...") + + dns := widget.NewEntry() + dns.SetPlaceHolder("1.1.1.1") + if p.Config.DNS != "" { + dns.SetText(p.Config.DNS) + } + + encrpKey := widget.NewEntry() + if p.Config.EncryptionKey != "" { + encrpKey.SetText(p.Config.EncryptionKey) + } + + socksPort := widget.NewEntry() + socksPort.SetPlaceHolder("1080") + if p.Config.SocksPort != "" { + socksPort.SetText(p.Config.SocksPort) + } + + conferenceId := widget.NewEntry() + if p.Config.ConferenceID != "" { + conferenceId.SetText(p.Config.ConferenceID) + } + + applyBtn := widget.NewButtonWithIcon("Apply", theme.CheckButtonCheckedIcon(), func() { + log("Applying settings...") + p.buildRunString(conferenceId.Text, encrpKey.Text, socksPort.Text, dns.Text) + p.saveConfig(dns.Text, encrpKey.Text, socksPort.Text, conferenceId.Text) + }) + + content := container.NewVBox( + widget.NewLabel("Custom DNS Server"), + dns, + widget.NewLabel("Encryption Key"), + encrpKey, + widget.NewLabel("Socks Port"), + socksPort, + widget.NewLabel("Conference ID"), + conferenceId, + applyBtn, + ) + dialog.ShowCustom("Settings", "Close", content, p.ParentWindow) +} + +func (p *Program) buildRunString(conferenceId, encryptionKey, socksPort, dns string) { + log("Building run string...") + log(" Conference ID: %s", conferenceId) + log(" Encryption Key: %s", encryptionKey) + log(" Socks Port: %s", socksPort) + log(" DNS Server: %s", dns) + + p.RunString = fmt.Sprintf("./olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + log("Generated command: %s", p.RunString) +} + +func (p *Program) showError(err error) { + dialog.ShowError(err, p.ParentWindow) +} From 0011e91938eae23c4f321da26ee2258c51b623bd Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Fri, 10 Apr 2026 20:58:36 +0300 Subject: [PATCH 02/28] fix: shell installation script fix --- build_ui.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) mode change 100644 => 100755 build_ui.sh diff --git a/build_ui.sh b/build_ui.sh old mode 100644 new mode 100755 index b4a8d45..2e0a23f --- a/build_ui.sh +++ b/build_ui.sh @@ -15,12 +15,13 @@ echo "✓ olcrtc binary built: ./olcrtc" echo "" echo "[2/2] Building UI binary..." cd ui -go build -o ../ui . +go build -o olcrtc-ui . +mv olcrtc-ui ../olcrtc-ui cd .. -echo "✓ UI binary built: ./ui" +echo "✓ UI binary built: ./olcrtc-ui" echo "" echo "=== Build Complete ===" echo "Binaries ready:" echo " - ./olcrtc" -echo " - ./ui" +echo " - ./olcrtc-ui" From 144dc867c84bae11c2e04ab229a4d0e563548715 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 11 Apr 2026 04:31:26 +0300 Subject: [PATCH 03/28] docs: Remove AI-generated YTWA documentation file --- doc/YTWA.md | 1185 --------------------------------------------------- 1 file changed, 1185 deletions(-) delete mode 100644 doc/YTWA.md diff --git a/doc/YTWA.md b/doc/YTWA.md deleted file mode 100644 index 7e0168a..0000000 --- a/doc/YTWA.md +++ /dev/null @@ -1,1185 +0,0 @@ -=========================================== -AI GENERATED / AI GENERATED / AI GENERATED -=========================================== - - -# Yandex Telemost WebRTC API Documentation - YTWA - -## Overview - -Yandex Telemost implements a Selective Forwarding Unit (SFU) architecture for WebRTC conferencing. The system supports audio, video, and SCTP DataChannel transport over WebRTC with separate publisher and subscriber peer connections. - -**Project includes practical implementations:** -- `dcsend.py` - HTTP requests via DataChannel with chunking (verified: 892B in 1 chunk) -- `dcstream.py` - High-speed streaming (verified: 42.35 MB at 45.75 Mbps) -- `vcsend.py` - Data transfer via video QR codes (verified: 892B in 3 frames) -- `invicible.py` - Encrypted dual-channel transfer (ChaCha20-Poly1305 over DC+VC) -- `flood.py` - Stress testing connections (verified: 40 peers max, 409 after) -- `limits.py` - Limits and performance verification (verified: all tests pass) -- `info.py` - Conference information gathering (verified: full WebRTC details) -- `poc.py` - Basic proof-of-concept - -## Architecture - -### Connection Model - -- SFU-based routing (not P2P) -- Separate PeerConnections for publishing and subscribing -- WebSocket signaling channel -- STUN/TURN infrastructure for NAT traversal - -### Transport Capabilities - -- Audio: Opus codec (48kHz, stereo, DTX, FEC) -- Video: VP8, VP9, H.264, AV1 codecs with simulcast support -- DataChannel: SCTP over DTLS (max message size: 1023MB, port: 5000) - -## API Endpoints - -### Base URL - -``` -https://cloud-api.yandex.ru/telemost_front/v2/telemost -``` - -### 1. Connection Initialization - -**Endpoint:** `GET /conferences/{encoded_conference_url}/connection` - -**Parameters:** -- `next_gen_media_platform_allowed`: boolean (string "true") -- `display_name`: string (URL-encoded participant name) -- `waiting_room_supported`: boolean (string "true") - -**Headers:** -- `User-Agent`: Browser user agent string -- `Accept`: "*/*" -- `content-type`: "application/json" -- `Client-Instance-Id`: UUID v4 -- `X-Telemost-Client-Version`: Version string (e.g., "187.1.0") -- `idempotency-key`: UUID v4 -- `Origin`: "https://telemost.yandex.ru" -- `Referer`: "https://telemost.yandex.ru/" - -**Response:** -```json -{ - "connection_type": "CONFERENCE", - "uri": "https://telemost.yandex.ru/j/{conference_id}", - "room_id": "uuid", - "safe_room_id": "uuid", - "peer_id": "uuid", - "session_id": "uuid", - "peer_session_id": "uuid", - "credentials": "string", - "expiration_time": 1775424866469, - "conference_limit": 40, - "media_platform": "GOLOOM", - "client_configuration": { - "media_server_url": "wss://goloom.strm.yandex.net/join", - "service_name": "telemost", - "ice_servers": [ - { - "urls": ["stun:stun.rtc.yandex.net:3478"] - } - ], - "goloom_session_open_ms": 120000, - "wait_time_to_reconnect_ms": 3553 - } -} -``` - -## WebSocket Protocol - -### Connection - -**URL:** Obtained from `client_configuration.media_server_url` - -**Protocol:** WSS (WebSocket Secure) - -### Message Format - -All messages are JSON objects with a `uid` field (UUID v4) and one message-specific field. - -### Message Types - -#### 1. Client Hello - -**Direction:** Client → Server - -**Structure:** -```json -{ - "uid": "uuid", - "hello": { - "participantMeta": { - "name": "string", - "role": "SPEAKER", - "description": "string", - "sendAudio": boolean, - "sendVideo": boolean - }, - "participantAttributes": { - "name": "string", - "role": "SPEAKER", - "description": "string" - }, - "sendAudio": boolean, - "sendVideo": boolean, - "sendSharing": boolean, - "participantId": "uuid", - "roomId": "uuid", - "serviceName": "telemost", - "credentials": "string", - "capabilitiesOffer": { - "offerAnswerMode": ["SEPARATE"], - "initialSubscriberOffer": ["ON_HELLO"], - "slotsMode": ["FROM_CONTROLLER"], - "simulcastMode": ["DISABLED", "STATIC"], - "selfVadStatus": ["FROM_SERVER", "FROM_CLIENT"], - "dataChannelSharing": ["TO_RTP"] - }, - "sdkInfo": { - "implementation": "string", - "version": "string", - "userAgent": "string", - "hwConcurrency": integer - }, - "sdkInitializationId": "uuid", - "disablePublisher": boolean, - "disableSubscriber": boolean, - "disableSubscriberAudio": boolean - } -} -``` - -#### 2. Server Hello - -**Direction:** Server → Client - -**Structure:** -```json -{ - "uid": "uuid", - "serverHello": { - "capabilitiesAnswer": { - "offerAnswerMode": "SEPARATE", - "initialSubscriberOffer": "ON_HELLO", - "slotsMode": "FROM_CONTROLLER", - "simulcastMode": "DISABLED", - "selfVadStatus": "FROM_SERVER", - "dataChannelSharing": "TO_RTP", - "videoEncoderConfig": "NO_CONFIG", - "dataChannelVideoCodec": "UNIQUE_CODEC_FROM_TRACK_DESCRIPTION", - "bandwidthLimitationReason": "BANDWIDTH_REASON_ENABLED", - "publisherVp9": "PUBLISH_VP9_ENABLED", - "svcMode": "SVC_MODE_L3T3_KEY" - }, - "servingComponents": [ - { - "type": "BORDER|WEBRTC_SERVER|CONTROLLER", - "host": "string", - "version": "string" - } - ], - "sessionSecret": "uuid", - "sfuPeerInitializationId": "uuid", - "rtcConfiguration": { - "iceServers": [ - { - "urls": ["stun:turn.tel.yandex.net", "stun:stun.rtc.yandex.net"], - "credential": "", - "username": "" - }, - { - "urls": ["turn:turn.tel.yandex.net:443"], - "credential": "string", - "username": "string" - } - ] - }, - "pingPongConfiguration": { - "pingInterval": 5000, - "ackTimeout": 9000 - }, - "telemetryConfiguration": { - "sendingInterval": 20000 - } - } -} -``` - -#### 3. Acknowledgment - -**Direction:** Client → Server - -**Structure:** -```json -{ - "uid": "uuid", - "ack": { - "status": { - "code": "OK", - "description": "string" - } - } -} -``` - -#### 4. Subscriber SDP Offer - -**Direction:** Server → Client - -**Structure:** -```json -{ - "uid": "uuid", - "subscriberSdpOffer": { - "pcSeq": integer, - "sdp": "string" - } -} -``` - -**SDP Format:** Standard WebRTC SDP with bundled media streams. Includes: -- Video tracks (m=video) with VP8, VP9, H.264, AV1 codecs -- Audio tracks (m=audio) with Opus, PCMA, PCMU, G722 codecs -- Application track (m=application) for SCTP DataChannel - -#### 5. Subscriber SDP Answer - -**Direction:** Client → Server - -**Structure:** -```json -{ - "uid": "uuid", - "subscriberSdpAnswer": { - "pcSeq": integer, - "sdp": "string" - } -} -``` - -#### 6. Publisher SDP Offer - -**Direction:** Client → Server - -**Structure:** -```json -{ - "uid": "uuid", - "publisherSdpOffer": { - "pcSeq": integer, - "sdp": "string" - } -} -``` - -#### 7. Publisher SDP Answer - -**Direction:** Server → Client - -**Structure:** -```json -{ - "uid": "uuid", - "publisherSdpAnswer": { - "pcSeq": integer, - "sdp": "string" - } -} -``` - -#### 8. ICE Candidate - -**Direction:** Bidirectional - -**Structure:** -```json -{ - "uid": "uuid", - "webrtcIceCandidate": { - "candidate": "string", - "sdpMid": "string", - "sdpMlineIndex": integer, - "usernameFragment": "string", - "target": "PUBLISHER|SUBSCRIBER", - "pcSeq": integer - } -} -``` - -**Candidate Format:** Standard ICE candidate string -``` -candidate:{foundation} {component} {protocol} {priority} {ip} {port} typ {type} [tcptype {tcptype}] -``` - -#### 9. VAD Activity - -**Direction:** Server → Client - -**Structure:** -```json -{ - "uid": "uuid", - "vadActivity": { - "active": boolean - } -} -``` - -#### 10. Update Description - -**Direction:** Server → Client - -**Structure:** -```json -{ - "uid": "uuid", - "updateDescription": { - "description": [ - { - "id": "peer-uuid", - "meta": { - "name": "string", - "role": "SPEAKER", - "description": "string", - "sendAudio": boolean, - "sendVideo": boolean - }, - "participantAttributes": { - "name": "string", - "role": "SPEAKER", - "description": "string" - }, - "sendSharing": boolean, - "tracks": [] - } - ] - } -} -``` - -**Description:** Sent by server when participants join/leave or change their media state. Contains full state of all participants in the conference. - -## Connection Flow - -### 1. Initialization Phase - -1. Client requests connection info via REST API -2. Server responds with room credentials and WebSocket URL -3. Client establishes WebSocket connection - -### 2. Handshake Phase - -1. Client sends `hello` message with capabilities -2. Server responds with `serverHello` containing negotiated capabilities -3. Client acknowledges with `ack` message - -### 3. Subscriber Setup - -1. Server sends `subscriberSdpOffer` with remote media tracks -2. Client creates answer and sends `subscriberSdpAnswer` -3. Client acknowledges offer -4. ICE candidates exchanged for subscriber PeerConnection - -### 4. Publisher Setup - -1. Client creates offer and sends `publisherSdpOffer` -2. Server responds with `publisherSdpAnswer` -3. Client acknowledges answer -4. ICE candidates exchanged for publisher PeerConnection - -### 5. Media Exchange - -1. DataChannel opens on publisher PeerConnection -2. Audio/video tracks activated based on configuration -3. Bidirectional media flow through SFU - -## DataChannel Specifications - -### Configuration - -- **Protocol:** SCTP over DTLS -- **Port:** 5000 -- **Max Message Size (Advertised):** 1,073,741,823 bytes (1023 MB) -- **Max Message Size (Actual):** 8,192 bytes (8 KB) -- **Ordered:** Configurable (recommended: true) -- **Label:** Custom (e.g., "dcsend", "olcrtc", "limits_test") - -### SDP Attributes - -``` -m=application 9 UDP/DTLS/SCTP webrtc-datachannel -a=sctp-port:5000 -a=max-message-size:1073741823 -``` - -### Message Size Limitations - -**CRITICAL LIMITATION:** GOLOOM media server enforces SCTP message limit to 8KB despite advertising 1023MB in SDP. Messages over 8KB are silently dropped. - -**Verified limits (from `limits.py`):** -- + 8KB (8,192 bytes): Delivered -- X 10KB (10,240 bytes): Dropped -- X 12KB+ : Dropped - -**Root cause:** SCTP fragmentation limit. Messages requiring more than ~6-7 UDP packets (MTU 1500) exceed server's reassembly buffer. - -### Large Data Transfer - -Для данных свыше 8KB используйте чанкинг на уровне приложения: - -**Implementation from `dcsend.py`:** -```python -CHUNK_SIZE = 7168 -HEADER_SIZE = 1024 - -def chunk_data(data, transfer_id): - total_size = len(data) - chunk_count = (total_size + CHUNK_SIZE - 1) // CHUNK_SIZE - packets = [] - - for i in range(chunk_count): - start = i * CHUNK_SIZE - end = min(start + CHUNK_SIZE, total_size) - chunk = data[start:end] - - header = {"tid": transfer_id, "idx": i, "total": chunk_count, "size": total_size} - header_json = json.dumps(header).encode() - header_padded = header_json.ljust(HEADER_SIZE, b'\x00') - - packets.append(header_padded + chunk) - - return packets - -class ChunkedReceiver: - def handle_chunk(self, packet): - header_bytes = packet[:HEADER_SIZE].rstrip(b'\x00') - chunk_data = packet[HEADER_SIZE:] - - header = json.loads(header_bytes) - tid, idx, total = header["tid"], header["idx"], header["total"] - - if tid not in self.buffers: - self.buffers[tid] = {"chunks": {}, "total": total, "received": 0} - - buf = self.buffers[tid] - if idx not in buf["chunks"]: - buf["chunks"][idx] = chunk_data - buf["received"] += 1 - - if buf["received"] == buf["total"]: - complete = b"".join(buf["chunks"][i] for i in range(buf["total"])) - self.completed[tid] = complete - del self.buffers[tid] - return tid - - return None -``` - -**Verified Performance (from `dcsend.py` and `limits.py`):** -- 892 bytes: 1 chunk, instant delivery -- 64KB: 2,128ms (246 Kbps) -- 128KB: 2,163ms (485 Kbps) -- 256KB: 2,203ms (952 Kbps) - -**Throttling to prevent overflow:** -```python -while datachannel.bufferedAmount > CHUNK_SIZE * 2: - await asyncio.sleep(0.001) -datachannel.send(chunk) -``` - -**Latency characteristics (RTT from `limits.py`):** -- 100 bytes: 42-53ms avg -- 1KB: 42-116ms avg (59ms typical) -- 4KB: 42-106ms avg (57ms typical) -- 8KB: 87-128ms avg (103ms typical) - -## Video Channel Data Transfer - -### QR Code Video Streaming - -**Alternative data transfer method via video stream (from `vcsend.py`):** - -```python -QR_SIZE = 600 -CHUNK_SIZE = 400 -FRAME_RATE = 1 - -def chunk_data(data, tid): - b64 = base64.b64encode(data).decode() - n = (len(b64) + CHUNK_SIZE - 1) // CHUNK_SIZE - return [json.dumps({"tid": tid, "idx": i, "total": n, - "data": b64[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]}) - for i in range(n)] - -class QRVideoTrack(MediaStreamTrack): - kind = "video" - - def set_data(self, chunks): - self._frames = [make_qr_frame(c, i) for i, c in enumerate(chunks)] - - async def recv(self): - await asyncio.sleep(1.0 / FRAME_RATE) - frame = self._frames[self._idx] - frame.pts = self._pts - frame.time_base = Fraction(1, FRAME_RATE) - self._pts += 1 - self._idx = (self._idx + 1) % len(self._frames) - return frame -``` - -**Decoding on receiver:** -```python -class QRReceiver: - def feed_frame(self, frame): - arr = frame.to_ndarray(format="rgb24") - gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY) - - variants = [ - gray, - cv2.resize(gray, (w * 2, h * 2), interpolation=cv2.INTER_CUBIC), - cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1], - cv2.resize(threshold, (w * 2, h * 2), interpolation=cv2.INTER_NEAREST) - ] - - for variant in variants: - for code in pyzbar.decode(variant): - decoded_data = code.data.decode('utf-8') - val, _, _ = cv2.QRCodeDetector().detectAndDecode(variant) -``` - -**Verified Performance (from `vcsend.py`):** -- 892 bytes: 3 QR frames, decoded in ~3 seconds -- Frame resolution: 600x600 pixels -- Successful decode with both pyzbar and cv2.QRCodeDetector -- Frames saved to /tmp/qr_recv_*.png for debugging - -**Characteristics:** -- QR size: 600x600 pixels -- Frame rate: 1 FPS -- Chunk size: 400 bytes (base64) -- Error correction: ERROR_CORRECT_M - -**Critical Requirements:** -- Receiver must send `setSlots` message to request video routing from server -- Video track must be added to publisher PeerConnection -- Proper ICE/DTLS negotiation required for video transport - -**Advantages:** -- Works even when DataChannel is blocked -- Visual debugging (frame saving to /tmp/) -- Resilient to packet loss -- Dual decoder (pyzbar + OpenCV) for reliability - -## Security and Encryption - -### Encrypted Dual-Channel Transfer (`invicible.py`) - -**ChaCha20-Poly1305 AEAD encryption over both DataChannel and Video QR:** - -```python -SHARED_KEY = os.urandom(32) - -def encrypt_payload(tag_str, data_bytes): - nonce = os.urandom(12) - chacha = ChaCha20Poly1305(SHARED_KEY) - ciphertext = chacha.encrypt(nonce, data_bytes, None) - blob = nonce + ciphertext - tag_bytes = tag_str.encode('ascii').ljust(4, b'\x00')[:4] - len_bytes = len(blob).to_bytes(4, 'big') - return tag_bytes + len_bytes + blob - -def decrypt_payload(envelope): - tag = envelope[:4].decode('ascii').strip('\x00') - length = int.from_bytes(envelope[4:8], 'big') - blob = envelope[8:8+length] - nonce = blob[:12] - ciphertext = blob[12:] - chacha = ChaCha20Poly1305(SHARED_KEY) - data = chacha.decrypt(nonce, ciphertext, None) - return tag, data -``` - -**Envelope Format:** -``` -[4 bytes: TAG] [4 bytes: LENGTH] [12 bytes: NONCE] [N bytes: CIPHERTEXT + AUTH_TAG] -``` - -**Dual-Channel Architecture:** -- Text data → DataChannel (instant delivery) -- Binary data → Video QR codes (resilient to DC failures) -- Both channels encrypted with same key -- Independent decryption on receiver - -**Verified Results:** -- Text payload: UTF-8 string encrypted and transmitted via DC -- Video payload: 2KB binary encrypted and transmitted via QR -- Both payloads successfully decrypted on receiver -- Authentication tags verified (AEAD integrity) - -**Use Cases:** -- Secure file transfer over untrusted SFU -- Covert communication (video channel appears as QR codes) -- Redundant transmission (DC primary, VC fallback) -- End-to-end encryption without server cooperation - -## Practical Implementations - -### HTTP Proxy via DataChannel (`dcsend.py`) - -**Client-server architecture with verified performance:** -```python -client["dc_pub"].send(f"GET {url}") - -async def handle_request(url, dc, stats): - response = requests.get(url, timeout=10) - data = response.content - - transfer_id = generate_uuid() - packets = chunk_data(data, transfer_id) - - for packet in packets: - while dc.bufferedAmount > CHUNK_SIZE * 2: - await asyncio.sleep(0.001) - dc.send(packet) -``` - -**Verified Results:** -- 892 bytes: 1 chunk, instant delivery, complete success -- Request: `GET zarazaex.xyz/curl.txt` -- Response received and decoded successfully -- Total time: ~3 seconds (including peer setup) - -### Stress Testing (`flood.py`) - -**Mass peer connections with verified limits:** -```python -for i in range(1, 412): - name = f"STOP LET'S BE FRIENDS... {suffix}" - task = asyncio.create_task(connect_peer(name, i)) - await asyncio.sleep(0.5) -``` - -**Verified Results:** -- Successfully connected: 40 peers (conference limit) -- Connection failures: 41st peer onwards receive HTTP 409 CONFLICT -- Connection time: ~0.5s per peer -- Stability: WebSocket keep-alive mandatory -- Error message: "409 Client Error: CONFLICT" when limit exceeded - -### Limits Analysis (`limits.py`) - -**Automatic verification of all limitations with real tests:** -```python -async def check_all_limits(): - dc_limits = await check_datachannel_limits() - conf_limits = await check_conference_limits() - audio_limits = await check_audio_limits() - video_limits = await check_video_limits() - ice_limits = await check_ice_limits() - - test_results = await test_message_size_limits() - test_results.extend(await test_latency_microbench()) - test_results.extend(await test_throughput_limits()) - test_results.extend(await test_chunked_transfer()) -``` - -**Verified Results:** -- DataChannel max size: 1,073,741,823 bytes (advertised) ✓ -- SCTP port: 5000 ✓ -- Max participants: 40 ✓ -- Session timeout: 120,000ms ✓ -- Ping interval: 5,000ms ✓ -- ACK timeout: 9,000ms ✓ - -**Real Transfer Tests:** -- 1KB: SUCCESS ✓ -- 6KB: SUCCESS ✓ -- 8KB: SUCCESS ✓ -- 10KB: FAILED (never reached server) ✗ -- Throughput: 73.96 Kbps (50 messages, 5.54s) ✓ -- 64KB chunked: SUCCESS (2,128ms) ✓ -- 128KB chunked: SUCCESS (2,163ms) ✓ -- 256KB chunked: SUCCESS (2,203ms) ✓ -- Multi-peer: 3 peers connected successfully ✓ - -**Conclusion:** ALL LIMITS VERIFIED - Documentation is accurate! - -### Information Gathering (`info.py`) - -**Complete conference analysis with verified output:** -```python -info = await collect_webrtc_info() -print_full_report(info) -``` - -**Verified Collected Data:** -- Connection info: room_id, peer_id, session_id, media platform (GOLOOM) -- Conference limits: 40 participants, 120s timeout, 2-4s reconnect wait -- Participants: Empty conference (0 participants in test) -- Audio codecs: Opus (48kHz, stereo), RED, PCMA, PCMU, G722 -- Video codecs: H264, AV1, VP8, VP9, FLEXFEC-03 -- DataChannel: SCTP port 5000, max 1024MB (advertised) -- ICE servers: 1 STUN, 3 TURN (with credentials) -- Server components: BORDER, WEBRTC_SERVER, CONTROLLER (with versions) -- SDP statistics: 255 lines, 9,011-9,026 bytes -- RTP extensions: 4 extensions (abs-send-time) -- Ping config: 5000ms interval, 9000ms timeout -- Telemetry: 20000ms sending interval - -**Conference State (from REST API):** -- Access level: PUBLIC -- Local recording: allowed -- Cloud recording: not allowed -- Chat: allowed -- Control: allowed -- Broadcast: not allowed - -## Audio Codec Configuration - -- **Sample Rate:** 48000 Hz -- **Channels:** 2 (stereo) -- **Parameters:** - - `minptime=10` - - `useinbandfec=1` (Forward Error Correction) - - `usedtx=1` (Discontinuous Transmission) - -### RED (Redundant Audio Data) - -- **Payload Type:** 101 -- **Format:** `111/111` (Opus redundancy) - -### Audio Channel Limitations - -**WARNING:** Audio channels are completely unsuitable for data transmission through Yandex Telemost SFU. - -**Critical Issue - Mandatory Opus Conversion:** - -Yandex Telemost's GOLOOM media server performs mandatory audio codec conversion at the SFU level. Regardless of the codec negotiated in the WebRTC SDP (PCMU, G.722, etc.), all audio streams are internally converted to Opus before being forwarded to other participants. This conversion is non-negotiable and happens transparently on the server side. - -**Why This Breaks Data Encoding:** - -1. **Lossy Codec Transformation:** Opus is a lossy codec optimized for human speech. It applies aggressive psychoacoustic filtering that destroys non-voice signals. - -2. **Irreversible Signal Degradation:** - - Transmitted signal energy: 0.4973 (normalized) - - Received signal energy: 0.0031-0.0125 (99% loss) - - Frequency content outside speech range (300-3400 Hz) is heavily attenuated - -3. **Voice Activity Detection (VAD):** Server-side VAD silences frames detected as non-speech, eliminating data-carrying tones entirely. - -4. **Discontinuous Transmission (DTX):** Opus DTX mode collapses silence periods, making timing-based encoding impossible. - -5. **Codec Parameters:** Opus configuration includes: - - `usedtx=1` (Discontinuous Transmission enabled) - - `useinbandfec=1` (Forward Error Correction for voice) - - These parameters are optimized for voice, not data - -**Attempted Workarounds - All Failed:** - -- **PCMU/G.711 Encoding:** Server converts to Opus anyway; PCMU tones become unrecognizable -- **DTMF Tones:** Completely destroyed by Opus processing -- **FSK Modulation:** Frequency shifts filtered out by codec -- **ggwave Encoding:** Ultrasonic and subsonic components removed -- **Tone-Based Schemes:** All non-voice frequencies attenuated below detection threshold - -**Test Results (acsend.py):** -``` -Sender: Transmitted PCMU-encoded data tones -Receiver: Received silence (max_amp=0 across all frames) -Conclusion: No recoverable signal after Opus conversion -``` - -**Recommendation:** - -Use DataChannel for reliable data transmission. Audio channels must only be used for actual voice communication. The Opus codec conversion is a fundamental architectural constraint of the GOLOOM SFU and cannot be bypassed. - -## Video Codec Configuration - -### Supported Codecs - -1. **VP8** - - Payload type: 96 - - RTX support: 97 - - NACK, PLI, REMB feedback - -2. **VP9** - - Payload type: 98 - - Profile: 0 - - RTX support: 99 - - NACK, PLI, REMB feedback - -3. **H.264** - - Multiple profiles supported: - - Baseline (42001f, 42e01f) - - Main (4d001f) - - High (64001f) - - Packetization modes: 0, 1 - - Level asymmetry allowed - -4. **AV1** - - Payload type: 45 - - RTX support: 46 - -### RTP Extensions - -- `http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time` - -## ICE Configuration - -### STUN Servers - -- `stun:stun.rtc.yandex.net:3478` -- `stun:turn.tel.yandex.net` - -### TURN Servers - -- `turn:turn.tel.yandex.net:443` -- `turn:turn.tel.yandex.net:443?transport=tcp` - -Credentials are time-limited and provided in `serverHello.rtcConfiguration.iceServers`. - -## Error Handling - -### Connection Errors - -- Retry with exponential backoff -- Maximum wait time: `wait_time_to_reconnect_ms` (typically 3553ms) - -### Session Timeout - -- Session open timeout: `goloom_session_open_ms` (typically 120000ms) -- Expiration time provided in connection response - -### Ping/Pong - -- Ping interval: 5000ms -- ACK timeout: 9000ms -- Connection considered dead if no ACK received - -**WebSocket Keep-Alive Implementation:** - -```python -async def send_pings(ws): - while True: - await asyncio.sleep(30) - try: - await ws.ping() - except Exception as e: - log.error(f"Ping failed: {e}") - break - -async def handle_pong(ws): - ws.pong_received = True -``` - -**Critical:** WebSocket connections close after ~1-2 minutes of inactivity. Implement periodic ping messages to maintain connection: - -```go -func (p *Peer) keepAlive() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - if p.ws != nil { - if err := p.ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil { - log.Printf("Ping error: %v", err) - return - } - } - case <-p.closeCh: - return - } - } -} -``` - -**Pong Handler:** - -```go -ws.SetPongHandler(func(string) error { - ws.SetReadDeadline(time.Now().Add(60 * time.Second)) - return nil -}) -``` - -**Observed Behavior:** -- Without pings: Connection closes with `websocket: close 4008` after ~90-120 seconds -- With pings: Connection remains stable indefinitely -- Recommended ping interval: 30 seconds -- Pong timeout: 10 seconds - -## Security Considerations - -### Authentication - -- Credentials obtained from REST API -- Time-limited session tokens -- Peer ID and room ID validation - -### Transport Security - -- WSS (WebSocket Secure) for signaling -- DTLS for media transport -- SRTP for audio/video encryption - -### Guest Access - -**Important:** The system allows anonymous access to conferences: -- No authentication required for participants -- Only conference initiator needs an account -- Display name can be arbitrary -- Possible flood attacks (see `flood.py`) - -### Rate Limiting - -**Recommendations based on testing:** -- Limit connection attempts per IP -- Exponential backoff on failures -- Respect session expiration times -- Throttle WebSocket messages - -## Implementation Notes - -### Conference Limits - -**Verified limits (from `limits.py`):** -- Maximum participants: 40 (default) -- Session timeout: 120,000ms (2 minutes) -- Ping interval: 5,000ms -- ACK timeout: 9,000ms - -### User Agent Spoofing - -- Server may validate `User-Agent` and `sdkInfo` -- Recommended to use realistic browser signatures -- Example from code: `"Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0"` - -### DataChannel Availability - -- DataChannel support is not guaranteed -- Check SDP for `m=application` line before assuming availability -- Server may disable DataChannel without notice -- Fallback to video QR codes (see `vcsend.py`) - -### Error Handling - -**Common errors and solutions:** - -```python -# Connection timeout -try: - await asyncio.wait_for(dc_pub_open.wait(), timeout=10.0) -except asyncio.TimeoutError: - # Retry with exponential backoff - -# Buffer overflow -while dc.bufferedAmount > CHUNK_SIZE * 2: - await asyncio.sleep(0.001) - -# WebSocket connection loss -async def ws_handler(): - try: - async for message in ws: - # Processing... - except websockets.exceptions.ConnectionClosed: - # Reconnection -``` - -### DataChannel Message Size Workaround - -**8KB limit bypass using chunking (from `dcsend.py`):** - -1. Split payload into 7KB chunks (accounting for headers) -2. Add sequence headers (chunk index, total chunks, transfer ID) -3. Implement reassembly buffer on receiver -4. Use bufferedAmount throttling to prevent congestion - -**Throughput:** ~950 Kbps sustained for 256KB transfers with 32 sequential 8KB messages. - -**Alternative method - QR Video (`vcsend.py`):** -- Encode data into QR codes -- Transmit via 1 FPS video stream -- Decode using pyzbar + OpenCV -- Resilient to losses with visual debugging - -## Rate Limits - -**Not explicitly documented. Recommended approach from testing:** -- Limit connection attempts per IP -- Exponential backoff on failures -- Respect session expiration times -- Connection interval: 0.5s (from `flood.py`) - -## Testing Tools - -### Running tests - -```bash -pip install -r requirements.txt - -# HTTP proxy via DataChannel (892 bytes in 1 chunk) -python code/dcsend.py - -# High-speed streaming (42.35 MB in 7.8s at 45.75 Mbps) -python code/dcstream.py - -# QR code transfer via video (892 bytes in 3 QR frames) -python code/vcsend.py - -# Encrypted dual-channel transfer (ChaCha20-Poly1305) -python code/invicible.py - -# Stress test connections (40 peers max, 409 CONFLICT after) -python code/flood.py - -# Verify all limits (all tests pass) -python code/limits.py - -# Conference analysis (full WebRTC info) -python code/info.py - -# Basic PoC -python code/poc.py -``` - -### Configuration - -All scripts use the same conference: -```python -CONFERENCE_ID = "75047680642749" -CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}" -``` - -For testing, create your own conference and update `CONFERENCE_ID`. - -### Dependencies - -``` -websockets>=12.0 -requests>=2.31.0 -aiortc>=1.9.0 -numpy>=1.24.0 -ggwave>=0.4.2 -qrcode>=7.4.2 -pillow>=10.0.0 -opencv-python>=4.8.0 -pyzbar>=0.1.9 -cryptography>=41.0.0 -imageio[ffmpeg]>=2.31.0 -``` - -Install via: -```bash -python -m venv venv -source venv/bin/activate # or venv/bin/activate.fish -pip install -r code/requirements.txt -``` - -## Implementation Details - -### Code Structure - -``` -code/ -├── poc.py - Basic proof-of-concept (echo server) -├── dcsend.py - HTTP proxy with chunking -├── dcstream.py - High-speed file streaming -├── vcsend.py - QR code video transfer -├── invicible.py - Encrypted dual-channel -├── flood.py - Connection stress testing -├── limits.py - Comprehensive limits verification -├── info.py - Conference information collector -├── requirements.txt - Python dependencies -└── init.fish - Setup script for Fish shell -``` - -### Common Patterns - -**Connection Setup:** -```python -conn_info = get_connection_info(display_name) -pc_sub = RTCPeerConnection(RTCConfiguration(iceServers=[...])) -pc_pub = RTCPeerConnection(RTCConfiguration(iceServers=[...])) -dc_pub = pc_pub.createDataChannel(label, ordered=True) -``` - -**WebSocket Handshake:** -```python -hello = {"uid": uuid, "hello": {...}} -await ws.send(json.dumps(hello)) -# Wait for serverHello -# Exchange SDP offers/answers -# Exchange ICE candidates -``` - -**DataChannel Throttling:** -```python -while dc.bufferedAmount > THRESHOLD: - await asyncio.sleep(0.001) -dc.send(data) -``` - -**Video Track Setup:** -```python -class CustomVideoTrack(MediaStreamTrack): - kind = "video" - async def recv(self): - frame = VideoFrame.from_ndarray(array, format="rgb24") - frame.pts = self._pts - frame.time_base = Fraction(1, FRAME_RATE) - return frame - -pc_pub.addTrack(video_track) -``` - -### Error Handling - -**Connection Failures:** -- HTTP 409: Conference limit reached (40 participants) -- WebSocket close: Network interruption or server restart -- ICE failure: NAT traversal issues (use TURN) -- DataChannel close: Peer disconnection - -**Data Transfer Failures:** -- Message > 8KB: Silent drop (use chunking) -- Buffer overflow: Throttle sends with bufferedAmount -- Incomplete transfer: Implement ACK/retry mechanism -- Timeout: Set reasonable timeouts (10-30s) - -## Bandwidth Configuration - -### Video Layers - -- **L1:** 1000 kbps (single layer) -- **L2:** 120 kbps (low), 360 kbps (med) -- **L3:** 120 kbps (low), 360 kbps (med), 800 kbps (hi) -- **L4:** 120 kbps (low), 360 kbps (med), 800 kbps (hi), 1000 kbps (ultra) - -### Screen Sharing (4K) - -- **Codec:** VP8 -- **Min Bitrate:** 300 kbps -- **Max Bitrate:** 2000 kbps -- **Min Framerate:** 8 fps -- **Max Framerate:** 30 fps -- **Content Hint:** detail - -## Telemetry - -- Sending interval: 20000ms -- Endpoint: `logEndpoint` (if provided in serverHello) -- Format: Not documented - -## Version Information - -- **API Version:** v2 -- **Client Version:** 187.1.0 (as of documentation date) -- **Media Platform:** GOLOOM -- **SDK Version:** Configurable in `sdkInfo` From ae99bf689e8c16cd42c7921ab62715b177a1e149 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 11 Apr 2026 04:43:03 +0300 Subject: [PATCH 04/28] chore: Add golangci-lint configuration file --- .golangci.yml | 271 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..5e7ec82 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,271 @@ +version: "2" + +run: + timeout: 5m + tests: true + build-tags: + - race + +linters: + enable: + - errcheck + - govet + - staticcheck + - unused + - ineffassign + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - containedctx + - contextcheck + - copyloopvar + - cyclop + - decorder + - dogsled + - dupl + - dupword + - durationcheck + - err113 + - errchkjson + - errname + - errorlint + - exhaustive + - fatcontext + - forbidigo + - forcetypeassert + - gocheckcompilerdirectives + - gochecknoglobals + - gochecknoinits + - gochecksumtype + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - goprintffuncname + - gosec + - grouper + - iface + - inamedparam + - interfacebloat + - intrange + - ireturn + - lll + - loggercheck + - maintidx + - makezero + - mirror + - misspell + - musttag + - nakedret + - nestif + - nilerr + - nilnesserr + - nilnil + - noctx + - nolintlint + - nonamedreturns + - nosprintfhostport + - perfsprint + - prealloc + - predeclared + - promlinter + - reassign + - recvcheck + - revive + - rowserrcheck + - sloglint + - spancheck + - sqlclosecheck + - tagalign + - tagliatelle + - testableexamples + - testifylint + - thelper + - tparallel + - unconvert + - unparam + - usestdlibvars + - wastedassign + - whitespace + - wrapcheck + - zerologlint + +linters-settings: + errcheck: + check-blank: true + check-type-assertions: true + exclude-functions: + - (io.Closer).Close + + govet: + enable-all: true + settings: + shadow: + strict: true + + gosec: + severity: medium + confidence: medium + excludes: + - G104 + - G114 + + gocritic: + enabled-tags: + - diagnostic + - style + - performance + - experimental + - opinionated + + revive: + severity: warning + enable-all-rules: false + rules: + - name: atomic + - name: blank-imports + - name: bool-literal-in-expr + - name: call-to-gc + - name: constant-logical-expr + - name: context-as-argument + - name: context-keys-type + - name: datarace + - name: defer + - name: dot-imports + - name: duplicated-imports + - name: early-return + - name: empty-block + - name: empty-lines + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: exported + - name: identical-branches + - name: if-return + - name: increment-decrement + - name: indent-error-flow + - name: modifies-parameter + - name: modifies-value-receiver + - name: range + - name: range-val-address + - name: range-val-in-closure + - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: struct-tag + - name: superfluous-else + - name: time-equal + - name: time-naming + - name: unconditional-recursion + - name: unexported-naming + - name: unexported-return + - name: unhandled-error + - name: unnecessary-stmt + - name: unreachable-code + - name: unused-parameter + - name: unused-receiver + - name: use-any + - name: useless-break + - name: var-declaration + - name: var-naming + - name: waitgroup-by-value + + stylecheck: + checks: ["all"] + + staticcheck: + checks: ["all"] + + unparam: + check-exported: true + + nakedret: + max-func-lines: 0 + + cyclop: + max-complexity: 10 + + gocyclo: + min-complexity: 10 + + gocognit: + min-complexity: 15 + + nestif: + min-complexity: 4 + + lll: + line-length: 120 + + maintidx: + under: 20 + + funlen: + lines: 80 + statements: 50 + + gochecknoglobals: + check-no-globals: true + + goconst: + min-len: 3 + min-occurrences: 3 + + dupl: + threshold: 100 + + forbidigo: + forbid: + - ^print.*$ + - ^fmt\.Print.*$ + + tagliatelle: + case: + rules: + json: snake + yaml: snake + xml: camel + bson: camel + avro: snake + mapstructure: kebab + + varnamelen: + min-name-length: 2 + ignore-names: + - err + - i + - j + - k + - v + - id + - ok + - db + - tx + - wg + - mu + + ireturn: + allow: + - anon + - error + - empty + - stdlib + + wrapcheck: + ignoreSigs: + - .Errorf( + - errors.New( + - errors.Unwrap( + - .Wrap( + - .Wrapf( + - .WithMessage( + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-use-default: false From c1a30b677bab8a4b7b4157c41a8a245edb3aec84 Mon Sep 17 00:00:00 2001 From: Kot-nikot <127394891+Kot-nikot@users.noreply.github.com> Date: Sat, 11 Apr 2026 09:43:13 +0300 Subject: [PATCH 05/28] Implement SOCKS5 proxy support in Server Added SOCKS5 proxy support to the server, including new fields for proxy address and port in the Server struct. Updated the Run function and related methods to handle proxy connections. --- internal/server/server.go | 153 ++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 49 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 75efcc4..881ea12 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" "log" "net" "sync" @@ -22,16 +23,18 @@ import ( ) type Server struct { - peers []*telemost.Peer - cipher *crypto.Cipher - mux *mux.Multiplexer - connections map[uint16]net.Conn - connMu sync.RWMutex - peerIdx atomic.Uint32 - wg sync.WaitGroup - dnsServer string - dnsCache sync.Map - resolver *net.Resolver + peers []*telemost.Peer + cipher *crypto.Cipher + mux *mux.Multiplexer + connections map[uint16]net.Conn + connMu sync.RWMutex + peerIdx atomic.Uint32 + wg sync.WaitGroup + dnsServer string + dnsCache sync.Map + resolver *net.Resolver + socksProxyAddr string + socksProxyPort int } type ConnectRequest struct { @@ -40,7 +43,7 @@ type ConnectRequest struct { Port int `json:"port"` } -func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer string) error { +func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer, socksProxyAddr string, socksProxyPort int) error { var key []byte var err error @@ -71,16 +74,18 @@ func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer string } s := &Server{ - cipher: cipher, - connections: make(map[uint16]net.Conn), - peers: make([]*telemost.Peer, 0), - dnsServer: dnsServer, + cipher: cipher, + connections: make(map[uint16]net.Conn), + peers: make([]*telemost.Peer, 0), + dnsServer: dnsServer, + socksProxyAddr: socksProxyAddr, + socksProxyPort: socksProxyPort, } - + if dnsServer == "" { dnsServer = "1.1.1.1:53" } - + s.resolver = &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { @@ -109,7 +114,7 @@ func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer string } time.Sleep(10 * time.Millisecond) } - + encrypted, err := s.cipher.Encrypt(frame) if err != nil { return err @@ -127,7 +132,7 @@ func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer string peer.SetReconnectCallback(func(dc *webrtc.DataChannel) { log.Printf("Server peer %d reconnected - resetting multiplexer state", i) - + s.connMu.Lock() for sid, conn := range s.connections { if conn != nil { @@ -136,7 +141,7 @@ func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer string delete(s.connections, sid) } s.connMu.Unlock() - + if dc != nil { s.mux.UpdateSendFunc(func(frame []byte) error { encrypted, err := s.cipher.Encrypt(frame) @@ -147,9 +152,9 @@ func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer string return s.peers[idx].Send(encrypted) }) } - + s.mux.Reset() - + log.Println("Server multiplexer reset complete") }) @@ -167,14 +172,47 @@ func Run(ctx context.Context, roomURL, keyHex string, duo bool, dnsServer string } err = s.run(ctx) - + log.Println("Waiting for server goroutines...") s.wg.Wait() log.Println("Server goroutines finished") - + return err } +func (s *Server) socks5Connect(conn net.Conn, targetAddr string, targetPort int) error { + if _, err := conn.Write([]byte{5, 1, 0}); err != nil { + return err + } + + resp := make([]byte, 2) + if _, err := io.ReadFull(conn, resp); err != nil { + return err + } + if resp[0] != 5 || resp[1] != 0 { + return fmt.Errorf("SOCKS5 auth failed") + } + + req := []byte{5, 1, 0, 3} + req = append(req, byte(len(targetAddr))) + req = append(req, []byte(targetAddr)...) + req = append(req, byte(targetPort>>8), byte(targetPort)) + + if _, err := conn.Write(req); err != nil { + return err + } + + resp = make([]byte, 10) + if _, err := io.ReadFull(conn, resp); err != nil { + return err + } + if resp[0] != 5 || resp[1] != 0 { + return fmt.Errorf("SOCKS5 connect failed: %d", resp[1]) + } + + return nil +} + func (s *Server) onData(data []byte) { plaintext, err := s.cipher.Decrypt(data) if err != nil { @@ -186,7 +224,7 @@ func (s *Server) onData(data []byte) { clientID := binary.BigEndian.Uint32(plaintext[0:4]) sid := binary.BigEndian.Uint16(plaintext[4:6]) length := binary.BigEndian.Uint16(plaintext[6:8]) - + if sid == 0xFFFF && length == 0xFFFF { log.Printf("Received reset signal from client (clientID=%d) - cleaning up", clientID) s.connMu.Lock() @@ -205,7 +243,7 @@ func (s *Server) onData(data []byte) { clientID := binary.BigEndian.Uint32(plaintext[0:4]) sid := binary.BigEndian.Uint16(plaintext[4:6]) length := binary.BigEndian.Uint16(plaintext[6:8]) - + if sid == 0xFFFF && length == 0xFFFF { log.Printf("Received reset signal from client (clientID=%d) - cleaning up", clientID) s.connMu.Lock() @@ -228,7 +266,7 @@ func (s *Server) onData(data []byte) { func (s *Server) run(ctx context.Context) error { ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() - + for { select { case <-ctx.Done(): @@ -240,21 +278,21 @@ func (s *Server) run(ctx context.Context) error { } } s.connMu.Unlock() - + log.Printf("Closing %d peer(s)...", len(s.peers)) for i, peer := range s.peers { log.Printf("Closing peer %d...", i) peer.Close() } log.Println("All peers closed") - + return nil - + case <-ticker.C: } - + sids := s.mux.GetStreams() - + for _, sid := range sids { go func(sid uint16) { data := s.mux.ReadStream(sid) @@ -262,7 +300,7 @@ func (s *Server) run(ctx context.Context) error { s.connMu.RLock() conn, exists := s.connections[sid] s.connMu.RUnlock() - + if exists && conn != nil { if _, err := conn.Write(data); err != nil { s.mux.CloseStream(sid) @@ -315,28 +353,45 @@ func (s *Server) handleConnect(sid uint16, req ConnectRequest) { s.connMu.Unlock() dialStart := time.Now() - - dialer := &net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 30 * time.Second, - Resolver: s.resolver, + var conn net.Conn + var err error + + if s.socksProxyAddr == "" { + dialer := &net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 30 * time.Second, + Resolver: s.resolver, + } + conn, err = dialer.Dial("tcp4", addr) + logger.Verbose("TCP dial took %v for sid=%d (direct)", time.Since(dialStart), sid) + } else { + proxyAddr := fmt.Sprintf("%s:%d", s.socksProxyAddr, s.socksProxyPort) + dialer := &net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 30 * time.Second, + } + conn, err = dialer.Dial("tcp4", proxyAddr) + if err == nil { + if err := s.socks5Connect(conn, req.Addr, req.Port); err != nil { + conn.Close() + err = fmt.Errorf("SOCKS5 connect failed: %v", err) + } + } + logger.Verbose("SOCKS5 proxy dial took %v for sid=%d", time.Since(dialStart), sid) } - - conn, err := dialer.Dial("tcp4", addr) + dialElapsed := time.Since(dialStart) - + if err != nil { log.Printf("[SERVER] sid=%d CONNECT_FAILED dial_time=%v total_elapsed=%v err=%v", sid, dialElapsed, time.Since(startTime), err) go s.mux.CloseStream(sid) return } - - logger.Verbose("TCP dial took %v for sid=%d", dialElapsed, sid) - + s.connMu.Lock() s.connections[sid] = conn s.connMu.Unlock() - + log.Printf("[SERVER] sid=%d CONNECT_SUCCESS dial_time=%v", sid, dialElapsed) s.mux.SendData(sid, []byte{0x00}) @@ -348,11 +403,11 @@ func (s *Server) handleConnect(sid uint16, req ConnectRequest) { delete(s.connections, sid) s.connMu.Unlock() }() - + buf := make([]byte, 16384) totalSent := uint64(0) lastLog := time.Now() - + for { n, err := conn.Read(buf) if err != nil { @@ -361,7 +416,7 @@ func (s *Server) handleConnect(sid uint16, req ConnectRequest) { } return } - + for !s.canSendData() { time.Sleep(20 * time.Millisecond) } @@ -369,7 +424,7 @@ func (s *Server) handleConnect(sid uint16, req ConnectRequest) { if err := s.mux.SendData(sid, buf[:n]); err != nil { return } - + totalSent += uint64(n) if time.Since(lastLog) > 5*time.Second { log.Printf("[SERVER] sid=%d TRANSFER_PROGRESS sent=%d MB", sid, totalSent/(1024*1024)) From d93c178f92c98ed3121388d720eeee898c32a73a Mon Sep 17 00:00:00 2001 From: Kot-nikot <127394891+Kot-nikot@users.noreply.github.com> Date: Sat, 11 Apr 2026 10:13:08 +0300 Subject: [PATCH 06/28] Add SOCKS5 proxy support to main.go --- cmd/olcrtc/main.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/olcrtc/main.go b/cmd/olcrtc/main.go index cac6b4c..fd378ae 100644 --- a/cmd/olcrtc/main.go +++ b/cmd/olcrtc/main.go @@ -27,6 +27,8 @@ func main() { dataDir string duo bool dnsServer string + socksProxyAddr string + socksProxyPort int ) flag.StringVar(&mode, "mode", "", "Mode: srv or cnc") @@ -38,6 +40,8 @@ func main() { flag.StringVar(&dataDir, "data", "data", "Path to data directory") flag.BoolVar(&duo, "duo", false, "Use dual channels for 2x throughput") flag.StringVar(&dnsServer, "dns", "1.1.1.1:53", "DNS server (default: Cloudflare 1.1.1.1)") + flag.StringVar(&socksProxyAddr, "socks-proxy", "", "SOCKS5 proxy address (server only)") + flag.IntVar(&socksProxyPort, "socks-proxy-port", 1080, "SOCKS5 proxy port (server only)") flag.Parse() if debug { @@ -85,7 +89,7 @@ func main() { go func() { switch mode { case "srv": - errCh <- server.Run(ctx, roomURL, keyHex, duo, dnsServer) + errCh <- server.Run(ctx, roomURL, keyHex, duo, dnsServer, socksProxyAddr, socksProxyPort) case "cnc": errCh <- client.Run(ctx, roomURL, keyHex, socksPort, duo) } From e2d522928368d72320fa17448bfdf0a4bf0a5ffe Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 10:36:45 +0300 Subject: [PATCH 07/28] docs(readme) now readme contains information about the ui --- readme.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 1dd9786..9883fa9 100644 --- a/readme.md +++ b/readme.md @@ -42,8 +42,10 @@ GOOS=android GOARCH=arm64 go build -ldflags="-checklinkname=0" -o build/olcrtc . # or native ( no podman ) windows GOOS=windows GOARCH=amd64 go build ./cmd/olcrtc - -``` +#Also there's a client UI version (currently in beta) +./ui.sh +#And then +./olcrtc-ui
From 6f0f87835b7356940056374d7e529c1b7a322d8b Mon Sep 17 00:00:00 2001 From: Kot-nikot <127394891+Kot-nikot@users.noreply.github.com> Date: Sat, 11 Apr 2026 10:44:30 +0300 Subject: [PATCH 08/28] Implement SOCKS5 proxy option in deployment script Added support for optional SOCKS5 proxy configuration during server startup. --- srv.sh | 261 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 144 insertions(+), 117 deletions(-) diff --git a/srv.sh b/srv.sh index baaa6cc..e293c05 100755 --- a/srv.sh +++ b/srv.sh @@ -1,118 +1,145 @@ -#!/bin/bash - - -echo "ЕСЛИ У ВАС ЕСТЬ ПРОБЛЕМЫ - Я В КУРСЕ, ПРОЕКТ В БЕТЕ, ПО ПРОБЛЕМАМ В ЧАТ t.me/openlibrecommunity ИЛИ ВООБЩЕ НЕКУДА, ЖДИТЕ РЕЛИЗА" - -set -e - -CONTAINER_NAME="olcrtc-server" -IMAGE_NAME="docker.io/library/golang:1.26-alpine" -REPO_URL="https://github.com/openlibrecommunity/olcrtc.git" -WORK_DIR="/tmp/olcrtc-deploy" - -echo "=== OlcRTC Server Deployment Script ===" -echo "" - -if ! command -v podman &> /dev/null; then - echo "[!] Installing Podman..." - - if command -v apt &> /dev/null; then - echo "[*] Detected apt (Debian/Ubuntu)" - sudo apt update - sudo apt install -y podman - elif command -v dnf &> /dev/null; then - echo "[*] Detected dnf (Fedora/RHEL)" - sudo dnf install -y podman - elif command -v yum &> /dev/null; then - echo "[*] Detected yum (CentOS/RHEL)" - sudo yum install -y podman - elif command -v pacman &> /dev/null; then - echo "[*] Detected pacman (Arch)" - sudo pacman -Sy --noconfirm podman - else - echo "[X] Unsupported package manager. Install podman manually." - exit 1 - fi -fi - -echo "[+] Using Podman" -echo "" -read -p "Enter Telemost Room ID: " ROOM_ID - -if [ -z "$ROOM_ID" ]; then - echo "[X] Room ID cannot be empty" - exit 1 -fi - -echo "" -echo "[*] Stopping old instance..." -podman stop $CONTAINER_NAME 2>/dev/null || true -podman rm $CONTAINER_NAME 2>/dev/null || true - -echo "[*] Cleaning workspace..." -rm -rf $WORK_DIR -mkdir -p $WORK_DIR - -echo "[*] Cloning repository..." -git clone --depth 1 $REPO_URL $WORK_DIR - -echo "[*] Pulling Go image..." -podman pull $IMAGE_NAME - -echo "[*] Building OlcRTC..." -podman run --rm \ - -v $WORK_DIR:/app:Z \ - -w /app \ - $IMAGE_NAME \ - sh -c "go mod tidy && go build -o olcrtc cmd/olcrtc/main.go" - -if [ ! -f "$WORK_DIR/olcrtc" ]; then - echo "[X] Build failed" - exit 1 -fi - -KEY_FILE="$HOME/.olcrtc_key" - -if [ -f "$KEY_FILE" ]; then - echo "[*] Loading existing encryption key..." - KEY=$(cat "$KEY_FILE") -else - echo "[*] Generating new encryption key..." - KEY=$(openssl rand -hex 32) - echo "$KEY" > "$KEY_FILE" - chmod 600 "$KEY_FILE" - echo "" - echo "==========================================" - echo "NEW ENCRYPTION KEY (saved to $KEY_FILE):" - echo "$KEY" - echo "==========================================" - echo "" -fi - -echo "[*] Starting OlcRTC server..." -podman run -d \ - --name $CONTAINER_NAME \ - --restart unless-stopped \ - -v $WORK_DIR:/app:Z \ - -w /app \ - $IMAGE_NAME \ - ./olcrtc -mode srv -id "$ROOM_ID" -key "$KEY" - -sleep 2 - -echo "" -echo "[+] Server started successfully!" -echo "" -echo "Container name: $CONTAINER_NAME" -echo "Room ID: $ROOM_ID" -echo "Encryption key: $KEY" -echo "" -echo "View logs:" -echo " podman logs -f $CONTAINER_NAME" -echo "" -echo "Stop server:" -echo " podman stop $CONTAINER_NAME" -echo "" -echo "Client command:" -echo " ./olcrtc -mode cnc -id \"$ROOM_ID\" -key \"$KEY\" -socks-port 1080" +#!/bin/bash + +echo "ЕСЛИ У ВАС ЕСТЬ ПРОБЛЕМЫ - Я В КУРСЕ, ПРОЕКТ В БЕТЕ, ПО ПРОБЛЕМАМ В ЧАТ t.me/openlibrecommunity ИЛИ ВООБЩЕ НЕКУДА, ЖДИТЕ РЕЛИЗА" + +set -e + +CONTAINER_NAME="olcrtc-server" +IMAGE_NAME="docker.io/library/golang:1.26-alpine" +REPO_URL="https://github.com/openlibrecommunity/olcrtc.git" +WORK_DIR="/tmp/olcrtc-deploy" + +echo "=== OlcRTC Server Deployment Script ===" +echo "" + +if ! command -v podman &> /dev/null; then + echo "[!] Installing Podman..." + + if command -v apt &> /dev/null; then + echo "[*] Detected apt (Debian/Ubuntu)" + sudo apt update + sudo apt install -y podman + elif command -v dnf &> /dev/null; then + echo "[*] Detected dnf (Fedora/RHEL)" + sudo dnf install -y podman + elif command -v yum &> /dev/null; then + echo "[*] Detected yum (CentOS/RHEL)" + sudo yum install -y podman + elif command -v pacman &> /dev/null; then + echo "[*] Detected pacman (Arch)" + sudo pacman -Sy --noconfirm podman + else + echo "[X] Unsupported package manager. Install podman manually." + exit 1 + fi +fi + +echo "[+] Using Podman" +echo "" +read -p "Enter Telemost Room ID: " ROOM_ID + +if [ -z "$ROOM_ID" ]; then + echo "[X] Room ID cannot be empty" + exit 1 +fi + +echo "" +read -p "Use SOCKS5 proxy for egress? (y/N): " USE_PROXY + +SOCKS_PROXY_ADDR="" +SOCKS_PROXY_PORT="" + +if [[ "$USE_PROXY" =~ ^[Yy]$ ]]; then + read -p "Enter SOCKS5 proxy address [default: 127.0.0.1]: " PROXY_ADDR_INPUT + SOCKS_PROXY_ADDR=${PROXY_ADDR_INPUT:-127.0.0.1} + + read -p "Enter SOCKS5 proxy port [default: 1080]: " PROXY_PORT_INPUT + SOCKS_PROXY_PORT=${PROXY_PORT_INPUT:-1080} + + echo "[*] Will use SOCKS5 proxy: $SOCKS_PROXY_ADDR:$SOCKS_PROXY_PORT" +fi + +echo "" +echo "[*] Stopping old instance..." +podman stop $CONTAINER_NAME 2>/dev/null || true +podman rm $CONTAINER_NAME 2>/dev/null || true + +echo "[*] Cleaning workspace..." +rm -rf $WORK_DIR +mkdir -p $WORK_DIR + +echo "[*] Cloning repository..." +git clone --depth 1 $REPO_URL $WORK_DIR + +echo "[*] Pulling Go image..." +podman pull $IMAGE_NAME + +echo "[*] Building OlcRTC..." +podman run --rm \ + -v $WORK_DIR:/app:Z \ + -w /app \ + $IMAGE_NAME \ + sh -c "go mod tidy && go build -o olcrtc cmd/olcrtc/main.go" + +if [ ! -f "$WORK_DIR/olcrtc" ]; then + echo "[X] Build failed" + exit 1 +fi + +KEY_FILE="$HOME/.olcrtc_key" + +if [ -f "$KEY_FILE" ]; then + echo "[*] Loading existing encryption key..." + KEY=$(cat "$KEY_FILE") +else + echo "[*] Generating new encryption key..." + KEY=$(openssl rand -hex 32) + echo "$KEY" > "$KEY_FILE" + chmod 600 "$KEY_FILE" + echo "" + echo "==========================================" + echo "NEW ENCRYPTION KEY (saved to $KEY_FILE):" + echo "$KEY" + echo "==========================================" + echo "" +fi + +# Build server command with optional proxy parameters +SERVER_CMD="./olcrtc -mode srv -id \"$ROOM_ID\" -key \"$KEY\"" + +if [ ! -z "$SOCKS_PROXY_ADDR" ]; then + SERVER_CMD="$SERVER_CMD -socks-proxy \"$SOCKS_PROXY_ADDR\" -socks-proxy-port $SOCKS_PROXY_PORT" +fi + +echo "[*] Starting OlcRTC server..." +podman run -d \ + --name $CONTAINER_NAME \ + --restart unless-stopped \ + -v $WORK_DIR:/app:Z \ + -w /app \ + $IMAGE_NAME \ + sh -c "$SERVER_CMD" + +sleep 2 + +echo "" +echo "[+] Server started successfully!" +echo "" +echo "Container name: $CONTAINER_NAME" +echo "Room ID: $ROOM_ID" +echo "Encryption key: $KEY" + +if [ ! -z "$SOCKS_PROXY_ADDR" ]; then + echo "SOCKS5 proxy: $SOCKS_PROXY_ADDR:$SOCKS_PROXY_PORT" +fi + +echo "" +echo "View logs:" +echo " podman logs -f $CONTAINER_NAME" +echo "" +echo "Stop server:" +echo " podman stop $CONTAINER_NAME" +echo "" +echo "Client command:" +echo " ./olcrtc -mode cnc -id \"$ROOM_ID\" -key \"$KEY\" -socks-port 1080" echo "" From 4940ee1c1eb2911747836d5d694c1a946c3de4a5 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 11:10:43 +0300 Subject: [PATCH 09/28] (refactor): replace deprecated ioutil with os --- ui/config.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/config.go b/ui/config.go index 2aa4fa5..999a379 100644 --- a/ui/config.go +++ b/ui/config.go @@ -2,13 +2,12 @@ package main import ( "encoding/json" - "io/ioutil" "os" "path/filepath" ) type Config struct { - DNS string `json:"dns"` // на потом + DNS string `json:"dns"` // todo EncryptionKey string `json:"encryption_key"` SocksPort string `json:"socks_port"` ConferenceID string `json:"conference_id"` @@ -38,7 +37,7 @@ func loadConfig() *Config { ConferenceID: "", } - data, err := ioutil.ReadFile(configPath) + data, err := os.ReadFile(configPath) if err != nil { if os.IsNotExist(err) { log("Config file not found. Using default configuration.") @@ -75,7 +74,7 @@ func (p *Program) saveConfig(dns, encryptionKey, socksPort, conferenceID string) return } - if err := ioutil.WriteFile(configPath, data, 0644); err != nil { + if err := os.WriteFile(configPath, data, 0644); err != nil { log("ERROR: Could not write config file: %v", err) p.showError(err) return From 4dbcae32f262e73a20281805e87d118f584fce8a Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 11:45:23 +0300 Subject: [PATCH 10/28] (feature): uncheck mark if error appears --- ui/main.go | 5 +++-- ui/process.go | 5 ++++- ui/ui.go | 8 ++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ui/main.go b/ui/main.go index 9c0c8b1..47e38c4 100644 --- a/ui/main.go +++ b/ui/main.go @@ -14,6 +14,7 @@ type Program struct { App fyne.App ParentWindow fyne.Window RunString string + RunCheck *widget.Check Config *Config Cmd *exec.Cmd } @@ -47,7 +48,7 @@ func (p *Program) Run() { p.settingsWindow() }) - runCheck := widget.NewCheck("Run", func(b bool) { + p.RunCheck = widget.NewCheck("Run", func(b bool) { if b { log("Run enabled") p.olcrtcRun() @@ -59,7 +60,7 @@ func (p *Program) Run() { w.SetContent(container.NewBorder( settingsBtn, - runCheck, nil, nil, + p.RunCheck, nil, nil, )) log("Window created and running...") w.ShowAndRun() diff --git a/ui/process.go b/ui/process.go index c17d51b..15b1daf 100644 --- a/ui/process.go +++ b/ui/process.go @@ -12,6 +12,7 @@ func (p *Program) olcrtcRun() { if p.RunString == "" { log("ERROR: Run string is empty. Please configure settings first.") p.showError(fmt.Errorf("run string is empty - please configure settings")) + p.MarkUncheck() return } @@ -21,12 +22,14 @@ func (p *Program) olcrtcRun() { log("ERROR: Failed to start olcrtc: %v", err) p.showError(err) p.Cmd = nil + p.MarkUncheck() } else { log("olcrtc process started (PID: %d)", p.Cmd.Process.Pid) go func() { - err := p.Cmd.Wait() + err = p.Cmd.Wait() if err != nil { log("olcrtc process exited with error: %v", err) + p.MarkUncheck() } else { log("olcrtc process exited successfully") } diff --git a/ui/ui.go b/ui/ui.go index 81b3b47..5627f6d 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/theme" @@ -68,3 +69,10 @@ func (p *Program) buildRunString(conferenceId, encryptionKey, socksPort, dns str func (p *Program) showError(err error) { dialog.ShowError(err, p.ParentWindow) } + +// fyne.Do used here to execute function in the main context frame +// we can just paste p.RunCheck.SetChecked(false) and that'll work. but if so +// there'll be a bunch of warnings(thread safety) +func (p *Program) MarkUncheck() { + fyne.Do(func() { p.RunCheck.SetChecked(false) }) +} From 6709e443e02dd2d92ef4dc2551994e3342ff7021 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 11:50:09 +0300 Subject: [PATCH 11/28] (fix): kill background process while closing the app --- ui/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/main.go b/ui/main.go index 47e38c4..e67f12f 100644 --- a/ui/main.go +++ b/ui/main.go @@ -41,6 +41,7 @@ func (p *Program) Run() { w := p.App.NewWindow("OlcRTC") w.CenterOnScreen() w.Resize(fyne.NewSize(1280, 700)) + w.SetOnClosed(p.olcrtcStop) p.ParentWindow = w settingsBtn := widget.NewButtonWithIcon("Settings", theme.SettingsIcon(), func() { From 86339e69b9ac58cddf90c48df0dd27448d1ae7ee Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 12:49:46 +0300 Subject: [PATCH 12/28] (feature): windows support --- ui/config.go | 25 ++++++++++++------------- ui/main.go | 10 +++++++--- ui/process.go | 9 ++++++++- ui/ui.go | 10 ++++++++-- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/ui/config.go b/ui/config.go index 999a379..e524e7b 100644 --- a/ui/config.go +++ b/ui/config.go @@ -7,36 +7,37 @@ import ( ) type Config struct { + Os string DNS string `json:"dns"` // todo EncryptionKey string `json:"encryption_key"` SocksPort string `json:"socks_port"` ConferenceID string `json:"conference_id"` } -func getConfigPath() string { - home, err := os.UserHomeDir() +func (p *Program) getConfigPath() string { + dir, err := os.UserConfigDir() if err != nil { - log("WARNING: Could not get home directory: %v", err) - return "./olcrtc_config.json" + log("WARNING: Could not get system config directory: %v", err) + return "config.json" } - configDir := filepath.Join(home, ".olcrtc") + configDir := filepath.Join(dir, "olcrtc") if err := os.MkdirAll(configDir, 0755); err != nil { log("WARNING: Could not create config directory: %v", err) } return filepath.Join(configDir, "config.json") + } -func loadConfig() *Config { - configPath := getConfigPath() +func (p *Program) loadConfig() *Config { + configPath := p.getConfigPath() log("Loading config from: %s", configPath) - + // default values cfg := &Config{ DNS: "1.1.1.1", EncryptionKey: "", SocksPort: "1080", ConferenceID: "", } - data, err := os.ReadFile(configPath) if err != nil { if os.IsNotExist(err) { @@ -46,12 +47,10 @@ func loadConfig() *Config { } return cfg } - - if err := json.Unmarshal(data, cfg); err != nil { + if err := json.Unmarshal(data, p.Config); err != nil { log("WARNING: Could not parse config file: %v", err) return cfg } - log("Config loaded successfully") return cfg } @@ -66,7 +65,7 @@ func (p *Program) saveConfig(dns, encryptionKey, socksPort, conferenceID string) ConferenceID: conferenceID, } - configPath := getConfigPath() + configPath := p.getConfigPath() data, err := json.MarshalIndent(p.Config, "", " ") if err != nil { log("ERROR: Could not marshal config: %v", err) diff --git a/ui/main.go b/ui/main.go index e67f12f..04f0347 100644 --- a/ui/main.go +++ b/ui/main.go @@ -2,6 +2,7 @@ package main import ( "os/exec" + "runtime" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" @@ -27,11 +28,14 @@ func main() { func NewProgram() *Program { log("Initializing program...") - cfg := loadConfig() + uOs := runtime.GOOS + log("RUNTIME: Detected OS - %v", uOs) p := &Program{ - App: app.New(), - Config: cfg, + App: app.New(), } + cfg := p.loadConfig() + cfg.Os = uOs + p.Config = cfg p.buildRunString(cfg.ConferenceID, cfg.EncryptionKey, cfg.SocksPort, cfg.DNS) return p } diff --git a/ui/process.go b/ui/process.go index 15b1daf..253b868 100644 --- a/ui/process.go +++ b/ui/process.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "runtime" "time" ) @@ -16,7 +17,13 @@ func (p *Program) olcrtcRun() { return } - p.Cmd = exec.Command("sh", "-c", p.RunString) + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd.exe", "/C", p.RunString) + } else { + cmd = exec.Command("sh", "-c", p.RunString) + } + p.Cmd = cmd err := p.Cmd.Start() if err != nil { log("ERROR: Failed to start olcrtc: %v", err) diff --git a/ui/ui.go b/ui/ui.go index 5627f6d..c336be9 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -61,8 +61,14 @@ func (p *Program) buildRunString(conferenceId, encryptionKey, socksPort, dns str log(" Encryption Key: %s", encryptionKey) log(" Socks Port: %s", socksPort) log(" DNS Server: %s", dns) - - p.RunString = fmt.Sprintf("./olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + switch p.Config.Os { + case "windows": + p.RunString = fmt.Sprintf("olcrtc.exe -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + case "linux", "darwin": + p.RunString = fmt.Sprintf("./olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + default: // in case for freeBSD and etc + p.RunString = fmt.Sprintf("olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + } log("Generated command: %s", p.RunString) } From 4a696d10026f2d79b80b1ec145cd8d685c2e51e1 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 12:52:56 +0300 Subject: [PATCH 13/28] (fix): typo fix --- ui/config.go | 2 +- ui/process.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/config.go b/ui/config.go index e524e7b..329d15f 100644 --- a/ui/config.go +++ b/ui/config.go @@ -47,7 +47,7 @@ func (p *Program) loadConfig() *Config { } return cfg } - if err := json.Unmarshal(data, p.Config); err != nil { + if err := json.Unmarshal(data, cfg); err != nil { log("WARNING: Could not parse config file: %v", err) return cfg } diff --git a/ui/process.go b/ui/process.go index 253b868..d74b9bd 100644 --- a/ui/process.go +++ b/ui/process.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "os/exec" - "runtime" "time" ) @@ -18,7 +17,7 @@ func (p *Program) olcrtcRun() { } var cmd *exec.Cmd - if runtime.GOOS == "windows" { + if p.Config.Os == "windows" { cmd = exec.Command("cmd.exe", "/C", p.RunString) } else { cmd = exec.Command("sh", "-c", p.RunString) From ff2716a449a786d8648eb7aa861356a90a909b00 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 12:57:03 +0300 Subject: [PATCH 14/28] (fix): replace os.interrupt with reliable solution --- ui/process.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/process.go b/ui/process.go index d74b9bd..cd85767 100644 --- a/ui/process.go +++ b/ui/process.go @@ -51,7 +51,12 @@ func (p *Program) olcrtcStop() { return } - err := p.Cmd.Process.Signal(os.Interrupt) + var err error + if p.Config.Os == "windows" { + err = p.Cmd.Process.Kill() + } else { + err = p.Cmd.Process.Signal(os.Interrupt) + } if err != nil { log("ERROR: Failed to signal olcrtc: %v", err) p.showError(err) From 4943d70bf206ae202c080ef8f7e81e8ae8298164 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:03:53 +0300 Subject: [PATCH 15/28] (fix): add timeout --- ui/process.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ui/process.go b/ui/process.go index cd85767..2872560 100644 --- a/ui/process.go +++ b/ui/process.go @@ -60,7 +60,29 @@ func (p *Program) olcrtcStop() { if err != nil { log("ERROR: Failed to signal olcrtc: %v", err) p.showError(err) - } else { - log("olcrtc process termination signal sent (PID: %d)", p.Cmd.Process.Pid) + return + } + + log("olcrtc process termination signal sent (PID: %d)", p.Cmd.Process.Pid) + + done := make(chan error, 1) + go func() { + done <- p.Cmd.Wait() + }() + + select { + case <-time.After(5 * time.Second): + log("WARNING: Process did not exit gracefully, forcing kill...") + if err := p.Cmd.Process.Kill(); err != nil { + log("ERROR: Failed to kill olcrtc: %v", err) + } else { + log("olcrtc process forcefully killed (PID: %d)", p.Cmd.Process.Pid) + } + case err := <-done: + if err != nil { + log("olcrtc process exited with error: %v", err) + } else { + log("olcrtc process exited gracefully") + } } } From 893d27d9b2611fc9bfce4a0aca0dc3dec3890953 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:05:50 +0300 Subject: [PATCH 16/28] (fix): update ui installation script --- build_ui.sh => ui.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename build_ui.sh => ui.sh (89%) diff --git a/build_ui.sh b/ui.sh similarity index 89% rename from build_ui.sh rename to ui.sh index 2e0a23f..973bb55 100755 --- a/build_ui.sh +++ b/ui.sh @@ -15,8 +15,7 @@ echo "✓ olcrtc binary built: ./olcrtc" echo "" echo "[2/2] Building UI binary..." cd ui -go build -o olcrtc-ui . -mv olcrtc-ui ../olcrtc-ui +go build -o ../olcrtc-ui . cd .. echo "✓ UI binary built: ./olcrtc-ui" From abf5f7971c54ebdfeeb27e47edc8831ca81f2699 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:07:38 +0300 Subject: [PATCH 17/28] (fix): sync.Mutex add --- ui/main.go | 2 ++ ui/process.go | 52 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/ui/main.go b/ui/main.go index 04f0347..a1df0ad 100644 --- a/ui/main.go +++ b/ui/main.go @@ -3,6 +3,7 @@ package main import ( "os/exec" "runtime" + "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" @@ -18,6 +19,7 @@ type Program struct { RunCheck *widget.Check Config *Config Cmd *exec.Cmd + CmdMu sync.Mutex } func main() { diff --git a/ui/process.go b/ui/process.go index 2872560..79ff60c 100644 --- a/ui/process.go +++ b/ui/process.go @@ -22,40 +22,64 @@ func (p *Program) olcrtcRun() { } else { cmd = exec.Command("sh", "-c", p.RunString) } + + p.CmdMu.Lock() p.Cmd = cmd err := p.Cmd.Start() + pid := 0 + if err == nil && p.Cmd.Process != nil { + pid = p.Cmd.Process.Pid + } + p.CmdMu.Unlock() + if err != nil { log("ERROR: Failed to start olcrtc: %v", err) p.showError(err) + p.CmdMu.Lock() p.Cmd = nil + p.CmdMu.Unlock() p.MarkUncheck() } else { - log("olcrtc process started (PID: %d)", p.Cmd.Process.Pid) + log("olcrtc process started (PID: %d)", pid) go func() { - err = p.Cmd.Wait() - if err != nil { - log("olcrtc process exited with error: %v", err) - p.MarkUncheck() - } else { - log("olcrtc process exited successfully") + p.CmdMu.Lock() + cmd := p.Cmd + p.CmdMu.Unlock() + + if cmd != nil { + err = cmd.Wait() + if err != nil { + log("olcrtc process exited with error: %v", err) + p.MarkUncheck() + } else { + log("olcrtc process exited successfully") + } } + + p.CmdMu.Lock() p.Cmd = nil + p.CmdMu.Unlock() }() } } func (p *Program) olcrtcStop() { log("%s - Stopping olcrtc process...", time.Now().Format("2006-01-02 15:04:05")) - if p.Cmd == nil || p.Cmd.Process == nil { + + p.CmdMu.Lock() + cmd := p.Cmd + p.CmdMu.Unlock() + + if cmd == nil || cmd.Process == nil { log("WARNING: No active olcrtc process to stop") return } var err error if p.Config.Os == "windows" { - err = p.Cmd.Process.Kill() + err = cmd.Process.Kill() } else { - err = p.Cmd.Process.Signal(os.Interrupt) + err = cmd.Process.Signal(os.Interrupt) } if err != nil { log("ERROR: Failed to signal olcrtc: %v", err) @@ -63,20 +87,20 @@ func (p *Program) olcrtcStop() { return } - log("olcrtc process termination signal sent (PID: %d)", p.Cmd.Process.Pid) + log("olcrtc process termination signal sent (PID: %d)", cmd.Process.Pid) done := make(chan error, 1) go func() { - done <- p.Cmd.Wait() + done <- cmd.Wait() }() select { case <-time.After(5 * time.Second): log("WARNING: Process did not exit gracefully, forcing kill...") - if err := p.Cmd.Process.Kill(); err != nil { + if err := cmd.Process.Kill(); err != nil { log("ERROR: Failed to kill olcrtc: %v", err) } else { - log("olcrtc process forcefully killed (PID: %d)", p.Cmd.Process.Pid) + log("olcrtc process forcefully killed (PID: %d)", cmd.Process.Pid) } case err := <-done: if err != nil { From 95fb9f015db43e1ca7ee43c8d778807dfc4c9fe3 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:15:20 +0300 Subject: [PATCH 18/28] (feature): remove spaces from conferenceId --- ui/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/config.go b/ui/config.go index 329d15f..f2b70a4 100644 --- a/ui/config.go +++ b/ui/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "strings" ) type Config struct { @@ -51,6 +52,7 @@ func (p *Program) loadConfig() *Config { log("WARNING: Could not parse config file: %v", err) return cfg } + cfg.ConferenceID = strings.ReplaceAll(cfg.ConferenceID, " ", "") log("Config loaded successfully") return cfg } @@ -58,6 +60,8 @@ func (p *Program) loadConfig() *Config { func (p *Program) saveConfig(dns, encryptionKey, socksPort, conferenceID string) { log("Saving configuration...") + conferenceID = strings.ReplaceAll(conferenceID, " ", "") + p.Config = &Config{ DNS: dns, EncryptionKey: encryptionKey, From 5e984603bb5a76fe7aafcc1ee3052b5acd87c9de Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:18:32 +0300 Subject: [PATCH 19/28] (feature): check if port is valid --- ui/config.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/ui/config.go b/ui/config.go index f2b70a4..8876af2 100644 --- a/ui/config.go +++ b/ui/config.go @@ -2,8 +2,10 @@ package main import ( "encoding/json" + "fmt" "os" "path/filepath" + "strconv" "strings" ) @@ -15,6 +17,18 @@ type Config struct { ConferenceID string `json:"conference_id"` } +func isValidPort(portStr string) bool { + portStr = strings.TrimSpace(portStr) + if portStr == "" { + return false + } + port, err := strconv.Atoi(portStr) + if err != nil { + return false + } + return port > 0 && port <= 65535 +} + func (p *Program) getConfigPath() string { dir, err := os.UserConfigDir() if err != nil { @@ -53,6 +67,10 @@ func (p *Program) loadConfig() *Config { return cfg } cfg.ConferenceID = strings.ReplaceAll(cfg.ConferenceID, " ", "") + if !isValidPort(cfg.SocksPort) { + log("WARNING: Invalid port in config, using default: 1080") + cfg.SocksPort = "1080" + } log("Config loaded successfully") return cfg } @@ -62,6 +80,12 @@ func (p *Program) saveConfig(dns, encryptionKey, socksPort, conferenceID string) conferenceID = strings.ReplaceAll(conferenceID, " ", "") + if !isValidPort(socksPort) { + log("ERROR: Invalid port: %s", socksPort) + p.showError(fmt.Errorf("invalid port: must be between 1 and 65535")) + return + } + p.Config = &Config{ DNS: dns, EncryptionKey: encryptionKey, From 21fdb8aeefc0b300b311825fff4c90cafc7d0497 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:20:07 +0300 Subject: [PATCH 20/28] (feature): check if conferenceId is valid --- ui/config.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ui/config.go b/ui/config.go index 8876af2..3a076f9 100644 --- a/ui/config.go +++ b/ui/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strconv" "strings" ) @@ -29,6 +30,18 @@ func isValidPort(portStr string) bool { return port > 0 && port <= 65535 } +func isValidConferenceID(conferenceID string) bool { + conferenceID = strings.TrimSpace(conferenceID) + if conferenceID == "" { + return false + } + matched, err := regexp.MatchString(`^\d+$`, conferenceID) + if err != nil { + return false + } + return matched +} + func (p *Program) getConfigPath() string { dir, err := os.UserConfigDir() if err != nil { @@ -67,6 +80,10 @@ func (p *Program) loadConfig() *Config { return cfg } cfg.ConferenceID = strings.ReplaceAll(cfg.ConferenceID, " ", "") + if !isValidConferenceID(cfg.ConferenceID) { + log("WARNING: Invalid conference ID in config (must be numbers only)") + cfg.ConferenceID = "" + } if !isValidPort(cfg.SocksPort) { log("WARNING: Invalid port in config, using default: 1080") cfg.SocksPort = "1080" @@ -86,6 +103,12 @@ func (p *Program) saveConfig(dns, encryptionKey, socksPort, conferenceID string) return } + if !isValidConferenceID(conferenceID) { + log("ERROR: Invalid conference ID: %s", conferenceID) + p.showError(fmt.Errorf("invalid conference ID: must contain only numbers")) + return + } + p.Config = &Config{ DNS: dns, EncryptionKey: encryptionKey, From c8125770a7e164d917ea5757854256dde50a8a15 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:35:35 +0300 Subject: [PATCH 21/28] (fix): dns param is no longer ignored --- ui/ui.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/ui.go b/ui/ui.go index c336be9..229c07f 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -63,11 +63,11 @@ func (p *Program) buildRunString(conferenceId, encryptionKey, socksPort, dns str log(" DNS Server: %s", dns) switch p.Config.Os { case "windows": - p.RunString = fmt.Sprintf("olcrtc.exe -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + p.RunString = fmt.Sprintf("olcrtc.exe -mode cnc -id \"%s\" -key \"%s\" -socks-port %s -dns %s", conferenceId, encryptionKey, socksPort, dns) case "linux", "darwin": - p.RunString = fmt.Sprintf("./olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + p.RunString = fmt.Sprintf("./olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s -dns %s", conferenceId, encryptionKey, socksPort, dns) default: // in case for freeBSD and etc - p.RunString = fmt.Sprintf("olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s", conferenceId, encryptionKey, socksPort) + p.RunString = fmt.Sprintf("olcrtc -mode cnc -id \"%s\" -key \"%s\" -socks-port %s -dns %s", conferenceId, encryptionKey, socksPort, dns) } log("Generated command: %s", p.RunString) } From f6cb72554272f8b2c5d75ff9b1a2ecdf38cbdd17 Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:36:53 +0300 Subject: [PATCH 22/28] (feature): logs in ui --- ui/logger.go | 14 +++++++++++- ui/main.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++--- ui/process.go | 2 ++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/ui/logger.go b/ui/logger.go index 508bef7..f6fd5f7 100644 --- a/ui/logger.go +++ b/ui/logger.go @@ -2,10 +2,22 @@ package main import "fmt" +var currentProgram *Program + func log(msg string, args ...interface{}) { + var formattedMsg string if len(args) > 0 { - fmt.Printf(msg+"\n", args...) + formattedMsg = fmt.Sprintf(msg, args...) + fmt.Println(formattedMsg) } else { + formattedMsg = msg fmt.Println(msg) } + + if currentProgram != nil && currentProgram.LogsChannel != nil { + select { + case currentProgram.LogsChannel <- formattedMsg: + default: + } + } } diff --git a/ui/main.go b/ui/main.go index a1df0ad..e24ca4e 100644 --- a/ui/main.go +++ b/ui/main.go @@ -5,8 +5,11 @@ import ( "runtime" "sync" + "image/color" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" @@ -20,6 +23,11 @@ type Program struct { Config *Config Cmd *exec.Cmd CmdMu sync.Mutex + LogsArea *widget.RichText + LogsText *widget.Label + LogsChannel chan string + LogsContent string + LogsMu sync.Mutex } func main() { @@ -33,8 +41,10 @@ func NewProgram() *Program { uOs := runtime.GOOS log("RUNTIME: Detected OS - %v", uOs) p := &Program{ - App: app.New(), + App: app.New(), + LogsChannel: make(chan string, 100), } + currentProgram = p cfg := p.loadConfig() cfg.Os = uOs p.Config = cfg @@ -65,10 +75,55 @@ func (p *Program) Run() { } }) - w.SetContent(container.NewBorder( + // Create logs display area + p.LogsText = widget.NewLabel("") + p.LogsText.Wrapping = fyne.TextWrapWord + logsScroll := container.NewScroll(p.LogsText) + logsScroll.SetMinSize(fyne.NewSize(0, 300)) + + // Create styled logs box with darker background + bgRect := canvas.NewRectangle(color.NRGBA{R: 40, G: 40, B: 40, A: 255}) + logsWithPadding := container.NewBorder( + widget.NewLabel("Logs"), + nil, nil, nil, + logsScroll, + ) + logsBox := container.NewStack( + bgRect, + container.NewBorder( + nil, nil, nil, nil, + logsWithPadding, + ), + ) + + topBar := container.NewBorder( settingsBtn, p.RunCheck, nil, nil, - )) + ) + + mainContent := container.NewVBox( + topBar, + logsBox, + ) + + w.SetContent(mainContent) + + go p.listenLogs() + log("Window created and running...") w.ShowAndRun() } + +func (p *Program) listenLogs() { + for logMsg := range p.LogsChannel { + fyne.Do(func() { + if p.LogsText != nil { + p.LogsMu.Lock() + p.LogsContent += logMsg + "\n" + logsToDisplay := p.LogsContent + p.LogsMu.Unlock() + p.LogsText.SetText(logsToDisplay) + } + }) + } +} diff --git a/ui/process.go b/ui/process.go index 79ff60c..17103b3 100644 --- a/ui/process.go +++ b/ui/process.go @@ -1,7 +1,9 @@ package main import ( + "bufio" "fmt" + "io" "os" "os/exec" "time" From 01098f8bd6dc7fb05cf44b52cf225c0454a8bd0f Mon Sep 17 00:00:00 2001 From: TheDevisi Date: Sat, 11 Apr 2026 13:41:05 +0300 Subject: [PATCH 23/28] (fix): prevent crashing from buffer overflow --- ui/process.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/ui/process.go b/ui/process.go index 17103b3..032b08b 100644 --- a/ui/process.go +++ b/ui/process.go @@ -9,6 +9,22 @@ import ( "time" ) +func pipeOutput(reader io.ReadCloser, prefix string) { + defer reader.Close() + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + if prefix != "" { + log("olcrtc %s: %s", prefix, line) + } else { + log("olcrtc: %s", line) + } + } + if err := scanner.Err(); err != nil { + log("ERROR: Failed to read %s: %v", prefix, err) + } +} + func (p *Program) olcrtcRun() { log("%s - Starting olcrtc process...", time.Now().Format("2006-01-02 15:04:05")) if p.RunString == "" { @@ -25,9 +41,25 @@ func (p *Program) olcrtcRun() { cmd = exec.Command("sh", "-c", p.RunString) } + stdout, err := cmd.StdoutPipe() + if err != nil { + log("ERROR: Failed to create stdout pipe: %v", err) + p.showError(err) + p.MarkUncheck() + return + } + + stderr, err := cmd.StderrPipe() + if err != nil { + log("ERROR: Failed to create stderr pipe: %v", err) + p.showError(err) + p.MarkUncheck() + return + } + p.CmdMu.Lock() p.Cmd = cmd - err := p.Cmd.Start() + err = p.Cmd.Start() pid := 0 if err == nil && p.Cmd.Process != nil { pid = p.Cmd.Process.Pid @@ -43,6 +75,10 @@ func (p *Program) olcrtcRun() { p.MarkUncheck() } else { log("olcrtc process started (PID: %d)", pid) + + go pipeOutput(stdout, "stdout") + go pipeOutput(stderr, "stderr") + go func() { p.CmdMu.Lock() cmd := p.Cmd From 2b1b3aac5a6ff44bac7cfb311390844049dca82d Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 11 Apr 2026 13:59:30 +0300 Subject: [PATCH 24/28] chore(init): migrate init script from fish to bash --- code/{init.fish => init.sh} | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename code/{init.fish => init.sh} (53%) diff --git a/code/init.fish b/code/init.sh similarity index 53% rename from code/init.fish rename to code/init.sh index a368128..51086ca 100755 --- a/code/init.fish +++ b/code/init.sh @@ -1,8 +1,7 @@ -#!/usr/bin/fish - +#!/bin/bash python -m venv venv -source venv/bin/activate.fish +source venv/bin/activate pip install -r requirements.txt From e276a3822a77549e392e19313721ab4601b602da Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 11 Apr 2026 14:09:03 +0300 Subject: [PATCH 25/28] chore(scripts): move shell scripts to script directory --- cnc.sh => script/cnc.sh | 0 srv.sh => script/srv.sh | 0 ui.sh => script/ui.sh | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename cnc.sh => script/cnc.sh (100%) rename srv.sh => script/srv.sh (100%) rename ui.sh => script/ui.sh (100%) diff --git a/cnc.sh b/script/cnc.sh similarity index 100% rename from cnc.sh rename to script/cnc.sh diff --git a/srv.sh b/script/srv.sh similarity index 100% rename from srv.sh rename to script/srv.sh diff --git a/ui.sh b/script/ui.sh similarity index 100% rename from ui.sh rename to script/ui.sh From ae595acd302281780e152124a21ba804978a41eb Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 11 Apr 2026 14:12:39 +0300 Subject: [PATCH 26/28] chore(scripts): consolidate build outputs to build directory --- script/ui.sh | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/script/ui.sh b/script/ui.sh index 973bb55..23b7513 100755 --- a/script/ui.sh +++ b/script/ui.sh @@ -2,25 +2,28 @@ set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BUILD_DIR="$PROJECT_ROOT/build" + +mkdir -p "$BUILD_DIR" + echo "=== Building OlcRTC ===" echo "" -# Build olcrtc binary echo "[1/2] Building olcrtc binary..." -cd "$(dirname "$0")" -go build -o olcrtc ./cmd/olcrtc/main.go -echo "✓ olcrtc binary built: ./olcrtc" +cd "$PROJECT_ROOT" +go build -o "$BUILD_DIR/olcrtc" ./cmd/olcrtc/main.go +echo "✓ olcrtc binary built: $BUILD_DIR/olcrtc" -# Build UI binary echo "" echo "[2/2] Building UI binary..." -cd ui -go build -o ../olcrtc-ui . -cd .. -echo "✓ UI binary built: ./olcrtc-ui" +cd "$PROJECT_ROOT/ui" +go build -o "$BUILD_DIR/olcrtc-ui" . +echo "✓ UI binary built: $BUILD_DIR/olcrtc-ui" echo "" echo "=== Build Complete ===" echo "Binaries ready:" -echo " - ./olcrtc" -echo " - ./olcrtc-ui" +echo " - $BUILD_DIR/olcrtc" +echo " - $BUILD_DIR/olcrtc-ui" From 74755abbd3c830a90302da3a09edef226a8eb999 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 11 Apr 2026 14:13:12 +0300 Subject: [PATCH 27/28] docs(readme): update script paths --- readme.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 9883fa9..e378540 100644 --- a/readme.md +++ b/readme.md @@ -28,10 +28,10 @@ or wait for the release or at least a beta ```bash # server ( podman, pre configured, easy ) -./srv.sh +./script/srv.sh # client ( podman, pre configured, easy -./cnc.sh +./script/cnc.sh # or native ( no podman ) linux GOOS=linux GOARCH=amd64 go build ./cmd/olcrtc @@ -42,16 +42,19 @@ GOOS=android GOARCH=arm64 go build -ldflags="-checklinkname=0" -o build/olcrtc . # or native ( no podman ) windows GOOS=windows GOARCH=amd64 go build ./cmd/olcrtc -#Also there's a client UI version (currently in beta) -./ui.sh -#And then -./olcrtc-ui + +# Also there's a client UI version (currently in beta) +./script/ui.sh + +# And then +./build/olcrtc-ui + +```
--- -### Contact Telegram: [zarazaex](https://t.me/zarazaexe)
From 702a4f7f9cd3ddac2cf303555f2929c9f7152df7 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 11 Apr 2026 14:14:41 +0300 Subject: [PATCH 28/28] docs(readme): reorganize client setup instructions --- readme.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index e378540..bcd2eab 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,14 @@ or wait for the release or at least a beta # client ( podman, pre configured, easy ./script/cnc.sh + +# Also there's a client UI version (currently in beta) +./script/ui.sh + +# And then +./build/olcrtc-ui + + # or native ( no podman ) linux GOOS=linux GOARCH=amd64 go build ./cmd/olcrtc @@ -43,12 +51,6 @@ GOOS=android GOARCH=arm64 go build -ldflags="-checklinkname=0" -o build/olcrtc . GOOS=windows GOARCH=amd64 go build ./cmd/olcrtc -# Also there's a client UI version (currently in beta) -./script/ui.sh - -# And then -./build/olcrtc-ui - ```