Skip to content

Commit c0435b8

Browse files
authored
Merge pull request #148 from SenseUnit/socks
SOCKS5 server
2 parents 761f00a + 3bc3deb commit c0435b8

File tree

11 files changed

+272
-52
lines changed

11 files changed

+272
-52
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ dumbproxy
33

44
[![dumbproxy](https://snapcraft.io//dumbproxy/badge.svg)](https://snapcraft.io/dumbproxy)
55

6-
Simple, scriptable, secure forward proxy.
6+
Simple, scriptable, secure HTTP/SOCKS5 forward proxy.
77

88
## Features
99

10+
* Multiple protocol support: both HTTP and SOCKS5 are supported
1011
* Cross-platform (Windows/Mac OS/Linux/Android (via shell)/\*BSD)
1112
* Deployment with a single self-contained binary
1213
* Zero-configuration
13-
* Supports CONNECT method and forwarding of HTTPS connections
14+
* Seamless forwarding of all kinds of TCP connections in addition to regular web-traffic forwarding
1415
* Supports `Basic` proxy authentication
1516
* Via auto-reloaded NCSA httpd-style passwords file
1617
* Via static login and password
@@ -527,6 +528,8 @@ Usage of /home/user/go/bin/dumbproxy:
527528
maximum TLS version accepted by server (default TLS13)
528529
-min-tls-version value
529530
minimum TLS version accepted by server (default TLS12)
531+
-mode value
532+
proxy operation mode (http/socks5) (default http)
530533
-passwd string
531534
update given htpasswd file and add/set password for username. Username and password can be passed as positional arguments or requested interactively
532535
-passwd-cost int

auth/basic.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ func (auth *BasicAuth) reloadLoop(interval time.Duration) {
115115
}
116116
}
117117

118+
func (auth *BasicAuth) Valid(user, password, userAddr string) bool {
119+
pwFile := auth.pw.Load().file
120+
return pwFile.Match(user, password)
121+
}
122+
118123
func (auth *BasicAuth) Validate(_ context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
119124
hdr := req.Header.Get("Proxy-Authorization")
120125
if hdr == "" {

auth/hmac.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ func VerifyHMACLoginAndPassword(secret []byte, login, password string) bool {
8484
return hmac.Equal(token.Signature[:], expectedMAC)
8585
}
8686

87+
func (auth *HMACAuth) Valid(user, password, userAddr string) bool {
88+
return VerifyHMACLoginAndPassword(auth.secret, user, password)
89+
}
90+
8791
func (auth *HMACAuth) Validate(_ context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
8892
hdr := req.Header.Get("Proxy-Authorization")
8993
if hdr == "" {

auth/redis.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/url"
88
"strconv"
99
"strings"
10+
"time"
1011

1112
clog "github.com/SenseUnit/dumbproxy/log"
1213

@@ -46,6 +47,23 @@ func NewRedisAuth(param_url *url.URL, cluster bool, logger *clog.CondLogger) (*R
4647
return auth, nil
4748
}
4849

50+
func (auth *RedisAuth) Valid(user, password, userAddr string) bool {
51+
ctx, cl := context.WithTimeout(context.Background(), 10*time.Second)
52+
defer cl()
53+
encodedPasswd, err := auth.r.Get(ctx, auth.keyPrefix+user).Result()
54+
if err != nil {
55+
auth.logger.Debug("error fetching key %q from Redis: %v", auth.keyPrefix+user, err)
56+
return false
57+
}
58+
matcher, err := makePasswdMatcher(encodedPasswd)
59+
if err != nil {
60+
auth.logger.Debug("can't create password matcher from Redis key %q: %v", auth.keyPrefix+user, err)
61+
return false
62+
}
63+
64+
return matcher.MatchesPassword(password)
65+
}
66+
4967
func (auth *RedisAuth) Validate(ctx context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
5068
hdr := req.Header.Get("Proxy-Authorization")
5169
if hdr == "" {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/redis/go-redis/v9 v9.13.0
1616
github.com/refraction-networking/utls v1.8.0
1717
github.com/tg123/go-htpasswd v1.2.4
18+
github.com/things-go/go-socks5 v0.1.0
1819
github.com/zeebo/xxh3 v1.0.2
1920
golang.org/x/crypto v0.41.0
2021
golang.org/x/crypto/x509roots/fallback v0.0.0-20250826074233-8f580defa01d

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ github.com/redis/go-redis/v9 v9.13.0 h1:PpmlVykE0ODh8P43U0HqC+2NXHXwG+GUtQyz+MPK
5151
github.com/redis/go-redis/v9 v9.13.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
5252
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
5353
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
54-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
55-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
54+
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
55+
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
5656
github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=
5757
github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0=
58+
github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg=
59+
github.com/things-go/go-socks5 v0.1.0/go.mod h1:Riabiyu52kLsla0YmJqunt1c1JEl6iXSr4bRd7swFEA=
5860
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
5961
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
6062
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=

handler/adapter.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,30 @@ func (w wrappedH1RespWriter) Close() error {
134134
}
135135

136136
var _ io.ReadWriteCloser = wrappedH1RespWriter{}
137+
138+
type wrappedSOCKS struct {
139+
r io.Reader
140+
w io.Writer
141+
}
142+
143+
func wrapSOCKS(r io.Reader, w io.Writer) wrappedSOCKS {
144+
return wrappedSOCKS{
145+
r: r,
146+
w: w,
147+
}
148+
}
149+
150+
func (w wrappedSOCKS) Read(p []byte) (n int, err error) {
151+
return w.r.Read(p)
152+
}
153+
154+
func (w wrappedSOCKS) Write(p []byte) (n int, err error) {
155+
return w.w.Write(p)
156+
}
157+
158+
func (w wrappedSOCKS) Close() error {
159+
// can't really close SOCKS reader or writer
160+
return nil
161+
}
162+
163+
var _ io.ReadWriteCloser = wrappedSOCKS{}

handler/handler.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ type HandlerDialer interface {
2828
DialContext(ctx context.Context, net, address string) (net.Conn, error)
2929
}
3030

31+
type ForwardFunc = func(ctx context.Context, username string, incoming, outgoing io.ReadWriteCloser) error
32+
3133
type ProxyHandler struct {
3234
auth auth.Auth
3335
logger *clog.CondLogger
3436
dialer HandlerDialer
35-
forward func(ctx context.Context, username string, incoming, outgoing io.ReadWriteCloser) error
37+
forward ForwardFunc
3638
httptransport http.RoundTripper
3739
outbound map[string]string
3840
outboundMux sync.RWMutex

handler/socks.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"strings"
8+
"sync"
9+
10+
ddto "github.com/SenseUnit/dumbproxy/dialer/dto"
11+
clog "github.com/SenseUnit/dumbproxy/log"
12+
"github.com/things-go/go-socks5"
13+
"github.com/things-go/go-socks5/statute"
14+
)
15+
16+
func SOCKSHandler(dialer HandlerDialer, logger *clog.CondLogger, forward ForwardFunc) func(ctx context.Context, writer io.Writer, request *socks5.Request) error {
17+
var (
18+
outboundMux sync.RWMutex
19+
)
20+
outbound := make(map[string]string)
21+
isLoopback := func(addr string) (string, bool) {
22+
outboundMux.RLock()
23+
defer outboundMux.RUnlock()
24+
originator, ok := outbound[addr]
25+
return originator, ok
26+
}
27+
return func(ctx context.Context, writer io.Writer, request *socks5.Request) error {
28+
if originator, isLoopback := isLoopback(request.RemoteAddr.String()); isLoopback {
29+
logger.Critical("Loopback tunnel detected: %s is an outbound "+
30+
"address for another request from %s", request.RemoteAddr.String(), originator)
31+
socks5.SendReply(writer, statute.RepConnectionRefused, nil)
32+
return fmt.Errorf("Loopback tunnel detected: %s is an outbound "+
33+
"address for another request from %s", request.RemoteAddr.String(), originator)
34+
}
35+
username := ""
36+
if request.AuthContext != nil {
37+
username = request.AuthContext.Payload["username"]
38+
}
39+
localAddr := request.LocalAddr.String()
40+
ctx = ddto.BoundDialerParamsToContext(ctx, nil, trimAddrPort(localAddr))
41+
ctx = ddto.FilterParamsToContext(ctx, nil, username)
42+
logger.Info("Request: %v => %v %q %v %v %v", request.RemoteAddr, localAddr, username, "SOCKS5", "CONNECT", request.DestAddr)
43+
target, err := dialer.DialContext(ctx, "tcp", request.DestAddr.String())
44+
if err != nil {
45+
msg := err.Error()
46+
resp := statute.RepHostUnreachable
47+
if strings.Contains(msg, "refused") {
48+
resp = statute.RepConnectionRefused
49+
} else if strings.Contains(msg, "network is unreachable") {
50+
resp = statute.RepNetworkUnreachable
51+
}
52+
if err := socks5.SendReply(writer, resp, nil); err != nil {
53+
return fmt.Errorf("failed to send reply: %w", err)
54+
}
55+
return fmt.Errorf("connect to %v failed: %w", request.RawDestAddr, err)
56+
}
57+
outboundMux.Lock()
58+
outbound[target.LocalAddr().String()] = request.RemoteAddr.String()
59+
outboundMux.Unlock()
60+
defer func() {
61+
target.Close() // nolint: errcheck
62+
outboundMux.Lock()
63+
delete(outbound, localAddr)
64+
outboundMux.Unlock()
65+
}()
66+
67+
// Send success
68+
if err := socks5.SendReply(writer, statute.RepSuccess, target.LocalAddr()); err != nil {
69+
return fmt.Errorf("failed to send reply, %v", err)
70+
}
71+
72+
return forward(ctx, username, wrapSOCKS(request.Reader, writer), target)
73+
}
74+
}

jsext/dto.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ type JSRequestInfo struct {
2424
}
2525

2626
func JSRequestInfoFromRequest(req *http.Request) *JSRequestInfo {
27+
if req == nil {
28+
return nil
29+
}
2730
return &JSRequestInfo{
2831
Method: req.Method,
2932
URL: req.URL.String(),

0 commit comments

Comments
 (0)