● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / client / proxy / socks5.go
← Back to directory
309 lines · 7.3 KB
Raw
1package proxy
2
3import (
4 "context"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "io"
9 "net"
10 "net/url"
11 "strconv"
12 "strings"
13 "time"
14)
15
16const (
17 socks5Version = 0x05
18
19 // Auth methods
20 socks5AuthNone = 0x00
21 socks5AuthUserPass = 0x02
22 socks5AuthNoAccept = 0xFF
23
24 // Command
25 socks5CmdConnect = 0x01
26
27 // Address types
28 socks5AtypIPv4 = 0x01
29 socks5AtypDomain = 0x03
30 socks5AtypIPv6 = 0x04
31
32 // Reply codes
33 socks5RepSuccess = 0x00
34)
35
36var socks5Errors = map[byte]string{
37 0x01: "general SOCKS server failure",
38 0x02: "connection not allowed by ruleset",
39 0x03: "network unreachable",
40 0x04: "host unreachable",
41 0x05: "connection refused",
42 0x06: "TTL expired",
43 0x07: "command not supported",
44 0x08: "address type not supported",
45}
46
47// SOCKS5Dialer implements ProxyDialer for RFC 1928 / RFC 1929 SOCKS5 proxies.
48type SOCKS5Dialer struct {
49 proxyAddr string
50 username string
51 password string
52 timeout time.Duration
53}
54
55// NewSOCKS5Dialer creates a new SOCKS5Dialer from Options.
56func NewSOCKS5Dialer(opts Options) (*SOCKS5Dialer, error) {
57 addr := opts.Mode
58 if strings.Contains(addr, "://") {
59 u, err := url.Parse(addr)
60 if err != nil {
61 return nil, fmt.Errorf("invalid socks5 proxy URL: %w", err)
62 }
63 addr = u.Host
64 if u.User != nil {
65 if opts.Username == "" {
66 opts.Username = u.User.Username()
67 }
68 if opts.Password == "" {
69 opts.Password, _ = u.User.Password()
70 }
71 }
72 }
73
74 if !strings.Contains(addr, ":") {
75 addr = addr + ":1080"
76 }
77
78 timeout := opts.Timeout
79 if timeout <= 0 {
80 timeout = 10 * time.Second
81 }
82
83 return &SOCKS5Dialer{
84 proxyAddr: addr,
85 username: opts.Username,
86 password: opts.Password,
87 timeout: timeout,
88 }, nil
89}
90
91func (s *SOCKS5Dialer) ProxyHop() string {
92 return s.proxyAddr
93}
94
95func (s *SOCKS5Dialer) Mode() string {
96 return "socks5"
97}
98
99func (s *SOCKS5Dialer) DialContext(ctx context.Context, network, targetAddr string) (net.Conn, error) {
100 dialer := &net.Dialer{
101 Timeout: s.timeout,
102 }
103
104 conn, err := dialer.DialContext(ctx, "tcp", s.proxyAddr)
105 if err != nil {
106 return nil, fmt.Errorf("failed to connect to SOCKS5 proxy %s: %w", s.proxyAddr, err)
107 }
108
109 // Apply deadline
110 if deadline, ok := ctx.Deadline(); ok {
111 _ = conn.SetDeadline(deadline)
112 } else if s.timeout > 0 {
113 _ = conn.SetDeadline(time.Now().Add(s.timeout))
114 }
115 defer func() {
116 // Clear deadline after handshake
117 _ = conn.SetDeadline(time.Time{})
118 }()
119
120 // 1. Method negotiation
121 var methods []byte
122 if s.username != "" {
123 methods = []byte{socks5AuthNone, socks5AuthUserPass}
124 } else {
125 methods = []byte{socks5AuthNone}
126 }
127
128 req := make([]byte, 2+len(methods))
129 req[0] = socks5Version
130 req[1] = byte(len(methods))
131 copy(req[2:], methods)
132
133 if _, err := conn.Write(req); err != nil {
134 conn.Close()
135 return nil, fmt.Errorf("failed to send SOCKS5 method request: %w", err)
136 }
137
138 resp := make([]byte, 2)
139 if _, err := io.ReadFull(conn, resp); err != nil {
140 conn.Close()
141 return nil, fmt.Errorf("failed to read SOCKS5 method response: %w", err)
142 }
143
144 if resp[0] != socks5Version {
145 conn.Close()
146 return nil, fmt.Errorf("unsupported SOCKS version %d", resp[0])
147 }
148
149 chosenMethod := resp[1]
150 switch chosenMethod {
151 case socks5AuthNone:
152 // No authentication required
153 case socks5AuthUserPass:
154 if err := s.authenticateUserPass(conn); err != nil {
155 conn.Close()
156 return nil, err
157 }
158 case socks5AuthNoAccept:
159 conn.Close()
160 return nil, errors.New("SOCKS5 proxy rejected authentication methods")
161 default:
162 conn.Close()
163 return nil, fmt.Errorf("unknown SOCKS5 auth method %d chosen by server", chosenMethod)
164 }
165
166 // 2. CONNECT Request
167 host, portStr, err := net.SplitHostPort(targetAddr)
168 if err != nil {
169 conn.Close()
170 return nil, fmt.Errorf("invalid target address %q: %w", targetAddr, err)
171 }
172
173 port, err := strconv.Atoi(portStr)
174 if err != nil || port <= 0 || port > 65535 {
175 conn.Close()
176 return nil, fmt.Errorf("invalid target port %q: %w", portStr, err)
177 }
178
179 var cmd []byte
180 if ip := net.ParseIP(host); ip != nil {
181 if ipv4 := ip.To4(); ipv4 != nil {
182 cmd = make([]byte, 4+4+2)
183 cmd[0] = socks5Version
184 cmd[1] = socks5CmdConnect
185 cmd[2] = 0x00 // Reserved
186 cmd[3] = socks5AtypIPv4
187 copy(cmd[4:8], ipv4)
188 binary.BigEndian.PutUint16(cmd[8:10], uint16(port))
189 } else {
190 ipv6 := ip.To16()
191 cmd = make([]byte, 4+16+2)
192 cmd[0] = socks5Version
193 cmd[1] = socks5CmdConnect
194 cmd[2] = 0x00 // Reserved
195 cmd[3] = socks5AtypIPv6
196 copy(cmd[4:20], ipv6)
197 binary.BigEndian.PutUint16(cmd[20:22], uint16(port))
198 }
199 } else {
200 // ATYP 0x03 Domain name
201 cmd = make([]byte, 4+1+len(host)+2)
202 cmd[0] = socks5Version
203 cmd[1] = socks5CmdConnect
204 cmd[2] = 0x00 // Reserved
205 cmd[3] = socks5AtypDomain
206 cmd[4] = byte(len(host))
207 copy(cmd[5:5+len(host)], []byte(host))
208 binary.BigEndian.PutUint16(cmd[5+len(host):], uint16(port))
209 }
210
211 if _, err := conn.Write(cmd); err != nil {
212 conn.Close()
213 return nil, fmt.Errorf("failed to send SOCKS5 CONNECT command: %w", err)
214 }
215
216 // 3. Read Server Reply
217 replyHdr := make([]byte, 4)
218 if _, err := io.ReadFull(conn, replyHdr); err != nil {
219 conn.Close()
220 return nil, fmt.Errorf("failed to read SOCKS5 reply header: %w", err)
221 }
222
223 if replyHdr[0] != socks5Version {
224 conn.Close()
225 return nil, fmt.Errorf("invalid SOCKS5 reply version %d", replyHdr[0])
226 }
227
228 if replyHdr[1] != socks5RepSuccess {
229 conn.Close()
230 errMsg, ok := socks5Errors[replyHdr[1]]
231 if !ok {
232 errMsg = fmt.Sprintf("error code 0x%02x", replyHdr[1])
233 }
234 return nil, fmt.Errorf("SOCKS5 connection failed: %s", errMsg)
235 }
236
237 // Consume bound address and port
238 atyp := replyHdr[3]
239 switch atyp {
240 case socks5AtypIPv4:
241 bnd := make([]byte, 4+2)
242 if _, err := io.ReadFull(conn, bnd); err != nil {
243 conn.Close()
244 return nil, err
245 }
246 case socks5AtypDomain:
247 lenBuf := make([]byte, 1)
248 if _, err := io.ReadFull(conn, lenBuf); err != nil {
249 conn.Close()
250 return nil, err
251 }
252 bnd := make([]byte, int(lenBuf[0])+2)
253 if _, err := io.ReadFull(conn, bnd); err != nil {
254 conn.Close()
255 return nil, err
256 }
257 case socks5AtypIPv6:
258 bnd := make([]byte, 16+2)
259 if _, err := io.ReadFull(conn, bnd); err != nil {
260 conn.Close()
261 return nil, err
262 }
263 default:
264 conn.Close()
265 return nil, fmt.Errorf("unsupported SOCKS5 bound address type 0x%02x", atyp)
266 }
267
268 return conn, nil
269}
270
271func (s *SOCKS5Dialer) authenticateUserPass(conn net.Conn) error {
272 // RFC 1929 subnegotiation:
273 // +----+------+----------+------+----------+
274 // |VER | ULEN | UNAME | PLEN | PASSWD |
275 // +----+------+----------+------+----------+
276 // | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
277 // +----+------+----------+------+----------+
278 uLen := len(s.username)
279 pLen := len(s.password)
280 if uLen > 255 || pLen > 255 {
281 return errors.New("SOCKS5 username or password exceeds 255 bytes")
282 }
283
284 authReq := make([]byte, 3+uLen+pLen)
285 authReq[0] = 0x01
286 authReq[1] = byte(uLen)
287 copy(authReq[2:2+uLen], []byte(s.username))
288 authReq[2+uLen] = byte(pLen)
289 copy(authReq[3+uLen:], []byte(s.password))
290
291 if _, err := conn.Write(authReq); err != nil {
292 return fmt.Errorf("failed to send SOCKS5 auth request: %w", err)
293 }
294
295 authResp := make([]byte, 2)
296 if _, err := io.ReadFull(conn, authResp); err != nil {
297 return fmt.Errorf("failed to read SOCKS5 auth response: %w", err)
298 }
299
300 if authResp[0] != 0x01 {
301 return fmt.Errorf("invalid SOCKS5 auth version %d", authResp[0])
302 }
303
304 if authResp[1] != 0x00 {
305 return errors.New("SOCKS5 username/password authentication failed")
306 }
307
308 return nil
309}