Skip to content

Commit b8e2fc0

Browse files
committed
socks5: add optional username + password auth
1 parent 7de9f8a commit b8e2fc0

6 files changed

Lines changed: 194 additions & 46 deletions

File tree

application_test.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,56 @@ func TestSOCKS5ProxyFallbackToOldProtocol(t *testing.T) {
425425
testSOCKS5Proxy(ts, peer1.app.Conf.SOCKS5.ListenAddress, "")
426426
}
427427

428+
func TestSOCKS5ProxyWithLocalAuth(t *testing.T) {
429+
ts := NewTestSuite(t)
430+
431+
peer1 := ts.NewTestPeerWithConfig(func(c *config.Config) {
432+
c.SOCKS5 = config.SOCKS5Config{
433+
ListenerEnabled: true,
434+
ProxyingEnabled: true,
435+
ListenAddress: pickFreeAddr(ts.t),
436+
Username: "testuser",
437+
Password: "testpass",
438+
}
439+
})
440+
peer2 := ts.NewTestPeer(false)
441+
442+
ts.makeFriends(peer2, peer1)
443+
444+
// Allow peer1 to use peer2 as exit node
445+
peer1Config, err := peer2.api.KnownPeerConfig(peer1.PeerID())
446+
ts.NoError(err)
447+
448+
err = peer2.api.UpdatePeerSettings(entity.UpdatePeerSettingsRequest{
449+
PeerID: peer1.PeerID(),
450+
Alias: peer1Config.Alias,
451+
DomainName: peer1Config.DomainName,
452+
IPAddr: peer1Config.IPAddr,
453+
AllowUsingAsExitNode: true,
454+
})
455+
ts.NoError(err)
456+
457+
ts.Eventually(func() bool {
458+
peer2Config, err := peer1.api.KnownPeerConfig(peer2.PeerID())
459+
ts.NoError(err)
460+
return peer2Config.AllowedUsingAsExitNode
461+
}, 15*time.Second, 100*time.Millisecond)
462+
463+
peer1.app.SOCKS5.SetProxyPeerID(peer2.PeerID())
464+
peer2.app.SOCKS5.SetProxyingLocalhostEnabled(true)
465+
466+
proxyAddr := peer1.app.Conf.SOCKS5.ListenAddress
467+
468+
// Correct credentials — should succeed
469+
testSOCKS5ProxyWithAuth(ts, proxyAddr, &proxy.Auth{User: "testuser", Password: "testpass"}, 1, "")
470+
471+
// Wrong password — should fail with auth error
472+
testSOCKS5ProxyWithAuth(ts, proxyAddr, &proxy.Auth{User: "testuser", Password: "wrong"}, 1, "username/password authentication failed")
473+
474+
// No credentials — should fail (server requires user/pass, client offers no auth only)
475+
testSOCKS5ProxyWithAuth(ts, proxyAddr, nil, 1, "no acceptable authentication methods")
476+
}
477+
428478
func TestUpdatePeerSettingsIPAddr(t *testing.T) {
429479
ts := NewTestSuite(t)
430480

@@ -782,6 +832,10 @@ func TestDisableVPNInterface(t *testing.T) {
782832
}
783833

784834
func testSOCKS5Proxy(ts *TestSuite, proxyAddr string, expectSocksErr string) {
835+
testSOCKS5ProxyWithAuth(ts, proxyAddr, nil, 20, expectSocksErr)
836+
}
837+
838+
func testSOCKS5ProxyWithAuth(ts *TestSuite, proxyAddr string, auth *proxy.Auth, iterations int, expectSocksErr string) {
785839
// setup mock server
786840
expectedBody := strings.Repeat("test text", 10_000)
787841
addr := pickFreeAddr(ts.t)
@@ -799,13 +853,13 @@ func testSOCKS5Proxy(ts *TestSuite, proxyAddr string, expectSocksErr string) {
799853
}()
800854

801855
// client
802-
dialer, err := proxy.SOCKS5("tcp", proxyAddr, nil, nil)
856+
dialer, err := proxy.SOCKS5("tcp", proxyAddr, auth, nil)
803857
ts.NoError(err)
804858
httpTransport := &http.Transport{DialContext: dialer.(proxy.ContextDialer).DialContext}
805859
httpClient := http.Client{Transport: httpTransport}
806860

807861
// test
808-
for range 20 {
862+
for range iterations {
809863
response, err := httpClient.Get(fmt.Sprintf("http://%s/test", addr))
810864
if expectSocksErr != "" {
811865
ts.Error(err)
@@ -816,7 +870,7 @@ func testSOCKS5Proxy(ts *TestSuite, proxyAddr string, expectSocksErr string) {
816870
ts.ErrorAs(urlErr.Err, &netErr)
817871

818872
ts.Equal("socks connect", netErr.Op)
819-
ts.EqualError(netErr.Err, expectSocksErr)
873+
ts.Contains(netErr.Err.Error(), expectSocksErr)
820874

821875
continue
822876
}

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ type (
8484
ListenAddress string `json:"listenAddress"`
8585
// peer that is set as proxy
8686
UsingPeerID string `json:"usingPeerID"`
87+
// Optional local auth credentials. If both are set, SOCKS5 clients must authenticate.
88+
Username string `json:"username"`
89+
Password string `json:"password"`
8790
}
8891
DNSConfig struct {
8992
DisableDNS bool `json:"disableDNS"`

service/socks5.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func NewSOCKS5(p2pService P2p, conf *config.Config) (*SOCKS5, error) {
3535
var client *socks5.Client
3636
if conf.SOCKS5.ListenerEnabled {
3737
var err error
38-
client, err = socks5.NewClient(conf.SOCKS5.ListenAddress)
38+
client, err = socks5.NewClient(conf.SOCKS5.ListenAddress, conf.SOCKS5.Username, conf.SOCKS5.Password)
3939
if err != nil {
4040
return nil, fmt.Errorf("failed to start socks5 listener: %v", err)
4141
}

socks5/client.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import (
1111
)
1212

1313
type Client struct {
14-
listener net.Listener
15-
connsCh chan net.Conn
16-
logger *log.ZapEventLogger
14+
listener net.Listener
15+
connsCh chan net.Conn
16+
logger *log.ZapEventLogger
17+
authenticator socks5Lib.Authenticator
1718
}
1819

19-
func NewClient(listenAddr string) (*Client, error) {
20+
func NewClient(listenAddr string, username, password string) (*Client, error) {
2021
// TODO: add support for udp?
2122
listener, err := net.Listen("tcp", listenAddr)
2223
if err != nil {
@@ -25,10 +26,20 @@ func NewClient(listenAddr string) (*Client, error) {
2526

2627
logger := log.Logger("socks5/client")
2728

29+
var authenticator socks5Lib.Authenticator
30+
if username != "" && password != "" {
31+
authenticator = socks5Lib.UserPassAuthenticator{
32+
Credentials: socks5Lib.StaticCredentials{username: password},
33+
}
34+
} else {
35+
authenticator = socks5Lib.NoAuthAuthenticator{}
36+
}
37+
2838
cli := Client{
29-
listener: listener,
30-
connsCh: make(chan net.Conn, 1),
31-
logger: logger,
39+
listener: listener,
40+
connsCh: make(chan net.Conn, 1),
41+
logger: logger,
42+
authenticator: authenticator,
3243
}
3344
go func() {
3445
serveErr := cli.serve()
@@ -48,8 +59,8 @@ func (c *Client) ConnsChan() <-chan net.Conn {
4859
return c.connsCh
4960
}
5061

51-
// HandleLocalAuth performs the SOCKS5 auth negotiation locally, responding with NoAuth.
52-
// This avoids sending the auth handshake over the network to the remote peer.
62+
// HandleLocalAuth performs the SOCKS5 auth negotiation locally.
63+
// It reads the version byte and offered methods, then delegates to the configured authenticator.
5364
func (c *Client) HandleLocalAuth(conn net.Conn) error {
5465
// Read version byte
5566
version := []byte{0}
@@ -66,21 +77,22 @@ func (c *Client) HandleLocalAuth(conn net.Conn) error {
6677
return fmt.Errorf("failed to read auth methods: %w", err)
6778
}
6879

69-
// Check NoAuth is offered
70-
hasNoAuth := false
80+
// Check if the client offers our required method
81+
requiredMethod := c.authenticator.GetCode()
82+
hasMethod := false
7183
for _, m := range methods {
72-
if m == socks5Lib.AuthMethodNoAuth {
73-
hasNoAuth = true
84+
if m == requiredMethod {
85+
hasMethod = true
7486
break
7587
}
7688
}
77-
if !hasNoAuth {
89+
if !hasMethod {
7890
_, _ = conn.Write([]byte{0x05, socks5Lib.AuthMethodNoAcceptable})
79-
return fmt.Errorf("client does not support NoAuth method")
91+
return fmt.Errorf("client does not support required auth method %d", requiredMethod)
8092
}
8193

82-
// Respond: NoAuth selected
83-
_, err = conn.Write([]byte{0x05, socks5Lib.AuthMethodNoAuth})
94+
// Delegate to the authenticator (writes method selection + handles subnegotiation)
95+
_, err = c.authenticator.Authenticate(conn, conn)
8496
return err
8597
}
8698

socks5/server.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ func NewServer() *Server {
3131
Rules: rule,
3232
Logger: NewLogger(),
3333
Resolver: nil,
34-
// TODO: add optional password authentication method support
3534
}
3635
server, err := socks5.New(conf)
3736
if err != nil {

socks5/server_test.go

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,55 +20,102 @@ func TestProxy(t *testing.T) {
2020
listenAddr := pickFreeAddr(t)
2121
socksServer := NewServer()
2222
socksServer.SetRules(NewRulePermitAll())
23-
socksClient, err := NewClient(listenAddr)
23+
socksClient, err := NewClient(listenAddr, "", "")
2424
require.NoError(t, err)
2525

2626
wg := &sync.WaitGroup{}
2727
wg.Add(1)
2828
go func() {
2929
defer wg.Done()
30-
3130
conn := <-socksClient.ConnsChan()
3231
socksServer.ServeConn(conn)
3332
}()
3433

35-
upstreamAddr := pickFreeAddr(t)
36-
mux := http.NewServeMux()
37-
mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
38-
_, _ = fmt.Fprintf(w, "test text")
39-
})
40-
//nolint
41-
httpServer := &http.Server{Addr: upstreamAddr, Handler: mux}
34+
upstreamAddr := startUpstreamServer(t)
35+
httpClient, transport := newSOCKS5HttpClient(listenAddr, nil)
36+
37+
response, err := httpClient.Get(fmt.Sprintf("http://%s/test", upstreamAddr))
38+
require.NoError(t, err)
39+
body, err := io.ReadAll(response.Body)
40+
require.NoError(t, err)
41+
err = response.Body.Close()
42+
require.NoError(t, err)
43+
44+
require.Equal(t, "test text", string(body))
45+
46+
transport.CloseIdleConnections()
47+
wg.Wait()
48+
}
49+
50+
func TestProxyWithAuth(t *testing.T) {
51+
listenAddr := pickFreeAddr(t)
52+
socksServer := NewServer()
53+
socksServer.SetRules(NewRulePermitAll())
54+
socksClient, err := NewClient(listenAddr, "testuser", "testpass")
55+
require.NoError(t, err)
56+
57+
wg := &sync.WaitGroup{}
58+
wg.Add(1)
4259
go func() {
43-
_ = httpServer.ListenAndServe()
44-
}()
45-
defer func() {
46-
httpServer.Shutdown(context.Background())
60+
defer wg.Done()
61+
conn := <-socksClient.ConnsChan()
62+
socksServer.ServeConn(conn)
4763
}()
4864

49-
httpTransport := &http.Transport{
50-
Proxy: func(*http.Request) (*url.URL, error) {
51-
return &url.URL{
52-
Scheme: "socks5",
53-
Host: listenAddr,
54-
}, nil
55-
},
56-
}
57-
httpClient := http.Client{Transport: httpTransport}
65+
upstreamAddr := startUpstreamServer(t)
66+
httpClient, transport := newSOCKS5HttpClient(listenAddr, url.UserPassword("testuser", "testpass"))
5867

5968
response, err := httpClient.Get(fmt.Sprintf("http://%s/test", upstreamAddr))
6069
require.NoError(t, err)
6170
body, err := io.ReadAll(response.Body)
6271
require.NoError(t, err)
6372
err = response.Body.Close()
6473
require.NoError(t, err)
65-
6674
require.Equal(t, "test text", string(body))
6775

68-
httpTransport.CloseIdleConnections()
76+
transport.CloseIdleConnections()
6977
wg.Wait()
7078
}
7179

80+
func TestProxyWithAuthRejection(t *testing.T) {
81+
tests := []struct {
82+
name string
83+
userinfo *url.Userinfo
84+
}{
85+
{"WrongPassword", url.UserPassword("testuser", "wrongpass")},
86+
{"NoCredentials", nil},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
listenAddr := pickFreeAddr(t)
92+
socksClient, err := NewClient(listenAddr, "testuser", "testpass")
93+
require.NoError(t, err)
94+
95+
wg := &sync.WaitGroup{}
96+
wg.Add(1)
97+
go func() {
98+
defer wg.Done()
99+
conn := <-socksClient.ConnsChan()
100+
_ = socksClient.HandleLocalAuth(conn)
101+
conn.Close()
102+
}()
103+
104+
upstreamAddr := startUpstreamServer(t)
105+
httpClient, transport := newSOCKS5HttpClient(listenAddr, tt.userinfo)
106+
107+
resp, err := httpClient.Get(fmt.Sprintf("http://%s/test", upstreamAddr))
108+
if resp != nil {
109+
resp.Body.Close()
110+
}
111+
require.Error(t, err)
112+
113+
transport.CloseIdleConnections()
114+
wg.Wait()
115+
})
116+
}
117+
}
118+
72119
func pickFreeAddr(t testing.TB) string {
73120
l, err := net.Listen("tcp", "127.0.0.1:0")
74121
if err != nil {
@@ -78,3 +125,36 @@ func pickFreeAddr(t testing.TB) string {
78125

79126
return l.Addr().String()
80127
}
128+
129+
// startUpstreamServer starts an HTTP server that responds with "test text" on /test.
130+
func startUpstreamServer(t testing.TB) string {
131+
addr := pickFreeAddr(t)
132+
mux := http.NewServeMux()
133+
mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
134+
_, _ = fmt.Fprintf(w, "test text")
135+
})
136+
//nolint
137+
httpServer := &http.Server{Addr: addr, Handler: mux}
138+
go func() {
139+
_ = httpServer.ListenAndServe()
140+
}()
141+
t.Cleanup(func() {
142+
httpServer.Shutdown(context.Background())
143+
})
144+
return addr
145+
}
146+
147+
// newSOCKS5HttpClient creates an HTTP client that routes through a SOCKS5 proxy.
148+
// Pass nil userinfo for no auth credentials.
149+
func newSOCKS5HttpClient(proxyAddr string, userinfo *url.Userinfo) (http.Client, *http.Transport) {
150+
transport := &http.Transport{
151+
Proxy: func(*http.Request) (*url.URL, error) {
152+
return &url.URL{
153+
Scheme: "socks5",
154+
User: userinfo,
155+
Host: proxyAddr,
156+
}, nil
157+
},
158+
}
159+
return http.Client{Transport: transport}, transport
160+
}

0 commit comments

Comments
 (0)