socks.go

v0.7.0
Doc Versions Source
1
package proxy
2
3
import (
4
	"errors"
5
	"fmt"
6
	"io"
7
	"log"
8
	"net"
9
	"net/http"
10
	"net/url"
11
	"strconv"
12
)
13
14
// StartSOCKSBridge starts a local HTTP CONNECT proxy that forwards connections
15
// through the given SOCKS5 proxy. This allows clients that only support
16
// HTTP(S) proxies (like Node.js / Claude Code) to route through SOCKS5.
17
func StartSOCKSBridge(socksURL string) (addr string, shutdown func(), err error) {
18
	dialer, err := newSOCKS5Dialer(socksURL)
19
	if err != nil {
20
		return "", nil, fmt.Errorf("parsing SOCKS URL: %w", err)
21
	}
22
23
	ln, err := net.Listen("tcp", "127.0.0.1:0")
24
	if err != nil {
25
		return "", nil, err
26
	}
27
28
	srv := &http.Server{Handler: &connectProxy{dialer: dialer}}
29
	go func() {
30
		if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
31
			log.Printf("socks bridge: %v", err)
32
		}
33
	}()
34
35
	return ln.Addr().String(), func() { srv.Close() }, nil
36
}
37
38
// connectProxy is an HTTP CONNECT proxy that dials through SOCKS5.
39
type connectProxy struct {
40
	dialer *socks5Dialer
41
}
42
43
func (p *connectProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
44
	if r.Method != http.MethodConnect {
45
		http.Error(w, "only CONNECT supported", http.StatusMethodNotAllowed)
46
		return
47
	}
48
49
	upstream, err := p.dialer.Dial("tcp", r.Host)
50
	if err != nil {
51
		http.Error(w, err.Error(), http.StatusBadGateway)
52
		return
53
	}
54
55
	hijacker, ok := w.(http.Hijacker)
56
	if !ok {
57
		upstream.Close()
58
		http.Error(w, "hijacking not supported", http.StatusInternalServerError)
59
		return
60
	}
61
62
	client, _, err := hijacker.Hijack()
63
	if err != nil {
64
		upstream.Close()
65
		return
66
	}
67
68
	client.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
69
70
	go func() {
71
		io.Copy(upstream, client)
72
		upstream.Close()
73
	}()
74
	io.Copy(client, upstream)
75
	client.Close()
76
}
77
78
// socks5Dialer dials TCP connections through a SOCKS5 proxy.
79
type socks5Dialer struct {
80
	addr     string
81
	username string
82
	password string
83
}
84
85
func newSOCKS5Dialer(socksURL string) (*socks5Dialer, error) {
86
	u, err := url.Parse(socksURL)
87
	if err != nil {
88
		return nil, err
89
	}
90
	d := &socks5Dialer{addr: u.Host}
91
	if u.User != nil {
92
		d.username = u.User.Username()
93
		d.password, _ = u.User.Password()
94
	}
95
	return d, nil
96
}
97
98
func (d *socks5Dialer) Dial(_, addr string) (net.Conn, error) {
99
	conn, err := net.Dial("tcp", d.addr)
100
	if err != nil {
101
		return nil, err
102
	}
103
104
	// SOCKS5 greeting.
105
	if d.username != "" {
106
		conn.Write([]byte{0x05, 0x02, 0x00, 0x02}) // no-auth + username/password
107
	} else {
108
		conn.Write([]byte{0x05, 0x01, 0x00}) // no-auth only
109
	}
110
111
	buf := make([]byte, 2)
112
	if _, err := io.ReadFull(conn, buf); err != nil {
113
		conn.Close()
114
		return nil, fmt.Errorf("socks5 greeting: %w", err)
115
	}
116
	if buf[0] != 0x05 {
117
		conn.Close()
118
		return nil, fmt.Errorf("socks5: unexpected version %d", buf[0])
119
	}
120
121
	switch buf[1] {
122
	case 0x00:
123
		// No authentication required.
124
	case 0x02:
125
		// Username/password (RFC 1929).
126
		auth := []byte{0x01, byte(len(d.username))}
127
		auth = append(auth, d.username...)
128
		auth = append(auth, byte(len(d.password)))
129
		auth = append(auth, d.password...)
130
		conn.Write(auth)
131
		resp := make([]byte, 2)
132
		if _, err := io.ReadFull(conn, resp); err != nil {
133
			conn.Close()
134
			return nil, fmt.Errorf("socks5 auth: %w", err)
135
		}
136
		if resp[1] != 0x00 {
137
			conn.Close()
138
			return nil, fmt.Errorf("socks5: authentication failed")
139
		}
140
	default:
141
		conn.Close()
142
		return nil, fmt.Errorf("socks5: unsupported auth method %d", buf[1])
143
	}
144
145
	// CONNECT request.
146
	host, portStr, err := net.SplitHostPort(addr)
147
	if err != nil {
148
		conn.Close()
149
		return nil, err
150
	}
151
	port, err := strconv.Atoi(portStr)
152
	if err != nil {
153
		conn.Close()
154
		return nil, err
155
	}
156
157
	req := []byte{0x05, 0x01, 0x00} // ver, connect, reserved
158
	if ip := net.ParseIP(host); ip != nil {
159
		if ip4 := ip.To4(); ip4 != nil {
160
			req = append(req, 0x01)
161
			req = append(req, ip4...)
162
		} else {
163
			req = append(req, 0x04)
164
			req = append(req, ip...)
165
		}
166
	} else {
167
		req = append(req, 0x03, byte(len(host)))
168
		req = append(req, host...)
169
	}
170
	req = append(req, byte(port>>8), byte(port&0xff))
171
	conn.Write(req)
172
173
	// Read CONNECT response.
174
	resp := make([]byte, 4)
175
	if _, err := io.ReadFull(conn, resp); err != nil {
176
		conn.Close()
177
		return nil, fmt.Errorf("socks5 connect: %w", err)
178
	}
179
	if resp[1] != 0x00 {
180
		conn.Close()
181
		return nil, fmt.Errorf("socks5: connect failed (reply %d)", resp[1])
182
	}
183
184
	// Drain bound address.
185
	switch resp[3] {
186
	case 0x01: // IPv4
187
		_, err = io.ReadFull(conn, make([]byte, 4+2))
188
	case 0x03: // Domain
189
		lb := make([]byte, 1)
190
		if _, err = io.ReadFull(conn, lb); err == nil {
191
			_, err = io.ReadFull(conn, make([]byte, int(lb[0])+2))
192
		}
193
	case 0x04: // IPv6
194
		_, err = io.ReadFull(conn, make([]byte, 16+2))
195
	}
196
	if err != nil {
197
		conn.Close()
198
		return nil, fmt.Errorf("socks5 connect: %w", err)
199
	}
200
201
	return conn, nil
202
}
203

Source Files