Skip to content

Commit 61147c4

Browse files
committed
internal/{socks,sockstest}: new packages
This change factors out the code related to SOCKS protocol version 5 from the golang/x/net/proxy package and provides new SOCKS-specific API to fix the following: - inflexbility of forward proxy connection setup; e.g., no support for context-based deadline or canceling, no support for dial deadline, no support for working with external authentication mechanisms, - useless error values for troubleshooting. The new package socks is supposed to be used by the net/http package of standard library and proxy package of golang.org/x/net repository. Fixes golang/go#11682. Updates golang/go#17759. Updates golang/go#19354. Updates golang/go#19688. Fixes golang/go#21333. Change-Id: I24098ac8522dcbdceb03d534147c5101ec9e7350 Reviewed-on: https://go-review.googlesource.com/38278 Run-TryBot: Mikio Hara <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent b3c676e commit 61147c4

File tree

7 files changed

+974
-309
lines changed

7 files changed

+974
-309
lines changed

internal/socks/client.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package socks
6+
7+
import (
8+
"context"
9+
"errors"
10+
"io"
11+
"net"
12+
"strconv"
13+
"time"
14+
)
15+
16+
var (
17+
noDeadline = time.Time{}
18+
aLongTimeAgo = time.Unix(1, 0)
19+
)
20+
21+
func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net.Addr, ctxErr error) {
22+
host, port, err := splitHostPort(address)
23+
if err != nil {
24+
return nil, err
25+
}
26+
if deadline, ok := ctx.Deadline(); ok && !deadline.IsZero() {
27+
c.SetDeadline(deadline)
28+
defer c.SetDeadline(noDeadline)
29+
}
30+
if ctx != context.Background() {
31+
errCh := make(chan error, 1)
32+
done := make(chan struct{})
33+
defer func() {
34+
close(done)
35+
if ctxErr == nil {
36+
ctxErr = <-errCh
37+
}
38+
}()
39+
go func() {
40+
select {
41+
case <-ctx.Done():
42+
c.SetDeadline(aLongTimeAgo)
43+
errCh <- ctx.Err()
44+
case <-done:
45+
errCh <- nil
46+
}
47+
}()
48+
}
49+
50+
b := make([]byte, 0, 6+len(host)) // the size here is just an estimate
51+
b = append(b, Version5)
52+
if len(d.AuthMethods) == 0 || d.Authenticate == nil {
53+
b = append(b, 1, byte(AuthMethodNotRequired))
54+
} else {
55+
ams := d.AuthMethods
56+
if len(ams) > 255 {
57+
return nil, errors.New("too many authentication methods")
58+
}
59+
b = append(b, byte(len(ams)))
60+
for _, am := range ams {
61+
b = append(b, byte(am))
62+
}
63+
}
64+
if _, ctxErr = c.Write(b); ctxErr != nil {
65+
return
66+
}
67+
68+
if _, ctxErr = io.ReadFull(c, b[:2]); ctxErr != nil {
69+
return
70+
}
71+
if b[0] != Version5 {
72+
return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0])))
73+
}
74+
am := AuthMethod(b[1])
75+
if am == AuthMethodNoAcceptableMethods {
76+
return nil, errors.New("no acceptable authentication methods")
77+
}
78+
if d.Authenticate != nil {
79+
if ctxErr = d.Authenticate(ctx, c, am); ctxErr != nil {
80+
return
81+
}
82+
}
83+
84+
b = b[:0]
85+
b = append(b, Version5, byte(d.cmd), 0)
86+
if ip := net.ParseIP(host); ip != nil {
87+
if ip4 := ip.To4(); ip4 != nil {
88+
b = append(b, AddrTypeIPv4)
89+
b = append(b, ip4...)
90+
} else if ip6 := ip.To16(); ip6 != nil {
91+
b = append(b, AddrTypeIPv6)
92+
b = append(b, ip6...)
93+
} else {
94+
return nil, errors.New("unknown address type")
95+
}
96+
} else {
97+
if len(host) > 255 {
98+
return nil, errors.New("FQDN too long")
99+
}
100+
b = append(b, AddrTypeFQDN)
101+
b = append(b, byte(len(host)))
102+
b = append(b, host...)
103+
}
104+
b = append(b, byte(port>>8), byte(port))
105+
if _, ctxErr = c.Write(b); ctxErr != nil {
106+
return
107+
}
108+
109+
if _, ctxErr = io.ReadFull(c, b[:4]); ctxErr != nil {
110+
return
111+
}
112+
if b[0] != Version5 {
113+
return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0])))
114+
}
115+
if cmdErr := Reply(b[1]); cmdErr != StatusSucceeded {
116+
return nil, errors.New("unknown error " + cmdErr.String())
117+
}
118+
if b[2] != 0 {
119+
return nil, errors.New("non-zero reserved field")
120+
}
121+
l := 2
122+
var a Addr
123+
switch b[3] {
124+
case AddrTypeIPv4:
125+
l += net.IPv4len
126+
a.IP = make(net.IP, net.IPv4len)
127+
case AddrTypeIPv6:
128+
l += net.IPv6len
129+
a.IP = make(net.IP, net.IPv6len)
130+
case AddrTypeFQDN:
131+
if _, err := io.ReadFull(c, b[:1]); err != nil {
132+
return nil, err
133+
}
134+
l += int(b[0])
135+
default:
136+
return nil, errors.New("unknown address type " + strconv.Itoa(int(b[3])))
137+
}
138+
if cap(b) < l {
139+
b = make([]byte, l)
140+
} else {
141+
b = b[:l]
142+
}
143+
if _, ctxErr = io.ReadFull(c, b); ctxErr != nil {
144+
return
145+
}
146+
if a.IP != nil {
147+
copy(a.IP, b)
148+
} else {
149+
a.Name = string(b[:len(b)-2])
150+
}
151+
a.Port = int(b[len(b)-2])<<8 | int(b[len(b)-1])
152+
return &a, nil
153+
}
154+
155+
func splitHostPort(address string) (string, int, error) {
156+
host, port, err := net.SplitHostPort(address)
157+
if err != nil {
158+
return "", 0, err
159+
}
160+
portnum, err := strconv.Atoi(port)
161+
if err != nil {
162+
return "", 0, err
163+
}
164+
if 1 > portnum || portnum > 0xffff {
165+
return "", 0, errors.New("port number out of range " + port)
166+
}
167+
return host, portnum, nil
168+
}

