socks.go

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

Source Files