blob: dfd32c423aadec08010966115d891dee3e75d76c [file] [log] [blame]
Serge Bazanskife7134b2022-04-01 15:46:29 +02001// package socksproxy implements a limited subset of the SOCKS 5 (RFC1928)
2// protocol in the form of a pluggable Proxy object. However, this
3// implementation is _not_ RFC1928 compliant, as it does not implement GSSAPI
4// (which is mandated by the spec). It currently only implements CONNECT
5// requests to IPv4/IPv6 addresses. It also doesn't implement any
6// timeout/keepalive system for killing inactive connections.
7//
8// The intended use of the library is internally within Metropolis development
9// environments for contacting test clusters. The code is simple and robust, but
10// not really productionized (as noted above - no timeouts and no authentication
11// make it a bad idea to ever expose this proxy server publicly).
12//
13// There are multiple other, existing Go SOCKS4/5 server implementations, but
14// many of them are either not context aware, part of a larger project (and thus
15// difficult to extract) or are brand new/untested/bleeding edge code.
16package socksproxy
17
18import (
19 "context"
20 "fmt"
21 "io"
22 "log"
23 "net"
24 "strconv"
25)
26
27// Handler should be implemented by socksproxy users to allow SOCKS connections
28// to be proxied in any other way than via the HostHandler.
29type Handler interface {
30 // Connect is called by the server any time a SOCKS client sends a CONNECT
31 // request. The function should return a ConnectResponse describing some
32 // 'backend' connection, ie. the connection that will then be exposed to the
33 // SOCKS client.
34 //
35 // Connect should return with Error set to a non-default value to abort/deny the
36 // connection request.
37 //
38 // The underlying incoming socket is managed by the proxy server and is not
39 // visible to the client. However, any sockets/connections/files opened by the
40 // Handler should be cleaned up by tying them to the given context, which will
41 // be canceled whenever the connection is closed.
42 Connect(context.Context, *ConnectRequest) *ConnectResponse
43}
44
45// ConnectRequest represents a pending CONNECT request from a client.
46type ConnectRequest struct {
47 // Address is an IPv4 or IPv6 address that the client requested to connect to.
48 // This address might be invalid/malformed/internal, and the Connect method
49 // should sanitize it before using it.
50 Address net.IP
51 // Port is the TCP port number that the client requested to connect to.
52 Port uint16
53}
54
55// ConnectResponse indicates a 'backend' connection that the proxy should expose
56// to the client, or an error if the connection cannot be made.
57type ConnectResponse struct {
58 // Error will cause an error to be returned if it is anything else than the
59 // default value (ReplySucceeded).
60 Error Reply
61
Serge Bazanski1ec1fe92023-03-22 18:29:28 +010062 // Backend is the ReadWriteCloser that will be bridged over to the connecting
63 // client if no Error is set.
64 Backend io.ReadWriteCloser
Serge Bazanskife7134b2022-04-01 15:46:29 +020065 // LocalAddress is the IP address that is returned to the client as the local
66 // address of the newly established backend connection.
67 LocalAddress net.IP
68 // LocalPort is the local TCP port number that is returned to the client as the
69 // local port of the newly established backend connection.
70 LocalPort uint16
71}
72
73// ConnectResponseFromConn builds a ConnectResponse from a net.Conn. This can be
74// used by custom Handlers to easily return a ConnectResponse for a newly
75// established net.Conn, eg. from a Dial call.
76//
77// An error is returned if the given net.Conn does not carry a properly formed
78// LocalAddr.
79func ConnectResponseFromConn(c net.Conn) (*ConnectResponse, error) {
80 laddr := c.LocalAddr().String()
81 host, port, err := net.SplitHostPort(laddr)
82 if err != nil {
83 return nil, fmt.Errorf("could not parse LocalAddr %q: %w", laddr, err)
84 }
85 addr := net.ParseIP(host)
86 if addr == nil {
87 return nil, fmt.Errorf("could not parse LocalAddr host %q as IP", host)
88 }
89 portNum, err := strconv.ParseUint(port, 10, 16)
90 if err != nil {
91 return nil, fmt.Errorf("could not parse LocalAddr port %q", port)
92 }
93 return &ConnectResponse{
94 Backend: c,
95 LocalAddress: addr,
96 LocalPort: uint16(portNum),
97 }, nil
98}
99
100type hostHandler struct{}
101
102func (h *hostHandler) Connect(ctx context.Context, req *ConnectRequest) *ConnectResponse {
103 port := fmt.Sprintf("%d", req.Port)
104 addr := net.JoinHostPort(req.Address.String(), port)
105 s, err := net.Dial("tcp", addr)
106 if err != nil {
107 log.Printf("HostHandler could not dial %q: %v", addr, err)
108 return &ConnectResponse{Error: ReplyConnectionRefused}
109 }
110 go func() {
111 <-ctx.Done()
112 s.Close()
113 }()
114 res, err := ConnectResponseFromConn(s)
115 if err != nil {
116 log.Printf("HostHandler could not build response: %v", err)
117 return &ConnectResponse{Error: ReplyGeneralFailure}
118 }
119 return res
120}
121
122var (
123 // HostHandler is an unsafe SOCKS5 proxy Handler which passes all incoming
124 // connections into the local network stack. The incoming addresses/ports are
125 // not sanitized, and as the proxy does not perform authentication, this handler
126 // is an open proxy. This handler should never be used in cases where the proxy
127 // server is publicly available.
128 HostHandler = &hostHandler{}
129)
130
131// Serve runs a SOCKS5 proxy server for a given Handler at a given listener.
132//
133// When the given context is canceled, the server will stop and the listener
134// will be closed. All pending connections will also be canceled and their
135// sockets closed.
136func Serve(ctx context.Context, handler Handler, lis net.Listener) error {
137 go func() {
138 <-ctx.Done()
139 lis.Close()
140 }()
141
142 for {
143 con, err := lis.Accept()
144 if err != nil {
145 // Context cancellation will close listener socket with a generic 'use of closed
146 // network connection' error, translate that back to context error.
147 if ctx.Err() != nil {
148 return ctx.Err()
149 }
150 return err
151 }
152 go handle(ctx, handler, con)
153 }
154}
155
156// handle runs in a goroutine per incoming SOCKS connection. Its lifecycle
157// corresponds to the lifecycle of a running proxy connection.
158func handle(ctx context.Context, handler Handler, con net.Conn) {
159 // ctxR is a per-request context, and will be canceled whenever the handler
160 // exits or the server is stopped.
161 ctxR, ctxRC := context.WithCancel(ctx)
162 defer ctxRC()
163
164 go func() {
165 <-ctxR.Done()
166 con.Close()
167 }()
168
169 // Perform method negotiation with the client.
170 if err := negotiateMethod(con); err != nil {
171 return
172 }
173
174 // Read request from the client and translate problems into early error replies.
175 req, err := readRequest(con)
176 switch err {
177 case errNotConnect:
178 writeReply(con, ReplyCommandNotSupported, net.IPv4(0, 0, 0, 0), 0)
179 return
180 case errUnsupportedAddressType:
181 writeReply(con, ReplyAddressTypeNotSupported, net.IPv4(0, 0, 0, 0), 0)
182 return
183 case nil:
184 default:
185 writeReply(con, ReplyGeneralFailure, net.IPv4(0, 0, 0, 0), 0)
186 return
187 }
188
189 // Ask handler.Connect for a backend.
190 conRes := handler.Connect(ctxR, &ConnectRequest{
191 Address: req.address,
192 Port: req.port,
193 })
194 // Handle programming error when returned value is nil.
195 if conRes == nil {
196 writeReply(con, ReplyGeneralFailure, net.IPv4(0, 0, 0, 0), 0)
197 return
198 }
199 // Handle returned errors.
200 if conRes.Error != ReplySucceeded {
201 writeReply(con, conRes.Error, net.IPv4(0, 0, 0, 0), 0)
202 return
203 }
204
205 // Ensure Bound.* fields are set.
206 if conRes.Backend == nil || conRes.LocalAddress == nil || conRes.LocalPort == 0 {
207 writeReply(con, ReplyGeneralFailure, net.IPv4(0, 0, 0, 0), 0)
208 return
209 }
210 // Send reply.
211 if err := writeReply(con, ReplySucceeded, conRes.LocalAddress, conRes.LocalPort); err != nil {
212 return
213 }
214
215 // Pipe returned backend into connection.
Serge Bazanski1ec1fe92023-03-22 18:29:28 +0100216 go func() {
217 io.Copy(conRes.Backend, con)
218 conRes.Backend.Close()
219 }()
Serge Bazanskife7134b2022-04-01 15:46:29 +0200220 io.Copy(con, conRes.Backend)
Serge Bazanski1ec1fe92023-03-22 18:29:28 +0100221 conRes.Backend.Close()
Serge Bazanskife7134b2022-04-01 15:46:29 +0200222}