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..036d434 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 ( + 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) 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() {} 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` опционален, может быть пустым). --- 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= 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/e2e/local_soak_test.go b/internal/e2e/local_soak_test.go index 2f9fbcb..8be48b4 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", @@ -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 { diff --git a/internal/server/server.go b/internal/server/server.go index 02e109b..73e4bfb 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,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 +} 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,