From bfc0e0fb5255237918f288d429f41278ea59861a Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Mon, 1 Jun 2026 01:58:49 +0800 Subject: [PATCH] fix: support SOCKS5 proxy auth Fixes #585 --- README.md | 2 + README_EN.md | 2 + common/i18n/locales/en.yaml | 2 +- common/i18n/locales/zh.yaml | 2 +- common/proxy/manager.go | 26 ++---- common/proxy/manager_test.go | 156 +++++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c6f661b..5d7d011 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,8 @@ yay -S fscan-git `fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080` ![](image/2020-12-12-13-34-44.png) +`fscan.exe -h 192.168.x.x -socks5 socks5://user:pass@127.0.0.1:1080` (SOCKS5认证代理) + `fscan.exe -h 192.168.x.x -p 139 -m netbios` ![](image/netbios.png) diff --git a/README_EN.md b/README_EN.md index d91849c..f931296 100644 --- a/README_EN.md +++ b/README_EN.md @@ -214,6 +214,8 @@ yay -S fscan-git `fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080` ![](image/2020-12-12-13-34-44.png) +`fscan.exe -h 192.168.x.x -socks5 socks5://user:pass@127.0.0.1:1080` (SOCKS5 proxy with authentication) + `fscan.exe -h 192.168.x.x -p 139 -m netbios` ![](image/netbios.png) diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index 5680f90..e9c7e88 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -71,7 +71,7 @@ flag_max_redirects: flag_http_proxy: other: "HTTP proxy" flag_socks5_proxy: - other: "Use SOCKS5 proxy (e.g.: 127.0.0.1:1080)" + other: "Use SOCKS5 proxy (e.g.: 127.0.0.1:1080 or socks5://user:pass@127.0.0.1:1080)" flag_iface: other: "Specify local interface IP address (VPN scenario, e.g.: 10.8.0.5)" flag_poc_path: diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index e47d9cd..342f6b0 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -71,7 +71,7 @@ flag_max_redirects: flag_http_proxy: other: "HTTP代理" flag_socks5_proxy: - other: "使用SOCKS5代理 (如: 127.0.0.1:1080)" + other: "使用SOCKS5代理 (如: 127.0.0.1:1080 或 socks5://user:pass@127.0.0.1:1080)" flag_iface: other: "指定本地网卡IP地址 (VPN场景,如: 10.8.0.5)" flag_poc_path: diff --git a/common/proxy/manager.go b/common/proxy/manager.go index 5e1c24a..2080f2b 100644 --- a/common/proxy/manager.go +++ b/common/proxy/manager.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net" - "net/url" "sync" "sync/atomic" "time" @@ -156,6 +155,9 @@ func (m *manager) createDirectDialer() Dialer { func (m *manager) createSOCKS5Dialer() (Dialer, error) { // 检查缓存 cacheKey := fmt.Sprintf(CacheKeySOCKS5, m.config.Address) + if m.config.Username != "" || m.config.Password != "" { + cacheKey = fmt.Sprintf("%s_%s_%s", cacheKey, m.config.Username, m.config.Password) + } m.cacheMu.RLock() if time.Now().Before(m.cacheExpiry) { if cached, exists := m.dialerCache[cacheKey]; exists { @@ -165,18 +167,6 @@ func (m *manager) createSOCKS5Dialer() (Dialer, error) { } m.cacheMu.RUnlock() - // 解析代理地址 - proxyURL := fmt.Sprintf(SOCKS5URLFormat, m.config.Address) - if m.config.Username != "" { - proxyURL = fmt.Sprintf(SOCKS5URLAuthFormat, - m.config.Username, m.config.Password, m.config.Address) - } - - u, err := url.Parse(proxyURL) - if err != nil { - return nil, NewProxyError(ErrTypeConfig, ErrMsgSOCKS5ParseFailed, ErrCodeSOCKS5ParseFailed, err) - } - // 创建基础拨号器 baseDial := &net.Dialer{ Timeout: m.config.Timeout, @@ -185,16 +175,14 @@ func (m *manager) createSOCKS5Dialer() (Dialer, error) { // 创建SOCKS5拨号器 var auth *proxy.Auth - if u.User != nil { + if m.config.Username != "" || m.config.Password != "" { auth = &proxy.Auth{ - User: u.User.Username(), - } - if password, hasPassword := u.User.Password(); hasPassword { - auth.Password = password + User: m.config.Username, + Password: m.config.Password, } } - socksDialer, err := proxy.SOCKS5(NetworkTCP, u.Host, auth, baseDial) + socksDialer, err := proxy.SOCKS5(NetworkTCP, m.config.Address, auth, baseDial) if err != nil { return nil, NewProxyError(ErrTypeConnection, ErrMsgSOCKS5CreateFailed, ErrCodeSOCKS5CreateFailed, err) } diff --git a/common/proxy/manager_test.go b/common/proxy/manager_test.go index 4935d94..3ba41e8 100644 --- a/common/proxy/manager_test.go +++ b/common/proxy/manager_test.go @@ -1,6 +1,9 @@ package proxy import ( + "fmt" + "io" + "net" "testing" "time" ) @@ -242,6 +245,73 @@ func TestGetDialer_HTTPS(t *testing.T) { t.Logf("✓ GetDialer 返回HTTPS代理拨号器") } +func TestGetDialer_SOCKS5AuthSpecialChars(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen failed: %v", err) + } + defer ln.Close() + + type credential struct { + user string + pass string + } + authCh := make(chan credential, 1) + errCh := make(chan error, 1) + + go func() { + conn, err := ln.Accept() + if err != nil { + errCh <- err + return + } + defer conn.Close() + + user, pass, err := handleTestSOCKS5Auth(conn) + if err != nil { + errCh <- err + return + } + authCh <- credential{user: user, pass: pass} + }() + + origProbed := IsProxyProbed() + SetProxyProbed(true) + defer SetProxyProbed(origProbed) + + config := &ProxyConfig{ + Type: ProxyTypeSOCKS5, + Address: ln.Addr().String(), + Username: "user", + Password: "p@ss:word#1", + Timeout: time.Second, + } + manager := NewProxyManager(config) + dialer, err := manager.GetDialer() + if err != nil { + t.Fatalf("GetDialer failed: %v", err) + } + + conn, err := dialer.Dial("tcp", "127.0.0.1:80") + if err != nil { + t.Fatalf("SOCKS5 dial failed: %v", err) + } + _ = conn.Close() + + select { + case got := <-authCh: + if got.user != config.Username || got.pass != config.Password { + t.Fatalf("auth = %q/%q, want %q/%q", got.user, got.pass, config.Username, config.Password) + } + case err := <-errCh: + t.Fatalf("SOCKS5 test server failed: %v", err) + case <-time.After(time.Second): + t.Fatal("timed out waiting for SOCKS5 auth") + } + + t.Logf("✓ SOCKS5认证支持特殊字符密码") +} + // ============================================================================= // GetTLSDialer - TLS拨号器获取测试 // ============================================================================= @@ -559,3 +629,89 @@ func TestDirectDialer_LocalAddr_Loopback(t *testing.T) { t.Logf("✓ LocalAddr 绑定正常工作(连接失败是预期的): %v", err) } } + +func handleTestSOCKS5Auth(conn net.Conn) (string, string, error) { + header := make([]byte, 2) + if _, err := io.ReadFull(conn, header); err != nil { + return "", "", err + } + if header[0] != 0x05 { + return "", "", fmt.Errorf("unexpected socks version: %d", header[0]) + } + methods := make([]byte, int(header[1])) + if _, err := io.ReadFull(conn, methods); err != nil { + return "", "", err + } + hasAuth := false + for _, method := range methods { + if method == 0x02 { + hasAuth = true + break + } + } + if !hasAuth { + return "", "", fmt.Errorf("client did not offer username/password auth") + } + if _, err := conn.Write([]byte{0x05, 0x02}); err != nil { + return "", "", err + } + + authHeader := make([]byte, 2) + if _, err := io.ReadFull(conn, authHeader); err != nil { + return "", "", err + } + if authHeader[0] != 0x01 { + return "", "", fmt.Errorf("unexpected auth version: %d", authHeader[0]) + } + userBytes := make([]byte, int(authHeader[1])) + if _, err := io.ReadFull(conn, userBytes); err != nil { + return "", "", err + } + passLen := make([]byte, 1) + if _, err := io.ReadFull(conn, passLen); err != nil { + return "", "", err + } + passBytes := make([]byte, int(passLen[0])) + if _, err := io.ReadFull(conn, passBytes); err != nil { + return "", "", err + } + if _, err := conn.Write([]byte{0x01, 0x00}); err != nil { + return "", "", err + } + + reqHeader := make([]byte, 4) + if _, err := io.ReadFull(conn, reqHeader); err != nil { + return "", "", err + } + if reqHeader[0] != 0x05 || reqHeader[1] != 0x01 { + return "", "", fmt.Errorf("unexpected request header: %v", reqHeader) + } + if err := discardSOCKS5Address(conn, reqHeader[3]); err != nil { + return "", "", err + } + if _, err := conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil { + return "", "", err + } + + return string(userBytes), string(passBytes), nil +} + +func discardSOCKS5Address(conn net.Conn, atyp byte) error { + switch atyp { + case 0x01: + _, err := io.CopyN(io.Discard, conn, 6) + return err + case 0x03: + length := make([]byte, 1) + if _, err := io.ReadFull(conn, length); err != nil { + return err + } + _, err := io.CopyN(io.Discard, conn, int64(length[0])+2) + return err + case 0x04: + _, err := io.CopyN(io.Discard, conn, 18) + return err + default: + return fmt.Errorf("unsupported atyp: %d", atyp) + } +}