mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-05-28 16:09:48 +00:00
Address PR review feedback
This commit is contained in:
@@ -32,9 +32,8 @@ RUN apk add --no-cache ca-certificates tzdata && \
|
||||
chown -R olcrtc:olcrtc /var/lib/olcrtc
|
||||
|
||||
COPY --from=build /out/olcrtc /usr/local/bin/olcrtc
|
||||
COPY internal/names/data/names internal/names/data/surnames /usr/share/olcrtc/
|
||||
COPY docker/olcrtc-entrypoint.sh /usr/local/bin/olcrtc-entrypoint
|
||||
COPY docker/olcrtc-healthcheck.sh /usr/local/bin/olcrtc-healthcheck
|
||||
COPY script/docker/olcrtc-entrypoint.sh /usr/local/bin/olcrtc-entrypoint
|
||||
COPY script/docker/olcrtc-healthcheck.sh /usr/local/bin/olcrtc-healthcheck
|
||||
|
||||
RUN chmod 0755 /usr/local/bin/olcrtc /usr/local/bin/olcrtc-entrypoint /usr/local/bin/olcrtc-healthcheck
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Package main provides the olcrtc CLI entrypoint.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -16,68 +19,51 @@ import (
|
||||
"github.com/openlibrecommunity/olcrtc/internal/server"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
mode string
|
||||
roomID string
|
||||
provider string
|
||||
socksPort int
|
||||
socksHost string
|
||||
keyHex string
|
||||
debug bool
|
||||
dataDir string
|
||||
duo bool
|
||||
dnsServer string
|
||||
socksProxyAddr string
|
||||
socksProxyPort int
|
||||
}
|
||||
|
||||
var (
|
||||
errUnsupportedProvider = errors.New("only telemost provider supported")
|
||||
errRoomIDRequired = errors.New("room ID required")
|
||||
errModeRequired = errors.New("specify -mode srv or -mode cnc")
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
mode string
|
||||
roomID string
|
||||
provider string
|
||||
socksPort int
|
||||
keyHex string
|
||||
debug bool
|
||||
dataDir string
|
||||
duo bool
|
||||
dnsServer string
|
||||
socksProxyAddr string
|
||||
socksProxyPort int
|
||||
)
|
||||
if err := run(); err != nil {
|
||||
log.Print(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
flag.StringVar(&mode, "mode", "", "Mode: srv or cnc")
|
||||
flag.StringVar(&roomID, "id", "", "Telemost room ID")
|
||||
flag.StringVar(&provider, "provider", "telemost", "Provider (telemost only)")
|
||||
flag.IntVar(&socksPort, "socks-port", 1080, "SOCKS5 port (client only)")
|
||||
flag.StringVar(&keyHex, "key", "", "Shared encryption key (hex)")
|
||||
flag.BoolVar(&debug, "debug", false, "Enable verbose logging")
|
||||
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()
|
||||
func run() error {
|
||||
cfg := parseFlags()
|
||||
configureLogging(cfg.debug)
|
||||
|
||||
if debug {
|
||||
log.SetFlags(log.Ltime | log.Lshortfile)
|
||||
logger.SetVerbose(true)
|
||||
} else {
|
||||
log.SetFlags(log.Ltime)
|
||||
if err := validateConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if provider != "telemost" {
|
||||
log.Fatal("Only telemost provider supported")
|
||||
dataDir, err := resolveDataDir(cfg.dataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if roomID == "" {
|
||||
log.Fatal("Room ID required")
|
||||
if err := loadNames(dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mode != "srv" && mode != "cnc" {
|
||||
log.Fatal("Specify -mode srv or -mode cnc")
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(dataDir) {
|
||||
exePath, err := os.Executable()
|
||||
if err == nil {
|
||||
exeDir := filepath.Dir(exePath)
|
||||
dataDir = filepath.Join(exeDir, dataDir)
|
||||
}
|
||||
}
|
||||
|
||||
namesPath := filepath.Join(dataDir, "names")
|
||||
surnamesPath := filepath.Join(dataDir, "surnames")
|
||||
|
||||
names.LoadNameFiles(namesPath, surnamesPath)
|
||||
|
||||
roomURL := "https://telemost.yandex.ru/j/" + roomID
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -85,36 +71,126 @@ func main() {
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
switch mode {
|
||||
case "srv":
|
||||
errCh <- server.Run(ctx, roomURL, keyHex, duo, dnsServer, socksProxyAddr, socksProxyPort)
|
||||
case "cnc":
|
||||
errCh <- client.Run(ctx, roomURL, keyHex, socksPort, duo, "", "")
|
||||
}
|
||||
}()
|
||||
go runMode(ctx, cfg, errCh)
|
||||
|
||||
select {
|
||||
case <-sigCh:
|
||||
log.Println("Shutting down gracefully...")
|
||||
cancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
<-errCh
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
log.Println("Shutdown complete")
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Println("Shutdown timeout, forcing exit")
|
||||
}
|
||||
return waitForShutdown(errCh)
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func parseFlags() config {
|
||||
cfg := config{}
|
||||
|
||||
flag.StringVar(&cfg.mode, "mode", "", "Mode: srv or cnc")
|
||||
flag.StringVar(&cfg.roomID, "id", "", "Telemost room ID")
|
||||
flag.StringVar(&cfg.provider, "provider", "telemost", "Provider (telemost only)")
|
||||
flag.IntVar(&cfg.socksPort, "socks-port", 1080, "SOCKS5 port (client only)")
|
||||
flag.StringVar(&cfg.socksHost, "socks-host", "127.0.0.1", "SOCKS5 listen host (client only)")
|
||||
flag.StringVar(&cfg.keyHex, "key", "", "Shared encryption key (hex)")
|
||||
flag.BoolVar(&cfg.debug, "debug", false, "Enable verbose logging")
|
||||
flag.StringVar(&cfg.dataDir, "data", "data", "Path to data directory")
|
||||
flag.BoolVar(&cfg.duo, "duo", false, "Use dual channels for 2x throughput")
|
||||
flag.StringVar(&cfg.dnsServer, "dns", "1.1.1.1:53", "DNS server (default: Cloudflare 1.1.1.1)")
|
||||
flag.StringVar(&cfg.socksProxyAddr, "socks-proxy", "", "SOCKS5 proxy address (server only)")
|
||||
flag.IntVar(&cfg.socksProxyPort, "socks-proxy-port", 1080, "SOCKS5 proxy port (server only)")
|
||||
flag.Parse()
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func configureLogging(debug bool) {
|
||||
if debug {
|
||||
log.SetFlags(log.Ltime | log.Lshortfile)
|
||||
logger.SetVerbose(true)
|
||||
return
|
||||
}
|
||||
|
||||
log.SetFlags(log.Ltime)
|
||||
}
|
||||
|
||||
func validateConfig(cfg config) error {
|
||||
switch {
|
||||
case cfg.provider != "telemost":
|
||||
return errUnsupportedProvider
|
||||
case cfg.roomID == "":
|
||||
return errRoomIDRequired
|
||||
case cfg.mode != "srv" && cfg.mode != "cnc":
|
||||
return errModeRequired
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDataDir(dataDir string) (string, error) {
|
||||
if filepath.IsAbs(dataDir) {
|
||||
return dataDir, nil
|
||||
}
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve executable path: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(filepath.Dir(exePath), dataDir), nil
|
||||
}
|
||||
|
||||
func loadNames(dataDir string) error {
|
||||
namesPath := filepath.Join(dataDir, "names")
|
||||
surnamesPath := filepath.Join(dataDir, "surnames")
|
||||
if err := names.LoadNameFiles(namesPath, surnamesPath); err != nil {
|
||||
return fmt.Errorf("load embedded names override: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMode(ctx context.Context, cfg config, errCh chan<- error) {
|
||||
roomURL := "https://telemost.yandex.ru/j/" + cfg.roomID
|
||||
|
||||
switch cfg.mode {
|
||||
case "srv":
|
||||
errCh <- server.Run(
|
||||
ctx,
|
||||
roomURL,
|
||||
cfg.keyHex,
|
||||
cfg.duo,
|
||||
cfg.dnsServer,
|
||||
cfg.socksProxyAddr,
|
||||
cfg.socksProxyPort,
|
||||
)
|
||||
case "cnc":
|
||||
errCh <- client.Run(
|
||||
ctx,
|
||||
roomURL,
|
||||
cfg.keyHex,
|
||||
cfg.socksPort,
|
||||
cfg.duo,
|
||||
cfg.socksHost,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForShutdown(errCh <-chan error) error {
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- <-errCh
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
log.Println("Shutdown complete")
|
||||
}
|
||||
return err
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Println("Shutdown timeout, forcing exit")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# OlcRTC server Docker image
|
||||
|
||||
This image runs `olcrtc` in server mode. The server does not expose an inbound
|
||||
TCP port; it keeps outbound WebSocket/WebRTC connections to Telemost and relays
|
||||
client traffic through the room.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
docker build -t olcrtc/server:local .
|
||||
```
|
||||
|
||||
For Podman:
|
||||
|
||||
```bash
|
||||
podman build -t olcrtc/server:local .
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name olcrtc-server \
|
||||
--restart unless-stopped \
|
||||
-e OLCRTC_ROOM_ID="your-room-id" \
|
||||
-e OLCRTC_KEY="64-hex-character-shared-key" \
|
||||
-v olcrtc-state:/var/lib/olcrtc \
|
||||
olcrtc/server:local
|
||||
```
|
||||
|
||||
If `OLCRTC_KEY` is omitted, the entrypoint generates a 32-byte key, stores it
|
||||
in `/var/lib/olcrtc/key.hex`, and prints it once to the logs:
|
||||
|
||||
```bash
|
||||
docker logs olcrtc-server
|
||||
```
|
||||
|
||||
Use the same key on clients.
|
||||
|
||||
## Compose
|
||||
|
||||
```bash
|
||||
export OLCRTC_ROOM_ID="your-room-id"
|
||||
export OLCRTC_KEY="64-hex-character-shared-key"
|
||||
docker compose -f docker-compose.server.yml up -d --build
|
||||
```
|
||||
|
||||
Optional environment variables:
|
||||
|
||||
- `OLCRTC_DNS`: DNS resolver for outbound TCP dials, default `1.1.1.1:53`
|
||||
- `OLCRTC_DUO`: set to `true` for two parallel WebRTC channels
|
||||
- `OLCRTC_DEBUG`: set to `true` for verbose logs
|
||||
- `OLCRTC_KEY_FILE`: persistent key path, default `/var/lib/olcrtc/key.hex`
|
||||
|
||||
## Operational notes
|
||||
|
||||
- The container runs as a non-root `olcrtc` user.
|
||||
- The runtime image includes CA certificates for Telemost HTTPS/WSS.
|
||||
- The healthcheck verifies that the container's PID 1 is the `olcrtc` process.
|
||||
- No `EXPOSE` is declared because server mode does not accept inbound traffic.
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package client implements the local SOCKS5 client side of the olcrtc tunnel.
|
||||
package client
|
||||
|
||||
import (
|
||||
@@ -6,10 +7,12 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -22,6 +25,13 @@ import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidKeyLength = errors.New("key must be 32 bytes")
|
||||
errInvalidKeyStringLength = errors.New("key string length must be 32")
|
||||
errNoConnectedPeers = errors.New("no connected peers available")
|
||||
)
|
||||
|
||||
// Client manages the client-side mux and SOCKS5 listener.
|
||||
type Client struct {
|
||||
peers []*telemost.Peer
|
||||
cipher *crypto.Cipher
|
||||
@@ -31,143 +41,70 @@ type Client struct {
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, roomURL, keyHex string, socksPort int, duo bool, socksUser, socksPass string) error {
|
||||
return RunWithReady(ctx, roomURL, keyHex, socksPort, duo, socksUser, socksPass, nil)
|
||||
const defaultSOCKSListenHost = "127.0.0.1"
|
||||
|
||||
// Run starts the client and listens for SOCKS5 traffic.
|
||||
func Run(
|
||||
ctx context.Context,
|
||||
roomURL,
|
||||
keyHex string,
|
||||
socksPort int,
|
||||
duo bool,
|
||||
socksHost,
|
||||
socksUser,
|
||||
socksPass string,
|
||||
) error {
|
||||
return RunWithReady(ctx, roomURL, keyHex, socksPort, duo, socksHost, socksUser, socksPass, nil)
|
||||
}
|
||||
|
||||
func RunWithReady(ctx context.Context, roomURL, keyHex string, socksPort int, duo bool, socksUser, socksPass string, onReady func()) error {
|
||||
// RunWithReady starts the client and invokes onReady once the local SOCKS5 listener is accepting connections.
|
||||
func RunWithReady(
|
||||
ctx context.Context,
|
||||
roomURL,
|
||||
keyHex string,
|
||||
socksPort int,
|
||||
duo bool,
|
||||
socksHost,
|
||||
socksUser,
|
||||
socksPass string,
|
||||
onReady func(),
|
||||
) error {
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var key []byte
|
||||
var err error
|
||||
|
||||
if keyHex == "" {
|
||||
key = make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Generated key: %x", key)
|
||||
} else {
|
||||
key, err = hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(key) != 32 {
|
||||
return fmt.Errorf("key must be 32 bytes, got %d", len(key))
|
||||
}
|
||||
}
|
||||
|
||||
keyStr := string(key)
|
||||
if len(keyStr) != 32 {
|
||||
return fmt.Errorf("key string length must be 32, got %d", len(keyStr))
|
||||
}
|
||||
|
||||
cipher, err := crypto.NewCipher(keyStr)
|
||||
key, err := decodeKey(keyHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientID := uint32(time.Now().UnixNano() & 0xFFFFFFFF)
|
||||
keyStr := string(key)
|
||||
if len(keyStr) != 32 {
|
||||
return fmt.Errorf("%w: got %d", errInvalidKeyStringLength, len(keyStr))
|
||||
}
|
||||
|
||||
cipher, err := crypto.NewCipher(keyStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create cipher: %w", err)
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
cipher: cipher,
|
||||
clientID: clientID,
|
||||
peers: make([]*telemost.Peer, 0),
|
||||
clientID: uint32(time.Now().UnixNano() & 0xFFFFFFFF),
|
||||
peers: make([]*telemost.Peer, 0, peerCount(duo)),
|
||||
}
|
||||
|
||||
peerCount := 1
|
||||
if duo {
|
||||
peerCount = 2
|
||||
log.Println("Duo mode: using 2 parallel channels")
|
||||
}
|
||||
c.mux = mux.New(c.clientID, c.sendFrame)
|
||||
|
||||
c.mux = mux.New(c.clientID, func(frame []byte) error {
|
||||
for {
|
||||
canSend := true
|
||||
for _, peer := range c.peers {
|
||||
if !peer.CanSend() {
|
||||
canSend = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if canSend {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
encrypted, err := c.cipher.Encrypt(frame)
|
||||
if err != nil {
|
||||
for peerID := range peerCount(duo) {
|
||||
if err := c.addPeer(runCtx, roomURL, peerID, cancel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx := c.peerIdx.Add(1) % uint32(len(c.peers))
|
||||
return c.peers[idx].Send(encrypted)
|
||||
})
|
||||
|
||||
for i := 0; i < peerCount; i++ {
|
||||
peerID := i
|
||||
peer, err := telemost.NewPeer(roomURL, names.Generate(), c.onData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.SetEndedCallback(func(reason string) {
|
||||
log.Printf("Client peer %d reported conference end: %s", peerID, reason)
|
||||
cancel()
|
||||
})
|
||||
c.peers = append(c.peers, peer)
|
||||
|
||||
peer.SetReconnectCallback(func(dc *webrtc.DataChannel) {
|
||||
if dc == nil {
|
||||
log.Printf("Client peer %d channel closed - resetting multiplexer state", peerID)
|
||||
} else {
|
||||
log.Printf("Client peer %d reconnected - resetting multiplexer state", peerID)
|
||||
}
|
||||
|
||||
c.mux.UpdateSendFunc(func(frame []byte) error {
|
||||
encrypted, err := c.cipher.Encrypt(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idx := c.peerIdx.Add(1) % uint32(len(c.peers))
|
||||
return c.peers[idx].Send(encrypted)
|
||||
})
|
||||
|
||||
c.mux.Reset()
|
||||
|
||||
log.Println("Client multiplexer reset complete")
|
||||
})
|
||||
|
||||
log.Printf("Connecting peer %d to Telemost...", peerID)
|
||||
if err := peer.Connect(runCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Peer %d connected", peerID)
|
||||
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
peer.WatchConnection(runCtx)
|
||||
}()
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
c.sendResetSignal()
|
||||
|
||||
resetFrame := mux.BuildControlFrame(c.clientID, mux.ControlResetClient)
|
||||
encrypted, err := cipher.Encrypt(resetFrame)
|
||||
if err != nil {
|
||||
log.Printf("Failed to encrypt reset signal: %v", err)
|
||||
} else {
|
||||
for _, peer := range c.peers {
|
||||
if err := peer.Send(encrypted); err != nil {
|
||||
log.Printf("Failed to send reset signal to server: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("Sent reset signal to server (clientID=%d)", c.clientID)
|
||||
}
|
||||
|
||||
err = c.runSOCKS5(runCtx, socksPort, socksUser, socksPass, onReady)
|
||||
err = c.runSOCKS5(runCtx, socksHost, socksPort, socksUser, socksPass, onReady)
|
||||
|
||||
log.Println("Waiting for client goroutines...")
|
||||
c.wg.Wait()
|
||||
@@ -176,6 +113,154 @@ func RunWithReady(ctx context.Context, roomURL, keyHex string, socksPort int, du
|
||||
return err
|
||||
}
|
||||
|
||||
func peerCount(duo bool) int {
|
||||
if duo {
|
||||
log.Println("Duo mode: using 2 parallel channels")
|
||||
return 2
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func decodeKey(keyHex string) ([]byte, error) {
|
||||
if keyHex == "" {
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, fmt.Errorf("generate random key: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Generated key: %x", key)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
key, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode hex key: %w", err)
|
||||
}
|
||||
|
||||
if len(key) != 32 {
|
||||
return nil, fmt.Errorf("%w: got %d", errInvalidKeyLength, len(key))
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (c *Client) sendFrame(frame []byte) error {
|
||||
waitUntilPeersCanSend(c.peers)
|
||||
|
||||
encrypted, err := c.cipher.Encrypt(frame)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt outgoing frame: %w", err)
|
||||
}
|
||||
|
||||
peer, err := c.nextPeer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := peer.Send(encrypted); err != nil {
|
||||
return fmt.Errorf("send frame via peer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitUntilPeersCanSend(peers []*telemost.Peer) {
|
||||
for {
|
||||
canSend := true
|
||||
for _, peer := range peers {
|
||||
if !peer.CanSend() {
|
||||
canSend = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if canSend {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) nextPeer() (*telemost.Peer, error) {
|
||||
switch len(c.peers) {
|
||||
case 0:
|
||||
return nil, errNoConnectedPeers
|
||||
case 1:
|
||||
return c.peers[0], nil
|
||||
default:
|
||||
return c.peers[int(c.peerIdx.Add(1)%2)], nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) addPeer(
|
||||
runCtx context.Context,
|
||||
roomURL string,
|
||||
peerID int,
|
||||
cancel context.CancelFunc,
|
||||
) error {
|
||||
peer, err := telemost.NewPeer(roomURL, names.Generate(), c.onData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create peer %d: %w", peerID, err)
|
||||
}
|
||||
|
||||
peer.SetEndedCallback(func(reason string) {
|
||||
log.Printf("Client peer %d reported conference end: %s", peerID, reason)
|
||||
cancel()
|
||||
})
|
||||
|
||||
peer.SetReconnectCallback(func(dc *webrtc.DataChannel) {
|
||||
c.onReconnect(peerID, dc)
|
||||
})
|
||||
|
||||
c.peers = append(c.peers, peer)
|
||||
|
||||
log.Printf("Connecting peer %d to Telemost...", peerID)
|
||||
if err := peer.Connect(runCtx); err != nil {
|
||||
return fmt.Errorf("connect peer %d: %w", peerID, err)
|
||||
}
|
||||
log.Printf("Peer %d connected", peerID)
|
||||
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
peer.WatchConnection(runCtx)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) onReconnect(peerID int, dc *webrtc.DataChannel) {
|
||||
if dc == nil {
|
||||
log.Printf("Client peer %d channel closed - resetting multiplexer state", peerID)
|
||||
} else {
|
||||
log.Printf("Client peer %d reconnected - resetting multiplexer state", peerID)
|
||||
}
|
||||
|
||||
c.mux.UpdateSendFunc(c.sendFrame)
|
||||
c.mux.Reset()
|
||||
|
||||
log.Println("Client multiplexer reset complete")
|
||||
}
|
||||
|
||||
func (c *Client) sendResetSignal() {
|
||||
resetFrame := mux.BuildControlFrame(c.clientID, mux.ControlResetClient)
|
||||
encrypted, err := c.cipher.Encrypt(resetFrame)
|
||||
if err != nil {
|
||||
log.Printf("Failed to encrypt reset signal: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, peer := range c.peers {
|
||||
if err := peer.Send(encrypted); err != nil {
|
||||
log.Printf("Failed to send reset signal to server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Sent reset signal to server (clientID=%d)", c.clientID)
|
||||
}
|
||||
|
||||
func (c *Client) onData(data []byte) {
|
||||
plaintext, err := c.cipher.Decrypt(data)
|
||||
if err != nil {
|
||||
@@ -186,13 +271,26 @@ func (c *Client) onData(data []byte) {
|
||||
c.mux.HandleFrame(plaintext)
|
||||
}
|
||||
|
||||
func (c *Client) runSOCKS5(ctx context.Context, port int, username, password string, onReady func()) error {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
if err != nil {
|
||||
return err
|
||||
func (c *Client) runSOCKS5(
|
||||
ctx context.Context,
|
||||
host string,
|
||||
port int,
|
||||
username,
|
||||
password string,
|
||||
onReady func(),
|
||||
) error {
|
||||
if host == "" {
|
||||
host = defaultSOCKSListenHost
|
||||
}
|
||||
|
||||
log.Printf("SOCKS5 proxy listening on 127.0.0.1:%d (auth=%v)", port, username != "")
|
||||
listenAddr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
var lc net.ListenConfig
|
||||
listener, err := lc.Listen(ctx, "tcp", listenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen on %s: %w", listenAddr, err)
|
||||
}
|
||||
|
||||
log.Printf("SOCKS5 proxy listening on %s (auth=%v)", listenAddr, username != "")
|
||||
if onReady != nil {
|
||||
onReady()
|
||||
}
|
||||
@@ -200,7 +298,9 @@ func (c *Client) runSOCKS5(ctx context.Context, port int, username, password str
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
log.Println("Closing SOCKS5 listener...")
|
||||
listener.Close()
|
||||
if err := listener.Close(); err != nil {
|
||||
logger.Debug("SOCKS5 listener close error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
@@ -209,11 +309,7 @@ func (c *Client) runSOCKS5(ctx context.Context, port int, username, password str
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("SOCKS5 listener closed")
|
||||
|
||||
for _, peer := range c.peers {
|
||||
peer.Close()
|
||||
}
|
||||
|
||||
c.closePeers()
|
||||
return nil
|
||||
default:
|
||||
log.Printf("Accept error: %v", err)
|
||||
@@ -225,16 +321,24 @@ func (c *Client) runSOCKS5(ctx context.Context, port int, username, password str
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) closePeers() {
|
||||
for _, peer := range c.peers {
|
||||
if err := peer.Close(); err != nil {
|
||||
logger.Debug("Peer close error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:cyclop // SOCKS5 parsing is inherently stateful and mirrors the protocol handshake.
|
||||
func (c *Client) handleSOCKS5(conn net.Conn, username, password string) {
|
||||
defer conn.Close()
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
logger.Debug("SOCKS5 connection close error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
buf := make([]byte, 513)
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if buf[0] != 5 {
|
||||
if !readSOCKSVersionAndMethods(conn, buf) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -248,94 +352,155 @@ func (c *Client) handleSOCKS5(conn net.Conn, username, password string) {
|
||||
if requireAuth {
|
||||
wantMethod = 0x02
|
||||
}
|
||||
hasMethod := false
|
||||
for i := 0; i < int(nmethods); i++ {
|
||||
if buf[i] == wantMethod {
|
||||
hasMethod = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMethod {
|
||||
conn.Write([]byte{5, 0xFF})
|
||||
|
||||
if !supportsMethod(buf[:nmethods], wantMethod) {
|
||||
writeResponse(conn, replyUnsupportedSOCKSMethod())
|
||||
return
|
||||
}
|
||||
conn.Write([]byte{5, wantMethod})
|
||||
writeResponse(conn, []byte{5, wantMethod})
|
||||
|
||||
if requireAuth {
|
||||
// RFC 1929: VER ULEN UNAME PLEN PASSWD
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return
|
||||
}
|
||||
if buf[0] != 0x01 {
|
||||
return
|
||||
}
|
||||
ulen := int(buf[1])
|
||||
if _, err := io.ReadFull(conn, buf[:ulen+1]); err != nil {
|
||||
return
|
||||
}
|
||||
gotUser := string(buf[:ulen])
|
||||
plen := int(buf[ulen])
|
||||
if _, err := io.ReadFull(conn, buf[:plen]); err != nil {
|
||||
return
|
||||
}
|
||||
gotPass := string(buf[:plen])
|
||||
if gotUser != username || gotPass != password {
|
||||
conn.Write([]byte{0x01, 0x01})
|
||||
return
|
||||
}
|
||||
conn.Write([]byte{0x01, 0x00})
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
if requireAuth && !authenticateSOCKSUser(conn, buf, username, password) {
|
||||
return
|
||||
}
|
||||
|
||||
if buf[1] != 1 {
|
||||
conn.Write([]byte{5, 7, 0, 1, 0, 0, 0, 0, 0, 0})
|
||||
addr, port, ok := readConnectTarget(conn, buf)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var addr string
|
||||
atyp := buf[3]
|
||||
|
||||
switch atyp {
|
||||
case 1:
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
return
|
||||
}
|
||||
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
|
||||
case 3:
|
||||
if _, err := io.ReadFull(conn, buf[:1]); err != nil {
|
||||
return
|
||||
}
|
||||
length := buf[0]
|
||||
if _, err := io.ReadFull(conn, buf[:length]); err != nil {
|
||||
return
|
||||
}
|
||||
addr = string(buf[:length])
|
||||
default:
|
||||
conn.Write([]byte{5, 8, 0, 1, 0, 0, 0, 0, 0, 0})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return
|
||||
}
|
||||
port := binary.BigEndian.Uint16(buf[:2])
|
||||
|
||||
sid := c.mux.OpenStream()
|
||||
logger.Verbose("SOCKS5 opened stream sid=%d for %s:%d", sid, addr, port)
|
||||
log.Printf("[CLIENT] sid=%d SOCKS5_START %s:%d", sid, addr, port)
|
||||
|
||||
req := map[string]interface{}{
|
||||
"cmd": "connect",
|
||||
"addr": addr,
|
||||
"port": port,
|
||||
if !c.sendConnectRequest(sid, addr, port) {
|
||||
return
|
||||
}
|
||||
|
||||
reqData, _ := json.Marshal(req)
|
||||
c.mux.SendData(sid, reqData)
|
||||
if !c.waitConnectResponse(conn, sid) {
|
||||
return
|
||||
}
|
||||
|
||||
c.mux.ReadStream(sid)
|
||||
writeResponse(conn, replySuccess())
|
||||
c.proxyStream(conn, sid)
|
||||
}
|
||||
|
||||
func readSOCKSVersionAndMethods(conn net.Conn, buf []byte) bool {
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return buf[0] == 5
|
||||
}
|
||||
|
||||
func supportsMethod(methods []byte, wantMethod byte) bool {
|
||||
for _, method := range methods {
|
||||
if method == wantMethod {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func authenticateSOCKSUser(conn net.Conn, buf []byte, username, password string) bool {
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return false
|
||||
}
|
||||
if buf[0] != 0x01 {
|
||||
return false
|
||||
}
|
||||
|
||||
ulen := int(buf[1])
|
||||
if _, err := io.ReadFull(conn, buf[:ulen+1]); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
gotUser := string(buf[:ulen])
|
||||
plen := int(buf[ulen])
|
||||
if _, err := io.ReadFull(conn, buf[:plen]); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
gotPass := string(buf[:plen])
|
||||
if gotUser != username || gotPass != password {
|
||||
writeResponse(conn, replyAuthFailed())
|
||||
return false
|
||||
}
|
||||
|
||||
writeResponse(conn, replyAuthOK())
|
||||
return true
|
||||
}
|
||||
|
||||
func readConnectTarget(conn net.Conn, buf []byte) (string, uint16, bool) {
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
|
||||
if buf[1] != 1 {
|
||||
writeResponse(conn, replyCommandNotSupported())
|
||||
return "", 0, false
|
||||
}
|
||||
|
||||
addr, ok := readTargetAddress(conn, buf, buf[3])
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
|
||||
return addr, binary.BigEndian.Uint16(buf[:2]), true
|
||||
}
|
||||
|
||||
func readTargetAddress(conn net.Conn, buf []byte, atyp byte) (string, bool) {
|
||||
switch atyp {
|
||||
case 1:
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
return "", false
|
||||
}
|
||||
return fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3]), true
|
||||
case 3:
|
||||
if _, err := io.ReadFull(conn, buf[:1]); err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
length := buf[0]
|
||||
if _, err := io.ReadFull(conn, buf[:length]); err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(buf[:length]), true
|
||||
default:
|
||||
writeResponse(conn, replyAddressNotSupported())
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) sendConnectRequest(sid uint16, addr string, port uint16) bool {
|
||||
reqData, err := json.Marshal(struct {
|
||||
Cmd string `json:"cmd"`
|
||||
Addr string `json:"addr"`
|
||||
Port uint16 `json:"port"`
|
||||
}{
|
||||
Cmd: "connect",
|
||||
Addr: addr,
|
||||
Port: port,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Debug("Connect request marshal error: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if err := c.mux.SendData(sid, reqData); err != nil {
|
||||
logger.Debug("Connect request send error: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Client) waitConnectResponse(conn net.Conn, sid uint16) bool {
|
||||
dataReady := c.mux.WaitForData(sid)
|
||||
timeout := time.NewTimer(10 * time.Second)
|
||||
defer timeout.Stop()
|
||||
@@ -344,18 +509,19 @@ func (c *Client) handleSOCKS5(conn net.Conn, username, password string) {
|
||||
case <-dataReady:
|
||||
stream := c.mux.GetStream(sid)
|
||||
if stream == nil || len(stream.RecvBuf()) == 0 {
|
||||
conn.Write([]byte{5, 4, 0, 1, 0, 0, 0, 0, 0, 0})
|
||||
return
|
||||
writeResponse(conn, replyHostUnreachable())
|
||||
return false
|
||||
}
|
||||
case <-timeout.C:
|
||||
conn.Write([]byte{5, 4, 0, 1, 0, 0, 0, 0, 0, 0})
|
||||
return
|
||||
writeResponse(conn, replyHostUnreachable())
|
||||
return false
|
||||
}
|
||||
|
||||
c.mux.ReadStream(sid)
|
||||
|
||||
conn.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0})
|
||||
return true
|
||||
}
|
||||
|
||||
//nolint:cyclop // The stream pump handles two coordinated goroutines and shutdown races in one place.
|
||||
func (c *Client) proxyStream(conn net.Conn, sid uint16) {
|
||||
done := make(chan struct{})
|
||||
streamClosed := make(chan struct{})
|
||||
|
||||
@@ -365,7 +531,9 @@ func (c *Client) handleSOCKS5(conn net.Conn, username, password string) {
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
c.mux.CloseStream(sid)
|
||||
if err := c.mux.CloseStream(sid); err != nil {
|
||||
logger.Debug("Close stream error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := c.mux.SendData(sid, buf[:n]); err != nil {
|
||||
@@ -387,14 +555,8 @@ func (c *Client) handleSOCKS5(conn net.Conn, username, password string) {
|
||||
return
|
||||
case <-ticker.C:
|
||||
data := c.mux.ReadStream(sid)
|
||||
if len(data) > 0 {
|
||||
for len(data) > 0 {
|
||||
n, err := conn.Write(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data = data[n:]
|
||||
}
|
||||
if len(data) > 0 && !writeStreamData(conn, data) {
|
||||
return
|
||||
}
|
||||
|
||||
if c.mux.StreamClosed(sid) {
|
||||
@@ -409,3 +571,49 @@ func (c *Client) handleSOCKS5(conn net.Conn, username, password string) {
|
||||
case <-streamClosed:
|
||||
}
|
||||
}
|
||||
|
||||
func writeStreamData(conn net.Conn, data []byte) bool {
|
||||
for len(data) > 0 {
|
||||
n, err := conn.Write(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
data = data[n:]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func writeResponse(conn net.Conn, response []byte) {
|
||||
if _, err := conn.Write(response); err != nil {
|
||||
logger.Debug("SOCKS5 response write error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func replyUnsupportedSOCKSMethod() []byte {
|
||||
return []byte{5, 0xFF}
|
||||
}
|
||||
|
||||
func replyAuthFailed() []byte {
|
||||
return []byte{0x01, 0x01}
|
||||
}
|
||||
|
||||
func replyAuthOK() []byte {
|
||||
return []byte{0x01, 0x00}
|
||||
}
|
||||
|
||||
func replyCommandNotSupported() []byte {
|
||||
return []byte{5, 7, 0, 1, 0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
|
||||
func replyAddressNotSupported() []byte {
|
||||
return []byte{5, 8, 0, 1, 0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
|
||||
func replyHostUnreachable() []byte {
|
||||
return []byte{5, 4, 0, 1, 0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
|
||||
func replySuccess() []byte {
|
||||
return []byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Package names generates display names for Telemost peers.
|
||||
package names
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"math/rand/v2"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -14,23 +17,12 @@ var embeddedNames string
|
||||
//go:embed data/surnames
|
||||
var embeddedSurnames string
|
||||
|
||||
//nolint:gochecknoglobals // Package-level state keeps the loaded name dictionaries cached for the process lifetime.
|
||||
var (
|
||||
firstNames []string
|
||||
lastNames []string
|
||||
firstNames = parseEmbedded(embeddedNames)
|
||||
lastNames = parseEmbedded(embeddedSurnames)
|
||||
)
|
||||
|
||||
var defaultFirstNames = []string{
|
||||
"Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артём", "Илья", "Кирилл", "Михаил",
|
||||
"Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван", "Денис", "Евгений", "Даниил", "Тимофей",
|
||||
"Владислав", "Игорь", "Владимир", "Павел", "Руслан", "Марк", "Константин", "Николай", "Олег", "Виктор",
|
||||
}
|
||||
|
||||
var defaultLastNames = []string{
|
||||
"Иванов", "Смирнов", "Кузнецов", "Попов", "Васильев", "Петров", "Соколов", "Михайлов", "Новиков", "Фёдоров",
|
||||
"Морозов", "Волков", "Алексеев", "Лебедев", "Семёнов", "Егоров", "Павлов", "Козлов", "Степанов", "Николаев",
|
||||
"Орлов", "Андреев", "Макаров", "Никитин", "Захаров", "Зайцев", "Соловьёв", "Борисов", "Яковлев", "Григорьев",
|
||||
}
|
||||
|
||||
func parseEmbedded(raw string) []string {
|
||||
var names []string
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
@@ -39,15 +31,19 @@ func parseEmbedded(raw string) []string {
|
||||
names = append(names, line)
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func loadNames(path string) ([]string, error) {
|
||||
//nolint:gosec // Paths come from local CLI/runtime configuration; loading override files is intentional here.
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("open names file %q: %w", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
var names []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
@@ -58,23 +54,14 @@ func loadNames(path string) ([]string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return names, scanner.Err()
|
||||
}
|
||||
|
||||
func init() {
|
||||
if names := parseEmbedded(embeddedNames); len(names) > 0 {
|
||||
firstNames = names
|
||||
} else {
|
||||
firstNames = defaultFirstNames
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("scan names file %q: %w", path, err)
|
||||
}
|
||||
|
||||
if names := parseEmbedded(embeddedSurnames); len(names) > 0 {
|
||||
lastNames = names
|
||||
} else {
|
||||
lastNames = defaultLastNames
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// LoadNameFiles overrides embedded name dictionaries from local files when they are present.
|
||||
func LoadNameFiles(firstPath, lastPath string) error {
|
||||
if names, err := loadNames(firstPath); err == nil && len(names) > 0 {
|
||||
firstNames = names
|
||||
@@ -87,16 +74,24 @@ func LoadNameFiles(firstPath, lastPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate returns a random display name assembled from the currently loaded dictionaries.
|
||||
func Generate() string {
|
||||
if len(firstNames) == 0 {
|
||||
firstNames = defaultFirstNames
|
||||
}
|
||||
if len(lastNames) == 0 {
|
||||
lastNames = defaultLastNames
|
||||
if len(firstNames) == 0 || len(lastNames) == 0 {
|
||||
return "anonymous user"
|
||||
}
|
||||
|
||||
first := firstNames[rand.IntN(len(firstNames))]
|
||||
last := lastNames[rand.IntN(len(lastNames))]
|
||||
|
||||
return first + " " + last
|
||||
return firstNames[randomIndex(len(firstNames))] + " " + lastNames[randomIndex(len(lastNames))]
|
||||
}
|
||||
|
||||
func randomIndex(limit int) int {
|
||||
if limit <= 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(limit)))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(n.Int64())
|
||||
}
|
||||
|
||||
112
mobile/mobile.go
112
mobile/mobile.go
@@ -4,7 +4,7 @@ package mobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -25,12 +25,22 @@ type LogWriter interface {
|
||||
WriteLog(msg string)
|
||||
}
|
||||
|
||||
var (
|
||||
errAlreadyRunning = errors.New("olcRTC already running")
|
||||
errRoomIDRequired = errors.New("roomID is required")
|
||||
errKeyHexRequired = errors.New("keyHex is required")
|
||||
errNotRunning = errors.New("olcRTC is not running")
|
||||
errStoppedBeforeReady = errors.New("olcRTC stopped before becoming ready")
|
||||
errStartTimedOut = errors.New("olcRTC start timed out")
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // Mobile bindings expose a singleton runtime controlled by the embedding app.
|
||||
var (
|
||||
mu sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
ready chan struct{}
|
||||
runErr error
|
||||
errRun error
|
||||
)
|
||||
|
||||
// SetProtector sets the Android VPN socket protector.
|
||||
@@ -57,9 +67,10 @@ func SetDebug(enabled bool) {
|
||||
logger.SetVerbose(enabled)
|
||||
if enabled {
|
||||
log.SetFlags(log.Ltime | log.Lshortfile)
|
||||
} else {
|
||||
log.SetFlags(log.Ltime)
|
||||
return
|
||||
}
|
||||
|
||||
log.SetFlags(log.Ltime)
|
||||
}
|
||||
|
||||
// Start launches the olcRTC client in background.
|
||||
@@ -67,42 +78,52 @@ func SetDebug(enabled bool) {
|
||||
// keyHex: 64-char hex encryption key
|
||||
// socksPort: local SOCKS5 proxy port (e.g. 10808)
|
||||
// duo: use dual channels for higher throughput
|
||||
// socksUser/socksPass: SOCKS5 credentials (empty = no auth)
|
||||
// socksUser/socksPass: SOCKS5 credentials (empty = no auth).
|
||||
func Start(roomID, keyHex string, socksPort int, duo bool, socksUser, socksPass string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if cancel != nil {
|
||||
return fmt.Errorf("olcRTC already running")
|
||||
}
|
||||
|
||||
if roomID == "" {
|
||||
return fmt.Errorf("roomID is required")
|
||||
}
|
||||
if keyHex == "" {
|
||||
return fmt.Errorf("keyHex is required")
|
||||
switch {
|
||||
case cancel != nil:
|
||||
return errAlreadyRunning
|
||||
case roomID == "":
|
||||
return errRoomIDRequired
|
||||
case keyHex == "":
|
||||
return errKeyHexRequired
|
||||
}
|
||||
|
||||
roomURL := "https://telemost.yandex.ru/j/" + roomID
|
||||
|
||||
ctx, c := context.WithCancel(context.Background())
|
||||
cancel = c
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
cancel = cancelFunc
|
||||
done = make(chan struct{})
|
||||
ready = make(chan struct{})
|
||||
localReady := ready
|
||||
runErr = nil
|
||||
errRun = nil
|
||||
|
||||
var readyOnce sync.Once
|
||||
|
||||
go func() {
|
||||
err := client.RunWithReady(ctx, roomURL, keyHex, socksPort, duo, socksUser, socksPass, func() {
|
||||
readyOnce.Do(func() {
|
||||
close(localReady)
|
||||
})
|
||||
})
|
||||
defer cancelFunc()
|
||||
|
||||
err := client.RunWithReady(
|
||||
ctx,
|
||||
roomURL,
|
||||
keyHex,
|
||||
socksPort,
|
||||
duo,
|
||||
"",
|
||||
socksUser,
|
||||
socksPass,
|
||||
func() {
|
||||
readyOnce.Do(func() {
|
||||
close(localReady)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
mu.Lock()
|
||||
cancel = nil
|
||||
runErr = err
|
||||
errRun = err
|
||||
mu.Unlock()
|
||||
close(done)
|
||||
}()
|
||||
@@ -111,19 +132,22 @@ func Start(roomID, keyHex string, socksPort int, duo bool, socksUser, socksPass
|
||||
}
|
||||
|
||||
// WaitReady blocks until the Telemost peers are connected and the local SOCKS5 listener is ready.
|
||||
//
|
||||
//nolint:cyclop // The control flow is intentionally linear so mobile callers can observe each startup state clearly.
|
||||
func WaitReady(timeoutMillis int) error {
|
||||
mu.Lock()
|
||||
r := ready
|
||||
d := done
|
||||
err := runErr
|
||||
runErr := errRun
|
||||
running := cancel != nil
|
||||
mu.Unlock()
|
||||
|
||||
if r == nil {
|
||||
if err != nil {
|
||||
return err
|
||||
if runErr != nil {
|
||||
return runErr
|
||||
}
|
||||
return fmt.Errorf("olcRTC is not running")
|
||||
|
||||
return errNotRunning
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -133,10 +157,11 @@ func WaitReady(timeoutMillis int) error {
|
||||
}
|
||||
|
||||
if !running {
|
||||
if err != nil {
|
||||
return err
|
||||
if runErr != nil {
|
||||
return runErr
|
||||
}
|
||||
return fmt.Errorf("olcRTC stopped before becoming ready")
|
||||
|
||||
return errStoppedBeforeReady
|
||||
}
|
||||
|
||||
timer := time.NewTimer(time.Duration(timeoutMillis) * time.Millisecond)
|
||||
@@ -147,32 +172,33 @@ func WaitReady(timeoutMillis int) error {
|
||||
return nil
|
||||
case <-d:
|
||||
mu.Lock()
|
||||
err := runErr
|
||||
runErr = errRun
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
if runErr != nil {
|
||||
return runErr
|
||||
}
|
||||
return fmt.Errorf("olcRTC stopped before becoming ready")
|
||||
|
||||
return errStoppedBeforeReady
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("olcRTC start timed out")
|
||||
return errStartTimedOut
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully stops the olcRTC client.
|
||||
func Stop() {
|
||||
mu.Lock()
|
||||
c := cancel
|
||||
d := done
|
||||
cancelFunc := cancel
|
||||
doneCh := done
|
||||
mu.Unlock()
|
||||
|
||||
if c == nil {
|
||||
if cancelFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c()
|
||||
cancelFunc()
|
||||
|
||||
if d != nil {
|
||||
<-d
|
||||
if doneCh != nil {
|
||||
<-doneCh
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +214,7 @@ type logBridge struct {
|
||||
w LogWriter
|
||||
}
|
||||
|
||||
func (b *logBridge) Write(p []byte) (n int, err error) {
|
||||
func (b *logBridge) Write(p []byte) (int, error) {
|
||||
b.w.WriteLog(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
0
docker/olcrtc-entrypoint.sh → script/docker/olcrtc-entrypoint.sh
Executable file → Normal file
0
docker/olcrtc-entrypoint.sh → script/docker/olcrtc-entrypoint.sh
Executable file → Normal file
0
docker/olcrtc-healthcheck.sh → script/docker/olcrtc-healthcheck.sh
Executable file → Normal file
0
docker/olcrtc-healthcheck.sh → script/docker/olcrtc-healthcheck.sh
Executable file → Normal file
Reference in New Issue
Block a user