net/socks5: add password auth support
Conforms to RFC 1929. To support Java HTTP clients via libtailscale, who offer no other reliable hooks into their sockets. Signed-off-by: David Crawshaw <crawshaw@tailscale.com>pull/7469/head
parent
0f4359116e
commit
96a555fc5a
|
@ -25,15 +25,20 @@ import (
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Authentication METHODs described in RFC 1928, section 3.
|
||||||
const (
|
const (
|
||||||
noAuthRequired byte = 0
|
noAuthRequired byte = 0
|
||||||
|
passwordAuth byte = 2
|
||||||
noAcceptableAuth byte = 255
|
noAcceptableAuth byte = 255
|
||||||
|
|
||||||
// socks5Version is the byte that represents the SOCKS version
|
|
||||||
// in requests.
|
|
||||||
socks5Version byte = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// passwordAuthVersion is the auth version byte described in RFC 1929.
|
||||||
|
const passwordAuthVersion = 1
|
||||||
|
|
||||||
|
// socks5Version is the byte that represents the SOCKS version
|
||||||
|
// in requests.
|
||||||
|
const socks5Version byte = 5
|
||||||
|
|
||||||
// commandType are the bytes sent in SOCKS5 packets
|
// commandType are the bytes sent in SOCKS5 packets
|
||||||
// that represent the kind of connection the client needs.
|
// that represent the kind of connection the client needs.
|
||||||
type commandType byte
|
type commandType byte
|
||||||
|
@ -83,6 +88,10 @@ type Server struct {
|
||||||
// Dialer optionally specifies the dialer to use for outgoing connections.
|
// Dialer optionally specifies the dialer to use for outgoing connections.
|
||||||
// If nil, the net package's standard dialer is used.
|
// If nil, the net package's standard dialer is used.
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
|
||||||
|
// Username and Password, if set, are the credential clients must provide.
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
@ -134,12 +143,29 @@ type Conn struct {
|
||||||
|
|
||||||
// Run starts the new connection.
|
// Run starts the new connection.
|
||||||
func (c *Conn) Run() error {
|
func (c *Conn) Run() error {
|
||||||
err := parseClientGreeting(c.clientConn)
|
needAuth := c.srv.Username != "" || c.srv.Password != ""
|
||||||
|
authMethod := noAuthRequired
|
||||||
|
if needAuth {
|
||||||
|
authMethod = passwordAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
err := parseClientGreeting(c.clientConn, authMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.clientConn.Write([]byte{socks5Version, noAcceptableAuth})
|
c.clientConn.Write([]byte{socks5Version, noAcceptableAuth})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.clientConn.Write([]byte{socks5Version, noAuthRequired})
|
c.clientConn.Write([]byte{socks5Version, authMethod})
|
||||||
|
if !needAuth {
|
||||||
|
return c.handleRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
user, pwd, err := parseClientAuth(c.clientConn)
|
||||||
|
if err != nil || user != c.srv.Username || pwd != c.srv.Password {
|
||||||
|
c.clientConn.Write([]byte{1, 1}) // auth error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.clientConn.Write([]byte{1, 0}) // auth success
|
||||||
|
|
||||||
return c.handleRequest()
|
return c.handleRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,10 +246,8 @@ func (c *Conn) handleRequest() error {
|
||||||
return <-errc
|
return <-errc
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseClientGreeting parses a request initiation packet
|
// parseClientGreeting parses a request initiation packet.
|
||||||
// and returns a slice that contains the acceptable auth methods
|
func parseClientGreeting(r io.Reader, authMethod byte) error {
|
||||||
// for the client.
|
|
||||||
func parseClientGreeting(r io.Reader) error {
|
|
||||||
var hdr [2]byte
|
var hdr [2]byte
|
||||||
_, err := io.ReadFull(r, hdr[:])
|
_, err := io.ReadFull(r, hdr[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -239,13 +263,38 @@ func parseClientGreeting(r io.Reader) error {
|
||||||
return fmt.Errorf("could not read methods")
|
return fmt.Errorf("could not read methods")
|
||||||
}
|
}
|
||||||
for _, m := range methods {
|
for _, m := range methods {
|
||||||
if m == noAuthRequired {
|
if m == authMethod {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("no acceptable auth methods")
|
return fmt.Errorf("no acceptable auth methods")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseClientAuth(r io.Reader) (usr, pwd string, err error) {
|
||||||
|
var hdr [2]byte
|
||||||
|
if _, err := io.ReadFull(r, hdr[:]); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not read auth packet header")
|
||||||
|
}
|
||||||
|
if hdr[0] != passwordAuthVersion {
|
||||||
|
return "", "", fmt.Errorf("bad SOCKS auth version")
|
||||||
|
}
|
||||||
|
usrLen := int(hdr[1])
|
||||||
|
usrBytes := make([]byte, usrLen)
|
||||||
|
if _, err := io.ReadFull(r, usrBytes); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not read auth packet username")
|
||||||
|
}
|
||||||
|
var hdrPwd [1]byte
|
||||||
|
if _, err := io.ReadFull(r, hdrPwd[:]); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not read auth packet password length")
|
||||||
|
}
|
||||||
|
pwdLen := int(hdrPwd[0])
|
||||||
|
pwdBytes := make([]byte, pwdLen)
|
||||||
|
if _, err := io.ReadFull(r, pwdBytes); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not read auth packet password")
|
||||||
|
}
|
||||||
|
return string(usrBytes), string(pwdBytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
// request represents data contained within a SOCKS5
|
// request represents data contained within a SOCKS5
|
||||||
// connection request packet.
|
// connection request packet.
|
||||||
type request struct {
|
type request struct {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package socks5
|
package socks5
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
@ -74,3 +75,80 @@ func TestRead(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadPassword(t *testing.T) {
|
||||||
|
// backend server which we'll use SOCKS5 to connect to
|
||||||
|
ln, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backendServerPort := ln.Addr().(*net.TCPAddr).Port
|
||||||
|
go backendServer(ln)
|
||||||
|
|
||||||
|
socks5ln, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
socks5ln.Close()
|
||||||
|
})
|
||||||
|
auth := &proxy.Auth{User: "foo", Password: "bar"}
|
||||||
|
go func() {
|
||||||
|
s := Server{Username: auth.User, Password: auth.Password}
|
||||||
|
err := s.Serve(socks5ln)
|
||||||
|
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("localhost:%d", socks5ln.Addr().(*net.TCPAddr).Port)
|
||||||
|
|
||||||
|
if d, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if _, err := d.Dial("tcp", addr); err == nil {
|
||||||
|
t.Fatal("expected no-auth dial error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
badPwd := &proxy.Auth{User: "foo", Password: "not right"}
|
||||||
|
if d, err := proxy.SOCKS5("tcp", addr, badPwd, proxy.Direct); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if _, err := d.Dial("tcp", addr); err == nil {
|
||||||
|
t.Fatal("expected bad password dial error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
badUsr := &proxy.Auth{User: "not right", Password: "bar"}
|
||||||
|
if d, err := proxy.SOCKS5("tcp", addr, badUsr, proxy.Direct); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if _, err := d.Dial("tcp", addr); err == nil {
|
||||||
|
t.Fatal("expected bad username dial error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socksDialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = fmt.Sprintf("localhost:%d", backendServerPort)
|
||||||
|
conn, err := socksDialer.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(buf) != "Test" {
|
||||||
|
t.Fatalf("got: %q want: Test", buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue