From d5973cf2c74b25856b7ef3dd7646bb652af5f039 Mon Sep 17 00:00:00 2001 From: spkprsnts Date: Sat, 23 May 2026 05:14:25 +0500 Subject: [PATCH 1/7] feat(server): add username/password auth for outbound SOCKS5 proxy (RFC 1929) --- internal/app/session/session.go | 4 +++ internal/config/config.go | 6 ++++ internal/server/server.go | 52 +++++++++++++++++++++++++++++++-- pkg/olcrtc/tunnel/tunnel.go | 4 +++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/internal/app/session/session.go b/internal/app/session/session.go index 6b2d8f4..169a4b9 100644 --- a/internal/app/session/session.go +++ b/internal/app/session/session.go @@ -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, diff --git a/internal/config/config.go b/internal/config/config.go index 2b3171f..55fc9b3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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) diff --git a/internal/server/server.go b/internal/server/server.go index 02e109b..dd4af57 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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,15 +920,55 @@ 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) + 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 || resp[1] != 0 { + if resp[0] != 5 { + return ErrSocks5AuthFailed + } + switch resp[1] { + case 0: // no auth accepted + if s.socksProxyUser != "" { + return ErrSocks5AuthFailed + } + case 2: // username/password + 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))) + authMsg = append(authMsg, []byte(user)...) + authMsg = append(authMsg, byte(len(pass))) + 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 + } + default: return ErrSocks5AuthFailed } diff --git a/pkg/olcrtc/tunnel/tunnel.go b/pkg/olcrtc/tunnel/tunnel.go index f7d2249..6bcca3c 100644 --- a/pkg/olcrtc/tunnel/tunnel.go +++ b/pkg/olcrtc/tunnel/tunnel.go @@ -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, From ff83e76cd28da3e2944091ba2261503ed0d33e9d Mon Sep 17 00:00:00 2001 From: spkprsnts Date: Sat, 23 May 2026 05:16:48 +0500 Subject: [PATCH 2/7] fix: drain stderr filter before exit to prevent log loss on startup errors --- cmd/olcrtc/main.go | 1 + cmd/olcrtc/stderr_filter_unix.go | 27 ++++++++++++++++++++++++--- cmd/olcrtc/stderr_filter_windows.go | 2 ++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/cmd/olcrtc/main.go b/cmd/olcrtc/main.go index 9a806c5..5890501 100644 --- a/cmd/olcrtc/main.go +++ b/cmd/olcrtc/main.go @@ -63,6 +63,7 @@ type failoverConfig struct { func main() { if err := run(); err != nil { logger.Error(err) + flushStderrFilter() os.Exit(1) } } diff --git a/cmd/olcrtc/stderr_filter_unix.go b/cmd/olcrtc/stderr_filter_unix.go index 613b28c..bc6c8b3 100644 --- a/cmd/olcrtc/stderr_filter_unix.go +++ b/cmd/olcrtc/stderr_filter_unix.go @@ -11,7 +11,12 @@ import ( "golang.org/x/sys/unix" ) -var stderrFilterOnce sync.Once //nolint:gochecknoglobals // process-wide stderr fd filter +var ( //nolint:gochecknoglobals // process-wide stderr fd filter + stderrFilterOnce sync.Once + stderrPipeWriter *os.File + stderrFilterDone chan struct{} + stderrFilterActive bool +) 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) diff --git a/cmd/olcrtc/stderr_filter_windows.go b/cmd/olcrtc/stderr_filter_windows.go index 760d7a8..c455367 100644 --- a/cmd/olcrtc/stderr_filter_windows.go +++ b/cmd/olcrtc/stderr_filter_windows.go @@ -3,3 +3,5 @@ package main func installStderrFilter() {} + +func flushStderrFilter() {} From a00d7f5e9f80d3de70c432c376dfec7f60d38653 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sat, 23 May 2026 23:46:27 +0300 Subject: [PATCH 3/7] deps: bump github.com/zarazaex69/j to 4015c3c4de75 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e38c603..06d5a35 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6d03686..16041ae 100644 --- a/go.sum +++ b/go.sum @@ -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= From 5e4c2e27ee5ea1ac7053593c8acd67c4a1a0915a Mon Sep 17 00:00:00 2001 From: spkprsnts Date: Sun, 24 May 2026 02:18:34 +0500 Subject: [PATCH 4/7] refactor(server): split socks5 connect logic - Extract auth logic to helper functions - Add linter directives for gosec and globals --- cmd/olcrtc/stderr_filter_unix.go | 10 +-- internal/server/server.go | 106 +++++++++++++++++-------------- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/cmd/olcrtc/stderr_filter_unix.go b/cmd/olcrtc/stderr_filter_unix.go index bc6c8b3..036d434 100644 --- a/cmd/olcrtc/stderr_filter_unix.go +++ b/cmd/olcrtc/stderr_filter_unix.go @@ -11,11 +11,11 @@ import ( "golang.org/x/sys/unix" ) -var ( //nolint:gochecknoglobals // process-wide stderr fd filter - stderrFilterOnce sync.Once - stderrPipeWriter *os.File - stderrFilterDone chan struct{} - stderrFilterActive bool +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() { diff --git a/internal/server/server.go b/internal/server/server.go index dd4af57..73e4bfb 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -920,6 +920,37 @@ func (s *Server) dial(req ConnectRequest) (net.Conn, error) { } func (s *Server) socks5Connect(conn net.Conn, targetAddr string, targetPort int) error { + if err := s.socks5Authenticate(conn); err != nil { + return err + } + + addrLen := len(targetAddr) + if addrLen > 255 { + addrLen = 255 + targetAddr = targetAddr[:255] + } + + req := make([]byte, 0, 7+addrLen) + req = append(req, 5, 1, 0, 3, byte(addrLen)) + req = append(req, []byte(targetAddr)...) + req = append(req, byte(targetPort>>8), byte(targetPort)) //nolint:gosec,lll // G115: bounded conversion verified by surrounding logic + + if _, err := conn.Write(req); err != nil { + return fmt.Errorf("failed to write socks5 connect req: %w", err) + } + + resp := make([]byte, 10) + if _, err := io.ReadFull(conn, resp); err != nil { + return fmt.Errorf("failed to read socks5 connect resp: %w", err) + } + if resp[0] != 5 || resp[1] != 0 { + return fmt.Errorf("%w: %d", ErrSocks5ConnectFailed, resp[1]) + } + + 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 { @@ -945,55 +976,36 @@ func (s *Server) socks5Connect(conn net.Conn, targetAddr string, targetPort int) return ErrSocks5AuthFailed } case 2: // username/password - 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))) - authMsg = append(authMsg, []byte(user)...) - authMsg = append(authMsg, byte(len(pass))) - 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 s.socks5SendCredentials(conn) default: return ErrSocks5AuthFailed } - - addrLen := len(targetAddr) - if addrLen > 255 { - addrLen = 255 - targetAddr = targetAddr[:255] - } - - req := make([]byte, 0, 7+addrLen) - req = append(req, 5, 1, 0, 3, byte(addrLen)) - req = append(req, []byte(targetAddr)...) - req = append(req, byte(targetPort>>8), byte(targetPort)) //nolint:gosec,lll // G115: bounded conversion verified by surrounding logic - - if _, err := conn.Write(req); err != nil { - return fmt.Errorf("failed to write socks5 connect req: %w", err) - } - - resp = make([]byte, 10) - if _, err := io.ReadFull(conn, resp); err != nil { - return fmt.Errorf("failed to read socks5 connect resp: %w", err) - } - if resp[0] != 5 || resp[1] != 0 { - return fmt.Errorf("%w: %d", ErrSocks5ConnectFailed, resp[1]) - } - + 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 } From 0f9388134ab2bb9f86d819d5265a42a8a9c20cc2 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sun, 24 May 2026 02:29:00 +0300 Subject: [PATCH 5/7] docs: add proxy_user/proxy_pass fields to server config examples and documentation --- docs/configuration.md | 1 + docs/examples/server.jitsi.datachannel.yaml | 2 ++ docs/examples/server.jitsi.seichannel.yaml | 2 ++ docs/examples/server.jitsi.videochannel.yaml | 2 ++ docs/examples/server.jitsi.vp8channel.yaml | 2 ++ docs/examples/server.telemost.datachannel.yaml | 2 ++ docs/examples/server.telemost.seichannel.yaml | 2 ++ docs/examples/server.telemost.videochannel.yaml | 2 ++ docs/examples/server.telemost.vp8channel.yaml | 2 ++ docs/examples/server.wbstream.datachannel.yaml | 2 ++ docs/examples/server.wbstream.seichannel.yaml | 2 ++ docs/examples/server.wbstream.videochannel.yaml | 2 ++ docs/examples/server.wbstream.vp8channel.yaml | 2 ++ docs/settings.md | 5 +++++ 14 files changed, 30 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 2090e3e..8902404 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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` | diff --git a/docs/examples/server.jitsi.datachannel.yaml b/docs/examples/server.jitsi.datachannel.yaml index c3daaef..0cff6de 100644 --- a/docs/examples/server.jitsi.datachannel.yaml +++ b/docs/examples/server.jitsi.datachannel.yaml @@ -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 diff --git a/docs/examples/server.jitsi.seichannel.yaml b/docs/examples/server.jitsi.seichannel.yaml index 9a6fe06..37caf69 100644 --- a/docs/examples/server.jitsi.seichannel.yaml +++ b/docs/examples/server.jitsi.seichannel.yaml @@ -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 diff --git a/docs/examples/server.jitsi.videochannel.yaml b/docs/examples/server.jitsi.videochannel.yaml index 0ca6055..b4c111d 100644 --- a/docs/examples/server.jitsi.videochannel.yaml +++ b/docs/examples/server.jitsi.videochannel.yaml @@ -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 diff --git a/docs/examples/server.jitsi.vp8channel.yaml b/docs/examples/server.jitsi.vp8channel.yaml index 8bf369c..11d9a42 100644 --- a/docs/examples/server.jitsi.vp8channel.yaml +++ b/docs/examples/server.jitsi.vp8channel.yaml @@ -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 diff --git a/docs/examples/server.telemost.datachannel.yaml b/docs/examples/server.telemost.datachannel.yaml index acf9d36..269fec9 100644 --- a/docs/examples/server.telemost.datachannel.yaml +++ b/docs/examples/server.telemost.datachannel.yaml @@ -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 diff --git a/docs/examples/server.telemost.seichannel.yaml b/docs/examples/server.telemost.seichannel.yaml index b72d163..7a4fb92 100644 --- a/docs/examples/server.telemost.seichannel.yaml +++ b/docs/examples/server.telemost.seichannel.yaml @@ -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 diff --git a/docs/examples/server.telemost.videochannel.yaml b/docs/examples/server.telemost.videochannel.yaml index d222115..336df5c 100644 --- a/docs/examples/server.telemost.videochannel.yaml +++ b/docs/examples/server.telemost.videochannel.yaml @@ -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 diff --git a/docs/examples/server.telemost.vp8channel.yaml b/docs/examples/server.telemost.vp8channel.yaml index 90a8f19..5421b3c 100644 --- a/docs/examples/server.telemost.vp8channel.yaml +++ b/docs/examples/server.telemost.vp8channel.yaml @@ -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 diff --git a/docs/examples/server.wbstream.datachannel.yaml b/docs/examples/server.wbstream.datachannel.yaml index 3adbd7c..01a87a9 100644 --- a/docs/examples/server.wbstream.datachannel.yaml +++ b/docs/examples/server.wbstream.datachannel.yaml @@ -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 diff --git a/docs/examples/server.wbstream.seichannel.yaml b/docs/examples/server.wbstream.seichannel.yaml index e2403f8..3b9635c 100644 --- a/docs/examples/server.wbstream.seichannel.yaml +++ b/docs/examples/server.wbstream.seichannel.yaml @@ -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 diff --git a/docs/examples/server.wbstream.videochannel.yaml b/docs/examples/server.wbstream.videochannel.yaml index fca6371..8e48e83 100644 --- a/docs/examples/server.wbstream.videochannel.yaml +++ b/docs/examples/server.wbstream.videochannel.yaml @@ -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 diff --git a/docs/examples/server.wbstream.vp8channel.yaml b/docs/examples/server.wbstream.vp8channel.yaml index b2a016c..57e746b 100644 --- a/docs/examples/server.wbstream.vp8channel.yaml +++ b/docs/examples/server.wbstream.vp8channel.yaml @@ -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 diff --git a/docs/settings.md b/docs/settings.md index bf067fd..77fd0c4 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -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` опционален, может быть пустым). --- From bb2e1ee1c8e2ae0ae7c4b5d65c8726885a62bcec Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sun, 24 May 2026 02:42:13 +0300 Subject: [PATCH 6/7] test(e2e): allow multiple transports in local soak test via comma-separated list or 'all' --- internal/e2e/local_soak_test.go | 72 +++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/internal/e2e/local_soak_test.go b/internal/e2e/local_soak_test.go index 2f9fbcb..0dc2f17 100644 --- a/internal/e2e/local_soak_test.go +++ b/internal/e2e/local_soak_test.go @@ -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", @@ -90,15 +97,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 +146,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 +155,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, errors.New("empty value") + } + 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, errors.New("no transports listed") + } + + 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("unknown transport %q", 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 { From f63aa0bc4397647e9659a5169145a2e3a6bb9bc2 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Sun, 24 May 2026 02:57:30 +0300 Subject: [PATCH 7/7] refactor(e2e): extract local soak transport error constants --- internal/e2e/local_soak_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/e2e/local_soak_test.go b/internal/e2e/local_soak_test.go index 0dc2f17..8be48b4 100644 --- a/internal/e2e/local_soak_test.go +++ b/internal/e2e/local_soak_test.go @@ -79,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 @@ -162,7 +167,7 @@ func runLocalSoakOnce(t *testing.T, transportName string) { func resolveLocalSoakTransports(value string) ([]string, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { - return nil, errors.New("empty value") + return nil, errLocalSoakTransportEmpty } if strings.EqualFold(trimmed, "all") { return builtInTransportNames(), nil @@ -175,14 +180,14 @@ func resolveLocalSoakTransports(value string) ([]string, error) { items := splitTestList(trimmed) if len(items) == 0 { - return nil, errors.New("no transports listed") + 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("unknown transport %q", name) + return nil, fmt.Errorf("%w: %q", errLocalSoakTransportUnknown, name) } if _, dup := seen[name]; dup { continue