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,