internal/socks/dial_test.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package socks_test
6+
7+
import (
8+
"context"
9+
"io"
10+
"math/rand"
11+
"net"
12+
"os"
13+
"testing"
14+
"time"
15+
16+
"golang.org/x/net/internal/socks"
17+
"golang.org/x/net/internal/sockstest"
18+
)
19+
20+
const (
21+
targetNetwork = "tcp6"
22+
targetHostname = "fqdn.doesnotexist"
23+
targetHostIP = "2001:db8::1"
24+
targetPort = "5963"
25+
)
26+
27+
func TestDial(t *testing.T) {
28+
t.Run("Connect", func(t *testing.T) {
29+
ss, err := sockstest.NewServer(sockstest.NoAuthRequired, sockstest.NoProxyRequired)
30+
if err != nil {
31+
t.Error(err)
32+
return
33+
}
34+
defer ss.Close()
35+
d := socks.NewDialer(ss.Addr().Network(), ss.Addr().String())
36+
d.AuthMethods = []socks.AuthMethod{
37+
socks.AuthMethodNotRequired,
38+
socks.AuthMethodUsernamePassword,
39+
}
40+
d.Authenticate = (&socks.UsernamePassword{
41+
Username: "username",
42+
Password: "password",
43+
}).Authenticate
44+
c, err := d.Dial(targetNetwork, net.JoinHostPort(targetHostIP, targetPort))
45+
if err == nil {
46+
c.(*socks.Conn).BoundAddr()
47+
c.Close()
48+
}
49+
if err != nil {
50+
t.Error(err)
51+
return
52+
}
53+
})
54+
t.Run("Cancel", func(t *testing.T) {
55+
ss, err := sockstest.NewServer(sockstest.NoAuthRequired, blackholeCmdFunc)
56+
if err != nil {
57+
t.Error(err)
58+
return
59+
}
60+
defer ss.Close()
61+
d := socks.NewDialer(ss.Addr().Network(), ss.Addr().String())
62+
ctx, cancel := context.WithCancel(context.Background())
63+
defer cancel()
64+
dialErr := make(chan error)
65+
go func() {
66+
c, err := d.DialContext(ctx, ss.TargetAddr().Network(), net.JoinHostPort(targetHostname, targetPort))
67+
if err == nil {
68+
c.Close()
69+
}
70+
dialErr <- err
71+
}()
72+
time.Sleep(100 * time.Millisecond)
73+
cancel()
74+
err = <-dialErr
75+
if perr, nerr := parseDialError(err); perr != context.Canceled && nerr == nil {
76+
t.Errorf("got %v; want context.Canceled or equivalent", err)
77+
return
78+
}
79+
})
80+
t.Run("Deadline", func(t *testing.T) {
81+
ss, err := sockstest.NewServer(sockstest.NoAuthRequired, blackholeCmdFunc)
82+
if err != nil {
83+
t.Error(err)
84+
return
85+
}
86+
defer ss.Close()
87+
d := socks.NewDialer(ss.Addr().Network(), ss.Addr().String())
88+
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(100*time.Millisecond))
89+
defer cancel()
90+
c, err := d.DialContext(ctx, ss.TargetAddr().Network(), net.JoinHostPort(targetHostname, targetPort))
91+
if err == nil {
92+
c.Close()
93+
}
94+
if perr, nerr := parseDialError(err); perr != context.DeadlineExceeded && nerr == nil {
95+
t.Errorf("got %v; want context.DeadlineExceeded or equivalent", err)
96+
return
97+
}
98+
})
99+
t.Run("WithRogueServer", func(t *testing.T) {
100+
ss, err := sockstest.NewServer(sockstest.NoAuthRequired, rogueCmdFunc)
101+
if err != nil {
102+
t.Error(err)
103+
return
104+
}
105+
defer ss.Close()
106+
d := socks.NewDialer(ss.Addr().Network(), ss.Addr().String())
107+
for i := 0; i < 2*len(rogueCmdList); i++ {
108+
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(100*time.Millisecond))
109+
defer cancel()
110+
c, err := d.DialContext(ctx, targetNetwork, net.JoinHostPort(targetHostIP, targetPort))
111+
if err == nil {
112+
t.Log(c.(*socks.Conn).BoundAddr())
113+
c.Close()
114+
t.Error("should fail")
115+
}
116+
}
117+
})
118+
}
119+
120+
func blackholeCmdFunc(rw io.ReadWriter, b []byte) error {
121+
if _, err := sockstest.ParseCmdRequest(b); err != nil {
122+
return err
123+
}
124+
var bb [1]byte
125+
for {
126+
if _, err := rw.Read(bb[:]); err != nil {
127+
return err
128+
}
129+
}
130+
}
131+
132+
func rogueCmdFunc(rw io.ReadWriter, b []byte) error {
133+
if _, err := sockstest.ParseCmdRequest(b); err != nil {
134+
return err
135+
}
136+
rw.Write(rogueCmdList[rand.Intn(len(rogueCmdList))])
137+
return nil
138+
}
139+
140+
var rogueCmdList = [][]byte{
141+
{0x05},
142+
{0x06, 0x00, 0x00, 0x01, 192, 0, 2, 1, 0x17, 0x4b},
143+
{0x05, 0x00, 0xff, 0x01, 192, 0, 2, 2, 0x17, 0x4b},
144+
{0x05, 0x00, 0x00, 0x01, 192, 0, 2, 3},
145+
{0x05, 0x00, 0x00, 0x03, 0x04, 'F', 'Q', 'D', 'N'},
146+
}
147+
148+
func parseDialError(err error) (perr, nerr error) {
149+
if e, ok := err.(*net.OpError); ok {
150+
err = e.Err
151+
nerr = e
152+
}
153+
if e, ok := err.(*os.SyscallError); ok {
154+
err = e.Err
155+
}
156+
perr = err
157+
return
158+
}

0 commit comments

Comments
 (0)