Skip to content

Commit

Permalink
feat: SOCKS5 outbound
Browse files Browse the repository at this point in the history
  • Loading branch information
tobyxdd committed Aug 18, 2023
1 parent c27e6fb commit acfb10e
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 0 deletions.
16 changes: 16 additions & 0 deletions app/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,17 @@ type serverConfigOutboundDirect struct {
BindDevice string `mapstructure:"bindDevice"`
}

type serverConfigOutboundSOCKS5 struct {
Addr string `mapstructure:"addr"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
}

type serverConfigOutboundEntry struct {
Name string `mapstructure:"name"`
Type string `mapstructure:"type"`
Direct serverConfigOutboundDirect `mapstructure:"direct"`
SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"`
}

type serverConfigMasqueradeFile struct {
Expand Down Expand Up @@ -315,6 +322,13 @@ func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outboun
return outbounds.NewDirectOutboundSimple(mode), nil
}

func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) {
if c.Addr == "" {
return nil, configError{Field: "outbounds.socks5.addr", Err: errors.New("empty socks5 address")}
}
return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil
}

func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {
// Resolver, ACL, actual outbound are all implemented through the Outbound interface.
// Depending on the config, we build a chain like this:
Expand All @@ -339,6 +353,8 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {
switch strings.ToLower(entry.Type) {
case "direct":
ob, err = serverConfigOutboundDirectToOutbound(entry.Direct)
case "socks5":
ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5)
default:
err = configError{Field: "outbounds.type", Err: errors.New("unsupported outbound type")}
}
Expand Down
9 changes: 9 additions & 0 deletions app/cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ func TestServerConfig(t *testing.T) {
BindDevice: "eth233",
},
},
{
Name: "badstuff",
Type: "socks5",
SOCKS5: serverConfigOutboundSOCKS5{
Addr: "shady.proxy.ru:1080",
Username: "hackerman",
Password: "Elliot Alderson",
},
},
},
Masquerade: serverConfigMasquerade{
Type: "proxy",
Expand Down
6 changes: 6 additions & 0 deletions app/cmd/server_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ outbounds:
bindIPv4: 2.4.6.8
bindIPv6: 0:0:0:0:0:ffff:0204:0608
bindDevice: eth233
- name: badstuff
type: socks5
socks5:
addr: shady.proxy.ru:1080
username: hackerman
password: Elliot Alderson

masquerade:
type: proxy
Expand Down
257 changes: 257 additions & 0 deletions extras/outbounds/ob_socks5.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package outbounds

import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"

"github.com/txthinking/socks5"
)

const (
socks5NegotiationTimeout = 10 * time.Second
socks5RequestTimeout = 10 * time.Second
)

var errSOCKS5AuthFailed = errors.New("SOCKS5 authentication failed")

type errSOCKS5UnsupportedAuthMethod struct {
Method byte
}

func (e errSOCKS5UnsupportedAuthMethod) Error() string {
return fmt.Sprintf("unsupported SOCKS5 authentication method: %d", e.Method)
}

type errSOCKS5RequestFailed struct {
Rep byte
}

func (e errSOCKS5RequestFailed) Error() string {
return fmt.Sprintf("SOCKS5 request failed: %d", e.Rep)
}

// socks5Outbound is a PluggableOutbound that connects to the target using
// a SOCKS5 proxy server.
// Since SOCKS5 supports using either IP or domain name as the target address,
// it will ignore ResolveInfo in AddrEx and always only use Host.
type socks5Outbound struct {
Dialer *net.Dialer
Addr string
Username string
Password string
}

func NewSOCKS5Outbound(addr, username, password string) PluggableOutbound {
return &socks5Outbound{
Dialer: &net.Dialer{
Timeout: defaultDialerTimeout,
},
Addr: addr,
Username: username,
Password: password,
}
}

// dialAndNegotiate creates a new TCP connection to the SOCKS5 proxy server
// and performs the negotiation. Returns an established connection ready to
// handle requests, or an error if the process fails.
func (o *socks5Outbound) dialAndNegotiate() (net.Conn, error) {
conn, err := o.Dialer.Dial("tcp", o.Addr)
if err != nil {
return nil, err
}
if err := conn.SetDeadline(time.Now().Add(socks5NegotiationTimeout)); err != nil {
_ = conn.Close()
return nil, err
}
authMethods := []byte{socks5.MethodNone}
if o.Username != "" && o.Password != "" {
authMethods = append(authMethods, socks5.MethodUsernamePassword)
}
req := socks5.NewNegotiationRequest(authMethods)
if _, err := req.WriteTo(conn); err != nil {
_ = conn.Close()
return nil, err
}
resp, err := socks5.NewNegotiationReplyFrom(conn)
if err != nil {
_ = conn.Close()
return nil, err
}
if resp.Method == socks5.MethodUsernamePassword {
upReq := socks5.NewUserPassNegotiationRequest([]byte(o.Username), []byte(o.Password))
if _, err := upReq.WriteTo(conn); err != nil {
_ = conn.Close()
return nil, err
}
upResp, err := socks5.NewUserPassNegotiationReplyFrom(conn)
if err != nil {
_ = conn.Close()
return nil, err
}
if upResp.Status != socks5.UserPassStatusSuccess {
_ = conn.Close()
return nil, errSOCKS5AuthFailed
}
} else if resp.Method != socks5.MethodNone {
// We only support none & username/password authentication methods.
_ = conn.Close()
return nil, errSOCKS5UnsupportedAuthMethod{resp.Method}
}
// Negotiation succeeded, reset the deadline.
if err := conn.SetDeadline(time.Time{}); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}

