Address PR review feedback

This commit is contained in:
Qtozdec
2026-04-11 20:07:22 +03:00
parent a30aeedb26
commit 245c6688ca
8 changed files with 689 additions and 445 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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.

View File

@@ -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}
}

View File

@@ -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())
}

View File

@@ -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
}

View File

View File