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