// request sends a SOCKS5 request to the proxy server and returns the reply.
// Note that it will return an error if the reply from the server indicates
// a failure.
func (o *socks5Outbound) request(conn net.Conn, req *socks5.Request) (*socks5.Reply, error) {
if err := conn.SetDeadline(time.Now().Add(socks5RequestTimeout)); err != nil {
return nil, err
}
if _, err := req.WriteTo(conn); err != nil {
return nil, err
}
resp, err := socks5.NewReplyFrom(conn)
if err != nil {
return nil, err
}
if resp.Rep != socks5.RepSuccess {
return nil, errSOCKS5RequestFailed{resp.Rep}
}
if err := conn.SetDeadline(time.Time{}); err != nil {
return nil, err
}
return resp, nil
}

func (s *socks5Outbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
conn, err := s.dialAndNegotiate()
if err != nil {
return nil, err
}
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr)
req := socks5.NewRequest(socks5.CmdConnect, atyp, dstAddr, dstPort)
if _, err := s.request(conn, req); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}

func (s *socks5Outbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
conn, err := s.dialAndNegotiate()
if err != nil {
return nil, err
}
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr)
req := socks5.NewRequest(socks5.CmdUDP, atyp, dstAddr, dstPort)
resp, err := s.request(conn, req)
if err != nil {
_ = conn.Close()
return nil, err
}
return newSOCKS5UDPConn(conn, resp.Address())
}

type socks5UDPConn struct {
tcpConn net.Conn
udpConn net.Conn
}

func newSOCKS5UDPConn(tcpConn net.Conn, udpAddr string) (*socks5UDPConn, error) {
udpConn, err := net.Dial("udp", udpAddr)
if err != nil {
return nil, err
}
sc := &socks5UDPConn{
tcpConn: tcpConn,
udpConn: udpConn,
}
go sc.hold()
return sc, nil
}

func (c *socks5UDPConn) hold() {
_, _ = io.Copy(io.Discard, c.tcpConn)
_ = c.tcpConn.Close()
_ = c.udpConn.Close()
}

func (c *socks5UDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
n, err := c.udpConn.Read(b)
if err != nil {
return 0, nil, err
}
d, err := socks5.NewDatagramFromBytes(b[:n])
if err != nil {
return 0, nil, err
}
addr := socks5AddrToAddrEx(d.Atyp, d.DstAddr, d.DstPort)
n = copy(b, d.Data)
return n, addr, nil
}

func (c *socks5UDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(addr)
d := socks5.NewDatagram(atyp, dstAddr, dstPort, b)
_, err := c.udpConn.Write(d.Bytes())
if err != nil {
return 0, err
}
return len(b), nil
}

func (c *socks5UDPConn) Close() error {
_ = c.tcpConn.Close()
_ = c.udpConn.Close()
return nil
}

func addrExToSOCKS5Addr(addr *AddrEx) (atyp byte, dstAddr, dstPort []byte) {
// Host
ip := net.ParseIP(addr.Host)
if ip != nil {
if ip.To4() != nil {
atyp = socks5.ATYPIPv4
dstAddr = ip.To4()
} else {
atyp = socks5.ATYPIPv6
dstAddr = ip.To16()
}
} else {
atyp = socks5.ATYPDomain
dstAddr = []byte(addr.Host)
}
// Port
dstPort = make([]byte, 2)
binary.BigEndian.PutUint16(dstPort, addr.Port)
return
}

func socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx {
// Host
var host string
if atyp == socks5.ATYPIPv4 {
host = net.IP(dstAddr).To4().String()
} else if atyp == socks5.ATYPIPv6 {
host = net.IP(dstAddr).To16().String()
} else if atyp == socks5.ATYPDomain {
// Need to strip the first byte which is the domain length.
host = string(dstAddr[1:])
}
// Port
port := binary.BigEndian.Uint16(dstPort)
return &AddrEx{
Host: host,
Port: port,
}
}

0 comments on commit acfb10e

Please sign in to comment.