Merge branch 'openlibrecommunity:master' into docker-local-20260523

This commit is contained in:
Yuriy Samorodov
2026-05-24 05:10:39 +04:00
committed by GitHub
24 changed files with 217 additions and 22 deletions

View File

@@ -63,6 +63,7 @@ type failoverConfig struct {
func main() {
if err := run(); err != nil {
logger.Error(err)
flushStderrFilter()
os.Exit(1)
}
}

View File

@@ -11,7 +11,12 @@ import (
"golang.org/x/sys/unix"
)
var stderrFilterOnce sync.Once //nolint:gochecknoglobals // process-wide stderr fd filter
var (
stderrFilterOnce sync.Once //nolint:gochecknoglobals // process-wide stderr fd filter
stderrPipeWriter *os.File //nolint:gochecknoglobals // process-wide stderr fd filter
stderrFilterDone chan struct{} //nolint:gochecknoglobals // process-wide stderr fd filter
stderrFilterActive bool //nolint:gochecknoglobals // process-wide stderr fd filter
)
func installStderrFilter() {
stderrFilterOnce.Do(func() {
@@ -30,13 +35,29 @@ func installStderrFilter() {
_ = unix.Close(origFD)
return
}
_ = writer.Close()
stderrPipeWriter = writer
stderrFilterDone = make(chan struct{})
stderrFilterActive = true
os.Stderr = os.NewFile(uintptr(unix.Stderr), "/dev/stderr")
orig := os.NewFile(uintptr(origFD), "/dev/stderr-original")
go copyFilteredStderr(reader, orig)
go func() {
defer close(stderrFilterDone)
copyFilteredStderr(reader, orig)
}()
})
}
// flushStderrFilter closes the pipe write ends so the filter goroutine
// sees EOF and drains any buffered output before the process exits.
func flushStderrFilter() {
if !stderrFilterActive {
return
}
_ = stderrPipeWriter.Close()
_ = unix.Close(unix.Stderr)
<-stderrFilterDone
}
func copyFilteredStderr(reader *os.File, out io.Writer) {
defer func() { _ = reader.Close() }()
br := bufio.NewReader(reader)

View File

@@ -3,3 +3,5 @@
package main
func installStderrFilter() {}
func flushStderrFilter() {}

View File

@@ -59,6 +59,7 @@ olcrtc /etc/olcrtc/client.yaml
| `socks.host` / `socks.port` | локальный SOCKS5 listener в `mode: cnc` |
| `socks.user` / `socks.pass` | необязательная auth для входящих SOCKS5-подключений |
| `socks.proxy_addr` / `socks.proxy_port` | исходящий SOCKS5-прокси на серверной стороне |
| `socks.proxy_user` / `socks.proxy_pass` | необязательная auth для upstream-прокси (RFC 1929) |
| `engine.name` / `engine.url` / `engine.token` | прямой engine-режим, только при `auth.provider: none` |
| `video.*` | настройки `videochannel` |
| `vp8.*` | настройки `vp8channel` |

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
data: data
debug: false

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
sei:
fps: 60

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
video:
width: 1920

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
vp8:
fps: 60

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
data: data
debug: false

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
sei:
fps: 60

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
video:
width: 1920

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
vp8:
fps: 60

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
data: data
debug: false

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
sei:
fps: 60

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
video:
width: 1920

View File

@@ -28,6 +28,8 @@ liveness:
socks:
proxy_addr: "" # например "127.0.0.1"
proxy_port: 0 # например 1080
proxy_user: "" # необязательная auth для upstream-прокси (RFC 1929)
proxy_pass: ""
vp8:
fps: 60

View File

@@ -110,6 +110,11 @@ transport. Используй одинаковые traffic-настройки н
|-----------|----------|
| `socks.proxy_addr` | Адрес SOCKS5-прокси для исходящего трафика сервера |
| `socks.proxy_port` | Порт этого прокси |
| `socks.proxy_user` | Логин для аутентификации на upstream-прокси (необязательно) |
| `socks.proxy_pass` | Пароль для аутентификации на upstream-прокси (необязательно) |
Если `socks.proxy_user` пуст - сервер ходит к прокси без аутентификации (метод `0x00`).
Если задан - используется username/password auth по RFC 1929 (`proxy_pass` опционален, может быть пустым).
---

2
go.mod
View File

@@ -15,7 +15,7 @@ require (
github.com/xtaci/kcp-go/v5 v5.6.72
github.com/xtaci/smux v1.5.57
github.com/zarazaex69/gr v0.0.0-20260430043628-45b595f4fef0
github.com/zarazaex69/j v0.0.0-20260518222913-cb593e3bc582
github.com/zarazaex69/j v0.0.0-20260523204249-4015c3c4de75
golang.org/x/crypto v0.50.0
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
golang.org/x/sys v0.43.0

4
go.sum
View File

@@ -235,8 +235,8 @@ github.com/xtaci/smux v1.5.57/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX8
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zarazaex69/gr v0.0.0-20260430043628-45b595f4fef0 h1:dMjHX/YPV3ZD/KJKFjQdlMBwj2/rZIuOVKOvGv26m9k=
github.com/zarazaex69/gr v0.0.0-20260430043628-45b595f4fef0/go.mod h1:7vALI2tjaLTOGiDKV7V2JkVU9bA1YADBDQA6uvpp1ac=
github.com/zarazaex69/j v0.0.0-20260518222913-cb593e3bc582 h1:5ZvS/7kBTqTMKMjMO3S/4neE4YHRoYKbQdx/4y8Kobc=
github.com/zarazaex69/j v0.0.0-20260518222913-cb593e3bc582/go.mod h1:7/ypJTenOIPx23fpo5uF7l4u+rxZqg9cFbTL/N77Ktc=
github.com/zarazaex69/j v0.0.0-20260523204249-4015c3c4de75 h1:VoS7CB/151kq8zrsI2keb/SuqIquF7LVtVnDRYLijC8=
github.com/zarazaex69/j v0.0.0-20260523204249-4015c3c4de75/go.mod h1:7/ypJTenOIPx23fpo5uF7l4u+rxZqg9cFbTL/N77Ktc=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=

View File

@@ -189,6 +189,8 @@ type Config struct {
DNSServer string
SOCKSProxyAddr string
SOCKSProxyPort int
SOCKSProxyUser string
SOCKSProxyPass string
Video VideoConfig
VP8 VP8Config
SEI SEIConfig
@@ -652,6 +654,8 @@ func runOnce(
DNSServer: cfg.DNSServer,
SOCKSProxyAddr: cfg.SOCKSProxyAddr,
SOCKSProxyPort: cfg.SOCKSProxyPort,
SOCKSProxyUser: cfg.SOCKSProxyUser,
SOCKSProxyPass: cfg.SOCKSProxyPass,
TransportOptions: opts,
Engine: cfg.Engine,
URL: cfg.URL,

View File

@@ -108,6 +108,8 @@ type SOCKS struct {
Pass string `yaml:"pass"`
ProxyAddr string `yaml:"proxy_addr"`
ProxyPort int `yaml:"proxy_port"`
ProxyUser string `yaml:"proxy_user"`
ProxyPass string `yaml:"proxy_pass"`
}
// Engine selects a direct SFU connection when Auth.Provider is "none".
@@ -262,6 +264,8 @@ func Apply(dst session.Config, f File) session.Config {
dst.DNSServer = pickString(dst.DNSServer, f.Net.DNS)
dst.SOCKSProxyAddr = pickString(dst.SOCKSProxyAddr, f.SOCKS.ProxyAddr)
dst.SOCKSProxyPort = pickInt(dst.SOCKSProxyPort, f.SOCKS.ProxyPort)
dst.SOCKSProxyUser = pickString(dst.SOCKSProxyUser, f.SOCKS.ProxyUser)
dst.SOCKSProxyPass = pickString(dst.SOCKSProxyPass, f.SOCKS.ProxyPass)
dst.Video.Width = pickInt(dst.Video.Width, f.Video.Width)
dst.Video.Height = pickInt(dst.Video.Height, f.Video.Height)
dst.Video.FPS = pickInt(dst.Video.FPS, f.Video.FPS)
@@ -307,6 +311,8 @@ func ApplyProfile(base session.Config, p Profile) session.Config {
dst.DNSServer = overlayString(dst.DNSServer, p.Net.DNS)
dst.SOCKSProxyAddr = overlayString(dst.SOCKSProxyAddr, p.SOCKS.ProxyAddr)
dst.SOCKSProxyPort = overlayInt(dst.SOCKSProxyPort, p.SOCKS.ProxyPort)
dst.SOCKSProxyUser = overlayString(dst.SOCKSProxyUser, p.SOCKS.ProxyUser)
dst.SOCKSProxyPass = overlayString(dst.SOCKSProxyPass, p.SOCKS.ProxyPass)
dst.Video.Width = overlayInt(dst.Video.Width, p.Video.Width)
dst.Video.Height = overlayInt(dst.Video.Height, p.Video.Height)
dst.Video.FPS = overlayInt(dst.Video.FPS, p.Video.FPS)

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net"
"strings"
"sync"
"sync/atomic"
"testing"
@@ -37,6 +38,11 @@ import (
// -olcrtc.local-soak-duration=12h \
// -timeout=13h
//
// To pump every built-in transport sequentially in a single run pass
// `-olcrtc.local-soak-transport=all` (or a comma-separated subset like
// `datachannel,vp8channel`). Each transport gets its own subtest and its
// own full -olcrtc.local-soak-duration window.
//
// The test is gated by -olcrtc.local-soak so it never runs in regular CI.
var (
@@ -53,7 +59,8 @@ var (
localSoakTransport = flag.String( //nolint:gochecknoglobals // package-level state intentional
"olcrtc.local-soak-transport",
transportData,
"transport to pump through: datachannel|videochannel|seichannel|vp8channel",
"transport(s) to pump through: datachannel|videochannel|seichannel|vp8channel, "+
"or 'all', or a comma-separated subset (e.g. datachannel,vp8channel)",
)
localSoakChunk = flag.Int( //nolint:gochecknoglobals // package-level state intentional
"olcrtc.local-soak-chunk",
@@ -72,7 +79,12 @@ var (
)
)
var errLocalSoakPayloadMismatch = errors.New("local soak payload mismatch")
var (
errLocalSoakPayloadMismatch = errors.New("local soak payload mismatch")
errLocalSoakTransportEmpty = errors.New("empty transport value")
errLocalSoakTransportNone = errors.New("no transports listed")
errLocalSoakTransportUnknown = errors.New("unknown transport")
)
// TestLocalThroughputSoak pumps a deterministic byte pattern through a
// locally-built tunnel for -olcrtc.local-soak-duration and reports
@@ -90,15 +102,34 @@ func TestLocalThroughputSoak(t *testing.T) {
t.Fatalf("invalid -olcrtc.local-soak-chunk=%d", *localSoakChunk)
}
transports, err := resolveLocalSoakTransports(*localSoakTransport)
if err != nil {
t.Fatalf("invalid -olcrtc.local-soak-transport=%q: %v", *localSoakTransport, err)
}
for _, transportName := range transports {
t.Run(transportName, func(t *testing.T) {
runLocalSoakOnce(t, transportName)
})
}
}
// runLocalSoakOnce builds a fresh tunnel for transportName and pumps it
// for one full -olcrtc.local-soak-duration window. Each subtest gets its
// own carrier, SOCKS port and goroutines via t.Cleanup, so transports
// don't share state and a leak in one of them won't poison the next.
func runLocalSoakOnce(t *testing.T, transportName string) {
t.Helper()
// Connection setup itself can be slow (first WebRTC handshake on
// some transports), so don't fold it into the duration budget.
const setupBudget = 30 * time.Second
t.Logf("[soak] transport=%s duration=%s chunk=%d verify=%t progress=%s",
*localSoakTransport, *localSoakDuration, *localSoakChunk,
transportName, *localSoakDuration, *localSoakChunk,
*localSoakVerify, *localSoakProgress)
rt := startLocalSoakTunnel(t, *localSoakTransport)
rt := startLocalSoakTunnel(t, transportName)
echoAddr := startEchoServer(t)
conn, err := connectViaSOCKSWithin(rt.socksAddr, echoAddr, setupBudget)
@@ -120,7 +151,7 @@ func TestLocalThroughputSoak(t *testing.T) {
}
t.Logf("[soak] DONE transport=%s elapsed=%s sent=%s recv=%s send=%s/s recv=%s/s",
*localSoakTransport,
transportName,
stats.elapsed.Round(time.Second),
humanBytes(stats.sent),
humanBytes(stats.recv),
@@ -129,6 +160,44 @@ func TestLocalThroughputSoak(t *testing.T) {
)
}
// resolveLocalSoakTransports turns the -olcrtc.local-soak-transport flag
// value into an ordered, deduplicated list of built-in transport names.
// Accepts "all" as a shorthand for builtInTransportNames(), or a
// comma-separated subset (with whitespace tolerated around items).
func resolveLocalSoakTransports(value string) ([]string, error) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil, errLocalSoakTransportEmpty
}
if strings.EqualFold(trimmed, "all") {
return builtInTransportNames(), nil
}
known := make(map[string]struct{}, 4)
for _, name := range builtInTransportNames() {
known[name] = struct{}{}
}
items := splitTestList(trimmed)
if len(items) == 0 {
return nil, errLocalSoakTransportNone
}
seen := make(map[string]struct{}, len(items))
out := make([]string, 0, len(items))
for _, name := range items {
if _, ok := known[name]; !ok {
return nil, fmt.Errorf("%w: %q", errLocalSoakTransportUnknown, name)
}
if _, dup := seen[name]; dup {
continue
}
seen[name] = struct{}{}
out = append(out, name)
}
return out, nil
}
// startLocalSoakTunnel mirrors startTunnel but lets the caller pick the
// transport (the original is hard-coded to datachannel).
func startLocalSoakTunnel(t *testing.T, transportName string) *tunnelRuntime {

View File

@@ -77,6 +77,8 @@ type Server struct {
resolver *net.Resolver
socksProxyAddr string
socksProxyPort int
socksProxyUser string
socksProxyPass string
liveness control.Config
health *runtime.HealthTracker
done chan struct{}
@@ -110,6 +112,8 @@ type Config struct {
DNSServer string
SOCKSProxyAddr string
SOCKSProxyPort int
SOCKSProxyUser string
SOCKSProxyPass string
TransportOptions transport.Options
Engine string
URL string
@@ -166,6 +170,8 @@ func Run(ctx context.Context, cfg Config) error {
dnsServer: cfg.DNSServer,
socksProxyAddr: cfg.SOCKSProxyAddr,
socksProxyPort: cfg.SOCKSProxyPort,
socksProxyUser: cfg.SOCKSProxyUser,
socksProxyPass: cfg.SOCKSProxyPass,
liveness: cfg.Liveness,
health: runtime.NewHealthTracker(cfg.OnHealth),
peerSessions: make(map[string]*peerSession),
@@ -914,16 +920,8 @@ func (s *Server) dial(req ConnectRequest) (net.Conn, error) {
}
func (s *Server) socks5Connect(conn net.Conn, targetAddr string, targetPort int) error {
if _, err := conn.Write([]byte{5, 1, 0}); err != nil {
return fmt.Errorf("failed to write socks5 auth: %w", err)
}
resp := make([]byte, 2)
if _, err := io.ReadFull(conn, resp); err != nil {
return fmt.Errorf("failed to read socks5 auth resp: %w", err)
}
if resp[0] != 5 || resp[1] != 0 {
return ErrSocks5AuthFailed
if err := s.socks5Authenticate(conn); err != nil {
return err
}
addrLen := len(targetAddr)
@@ -941,7 +939,7 @@ func (s *Server) socks5Connect(conn net.Conn, targetAddr string, targetPort int)
return fmt.Errorf("failed to write socks5 connect req: %w", err)
}
resp = make([]byte, 10)
resp := make([]byte, 10)
if _, err := io.ReadFull(conn, resp); err != nil {
return fmt.Errorf("failed to read socks5 connect resp: %w", err)
}
@@ -951,3 +949,63 @@ func (s *Server) socks5Connect(conn net.Conn, targetAddr string, targetPort int)
return nil
}
func (s *Server) socks5Authenticate(conn net.Conn) error {
if s.socksProxyUser != "" {
// Offer username/password auth (RFC 1929) only.
if _, err := conn.Write([]byte{5, 1, 2}); err != nil {
return fmt.Errorf("failed to write socks5 auth: %w", err)
}
} else {
// No authentication.
if _, err := conn.Write([]byte{5, 1, 0}); err != nil {
return fmt.Errorf("failed to write socks5 auth: %w", err)
}
}
resp := make([]byte, 2)
if _, err := io.ReadFull(conn, resp); err != nil {
return fmt.Errorf("failed to read socks5 auth resp: %w", err)
}
if resp[0] != 5 {
return ErrSocks5AuthFailed
}
switch resp[1] {
case 0: // no auth accepted
if s.socksProxyUser != "" {
return ErrSocks5AuthFailed
}
case 2: // username/password
return s.socks5SendCredentials(conn)
default:
return ErrSocks5AuthFailed
}
return nil
}
func (s *Server) socks5SendCredentials(conn net.Conn) error {
user := s.socksProxyUser
pass := s.socksProxyPass
if len(user) > 255 {
user = user[:255]
}
if len(pass) > 255 {
pass = pass[:255]
}
authMsg := make([]byte, 0, 3+len(user)+len(pass))
authMsg = append(authMsg, 1, byte(len(user))) //nolint:gosec // G115: len clamped to ≤255 above
authMsg = append(authMsg, []byte(user)...)
authMsg = append(authMsg, byte(len(pass))) //nolint:gosec // G115: len clamped to ≤255 above
authMsg = append(authMsg, []byte(pass)...)
if _, err := conn.Write(authMsg); err != nil {
return fmt.Errorf("failed to write socks5 credentials: %w", err)
}
authResp := make([]byte, 2)
if _, err := io.ReadFull(conn, authResp); err != nil {
return fmt.Errorf("failed to read socks5 credentials resp: %w", err)
}
if authResp[1] != 0 {
return ErrSocks5AuthFailed
}
return nil
}

View File

@@ -85,6 +85,8 @@ type Config struct {
DNSServer string // resolver used for target dials, e.g. "8.8.8.8:53"
SOCKSProxyAddr string // optional outbound SOCKS5 proxy host
SOCKSProxyPort int // optional outbound SOCKS5 proxy port
SOCKSProxyUser string // optional username for SOCKS5 proxy auth (RFC 1929)
SOCKSProxyPass string // optional password for SOCKS5 proxy auth (RFC 1929)
// --- transport tuning ---
// TransportOptions carries transport-specific tuning. Use the Options
@@ -128,6 +130,8 @@ func (s *Server) Run(ctx context.Context) error {
DNSServer: s.cfg.DNSServer,
SOCKSProxyAddr: s.cfg.SOCKSProxyAddr,
SOCKSProxyPort: s.cfg.SOCKSProxyPort,
SOCKSProxyUser: s.cfg.SOCKSProxyUser,
SOCKSProxyPass: s.cfg.SOCKSProxyPass,
TransportOptions: s.cfg.TransportOptions,
AuthHook: s.cfg.AuthHook,
OnSessionOpen: s.cfg.OnSessionOpen,